Added docx and pdf export support.

This commit is contained in:
Justin Marshall
2026-08-10 13:41:59 -07:00
parent ac5472e233
commit e8c9260ad7
9 changed files with 1568 additions and 30 deletions
+28
View File
@@ -721,6 +721,7 @@ add_library(opus_x64_runtime STATIC
port/original/opus_asm_search.cpp port/original/opus_asm_search.cpp
port/original/opus_sdm_cab.cpp port/original/opus_sdm_cab.cpp
port/original/opus_sdm_runtime.cpp port/original/opus_sdm_runtime.cpp
port/original/opus_modern_formats.cpp
) )
target_compile_features(opus_x64_runtime PRIVATE cxx_std_20) target_compile_features(opus_x64_runtime PRIVATE cxx_std_20)
target_compile_definitions(opus_x64_runtime PRIVATE target_compile_definitions(opus_x64_runtime PRIVATE
@@ -732,6 +733,7 @@ target_compile_definitions(opus_x64_runtime PRIVATE
NOMINMAX NOMINMAX
) )
target_include_directories(opus_x64_runtime PRIVATE ${OPUS_ORIGINAL_INCLUDE_DIRS}) target_include_directories(opus_x64_runtime PRIVATE ${OPUS_ORIGINAL_INCLUDE_DIRS})
target_link_libraries(opus_x64_runtime PUBLIC ole32 comdlg32 gdi32)
if(MSVC) if(MSVC)
# Opus was built with unsigned plain characters; the byte-oriented string # Opus was built with unsigned plain characters; the byte-oriented string
# and international-character assembly routines depend on that behavior. # and international-character assembly routines depend on that behavior.
@@ -943,6 +945,27 @@ foreach(config Debug Release RelWithDebInfo MinSizeRel)
) )
endforeach() endforeach()
add_executable(opus_modern_formats_test
port/original/opus_modern_formats_test.cpp
)
target_compile_features(opus_modern_formats_test PRIVATE cxx_std_20)
target_compile_definitions(opus_modern_formats_test PRIVATE NOMINMAX UNICODE _UNICODE)
target_link_libraries(opus_modern_formats_test PRIVATE opus_x64_runtime user32 gdi32)
if(MSVC)
target_compile_options(opus_modern_formats_test PRIVATE /W4 /utf-8)
endif()
set_target_properties(opus_modern_formats_test PROPERTIES
FOLDER "Original Opus x64/tests"
RUNTIME_OUTPUT_DIRECTORY "${MSWORD_BUILD_ROOT}/tests"
VS_GLOBAL_VcpkgApplocalDeps "false"
)
foreach(config Debug Release RelWithDebInfo MinSizeRel)
string(TOUPPER "${config}" config_upper)
set_target_properties(opus_modern_formats_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${MSWORD_BUILD_ROOT}/tests/${config}"
)
endforeach()
add_executable(opus_word1_ui_test add_executable(opus_word1_ui_test
port/original/opus_word1_ui_test.cpp port/original/opus_word1_ui_test.cpp
) )
@@ -968,6 +991,7 @@ endforeach()
enable_testing() enable_testing()
add_test(NAME opus_original_strtbl_test COMMAND $<TARGET_FILE:opus_original_strtbl_test>) add_test(NAME opus_original_strtbl_test COMMAND $<TARGET_FILE:opus_original_strtbl_test>)
add_test(NAME opus_x64_runtime_test COMMAND $<TARGET_FILE:opus_x64_runtime_test>) add_test(NAME opus_x64_runtime_test COMMAND $<TARGET_FILE:opus_x64_runtime_test>)
add_test(NAME opus_modern_formats_test COMMAND $<TARGET_FILE:opus_modern_formats_test>)
add_test(NAME opus_original_sttb_test COMMAND $<TARGET_FILE:opus_original_sttb_test>) add_test(NAME opus_original_sttb_test COMMAND $<TARGET_FILE:opus_original_sttb_test>)
add_test(NAME opus_original_plc_test COMMAND $<TARGET_FILE:opus_original_plc_test>) add_test(NAME opus_original_plc_test COMMAND $<TARGET_FILE:opus_original_plc_test>)
add_test(NAME opus_sdm_cab_test COMMAND $<TARGET_FILE:opus_sdm_cab_test>) add_test(NAME opus_sdm_cab_test COMMAND $<TARGET_FILE:opus_sdm_cab_test>)
@@ -1005,3 +1029,7 @@ add_test(NAME opus_word1_save_as_test
COMMAND $<TARGET_FILE:opus_word1_ui_test> $<TARGET_FILE:WORD1> --save-as COMMAND $<TARGET_FILE:opus_word1_ui_test> $<TARGET_FILE:WORD1> --save-as
) )
set_tests_properties(opus_word1_save_as_test PROPERTIES TIMEOUT 20) set_tests_properties(opus_word1_save_as_test PROPERTIES TIMEOUT 20)
add_test(NAME opus_word1_pdf_export_test
COMMAND $<TARGET_FILE:opus_word1_ui_test> $<TARGET_FILE:WORD1> --pdf-export
)
set_tests_properties(opus_word1_pdf_export_test PROPERTIES TIMEOUT 25)
+8 -1
View File
@@ -83,6 +83,9 @@ extern int vfConversion;
extern MUD ** vhmudUser; extern MUD ** vhmudUser;
extern KMP ** hkmpCur; extern KMP ** hkmpCur;
extern KMP ** vhkmpUser; extern KMP ** vhkmpUser;
#ifdef OPUS_X64
extern int OpusWin95OpenAliasIsDocx();
#endif
struct DTTM DttmCur(); struct DTTM DttmCur();
@@ -484,7 +487,11 @@ CHAR *stConverter, **pstSubset;
dff = dffOpenText; dff = dffOpenText;
/* dff is our best guess of format */ /* dff is our best guess of format */
if (fInput) if (fInput
#ifdef OPUS_X64
&& !OpusWin95OpenAliasIsDocx(PfcbFn(fn)->stFile)
#endif
)
/* verify the format */ /* verify the format */
if ((dff = DffVerifyDff (dff, fn)) == dffNil) if ((dff = DffVerifyDff (dff, fn)) == dffNil)
/* cancel operation */ /* cancel operation */
+1 -1
View File
@@ -833,7 +833,7 @@ int doc;
*/ */
/* %%Function: HWriteRTF %%Owner: bobz */ /* %%Function: HWriteRTF %%Owner: bobz */
HWriteRTF (doc, cpFirst, cpLim, cbInitial) HANDLE HWriteRTF (doc, cpFirst, cpLim, cbInitial)
int doc; int doc;
CP cpFirst, cpLim; CP cpFirst, cpLim;
int cbInitial; int cbInitial;
+79
View File
@@ -103,6 +103,85 @@ static int vWin95ZoomPercent = 100;
static int vWin95BaseDxsInch = 0; static int vWin95BaseDxsInch = 0;
static int vWin95BaseDysInch = 0; static int vWin95BaseDysInch = 0;
int OpusExportCurrentDocumentPdf()
{
int doc;
HANDLE hText;
LPCH lpch;
int result;
int cchText = 0;
int fAfterReturn = fFalse;
CP cp;
CP cpMac;
extern int vccpFetch;
extern CHAR HUGE *vhpchFetch;
extern int N_FetchCp();
extern int OpusExportTextToPdfDialog();
if (selCur.doc == docNil)
return fFalse;
doc = DocMother(selCur.doc);
cpMac = CpMacDocEdit(doc);
if (cpMac < cp0 || cpMac > 0x3fffffffL)
return fFalse;
hText = GlobalAlloc(GMEM_MOVEABLE, (DWORD)(cpMac * 2L + 1L));
if (hText == hNil || (lpch = GlobalLockClip(hText)) == NULL)
{
if (hText != hNil)
GlobalFree(hText);
return fFalse;
}
for (cp = cp0; cp < cpMac; )
{
int ich;
int cch;
FetchCp(doc, cp, fcmChars);
if (vccpFetch <= 0 || vhpchFetch == NULL)
{
GlobalUnlock(hText);
GlobalFree(hText);
return fFalse;
}
cch = (int)CpMin((CP)vccpFetch, cpMac - cp);
for (ich = 0; ich < cch; ++ich)
{
int ch = (unsigned char)vhpchFetch[ich];
if (ch == chReturn)
{
lpch[cchText++] = '\r';
lpch[cchText++] = '\n';
fAfterReturn = fTrue;
}
else if (ch == chEop)
{
if (!fAfterReturn)
{
lpch[cchText++] = '\r';
lpch[cchText++] = '\n';
}
fAfterReturn = fFalse;
}
else
{
fAfterReturn = fFalse;
if (ch == chTable || ch == chTab)
lpch[cchText++] = '\t';
else if (ch == chSect || ch == chColumnBreak)
lpch[cchText++] = '\f';
else if (ch >= chSpace || ch == chNonReqHyphen)
lpch[cchText++] = (char)ch;
}
}
cp += cch;
}
lpch[cchText] = '\0';
result = OpusExportTextToPdfDialog(vhwndApp, lpch, cchText);
GlobalUnlock(hText);
GlobalFree(hText);
return result;
}
/* The Word 1.x Color command opens a keyboard prompt. The Win95 chrome /* The Word 1.x Color command opens a keyboard prompt. The Win95 chrome
supplies a palette instead, but still applies the original character supplies a palette instead, but still applies the original character
property so document storage and formatting behavior remain unchanged. */ property so document storage and formatting behavior remain unchanged. */
+12 -1
View File
@@ -89,6 +89,7 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */
#ifdef OPUS_X64 #ifdef OPUS_X64
extern int OpusWin95SaveAliasMatches(); extern int OpusWin95SaveAliasMatches();
extern int OpusFinishWin95SaveAlias(); extern int OpusFinishWin95SaveAlias();
extern int OpusWin95SaveAliasRequiresRtf();
#define FWin95SaveAliasMatches(st) OpusWin95SaveAliasMatches(st) #define FWin95SaveAliasMatches(st) OpusWin95SaveAliasMatches(st)
#else #else
#define FWin95SaveAliasMatches(st) fFalse #define FWin95SaveAliasMatches(st) fFalse
@@ -661,7 +662,16 @@ LSaveAs:
/* perform the actual save */ /* perform the actual save */
PdodDoc(doc)->dop.fLockAtn = (*hcab)->fLockAnnot; PdodDoc(doc)->dop.fLockAtn = (*hcab)->fLockAnnot;
if (FSaveFile(stFile, doc, DffISaveFmt((*hcab)->iFmt), {
int dffSave = DffISaveFmt((*hcab)->iFmt);
#ifdef OPUS_X64
if (OpusWin95SaveAliasRequiresRtf(stFile))
{
dffSave = dffSaveRTF;
(*hcab)->fQuicksave = fFalse;
}
#endif
if (FSaveFile(stFile, doc, dffSave,
(*hcab)->fQuicksave, (*hcab)->fBackup, fTrue /* fReport */)) (*hcab)->fQuicksave, (*hcab)->fBackup, fTrue /* fReport */))
{ {
/* display number of characters written in prompt */ /* display number of characters written in prompt */
@@ -682,6 +692,7 @@ LSaveAs:
cmd = cmdError; cmd = cmdError;
StopProgressReport(hNil, pdcRestoreImmed); StopProgressReport(hNil, pdcRestoreImmed);
} }
}
vhpprSave = hNil; vhpprSave = hNil;
File diff suppressed because it is too large Load Diff
+89 -16
View File
@@ -22,6 +22,9 @@ extern HWND vhWndMsgBoxParent;
void GetCabSz(void**, char*, std::uint16_t, std::uint16_t); void GetCabSz(void**, char*, std::uint16_t, std::uint16_t);
int FSetCabSz(void**, const char*, std::uint16_t); int FSetCabSz(void**, const char*, std::uint16_t);
void OpusX64TraceRibbon(const char*, int, int, int, int, long, long, int); void OpusX64TraceRibbon(const char*, int, int, int, int, long, long, int);
int OpusModernPathIsDocx(const char*);
int OpusModernDocxToTextFile(const char*, const char*);
int OpusModernRtfFileToDocx(const char*, const char*);
} }
/* /*
@@ -151,7 +154,8 @@ std::string win95_alias_key(std::string path) {
return path; return path;
} }
bool make_win95_staging_path(std::string& path) { bool make_win95_staging_path(std::string& path,
const char* desired_extension = ".DOC") {
char module_path[32768]{}; char module_path[32768]{};
const DWORD module_length = GetModuleFileNameA( const DWORD module_length = GetModuleFileNameA(
nullptr, module_path, static_cast<DWORD>(std::size(module_path))); nullptr, module_path, static_cast<DWORD>(std::size(module_path)));
@@ -174,9 +178,9 @@ bool make_win95_staging_path(std::string& path) {
return false; return false;
} }
DeleteFileA(temporary); DeleteFileA(temporary);
char* extension = std::strrchr(temporary, '.'); char* existing_extension = std::strrchr(temporary, '.');
if (extension != nullptr) { if (existing_extension != nullptr) {
lstrcpyA(extension, ".DOC"); lstrcpyA(existing_extension, desired_extension);
} }
if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES && if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES &&
std::strlen(temporary) < 120) { std::strlen(temporary) < 120) {
@@ -260,6 +264,7 @@ constexpr Tmc kTmcSaveDirectoryList = kTmcUserMin + 3;
constexpr Tmc kTmcSaveDirectory = kTmcUserMin + 4; constexpr Tmc kTmcSaveDirectory = kTmcUserMin + 4;
constexpr Tmc kTmcSaveFormatPrompt = kTmcUserMin + 5; constexpr Tmc kTmcSaveFormatPrompt = kTmcUserMin + 5;
constexpr Tmc kTmcSaveFormat = kTmcUserMin + 6; constexpr Tmc kTmcSaveFormat = kTmcUserMin + 6;
constexpr int kSaveFormatRtf = 6;
constexpr Tmc kTmcSaveQuick = kTmcUserMin + 7; constexpr Tmc kTmcSaveQuick = kTmcUserMin + 7;
constexpr Tmc kTmcSaveBackup = kTmcUserMin + 8; constexpr Tmc kTmcSaveBackup = kTmcUserMin + 8;
constexpr Tmc kTmcSaveLockAnnotations = kTmcUserMin + 9; constexpr Tmc kTmcSaveLockAnnotations = kTmcUserMin + 9;
@@ -2146,12 +2151,13 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
} }
static const char open_filter[] = static const char open_filter[] =
"Word Documents (*.doc;*.dot)\0*.doc;*.dot\0" "Word Documents (*.doc;*.docx;*.dot)\0*.doc;*.docx;*.dot\0"
"Rich Text Format (*.rtf)\0*.rtf\0" "Rich Text Format (*.rtf)\0*.rtf\0"
"Text Files (*.txt)\0*.txt\0" "Text Files (*.txt)\0*.txt\0"
"All Files (*.*)\0*.*\0\0"; "All Files (*.*)\0*.*\0\0";
static const char save_filter[] = static const char save_filter[] =
"Word Documents (*.doc)\0*.doc\0" "Word Documents (*.doc)\0*.doc\0"
"Word 2007-2024 Documents (*.docx)\0*.docx\0"
"Document Templates (*.dot)\0*.dot\0" "Document Templates (*.dot)\0*.dot\0"
"All Files (*.*)\0*.*\0\0"; "All Files (*.*)\0*.*\0\0";
@@ -2180,8 +2186,23 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
OFN_OVERWRITEPROMPT; OFN_OVERWRITEPROMPT;
} }
const BOOL accepted = opening ? char test_path[32768]{};
GetOpenFileNameA(&file_dialog) : GetSaveFileNameA(&file_dialog); const DWORD test_path_length = GetEnvironmentVariableA(
"WORD1_TEST_FILE_DIALOG_PATH", test_path,
static_cast<DWORD>(std::size(test_path)));
BOOL accepted = FALSE;
if (test_path_length > 0 && test_path_length < std::size(test_path)) {
lstrcpynA(file_buffer.data(), test_path,
static_cast<int>(file_buffer.size()));
if (saving && OpusModernPathIsDocx(test_path)) {
file_dialog.nFilterIndex = 2;
}
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
accepted = TRUE;
} else {
accepted = opening ? GetOpenFileNameA(&file_dialog) :
GetSaveFileNameA(&file_dialog);
}
if (!accepted) { if (!accepted) {
if (CommDlgExtendedError() != 0) { if (CommDlgExtendedError() != 0) {
MessageBoxA(owner, MessageBoxA(owner,
@@ -2196,18 +2217,41 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
break; break;
} }
const std::string selected_path = file_buffer.data(); std::string selected_path = file_buffer.data();
if (saving && file_dialog.nFilterIndex == 2) {
const std::size_t selected_slash =
selected_path.find_last_of("\\/");
const std::size_t selected_dot = selected_path.find_last_of('.');
if (selected_dot == std::string::npos ||
(selected_slash != std::string::npos &&
selected_dot < selected_slash)) {
selected_path += ".docx";
} else if (_stricmp(selected_path.c_str() + selected_dot,
".doc") == 0) {
selected_path.replace(selected_dot, std::string::npos,
".docx");
}
}
std::string legacy_path; std::string legacy_path;
if (!make_win95_staging_path(legacy_path)) { const bool docx = OpusModernPathIsDocx(selected_path.c_str()) != 0;
if (!make_win95_staging_path(legacy_path,
docx ? ".TXT" : ".DOC")) {
MessageBoxA(owner, MessageBoxA(owner,
"Word could not prepare a temporary document file.", "Word could not prepare a temporary document file.",
opening ? "Open" : "Save As", opening ? "Open" : "Save As",
MB_OK | MB_ICONEXCLAMATION); MB_OK | MB_ICONEXCLAMATION);
continue; continue;
} }
if (opening && const bool staged_open = !opening ||
!CopyFileA(selected_path.c_str(), legacy_path.c_str(), FALSE)) { (docx ?
MessageBoxA(owner, "Word could not open the selected file.", OpusModernDocxToTextFile(selected_path.c_str(),
legacy_path.c_str()) != 0 :
CopyFileA(selected_path.c_str(), legacy_path.c_str(), FALSE) != 0);
if (!staged_open) {
MessageBoxA(owner,
docx ?
"Word could not read this DOCX document." :
"Word could not open the selected file.",
"Open", MB_OK | MB_ICONEXCLAMATION); "Open", MB_OK | MB_ICONEXCLAMATION);
continue; continue;
} }
@@ -2240,6 +2284,11 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) {
SetWindowTextA(edit.window, edit.text.c_str()); SetWindowTextA(edit.window, edit.text.c_str());
} }
sync_save_cab(dialog); sync_save_cab(dialog);
if (auto* cab = save_cab(dialog);
cab != nullptr && docx) {
cab->format = kSaveFormatRtf;
cab->quick_save = false;
}
} }
if (invoke_dialog_proc(dialog, kDlmTerm, kTmcOk)) { if (invoke_dialog_proc(dialog, kDlmTerm, kTmcOk)) {
@@ -2374,10 +2423,13 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file,
g_win95_save_alias.legacy_path.c_str()) == 0) { g_win95_save_alias.legacy_path.c_str()) == 0) {
bool copied = success != 0; bool copied = success != 0;
if (copied) { if (copied) {
copied = CopyFileA( copied = OpusModernPathIsDocx(
g_win95_save_alias.selected_path.c_str()) ?
OpusModernRtfFileToDocx(
g_win95_save_alias.legacy_path.c_str(), g_win95_save_alias.legacy_path.c_str(),
g_win95_save_alias.selected_path.c_str(), g_win95_save_alias.selected_path.c_str()) != 0 :
FALSE) != 0; CopyFileA(g_win95_save_alias.legacy_path.c_str(),
g_win95_save_alias.selected_path.c_str(), FALSE) != 0;
} }
if (!success) { if (!success) {
DeleteFileA(g_win95_save_alias.legacy_path.c_str()); DeleteFileA(g_win95_save_alias.legacy_path.c_str());
@@ -2396,7 +2448,28 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file,
if (!success) { if (!success) {
return false; return false;
} }
return CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0; return OpusModernPathIsDocx(saved->second.c_str()) ?
OpusModernRtfFileToDocx(path.c_str(), saved->second.c_str()) != 0 :
CopyFileA(path.c_str(), saved->second.c_str(), FALSE) != 0;
}
int OpusWin95SaveAliasRequiresRtf(const unsigned char* st_file) {
const std::string path = counted_path(st_file);
if (path.empty()) return false;
if (g_win95_save_alias.active &&
_stricmp(path.c_str(), g_win95_save_alias.legacy_path.c_str()) == 0)
return OpusModernPathIsDocx(g_win95_save_alias.selected_path.c_str());
const auto saved = g_win95_saved_aliases.find(win95_alias_key(path));
return saved != g_win95_saved_aliases.end() &&
OpusModernPathIsDocx(saved->second.c_str());
}
int OpusWin95OpenAliasIsDocx(const unsigned char* st_file) {
const std::string path = counted_path(st_file);
if (path.empty()) return false;
const auto saved = g_win95_saved_aliases.find(win95_alias_key(path));
return saved != g_win95_saved_aliases.end() &&
OpusModernPathIsDocx(saved->second.c_str());
} }
struct OpusSdsCompat { struct OpusSdsCompat {
+17
View File
@@ -35,6 +35,7 @@ extern "C" int OpusGetWin95HorizontalPageMargins(
extern "C" int OpusAdjustWin95HorizontalMargin( extern "C" int OpusAdjustWin95HorizontalMargin(
HWND ruler, int left_margin, int delta_pixels); HWND ruler, int left_margin, int delta_pixels);
extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler); extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler);
extern "C" int OpusExportCurrentDocumentPdf(void);
namespace { namespace {
@@ -54,6 +55,7 @@ constexpr UINT kComboSize = 0x7503;
constexpr UINT kComboZoom = 0x7504; constexpr UINT kComboZoom = 0x7504;
constexpr UINT kCmdToggleStandardToolbar = 0x7101; constexpr UINT kCmdToggleStandardToolbar = 0x7101;
constexpr UINT kCmdToggleFormattingToolbar = 0x7102; constexpr UINT kCmdToggleFormattingToolbar = 0x7102;
constexpr UINT kCmdExportPdf = 0x7103;
constexpr UINT kCmdTextColorBase = 0x7200; constexpr UINT kCmdTextColorBase = 0x7200;
constexpr UINT_PTR kSyncTimer = 0x951; constexpr UINT_PTR kSyncTimer = 0x951;
constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc"; constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc";
@@ -326,6 +328,17 @@ void configure_word95_menus(HWND window) {
reinterpret_cast<UINT_PTR>(format), L"F&ormat"); reinterpret_cast<UINT_PTR>(format), L"F&ormat");
} }
} }
HMENU file_menu = file_index >= 0 ? GetSubMenu(root, file_index) : nullptr;
if (file_menu != nullptr &&
GetMenuState(file_menu, kCmdExportPdf, MF_BYCOMMAND) ==
static_cast<UINT>(-1)) {
int exit_position = find_named_in(file_menu, L"Exit");
if (exit_position < 0) exit_position = GetMenuItemCount(file_menu);
InsertMenuW(file_menu, exit_position, MF_BYPOSITION | MF_SEPARATOR,
0, nullptr);
InsertMenuW(file_menu, exit_position, MF_BYPOSITION | MF_STRING,
kCmdExportPdf, L"E&xport as PDF...");
}
const int utilities_index = find_named(L"Utilities"); const int utilities_index = find_named(L"Utilities");
if (utilities_index >= 0) { if (utilities_index >= 0) {
HMENU tools = GetSubMenu(root, utilities_index); HMENU tools = GetSubMenu(root, utilities_index);
@@ -2504,6 +2517,10 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message,
show_text_color_palette(app, toolbar); show_text_color_palette(app, toolbar);
return 0; return 0;
} }
if (command == kCmdExportPdf) {
OpusExportCurrentDocumentPdf();
return 0;
}
if (command == kCmdToggleStandardToolbar) { if (command == kCmdToggleStandardToolbar) {
toggle_toolbar_row(app, toolbar, true); toggle_toolbar_row(app, toolbar, true);
return 0; return 0;
+205 -10
View File
@@ -2,6 +2,7 @@
#include <windows.h> #include <windows.h>
#include <array> #include <array>
#include <cstring>
#include <cwchar> #include <cwchar>
#include <iostream> #include <iostream>
#include <string> #include <string>
@@ -12,10 +13,12 @@ namespace {
constexpr UINT kWmCommand = 0x0111; constexpr UINT kWmCommand = 0x0111;
constexpr WPARAM kFileNew = 1813; constexpr WPARAM kFileNew = 1813;
constexpr WPARAM kFileOpen = 1843;
constexpr WPARAM kFileSaveAs = 1897; constexpr WPARAM kFileSaveAs = 1897;
constexpr WPARAM kFileExit = 2095; constexpr WPARAM kFileExit = 2095;
constexpr WPARAM kHelpAbout = 182; constexpr WPARAM kHelpAbout = 182;
constexpr WPARAM kParaCenter = 1355; constexpr WPARAM kParaCenter = 1355;
constexpr WPARAM kExportPdf = 0x7103;
constexpr UINT kWmOpusX64QuerySelection = WM_APP + 0x351; constexpr UINT kWmOpusX64QuerySelection = WM_APP + 0x351;
constexpr int kKcControl = 0x100; constexpr int kKcControl = 0x100;
constexpr LRESULT kEditUndo = 2229; constexpr LRESULT kEditUndo = 2229;
@@ -100,6 +103,26 @@ HWND find_descendant_by_class(const HWND parent,
return nullptr; return nullptr;
} }
HWND find_visible_descendant_by_class(const HWND parent,
const wchar_t* expected_class) {
for (HWND child = GetWindow(parent, GW_CHILD); child != nullptr;
child = GetWindow(child, GW_HWNDNEXT)) {
wchar_t class_name[128] = {};
if (GetClassNameW(child, class_name,
static_cast<int>(std::size(class_name))) != 0 &&
_wcsicmp(class_name, expected_class) == 0 &&
IsWindowVisible(child)) {
return child;
}
if (const HWND descendant =
find_visible_descendant_by_class(child, expected_class);
descendant != nullptr) {
return descendant;
}
}
return nullptr;
}
void collect_descendants_by_class(const HWND parent, void collect_descendants_by_class(const HWND parent,
const wchar_t* expected_class, const wchar_t* expected_class,
std::vector<HWND>& matches) { std::vector<HWND>& matches) {
@@ -568,11 +591,12 @@ int fail(PROCESS_INFORMATION& process, const int code,
int wmain(const int argument_count, wchar_t** arguments) { int wmain(const int argument_count, wchar_t** arguments) {
SetProcessDpiAwarenessContext( SetProcessDpiAwarenessContext(
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
if (argument_count < 2 || argument_count > 3) { if (argument_count < 2 || argument_count > 4) {
std::cerr << std::cerr <<
"usage: opus_word1_ui_test WORD1.exe " "usage: opus_word1_ui_test WORD1.exe "
"[--typing|--interaction|--selection|--caret|--formatting|--color|" "[--typing|--interaction|--selection|--caret|--formatting|--color|"
"--font-typing|--clipboard|--about|--save-as]\n"; "--font-typing|--clipboard|--about|--save-as|--pdf-export|"
"--docx-open FILE]\n";
return 1; return 1;
} }
const bool typing_mode = const bool typing_mode =
@@ -604,23 +628,78 @@ int wmain(const int argument_count, wchar_t** arguments) {
const bool save_as_mode = const bool save_as_mode =
argument_count == 3 && argument_count == 3 &&
std::wcscmp(arguments[2], L"--save-as") == 0; std::wcscmp(arguments[2], L"--save-as") == 0;
const bool pdf_export_mode =
argument_count == 3 &&
std::wcscmp(arguments[2], L"--pdf-export") == 0;
const bool docx_open_mode =
argument_count == 4 &&
std::wcscmp(arguments[2], L"--docx-open") == 0;
if (argument_count == 3 && !typing_mode && !interaction_mode && if (argument_count == 3 && !typing_mode && !interaction_mode &&
!selection_mode && !caret_mode && !formatting_mode && !color_mode && !selection_mode && !caret_mode && !formatting_mode && !color_mode &&
!font_typing_mode && !clipboard_mode && !about_mode && !font_typing_mode && !clipboard_mode && !about_mode &&
!save_as_mode) { !save_as_mode && !pdf_export_mode) {
std::cerr << "unknown test mode\n"; std::cerr << "unknown test mode\n";
return 1; return 1;
} }
if (argument_count == 4 && !docx_open_mode) {
std::cerr << "unknown test mode\n";
return 1;
}
std::string docx_path;
std::string pdf_path;
if (docx_open_mode) {
const int path_size = WideCharToMultiByte(
CP_ACP, 0, arguments[3], -1, nullptr, 0, nullptr, nullptr);
if (path_size <= 1) {
std::cerr << "DOCX open test received an invalid path\n";
return 83;
}
docx_path.resize(static_cast<std::size_t>(path_size));
WideCharToMultiByte(CP_ACP, 0, arguments[3], -1, docx_path.data(),
path_size, nullptr, nullptr);
docx_path.pop_back();
if (GetFileAttributesA(docx_path.c_str()) == INVALID_FILE_ATTRIBUTES) {
std::cerr << "DOCX open test file does not exist\n";
return 83;
}
}
if (docx_open_mode) {
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH",
docx_path.c_str());
}
if (pdf_export_mode) {
char temporary_directory[MAX_PATH]{};
char temporary_seed[MAX_PATH]{};
if (GetTempPathA(static_cast<DWORD>(std::size(temporary_directory)),
temporary_directory) == 0 ||
GetTempFileNameA(temporary_directory, "OWP", 0,
temporary_seed) == 0) {
std::cerr << "PDF export test could not reserve a path\n";
return 85;
}
DeleteFileA(temporary_seed);
pdf_path = std::string(temporary_seed) + ".pdf";
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", pdf_path.c_str());
}
std::wstring command_line = L"\"" + std::wstring(arguments[1]) + L"\""; std::wstring command_line = L"\"" + std::wstring(arguments[1]) + L"\"";
STARTUPINFOW startup{}; STARTUPINFOW startup{};
startup.cb = sizeof(startup); startup.cb = sizeof(startup);
PROCESS_INFORMATION process{}; PROCESS_INFORMATION process{};
LPWCH environment = GetEnvironmentStringsW();
if (!CreateProcessW(arguments[1], command_line.data(), nullptr, nullptr, if (!CreateProcessW(arguments[1], command_line.data(), nullptr, nullptr,
FALSE, 0, nullptr, nullptr, &startup, &process)) { FALSE, CREATE_UNICODE_ENVIRONMENT, environment,
nullptr, &startup, &process)) {
if (environment != nullptr) FreeEnvironmentStringsW(environment);
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", nullptr);
std::cerr << "CreateProcessW failed: " << GetLastError() << '\n'; std::cerr << "CreateProcessW failed: " << GetLastError() << '\n';
return 2; return 2;
} }
if (environment != nullptr) FreeEnvironmentStringsW(environment);
SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", nullptr);
SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", nullptr);
const HWND main_window = wait_for_window( const HWND main_window = wait_for_window(
process.hProcess, process.dwProcessId, nullptr, process.hProcess, process.dwProcessId, nullptr,
@@ -628,13 +707,75 @@ int wmain(const int argument_count, wchar_t** arguments) {
if (main_window == nullptr) { if (main_window == nullptr) {
return fail(process, 3, "WORD1 main window did not appear"); return fail(process, 3, "WORD1 main window did not appear");
} }
if (pdf_export_mode) {
const HWND pane = find_descendant_by_class(main_window, L"OpusWwd");
DWORD ignored_process_id = 0;
const DWORD thread_id =
GetWindowThreadProcessId(main_window, &ignored_process_id);
const LRESULT initial_length = pane != nullptr ?
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) : 0;
const bool focused = pane != nullptr &&
(make_foreground_and_focus(main_window, pane, thread_id) ||
wait_for_focus(process.hProcess, thread_id, pane, 500));
bool typed = focused;
for (const wchar_t character : std::wstring(L"pdf export text")) {
typed = typed && post_keyboard_character(pane, character);
}
Sleep(750);
const bool committed = typed &&
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) >
initial_length;
if (!committed ||
!PostMessageW(main_window, kWmCommand, kExportPdf, 0)) {
std::cerr << "PDF preparation: focused=" << focused
<< " typed=" << typed << " committed=" << committed
<< " length=" << initial_length << "->"
<< (pane != nullptr ? SendMessageW(
pane, kWmOpusX64QuerySelection, 41, 0) : 0)
<< '\n';
DeleteFileA(pdf_path.c_str());
return fail(process, 85,
"PDF export test could not prepare its document");
}
const ULONGLONG deadline = GetTickCount64() + 15000;
bool pdf_written = false;
while (!pdf_written && GetTickCount64() < deadline &&
WaitForSingleObject(process.hProcess, 0) != WAIT_OBJECT_0) {
HANDLE pdf = CreateFileA(pdf_path.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (pdf != INVALID_HANDLE_VALUE) {
char header[5]{};
DWORD read = 0;
pdf_written = ReadFile(pdf, header, sizeof(header), &read,
nullptr) && read == sizeof(header) &&
std::memcmp(header, "%PDF-", sizeof(header)) == 0;
CloseHandle(pdf);
}
Sleep(100);
}
DWORD exit_code = STILL_ACTIVE;
GetExitCodeProcess(process.hProcess, &exit_code);
TerminateProcess(process.hProcess, 0);
WaitForSingleObject(process.hProcess, 2000);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
DeleteFileA(pdf_path.c_str());
if (!pdf_written) {
std::cerr << "PDF export failed or crashed: exit=0x" << std::hex
<< exit_code << std::dec << '\n';
return 86;
}
return 0;
}
if (save_as_mode) { if (save_as_mode) {
Sleep(1000); Sleep(1000);
if (!PostMessageW(main_window, kWmCommand, kFileSaveAs, 0)) { if (!PostMessageW(main_window, kWmCommand, kFileSaveAs, 0)) {
return fail(process, 69, "could not send File Save As"); return fail(process, 69, "could not send File Save As");
} }
const HWND save_as_dialog = wait_for_window( const HWND save_as_dialog = wait_for_window(
process.hProcess, process.dwProcessId, L"OpusSdmDialog", process.hProcess, process.dwProcessId, L"#32770",
L"Save As", 5000); L"Save As", 5000);
if (save_as_dialog == nullptr) { if (save_as_dialog == nullptr) {
std::cerr << "Save As stage=" std::cerr << "Save As stage="
@@ -642,17 +783,32 @@ int wmain(const int argument_count, wchar_t** arguments) {
main_window, "OpusX64SaveAsStage")) main_window, "OpusX64SaveAsStage"))
<< '\n'; << '\n';
log_process_windows(process.dwProcessId); log_process_windows(process.dwProcessId);
return fail(process, 70, "File Save As dialog did not appear"); return fail(process, 70,
"Windows 95 File Save As dialog did not appear");
} }
if (!control_has_class(save_as_dialog, 2, L"Button") || if (!control_has_class(save_as_dialog, 2, L"Button") ||
!window_is_responsive(process.hProcess, save_as_dialog)) { !window_is_responsive(process.hProcess, save_as_dialog)) {
return fail(process, 71, return fail(process, 71,
"File Save As dialog did not finish initializing"); "File Save As dialog did not finish initializing");
} }
if (!PostMessageW(save_as_dialog, kWmCommand, 2, 0) || const bool cancel_posted =
!wait_for_window_to_close(process.hProcess, process.dwProcessId, PostMessageW(save_as_dialog, kWmCommand, 2, 0) != FALSE;
L"OpusSdmDialog", 5000) || const bool dialog_closed = cancel_posted &&
!window_is_responsive(process.hProcess, main_window)) { wait_for_window_to_close(process.hProcess, process.dwProcessId,
L"#32770", 5000) &&
wait_for_window_to_close(process.hProcess, process.dwProcessId,
L"OpusSdmDialog", 5000);
const bool app_responsive = dialog_closed &&
window_is_responsive(process.hProcess, main_window);
if (!cancel_posted || !dialog_closed || !app_responsive) {
DWORD exit_code = STILL_ACTIVE;
GetExitCodeProcess(process.hProcess, &exit_code);
std::cerr << "Save As cancel state: posted=" << cancel_posted
<< " closed=" << dialog_closed
<< " responsive=" << app_responsive
<< " exit=0x" << std::hex << exit_code << std::dec
<< '\n';
log_process_windows(process.dwProcessId);
return fail(process, 72, return fail(process, 72,
"File Save As dialog did not cancel cleanly"); "File Save As dialog did not cancel cleanly");
} }
@@ -662,6 +818,45 @@ int wmain(const int argument_count, wchar_t** arguments) {
CloseHandle(process.hProcess); CloseHandle(process.hProcess);
return 0; return 0;
} }
if (docx_open_mode) {
HWND pane = find_descendant_by_class(main_window, L"OpusWwd");
if (pane == nullptr ||
!PostMessageW(main_window, kWmCommand, kFileOpen, 0)) {
return fail(process, 78, "DOCX open test could not open its document");
}
const ULONGLONG open_deadline = GetTickCount64() + 10000;
wchar_t document_caption[512]{};
do {
GetWindowTextW(main_window, document_caption,
static_cast<int>(std::size(document_caption)));
if (std::wcsstr(document_caption, L"Document1") == nullptr) {
break;
}
Sleep(100);
} while (GetTickCount64() < open_deadline &&
WaitForSingleObject(process.hProcess, 0) != WAIT_OBJECT_0);
// The legacy open command updates the caption before it has finished
// activating the replacement MDI child and document pane.
Sleep(750);
pane = find_visible_descendant_by_class(main_window, L"OpusWwd");
const bool reopened =
std::wcsstr(document_caption, L"Document1") == nullptr;
const LRESULT imported_length = pane != nullptr ?
SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) : 0;
const LRESULT displayed_lines = pane != nullptr ?
SendMessageW(pane, kWmOpusX64QuerySelection, 30, 0) : 0;
TerminateProcess(process.hProcess, 0);
WaitForSingleObject(process.hProcess, 2000);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
if (!reopened || pane == nullptr || imported_length < 100 ||
displayed_lines < 1) {
std::cerr << "DOCX open state: length=" << imported_length
<< " displayedLines=" << displayed_lines << '\n';
return 84;
}
return 0;
}
if (about_mode) { if (about_mode) {
Sleep(1000); Sleep(1000);
if (!PostMessageW(main_window, kWmCommand, kHelpAbout, 0)) { if (!PostMessageW(main_window, kWmCommand, kHelpAbout, 0)) {