From e8c9260ad74d2758d65a5e0702e14024b23aa0a6 Mon Sep 17 00:00:00 2001 From: Justin Marshall Date: Mon, 10 Aug 2026 13:41:59 -0700 Subject: [PATCH] Added docx and pdf export support. --- src/CMakeLists.txt | 28 + src/Opus/openrare.c | 9 +- src/Opus/rtfout2.c | 2 +- src/Opus/rulerdrw.c | 79 ++ src/Opus/save.c | 13 +- src/port/original/opus_modern_formats.cpp | 1128 +++++++++++++++++++++ src/port/original/opus_sdm_runtime.cpp | 107 +- src/port/original/opus_win95_chrome.cpp | 17 + src/port/original/opus_word1_ui_test.cpp | 215 +++- 9 files changed, 1568 insertions(+), 30 deletions(-) create mode 100644 src/port/original/opus_modern_formats.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f83d51f..ff4fb42 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -721,6 +721,7 @@ add_library(opus_x64_runtime STATIC port/original/opus_asm_search.cpp port/original/opus_sdm_cab.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_definitions(opus_x64_runtime PRIVATE @@ -732,6 +733,7 @@ target_compile_definitions(opus_x64_runtime PRIVATE NOMINMAX ) target_include_directories(opus_x64_runtime PRIVATE ${OPUS_ORIGINAL_INCLUDE_DIRS}) +target_link_libraries(opus_x64_runtime PUBLIC ole32 comdlg32 gdi32) if(MSVC) # Opus was built with unsigned plain characters; the byte-oriented string # and international-character assembly routines depend on that behavior. @@ -943,6 +945,27 @@ foreach(config Debug Release RelWithDebInfo MinSizeRel) ) 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 port/original/opus_word1_ui_test.cpp ) @@ -968,6 +991,7 @@ endforeach() enable_testing() add_test(NAME opus_original_strtbl_test COMMAND $) add_test(NAME opus_x64_runtime_test COMMAND $) +add_test(NAME opus_modern_formats_test COMMAND $) add_test(NAME opus_original_sttb_test COMMAND $) add_test(NAME opus_original_plc_test COMMAND $) add_test(NAME opus_sdm_cab_test COMMAND $) @@ -1005,3 +1029,7 @@ add_test(NAME opus_word1_save_as_test COMMAND $ $ --save-as ) set_tests_properties(opus_word1_save_as_test PROPERTIES TIMEOUT 20) +add_test(NAME opus_word1_pdf_export_test + COMMAND $ $ --pdf-export +) +set_tests_properties(opus_word1_pdf_export_test PROPERTIES TIMEOUT 25) diff --git a/src/Opus/openrare.c b/src/Opus/openrare.c index 94c897d..cfcbd28 100644 --- a/src/Opus/openrare.c +++ b/src/Opus/openrare.c @@ -83,6 +83,9 @@ extern int vfConversion; extern MUD ** vhmudUser; extern KMP ** hkmpCur; extern KMP ** vhkmpUser; +#ifdef OPUS_X64 +extern int OpusWin95OpenAliasIsDocx(); +#endif struct DTTM DttmCur(); @@ -484,7 +487,11 @@ CHAR *stConverter, **pstSubset; dff = dffOpenText; /* dff is our best guess of format */ - if (fInput) + if (fInput +#ifdef OPUS_X64 + && !OpusWin95OpenAliasIsDocx(PfcbFn(fn)->stFile) +#endif + ) /* verify the format */ if ((dff = DffVerifyDff (dff, fn)) == dffNil) /* cancel operation */ diff --git a/src/Opus/rtfout2.c b/src/Opus/rtfout2.c index ee090e7..43691ba 100644 --- a/src/Opus/rtfout2.c +++ b/src/Opus/rtfout2.c @@ -833,7 +833,7 @@ int doc; */ /* %%Function: HWriteRTF %%Owner: bobz */ -HWriteRTF (doc, cpFirst, cpLim, cbInitial) +HANDLE HWriteRTF (doc, cpFirst, cpLim, cbInitial) int doc; CP cpFirst, cpLim; int cbInitial; diff --git a/src/Opus/rulerdrw.c b/src/Opus/rulerdrw.c index b7c2d6d..dc87a38 100644 --- a/src/Opus/rulerdrw.c +++ b/src/Opus/rulerdrw.c @@ -103,6 +103,85 @@ static int vWin95ZoomPercent = 100; static int vWin95BaseDxsInch = 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 supplies a palette instead, but still applies the original character property so document storage and formatting behavior remain unchanged. */ diff --git a/src/Opus/save.c b/src/Opus/save.c index 270d092..99ce772 100644 --- a/src/Opus/save.c +++ b/src/Opus/save.c @@ -89,6 +89,7 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */ #ifdef OPUS_X64 extern int OpusWin95SaveAliasMatches(); extern int OpusFinishWin95SaveAlias(); +extern int OpusWin95SaveAliasRequiresRtf(); #define FWin95SaveAliasMatches(st) OpusWin95SaveAliasMatches(st) #else #define FWin95SaveAliasMatches(st) fFalse @@ -661,7 +662,16 @@ LSaveAs: /* perform the actual save */ 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 */)) { /* display number of characters written in prompt */ @@ -682,6 +692,7 @@ LSaveAs: cmd = cmdError; StopProgressReport(hNil, pdcRestoreImmed); } + } vhpprSave = hNil; diff --git a/src/port/original/opus_modern_formats.cpp b/src/port/original/opus_modern_formats.cpp new file mode 100644 index 0000000..22555ed --- /dev/null +++ b/src/port/original/opus_modern_formats.cpp @@ -0,0 +1,1128 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +namespace { + +struct ComApartment { + HRESULT result = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + ~ComApartment() { + if (result == S_OK || result == S_FALSE) { + CoUninitialize(); + } + } + bool usable() const { + return SUCCEEDED(result) || result == RPC_E_CHANGED_MODE; + } +}; + +std::wstring wide_path(const char* path) { + if (path == nullptr || *path == '\0') { + return {}; + } + const int count = MultiByteToWideChar(CP_ACP, 0, path, -1, nullptr, 0); + std::wstring result(count > 0 ? static_cast(count) : 0, + L'\0'); + if (count > 1) { + MultiByteToWideChar(CP_ACP, 0, path, -1, result.data(), count); + result.resize(static_cast(count - 1)); + } + return result; +} + +std::wstring utf8_to_wide(std::string_view value) { + if (value.empty()) { + return {}; + } + const int count = MultiByteToWideChar(CP_UTF8, 0, value.data(), + static_cast(value.size()), + nullptr, 0); + std::wstring result(count > 0 ? static_cast(count) : 0, + L'\0'); + if (count > 0) { + MultiByteToWideChar(CP_UTF8, 0, value.data(), + static_cast(value.size()), result.data(), + count); + } + return result; +} + +std::string wide_to_utf8(std::wstring_view value) { + if (value.empty()) { + return {}; + } + const int count = WideCharToMultiByte(CP_UTF8, 0, value.data(), + static_cast(value.size()), + nullptr, 0, nullptr, nullptr); + std::string result(count > 0 ? static_cast(count) : 0, '\0'); + if (count > 0) { + WideCharToMultiByte(CP_UTF8, 0, value.data(), + static_cast(value.size()), result.data(), + count, nullptr, nullptr); + } + return result; +} + +std::string wide_to_ansi(std::wstring_view value) { + if (value.empty()) return {}; + const int count = WideCharToMultiByte( + 1252, WC_NO_BEST_FIT_CHARS, value.data(), + static_cast(value.size()), nullptr, 0, "?", nullptr); + std::string result(count > 0 ? static_cast(count) : 0, '\0'); + if (count > 0) { + WideCharToMultiByte(1252, WC_NO_BEST_FIT_CHARS, value.data(), + static_cast(value.size()), result.data(), + count, "?", nullptr); + } + return result; +} + +std::string xml_escape(std::wstring_view text) { + std::string result; + for (const wchar_t character : text) { + switch (character) { + case L'&': result += "&"; break; + case L'<': result += "<"; break; + case L'>': result += ">"; break; + case L'\"': result += """; break; + case L'\'': result += "'"; break; + default: result += wide_to_utf8(std::wstring_view(&character, 1)); + } + } + return result; +} + +std::wstring xml_unescape(std::string_view text) { + std::string decoded; + for (std::size_t position = 0; position < text.size();) { + if (text[position] != '&') { + decoded.push_back(text[position++]); + continue; + } + const std::size_t end = text.find(';', position + 1); + if (end == std::string_view::npos) { + decoded.push_back(text[position++]); + continue; + } + const std::string entity(text.substr(position + 1, end - position - 1)); + if (entity == "amp") decoded.push_back('&'); + else if (entity == "lt") decoded.push_back('<'); + else if (entity == "gt") decoded.push_back('>'); + else if (entity == "quot") decoded.push_back('"'); + else if (entity == "apos") decoded.push_back('\''); + else if (!entity.empty() && entity[0] == '#') { + unsigned long code = 0; + char* parse_end = nullptr; + const char* digits = entity.c_str() + 1; + int radix = 10; + if (entity.size() > 2 && (entity[1] == 'x' || entity[1] == 'X')) { + digits = entity.c_str() + 2; + radix = 16; + } + code = std::strtoul(digits, &parse_end, radix); + if (parse_end != digits && *parse_end == '\0' && + code <= 0x10ffff && !(code >= 0xd800 && code <= 0xdfff)) { + if (code <= 0xffff) { + const wchar_t scalar = static_cast(code); + decoded += wide_to_utf8(std::wstring_view(&scalar, 1)); + } else { + const unsigned long scalar = code - 0x10000; + const wchar_t pair[2] = { + static_cast(0xd800 + (scalar >> 10)), + static_cast(0xdc00 + (scalar & 0x3ff))}; + decoded += wide_to_utf8(std::wstring_view(pair, 2)); + } + } + } else { + decoded.append(text.substr(position, end - position + 1)); + } + position = end + 1; + } + return utf8_to_wide(decoded); +} + +bool has_extension(const std::string& path, const char* extension) { + const std::size_t length = std::strlen(extension); + return path.size() >= length && + _stricmp(path.c_str() + path.size() - length, extension) == 0; +} + +bool read_stream(IStream* stream, std::string& data) { + STATSTG status{}; + if (stream == nullptr || FAILED(stream->Stat(&status, STATFLAG_NONAME)) || + status.cbSize.QuadPart < 0 || + status.cbSize.QuadPart > static_cast(256 * 1024 * 1024)) { + return false; + } + data.resize(static_cast(status.cbSize.QuadPart)); + LARGE_INTEGER zero{}; + stream->Seek(zero, STREAM_SEEK_SET, nullptr); + ULONG read = 0; + return data.empty() || + (SUCCEEDED(stream->Read(data.data(), static_cast(data.size()), + &read)) && read == data.size()); +} + +bool read_opc_part(const std::wstring& path, const wchar_t* part_name, + std::string& data) { + ComApartment apartment; + if (!apartment.usable()) return false; + ComPtr factory; + ComPtr file; + ComPtr package; + ComPtr parts; + ComPtr uri; + ComPtr part; + ComPtr content; + return SUCCEEDED(CoCreateInstance(__uuidof(OpcFactory), nullptr, + CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&factory))) && + SUCCEEDED(factory->CreateStreamOnFile(path.c_str(), + OPC_STREAM_IO_READ, nullptr, + FILE_ATTRIBUTE_NORMAL, + &file)) && + SUCCEEDED(factory->ReadPackageFromStream(file.Get(), + OPC_READ_DEFAULT, + &package)) && + SUCCEEDED(package->GetPartSet(&parts)) && + SUCCEEDED(factory->CreatePartUri(part_name, &uri)) && + SUCCEEDED(parts->GetPart(uri.Get(), &part)) && + SUCCEEDED(part->GetContentStream(&content)) && + read_stream(content.Get(), data); +} + +std::string tag_attribute(std::string_view tag, std::string_view name) { + std::size_t position = 0; + while ((position = tag.find(name, position)) != std::string_view::npos) { + const bool boundary = position == 0 || + std::isspace(static_cast(tag[position - 1])) || + tag[position - 1] == ':'; + std::size_t equals = position + name.size(); + while (equals < tag.size() && std::isspace( + static_cast(tag[equals]))) ++equals; + if (!boundary || equals >= tag.size() || tag[equals] != '=') { + position += name.size(); + continue; + } + ++equals; + while (equals < tag.size() && std::isspace( + static_cast(tag[equals]))) ++equals; + if (equals >= tag.size() || (tag[equals] != '\'' && tag[equals] != '"')) + return {}; + const char quote = tag[equals++]; + const std::size_t end = tag.find(quote, equals); + return end == std::string_view::npos ? std::string{} : + std::string(tag.substr(equals, end - equals)); + } + return {}; +} + +std::string local_tag_name(std::string_view tag) { + std::size_t start = 0; + while (start < tag.size() && (tag[start] == '<' || tag[start] == '/' || + std::isspace(static_cast(tag[start])))) ++start; + std::size_t end = start; + while (end < tag.size() && !std::isspace(static_cast(tag[end])) && + tag[end] != '/' && tag[end] != '>') ++end; + const std::size_t colon = tag.substr(start, end - start).rfind(':'); + if (colon != std::string_view::npos) start += colon + 1; + return std::string(tag.substr(start, end - start)); +} + +struct RunStyle { + bool bold = false; + bool italic = false; + bool underline = false; + int half_points = 20; + COLORREF color = RGB(0, 0, 0); + bool auto_color = true; + std::wstring font = L"Arial"; + bool operator==(const RunStyle&) const = default; +}; + +struct TextRun { RunStyle style; std::wstring text; }; +struct Paragraph { int alignment = PFA_LEFT; std::vector runs; }; + +bool property_enabled(std::string_view properties, const char* local_name) { + std::size_t position = 0; + while ((position = properties.find('<', position)) != std::string_view::npos) { + const std::size_t end = properties.find('>', position + 1); + if (end == std::string_view::npos) break; + const std::string_view tag = properties.substr(position, end - position + 1); + if (local_tag_name(tag) == local_name) { + const std::string value = tag_attribute(tag, "val"); + return value.empty() || (value != "0" && value != "false" && + value != "none"); + } + position = end + 1; + } + return false; +} + +std::string first_property_value(std::string_view properties, + const char* local_name, + const char* attribute = "val") { + std::size_t position = 0; + while ((position = properties.find('<', position)) != std::string_view::npos) { + const std::size_t end = properties.find('>', position + 1); + if (end == std::string_view::npos) break; + const std::string_view tag = properties.substr(position, end - position + 1); + if (local_tag_name(tag) == local_name) return tag_attribute(tag, attribute); + position = end + 1; + } + return {}; +} + +RunStyle parse_run_style(std::string_view run) { + RunStyle style; + const std::size_t start = run.find("", start); + if (start == std::string_view::npos || end == std::string_view::npos) return style; + const std::string_view props = run.substr(start, end + 8 - start); + style.bold = property_enabled(props, "b"); + style.italic = property_enabled(props, "i"); + style.underline = property_enabled(props, "u"); + if (const std::string size = first_property_value(props, "sz"); !size.empty()) { + style.half_points = (std::max)(2, std::atoi(size.c_str())); + } + if (const std::string font = first_property_value(props, "rFonts", "ascii"); + !font.empty()) { + style.font = xml_unescape(font); + } + if (const std::string color = first_property_value(props, "color"); + !color.empty() && color != "auto" && color.size() == 6) { + const unsigned rgb = std::strtoul(color.c_str(), nullptr, 16); + style.color = RGB((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + style.auto_color = false; + } + return style; +} + +std::wstring parse_run_text(std::string_view run) { + std::wstring text; + std::size_t position = 0; + while ((position = run.find('<', position)) != std::string_view::npos) { + const std::size_t tag_end = run.find('>', position + 1); + if (tag_end == std::string_view::npos) break; + const std::string_view tag = run.substr(position, tag_end - position + 1); + const std::string name = local_tag_name(tag); + if (name == "t") { + const std::size_t close = run.find("", tag_end + 1); + if (close == std::string_view::npos) break; + text += xml_unescape(run.substr(tag_end + 1, close - tag_end - 1)); + position = close + 6; + } else { + if (name == "tab") text.push_back(L'\t'); + else if (name == "br" || name == "cr") text.push_back(L'\n'); + position = tag_end + 1; + } + } + return text; +} + +std::vector parse_document_xml(std::string_view xml) { + std::vector paragraphs; + std::size_t position = 0; + while ((position = xml.find("(next))) { + position += 4; + continue; + } + const std::size_t open_end = xml.find('>', position); + const std::size_t close = xml.find("", open_end); + if (open_end == std::string_view::npos || close == std::string_view::npos) break; + const std::string_view block = xml.substr(open_end + 1, close - open_end - 1); + Paragraph paragraph; + const std::string alignment = first_property_value(block, "jc"); + if (alignment == "center") paragraph.alignment = PFA_CENTER; + else if (alignment == "right") paragraph.alignment = PFA_RIGHT; + else if (alignment == "both" || alignment == "distribute") + paragraph.alignment = PFA_JUSTIFY; + + std::size_t run_position = 0; + while ((run_position = block.find("(run_next))) { + run_position += 4; + continue; + } + const std::size_t run_open_end = block.find('>', run_position); + const std::size_t run_close = block.find("", run_open_end); + if (run_open_end == std::string_view::npos || run_close == std::string_view::npos) + break; + const std::string_view run = block.substr( + run_position, run_close + 6 - run_position); + std::wstring text = parse_run_text(run); + if (!text.empty()) paragraph.runs.push_back({parse_run_style(run), std::move(text)}); + run_position = run_close + 6; + } + paragraphs.push_back(std::move(paragraph)); + position = close + 6; + } + if (paragraphs.empty()) paragraphs.push_back({}); + return paragraphs; +} + +std::string paragraphs_to_text(const std::vector& paragraphs) { + std::string text; + for (std::size_t index = 0; index < paragraphs.size(); ++index) { + for (const auto& run : paragraphs[index].runs) { + text += wide_to_ansi(run.text); + } + if (index + 1 < paragraphs.size()) text += "\r\n"; + } + return text; +} + +std::string ansi_text_to_rtf(std::string_view text) { + std::string rtf = + "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Arial;}}\\f0\\fs24 "; + static constexpr char hex[] = "0123456789abcdef"; + for (std::size_t index = 0; index < text.size(); ++index) { + const unsigned char character = + static_cast(text[index]); + if (character == '\r' || character == '\n') { + if (character == '\r' && index + 1 < text.size() && + text[index + 1] == '\n') { + ++index; + } + rtf += "\\par "; + } else if (character == '\t') { + rtf += "\\tab "; + } else if (character == '\f') { + rtf += "\\page "; + } else if (character == '\\' || character == '{' || + character == '}') { + rtf.push_back('\\'); + rtf.push_back(static_cast(character)); + } else if (character >= 0x80) { + rtf += "\\'"; + rtf.push_back(hex[character >> 4]); + rtf.push_back(hex[character & 0x0f]); + } else if (character >= 0x20) { + rtf.push_back(static_cast(character)); + } + } + rtf.push_back('}'); + return rtf; +} + +void append_rtf_text(std::string& rtf, std::wstring_view text) { + for (const wchar_t character : text) { + if (character == L'\\' || character == L'{' || character == L'}') { + rtf.push_back('\\'); + rtf.push_back(static_cast(character)); + } else if (character == L'\t') rtf += "\\tab "; + else if (character == L'\n') rtf += "\\line "; + else if (character >= 0x20 && character <= 0x7e) { + rtf.push_back(static_cast(character)); + } else { + char encoded = '?'; + WideCharToMultiByte(1252, WC_NO_BEST_FIT_CHARS, &character, 1, + &encoded, 1, "?", nullptr); + static constexpr char hex[] = "0123456789abcdef"; + const unsigned byte = static_cast(encoded); + rtf += "\\'"; + rtf.push_back(hex[byte >> 4]); + rtf.push_back(hex[byte & 0x0f]); + } + } +} + +std::string paragraphs_to_rtf(const std::vector& paragraphs) { + std::vector fonts{L"Arial"}; + std::vector colors; + for (const auto& paragraph : paragraphs) for (const auto& run : paragraph.runs) { + if (std::find(fonts.begin(), fonts.end(), run.style.font) == fonts.end()) + fonts.push_back(run.style.font); + if (!run.style.auto_color && + std::find(colors.begin(), colors.end(), run.style.color) == colors.end()) + colors.push_back(run.style.color); + } + std::string rtf = "{\\rtf1\\ansi \\deff0"; + rtf += "{\\fonttbl"; + for (std::size_t index = 0; index < fonts.size(); ++index) { + rtf += "{\\f" + std::to_string(index) + "\\fnil "; + append_rtf_text(rtf, fonts[index]); + rtf += ";}"; + } + rtf += "}{\\colortbl;"; + for (const COLORREF color : colors) { + rtf += "\\red" + std::to_string(GetRValue(color)) + + "\\green" + std::to_string(GetGValue(color)) + + "\\blue" + std::to_string(GetBValue(color)) + ";"; + } + rtf += "}"; + for (const auto& paragraph : paragraphs) { + rtf += "\\pard"; + if (paragraph.alignment == PFA_CENTER) rtf += "\\qc"; + else if (paragraph.alignment == PFA_RIGHT) rtf += "\\qr"; + else if (paragraph.alignment == PFA_JUSTIFY) rtf += "\\qj"; + else rtf += "\\ql"; + rtf.push_back(' '); + for (const auto& run : paragraph.runs) { + const auto font = std::find(fonts.begin(), fonts.end(), run.style.font); + const auto color = std::find(colors.begin(), colors.end(), run.style.color); + rtf += "{"; + rtf += run.style.bold ? "\\b" : "\\b0"; + rtf += run.style.italic ? "\\i" : "\\i0"; + rtf += run.style.underline ? "\\ul" : "\\ulnone"; + rtf += "\\fs" + std::to_string(run.style.half_points); + rtf += "\\f" + std::to_string(std::distance(fonts.begin(), font)); + if (!run.style.auto_color) + rtf += "\\cf" + std::to_string(std::distance(colors.begin(), color) + 1); + rtf.push_back(' '); + append_rtf_text(rtf, run.text); + rtf += "}"; + } + rtf += "\\par\n"; + } + rtf += "}"; + return rtf; +} + +bool write_bytes(const std::wstring& path, std::string_view bytes) { + HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) return false; + DWORD written = 0; + const bool ok = bytes.size() <= MAXDWORD && + WriteFile(file, bytes.data(), static_cast(bytes.size()), + &written, nullptr) && written == bytes.size(); + CloseHandle(file); + if (!ok) DeleteFileW(path.c_str()); + return ok; +} + +bool read_bytes(const std::wstring& path, std::string& bytes) { + HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, + nullptr); + if (file == INVALID_HANDLE_VALUE) return false; + LARGE_INTEGER size{}; + bool ok = GetFileSizeEx(file, &size) && size.QuadPart >= 0 && + size.QuadPart <= 256 * 1024 * 1024; + if (ok) { + bytes.resize(static_cast(size.QuadPart)); + DWORD read = 0; + ok = bytes.empty() || (ReadFile(file, bytes.data(), + static_cast(bytes.size()), &read, nullptr) && read == bytes.size()); + } + CloseHandle(file); + return ok; +} + +struct StreamCookie { const char* data; LONG length; LONG position; }; +DWORD CALLBACK rich_edit_stream_in(DWORD_PTR cookie, LPBYTE buffer, + LONG requested, LONG* copied) { + auto& source = *reinterpret_cast(cookie); + *copied = (std::min)(requested, source.length - source.position); + if (*copied > 0) { + std::memcpy(buffer, source.data + source.position, *copied); + source.position += *copied; + } + return 0; +} + +class RichEditDocument { +public: + bool load(std::string_view rtf) { + module_ = LoadLibraryW(L"Msftedit.dll"); + if (module_ == nullptr) return false; + window_ = CreateWindowExW(0, MSFTEDIT_CLASS, L"", WS_POPUP | ES_MULTILINE, + 0, 0, 100, 100, nullptr, nullptr, + GetModuleHandleW(nullptr), nullptr); + if (window_ == nullptr) return false; + SendMessageW(window_, EM_EXLIMITTEXT, 0, 0x7ffffffe); + StreamCookie cookie{rtf.data(), static_cast(rtf.size()), 0}; + EDITSTREAM stream{reinterpret_cast(&cookie), 0, + rich_edit_stream_in}; + SendMessageW(window_, EM_STREAMIN, SF_RTF, + reinterpret_cast(&stream)); + return stream.dwError == 0; + } + ~RichEditDocument() { + if (window_ != nullptr) DestroyWindow(window_); + if (module_ != nullptr) FreeLibrary(module_); + } + HWND window() const { return window_; } + std::wstring text() const { + const int length = GetWindowTextLengthW(window_); + std::wstring value(length > 0 ? static_cast(length + 1) : 0, + L'\0'); + if (length > 0) { + GetWindowTextW(window_, value.data(), length + 1); + value.resize(static_cast(length)); + } + return value; + } +private: + HMODULE module_ = nullptr; + HWND window_ = nullptr; +}; + +RunStyle rich_style_at(HWND rich, LONG position) { + CHARRANGE range{position, position + 1}; + SendMessageW(rich, EM_EXSETSEL, 0, reinterpret_cast(&range)); + CHARFORMAT2W format{}; + format.cbSize = sizeof(format); + SendMessageW(rich, EM_GETCHARFORMAT, SCF_SELECTION, + reinterpret_cast(&format)); + RunStyle style; + style.bold = (format.dwEffects & CFE_BOLD) != 0; + style.italic = (format.dwEffects & CFE_ITALIC) != 0; + style.underline = (format.dwEffects & CFE_UNDERLINE) != 0; + style.half_points = format.yHeight > 0 ? + (std::max)(2L, format.yHeight / 10) : 20; + style.auto_color = (format.dwEffects & CFE_AUTOCOLOR) != 0; + style.color = format.crTextColor; + if (format.szFaceName[0] != L'\0') style.font = format.szFaceName; + return style; +} + +int rich_alignment_at(HWND rich, LONG position) { + CHARRANGE range{position, position}; + SendMessageW(rich, EM_EXSETSEL, 0, reinterpret_cast(&range)); + PARAFORMAT2 format{}; + format.cbSize = sizeof(format); + format.dwMask = PFM_ALIGNMENT; + SendMessageW(rich, EM_GETPARAFORMAT, 0, reinterpret_cast(&format)); + return format.wAlignment; +} + +std::vector paragraphs_from_rich_edit(RichEditDocument& rich) { + const std::wstring text = rich.text(); + std::vector paragraphs; + LONG paragraph_start = 0; + while (paragraph_start <= static_cast(text.size())) { + LONG paragraph_end = paragraph_start; + while (paragraph_end < static_cast(text.size()) && + text[paragraph_end] != L'\r' && text[paragraph_end] != L'\n') + ++paragraph_end; + Paragraph paragraph; + paragraph.alignment = rich_alignment_at(rich.window(), paragraph_start); + LONG run_start = paragraph_start; + while (run_start < paragraph_end) { + RunStyle style = rich_style_at(rich.window(), run_start); + LONG run_end = run_start + 1; + while (run_end < paragraph_end && + rich_style_at(rich.window(), run_end) == style) ++run_end; + paragraph.runs.push_back({style, text.substr( + static_cast(run_start), + static_cast(run_end - run_start))}); + run_start = run_end; + } + paragraphs.push_back(std::move(paragraph)); + if (paragraph_end >= static_cast(text.size())) break; + paragraph_start = paragraph_end + 1; + if (paragraph_start < static_cast(text.size()) && + text[paragraph_end] == L'\r' && text[paragraph_start] == L'\n') + ++paragraph_start; + } + return paragraphs; +} + +std::string document_xml(const std::vector& paragraphs) { + std::string xml = + "" + "" + ""; + for (const auto& paragraph : paragraphs) { + xml += ""; + if (paragraph.alignment != PFA_LEFT) { + const char* value = paragraph.alignment == PFA_CENTER ? "center" : + paragraph.alignment == PFA_RIGHT ? "right" : "both"; + xml += std::string(""; + } + for (const auto& run : paragraph.runs) { + xml += ""; + if (run.style.bold) xml += ""; + if (run.style.italic) xml += ""; + if (run.style.underline) xml += ""; + xml += ""; + xml += ""; + if (!run.style.auto_color) { + char color[7]{}; + wsprintfA(color, "%02X%02X%02X", GetRValue(run.style.color), + GetGValue(run.style.color), GetBValue(run.style.color)); + xml += std::string(""; + } + xml += "" + + xml_escape(run.text) + ""; + } + xml += ""; + } + xml += "" + "" + ""; + return xml; +} + +bool add_part(IOpcFactory* factory, IOpcPartSet* parts, const wchar_t* name, + const wchar_t* content_type, std::string_view data, + IOpcPart** created = nullptr) { + ComPtr uri; + ComPtr part; + ComPtr stream; + ULONG written = 0; + if (FAILED(factory->CreatePartUri(name, &uri)) || + FAILED(parts->CreatePart(uri.Get(), content_type, + OPC_COMPRESSION_NORMAL, &part)) || + FAILED(part->GetContentStream(&stream)) || + (!data.empty() && (FAILED(stream->Write(data.data(), + static_cast(data.size()), &written)) || written != data.size()))) + return false; + if (created != nullptr) *created = part.Detach(); + return true; +} + +bool add_relationship(IOpcRelationshipSet* set, IOpcFactory* factory, + const wchar_t* target, const wchar_t* type) { + ComPtr uri; + ComPtr relationship; + return SUCCEEDED(factory->CreatePartUri(target, &uri)) && + SUCCEEDED(set->CreateRelationship(nullptr, type, uri.Get(), + OPC_URI_TARGET_MODE_INTERNAL, + &relationship)); +} + +bool write_docx(const std::wstring& path, const std::vector& paragraphs) { + ComApartment apartment; + if (!apartment.usable()) return false; + ComPtr factory; + ComPtr package; + ComPtr parts; + ComPtr main_part; + ComPtr package_relationships; + ComPtr document_relationships; + ComPtr output; + if (FAILED(CoCreateInstance(__uuidof(OpcFactory), nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&factory))) || + FAILED(factory->CreatePackage(&package)) || + FAILED(package->GetPartSet(&parts)) || + !add_part(factory.Get(), parts.Get(), L"/word/document.xml", + L"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + document_xml(paragraphs), &main_part)) return false; + + const std::string styles = + "" + "" + "" + ""; + const std::string settings = + "" + ""; + const std::string core = + "" + "Microsoft Word" + "Document"; + const std::string app = + "" + "" + "Microsoft Word"; + if (!add_part(factory.Get(), parts.Get(), L"/word/styles.xml", + L"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", styles) || + !add_part(factory.Get(), parts.Get(), L"/word/settings.xml", + L"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", settings) || + !add_part(factory.Get(), parts.Get(), L"/docProps/core.xml", + L"application/vnd.openxmlformats-package.core-properties+xml", core) || + !add_part(factory.Get(), parts.Get(), L"/docProps/app.xml", + L"application/vnd.openxmlformats-officedocument.extended-properties+xml", app) || + FAILED(package->GetRelationshipSet(&package_relationships)) || + !add_relationship(package_relationships.Get(), factory.Get(), + L"/word/document.xml", + L"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument") || + !add_relationship(package_relationships.Get(), factory.Get(), + L"/docProps/core.xml", + L"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties") || + !add_relationship(package_relationships.Get(), factory.Get(), + L"/docProps/app.xml", + L"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties") || + FAILED(main_part->GetRelationshipSet(&document_relationships)) || + !add_relationship(document_relationships.Get(), factory.Get(), + L"/word/styles.xml", + L"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles") || + !add_relationship(document_relationships.Get(), factory.Get(), + L"/word/settings.xml", + L"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings")) + return false; + DeleteFileW(path.c_str()); + return SUCCEEDED(factory->CreateStreamOnFile(path.c_str(), OPC_STREAM_IO_WRITE, + nullptr, FILE_ATTRIBUTE_NORMAL, + &output)) && + SUCCEEDED(factory->WritePackageToStream(package.Get(), + OPC_WRITE_DEFAULT, + output.Get())); +} + +bool rtf_to_docx(const std::wstring& rtf_path, const std::wstring& docx_path) { + std::string rtf; + RichEditDocument rich; + return read_bytes(rtf_path, rtf) && rich.load(rtf) && + write_docx(docx_path, paragraphs_from_rich_edit(rich)); +} + +struct PdfLineRun { + RunStyle style; + std::wstring text; + double width = 0.0; +}; + +struct PdfLine { + std::vector runs; + double width = 0.0; + double largest_font = 12.0; + int alignment = PFA_LEFT; +}; + +std::string pdf_number(const double value) { + char buffer[64]{}; + std::snprintf(buffer, std::size(buffer), "%.3f", value); + std::string result = buffer; + while (result.size() > 1 && result.back() == '0') result.pop_back(); + if (!result.empty() && result.back() == '.') result.pop_back(); + return result; +} + +double pdf_font_size(const RunStyle& style) { + return (std::max)(4.0, static_cast(style.half_points) / 2.0); +} + +double pdf_character_width(const wchar_t character, const RunStyle& style) { + double em = 0.556; + if (character == L' ') em = 0.278; + else if (character == L'i' || character == L'l' || character == L'I' || + character == L'.' || character == L',' || character == L'!' || + character == L'\'' || character == L'`' || character == L'|' || + character == L':' || character == L';') em = 0.278; + else if (character == L'm' || character == L'w' || character == L'M' || + character == L'W' || character == L'@' || character == L'%' || + character == L'&') em = 0.889; + else if (character >= L'A' && character <= L'Z') em = 0.667; + else if (character >= L'a' && character <= L'z') em = 0.500; + else if (character == L'(' || character == L')' || character == L'[' || + character == L']' || character == L'{' || character == L'}') + em = 0.333; + if (style.bold) em *= 1.03; + return em * pdf_font_size(style); +} + +std::wstring lower_ascii(std::wstring value) { + for (wchar_t& character : value) { + if (character >= L'A' && character <= L'Z') character += L'a' - L'A'; + } + return value; +} + +int pdf_font_resource(const RunStyle& style) { + const std::wstring font = lower_ascii(style.font); + int family = 0; + if (font.find(L"times") != std::wstring::npos || + font.find(L"roman") != std::wstring::npos || + font.find(L"serif") != std::wstring::npos) { + family = 1; + } else if (font.find(L"courier") != std::wstring::npos || + font.find(L"mono") != std::wstring::npos) { + family = 2; + } + const int face = (style.bold ? 1 : 0) + (style.italic ? 2 : 0); + return family * 4 + face + 1; +} + +std::string pdf_encoded_text(std::wstring_view text) { + std::string encoded; + for (const wchar_t character : text) { + char byte = '?'; + BOOL used_default = FALSE; + if (character >= 0x20) { + WideCharToMultiByte(1252, WC_NO_BEST_FIT_CHARS, &character, 1, + &byte, 1, "?", &used_default); + } else { + continue; + } + if (byte == '\\' || byte == '(' || byte == ')') encoded.push_back('\\'); + encoded.push_back(byte); + } + return encoded; +} + +void append_pdf_line(std::string& content, const PdfLine& line, + const double baseline) { + constexpr double left = 72.0; + constexpr double writable_width = 468.0; + double x = left; + if (line.alignment == PFA_CENTER) { + x += ((std::max)(0.0, writable_width - line.width)) / 2.0; + } else if (line.alignment == PFA_RIGHT) { + x += (std::max)(0.0, writable_width - line.width); + } + std::string underlines; + content += "BT\n1 0 0 1 " + pdf_number(x) + " " + + pdf_number(baseline) + " Tm\n"; + for (const PdfLineRun& run : line.runs) { + const double font_size = pdf_font_size(run.style); + const COLORREF color = run.style.auto_color ? RGB(0, 0, 0) : + run.style.color; + const double red = static_cast(GetRValue(color)) / 255.0; + const double green = static_cast(GetGValue(color)) / 255.0; + const double blue = static_cast(GetBValue(color)) / 255.0; + content += "/F" + std::to_string(pdf_font_resource(run.style)) + " " + + pdf_number(font_size) + " Tf\n"; + content += pdf_number(red) + " " + pdf_number(green) + " " + + pdf_number(blue) + " rg\n(" + + pdf_encoded_text(run.text) + ") Tj\n"; + if (run.style.underline && run.width > 0.0) { + const double underline_y = baseline - (std::max)(1.0, font_size / 12.0); + underlines += "q\n" + pdf_number(red) + " " + pdf_number(green) + + " " + pdf_number(blue) + " RG\n" + + pdf_number((std::max)(0.5, font_size / 18.0)) + + " w\n" + pdf_number(x) + " " + + pdf_number(underline_y) + " m\n" + + pdf_number(x + run.width) + " " + + pdf_number(underline_y) + " l\nS\nQ\n"; + } + x += run.width; + } + content += "ET\n" + underlines; +} + +std::vector pdf_page_contents( + const std::vector& paragraphs) { + constexpr double top = 720.0; + constexpr double bottom = 72.0; + constexpr double writable_width = 468.0; + std::vector pages(1); + double cursor = top; + PdfLine line; + + const auto begin_new_page = [&]() { + pages.emplace_back(); + cursor = top; + }; + const auto emit_line = [&](const bool force) { + if (!force && line.runs.empty()) return; + const double line_height = (std::max)(12.0, line.largest_font * 1.20); + if (cursor - line_height < bottom) begin_new_page(); + const double baseline = cursor - line.largest_font; + append_pdf_line(pages.back(), line, baseline); + cursor -= line_height; + const int alignment = line.alignment; + line = PdfLine{}; + line.alignment = alignment; + }; + const auto add_character = [&](const wchar_t character, + const RunStyle& style) { + const double width = pdf_character_width(character, style); + if (!line.runs.empty() && line.width + width > writable_width) { + emit_line(false); + if (character == L' ') return; + } + if (line.runs.empty() || !(line.runs.back().style == style)) { + line.runs.push_back({style, {}, 0.0}); + } + line.runs.back().text.push_back(character); + line.runs.back().width += width; + line.width += width; + line.largest_font = (std::max)(line.largest_font, pdf_font_size(style)); + }; + + for (const Paragraph& paragraph : paragraphs) { + line.alignment = paragraph.alignment; + for (const TextRun& run : paragraph.runs) { + for (const wchar_t character : run.text) { + if (character == L'\f') { + emit_line(false); + begin_new_page(); + } else if (character == L'\r' || character == L'\n') { + emit_line(true); + } else if (character == L'\t') { + for (int index = 0; index < 4; ++index) + add_character(L' ', run.style); + } else if (character >= 0x20) { + add_character(character, run.style); + } + } + } + emit_line(true); + } + if (pages.empty()) pages.emplace_back(); + return pages; +} + +bool write_pdf(const std::wstring& path, + const std::vector& paragraphs) { + static constexpr std::array font_names = { + "Helvetica", "Helvetica-Bold", "Helvetica-Oblique", + "Helvetica-BoldOblique", "Times-Roman", "Times-Bold", + "Times-Italic", "Times-BoldItalic", "Courier", "Courier-Bold", + "Courier-Oblique", "Courier-BoldOblique"}; + const std::vector contents = pdf_page_contents(paragraphs); + constexpr int catalog_object = 1; + constexpr int pages_object = 2; + constexpr int first_font_object = 3; + constexpr int first_page_object = first_font_object + + static_cast(font_names.size()); + const int object_count = first_page_object - 1 + + static_cast(contents.size()) * 2; + std::vector objects(static_cast(object_count + 1)); + objects[catalog_object] = "<< /Type /Catalog /Pages 2 0 R >>"; + + std::string kids; + for (std::size_t index = 0; index < contents.size(); ++index) { + const int page_object = first_page_object + static_cast(index) * 2; + kids += std::to_string(page_object) + " 0 R "; + } + objects[pages_object] = "<< /Type /Pages /Kids [ " + kids + + "] /Count " + std::to_string(contents.size()) + " >>"; + for (std::size_t index = 0; index < font_names.size(); ++index) { + objects[first_font_object + static_cast(index)] = + "<< /Type /Font /Subtype /Type1 /BaseFont /" + + std::string(font_names[index]) + " /Encoding /WinAnsiEncoding >>"; + } + + std::string resources = "<< /Font << "; + for (std::size_t index = 0; index < font_names.size(); ++index) { + resources += "/F" + std::to_string(index + 1) + " " + + std::to_string(first_font_object + + static_cast(index)) + " 0 R "; + } + resources += ">> >>"; + for (std::size_t index = 0; index < contents.size(); ++index) { + const int page_object = first_page_object + static_cast(index) * 2; + const int content_object = page_object + 1; + objects[page_object] = + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] " + "/Resources " + resources + " /Contents " + + std::to_string(content_object) + " 0 R >>"; + objects[content_object] = + "<< /Length " + std::to_string(contents[index].size()) + + " >>\nstream\n" + contents[index] + "endstream"; + } + + std::string pdf = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"; + std::vector offsets(static_cast(object_count + 1)); + for (int object = 1; object <= object_count; ++object) { + offsets[object] = pdf.size(); + pdf += std::to_string(object) + " 0 obj\n" + objects[object] + + "\nendobj\n"; + } + const std::size_t xref_offset = pdf.size(); + pdf += "xref\n0 " + std::to_string(object_count + 1) + + "\n0000000000 65535 f \n"; + for (int object = 1; object <= object_count; ++object) { + char entry[32]{}; + std::snprintf(entry, std::size(entry), "%010llu 00000 n \n", + static_cast(offsets[object])); + pdf += entry; + } + pdf += "trailer\n<< /Size " + std::to_string(object_count + 1) + + " /Root 1 0 R >>\nstartxref\n" + std::to_string(xref_offset) + + "\n%%EOF\n"; + return write_bytes(path, pdf); +} + +bool rtf_to_pdf(std::string_view rtf, const std::wstring& path) { + RichEditDocument rich; + return rich.load(rtf) && + write_pdf(path, paragraphs_from_rich_edit(rich)); +} + +int export_rtf_to_pdf_dialog(HWND owner, std::string_view rtf) { + wchar_t path[32768] = L"Document.pdf"; + static const wchar_t filter[] = L"PDF Files (*.pdf)\0*.pdf\0\0"; + OPENFILENAMEW dialog{}; + dialog.lStructSize = sizeof(dialog); + dialog.hwndOwner = owner; + dialog.lpstrFilter = filter; + dialog.lpstrFile = path; + dialog.nMaxFile = static_cast(std::size(path)); + dialog.lpstrTitle = L"Export as PDF"; + dialog.lpstrDefExt = L"pdf"; + dialog.Flags = OFN_EXPLORER | OFN_ENABLESIZING | OFN_LONGNAMES | + OFN_NOCHANGEDIR | OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT; + const DWORD test_path_length = GetEnvironmentVariableW( + L"WORD1_TEST_PDF_PATH", path, static_cast(std::size(path))); + const bool accepted = + test_path_length > 0 && test_path_length < std::size(path) ? + (SetEnvironmentVariableW(L"WORD1_TEST_PDF_PATH", nullptr), true) : + GetSaveFileNameW(&dialog) != FALSE; + if (!accepted) { + return CommDlgExtendedError() == 0 ? -1 : false; + } + if (!rtf_to_pdf(rtf, path)) { + MessageBoxW(owner, + L"Word could not create the PDF. Check that the selected folder is writable.", + L"Export as PDF", MB_OK | MB_ICONEXCLAMATION); + return false; + } + return true; +} + +} // namespace + +extern "C" int OpusModernPathIsDocx(const char* path) { + return path != nullptr && has_extension(path, ".docx"); +} + +extern "C" int OpusModernDocxToRtfFile(const char* docx_path, + const char* rtf_path) { + std::string xml; + return read_opc_part(wide_path(docx_path), L"/word/document.xml", xml) && + write_bytes(wide_path(rtf_path), + paragraphs_to_rtf(parse_document_xml(xml))); +} + +extern "C" int OpusModernDocxToTextFile(const char* docx_path, + const char* text_path) { + std::string xml; + return read_opc_part(wide_path(docx_path), L"/word/document.xml", xml) && + write_bytes(wide_path(text_path), + paragraphs_to_text(parse_document_xml(xml))); +} + +extern "C" int OpusModernRtfFileToDocx(const char* rtf_path, + const char* docx_path) { + return rtf_to_docx(wide_path(rtf_path), wide_path(docx_path)); +} + +extern "C" int OpusModernRtfFileToPdf(const char* rtf_path, + const char* pdf_path) { + std::string rtf; + return read_bytes(wide_path(rtf_path), rtf) && + rtf_to_pdf(rtf, wide_path(pdf_path)); +} + +extern "C" int OpusExportRtfToPdfDialog(HWND owner, const char* rtf) { + return rtf == nullptr ? false : + export_rtf_to_pdf_dialog(owner, rtf); +} + +extern "C" int OpusExportTextToPdfDialog(HWND owner, const char* text, + const int length) { + if (text == nullptr || length < 0) return false; + return export_rtf_to_pdf_dialog( + owner, ansi_text_to_rtf(std::string_view( + text, static_cast(length)))); +} diff --git a/src/port/original/opus_sdm_runtime.cpp b/src/port/original/opus_sdm_runtime.cpp index 4303572..7f8d760 100644 --- a/src/port/original/opus_sdm_runtime.cpp +++ b/src/port/original/opus_sdm_runtime.cpp @@ -22,6 +22,9 @@ extern HWND vhWndMsgBoxParent; void GetCabSz(void**, char*, std::uint16_t, std::uint16_t); int FSetCabSz(void**, const char*, std::uint16_t); 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; } -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]{}; const DWORD module_length = GetModuleFileNameA( nullptr, module_path, static_cast(std::size(module_path))); @@ -174,9 +178,9 @@ bool make_win95_staging_path(std::string& path) { return false; } DeleteFileA(temporary); - char* extension = std::strrchr(temporary, '.'); - if (extension != nullptr) { - lstrcpyA(extension, ".DOC"); + char* existing_extension = std::strrchr(temporary, '.'); + if (existing_extension != nullptr) { + lstrcpyA(existing_extension, desired_extension); } if (GetFileAttributesA(temporary) == INVALID_FILE_ATTRIBUTES && std::strlen(temporary) < 120) { @@ -260,6 +264,7 @@ constexpr Tmc kTmcSaveDirectoryList = kTmcUserMin + 3; constexpr Tmc kTmcSaveDirectory = kTmcUserMin + 4; constexpr Tmc kTmcSaveFormatPrompt = kTmcUserMin + 5; constexpr Tmc kTmcSaveFormat = kTmcUserMin + 6; +constexpr int kSaveFormatRtf = 6; constexpr Tmc kTmcSaveQuick = kTmcUserMin + 7; constexpr Tmc kTmcSaveBackup = kTmcUserMin + 8; constexpr Tmc kTmcSaveLockAnnotations = kTmcUserMin + 9; @@ -2146,12 +2151,13 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { } 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" "Text Files (*.txt)\0*.txt\0" "All Files (*.*)\0*.*\0\0"; static const char save_filter[] = "Word Documents (*.doc)\0*.doc\0" + "Word 2007-2024 Documents (*.docx)\0*.docx\0" "Document Templates (*.dot)\0*.dot\0" "All Files (*.*)\0*.*\0\0"; @@ -2180,8 +2186,23 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { OFN_OVERWRITEPROMPT; } - const BOOL accepted = opening ? - GetOpenFileNameA(&file_dialog) : GetSaveFileNameA(&file_dialog); + char test_path[32768]{}; + const DWORD test_path_length = GetEnvironmentVariableA( + "WORD1_TEST_FILE_DIALOG_PATH", test_path, + static_cast(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(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 (CommDlgExtendedError() != 0) { MessageBoxA(owner, @@ -2196,18 +2217,41 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { 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; - 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, "Word could not prepare a temporary document file.", opening ? "Open" : "Save As", MB_OK | MB_ICONEXCLAMATION); continue; } - if (opening && - !CopyFileA(selected_path.c_str(), legacy_path.c_str(), FALSE)) { - MessageBoxA(owner, "Word could not open the selected file.", + const bool staged_open = !opening || + (docx ? + 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); continue; } @@ -2240,6 +2284,11 @@ Tmc run_word95_common_file_dialog(DialogState& dialog) { SetWindowTextA(edit.window, edit.text.c_str()); } 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)) { @@ -2374,10 +2423,13 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, g_win95_save_alias.legacy_path.c_str()) == 0) { bool copied = success != 0; if (copied) { - copied = CopyFileA( - g_win95_save_alias.legacy_path.c_str(), - g_win95_save_alias.selected_path.c_str(), - FALSE) != 0; + copied = OpusModernPathIsDocx( + g_win95_save_alias.selected_path.c_str()) ? + OpusModernRtfFileToDocx( + g_win95_save_alias.legacy_path.c_str(), + g_win95_save_alias.selected_path.c_str()) != 0 : + CopyFileA(g_win95_save_alias.legacy_path.c_str(), + g_win95_save_alias.selected_path.c_str(), FALSE) != 0; } if (!success) { DeleteFileA(g_win95_save_alias.legacy_path.c_str()); @@ -2396,7 +2448,28 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, if (!success) { 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 { diff --git a/src/port/original/opus_win95_chrome.cpp b/src/port/original/opus_win95_chrome.cpp index 21c1e22..8e07c17 100644 --- a/src/port/original/opus_win95_chrome.cpp +++ b/src/port/original/opus_win95_chrome.cpp @@ -35,6 +35,7 @@ extern "C" int OpusGetWin95HorizontalPageMargins( extern "C" int OpusAdjustWin95HorizontalMargin( HWND ruler, int left_margin, int delta_pixels); extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler); +extern "C" int OpusExportCurrentDocumentPdf(void); namespace { @@ -54,6 +55,7 @@ constexpr UINT kComboSize = 0x7503; constexpr UINT kComboZoom = 0x7504; constexpr UINT kCmdToggleStandardToolbar = 0x7101; constexpr UINT kCmdToggleFormattingToolbar = 0x7102; +constexpr UINT kCmdExportPdf = 0x7103; constexpr UINT kCmdTextColorBase = 0x7200; constexpr UINT_PTR kSyncTimer = 0x951; constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc"; @@ -326,6 +328,17 @@ void configure_word95_menus(HWND window) { reinterpret_cast(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(-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"); if (utilities_index >= 0) { 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); return 0; } + if (command == kCmdExportPdf) { + OpusExportCurrentDocumentPdf(); + return 0; + } if (command == kCmdToggleStandardToolbar) { toggle_toolbar_row(app, toolbar, true); return 0; diff --git a/src/port/original/opus_word1_ui_test.cpp b/src/port/original/opus_word1_ui_test.cpp index 1942072..5791ab6 100644 --- a/src/port/original/opus_word1_ui_test.cpp +++ b/src/port/original/opus_word1_ui_test.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -12,10 +13,12 @@ namespace { constexpr UINT kWmCommand = 0x0111; constexpr WPARAM kFileNew = 1813; +constexpr WPARAM kFileOpen = 1843; constexpr WPARAM kFileSaveAs = 1897; constexpr WPARAM kFileExit = 2095; constexpr WPARAM kHelpAbout = 182; constexpr WPARAM kParaCenter = 1355; +constexpr WPARAM kExportPdf = 0x7103; constexpr UINT kWmOpusX64QuerySelection = WM_APP + 0x351; constexpr int kKcControl = 0x100; constexpr LRESULT kEditUndo = 2229; @@ -100,6 +103,26 @@ HWND find_descendant_by_class(const HWND parent, 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(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, const wchar_t* expected_class, std::vector& matches) { @@ -568,11 +591,12 @@ int fail(PROCESS_INFORMATION& process, const int code, int wmain(const int argument_count, wchar_t** arguments) { SetProcessDpiAwarenessContext( DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - if (argument_count < 2 || argument_count > 3) { + if (argument_count < 2 || argument_count > 4) { std::cerr << "usage: opus_word1_ui_test WORD1.exe " "[--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; } const bool typing_mode = @@ -604,23 +628,78 @@ int wmain(const int argument_count, wchar_t** arguments) { const bool save_as_mode = argument_count == 3 && 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 && !selection_mode && !caret_mode && !formatting_mode && !color_mode && !font_typing_mode && !clipboard_mode && !about_mode && - !save_as_mode) { + !save_as_mode && !pdf_export_mode) { std::cerr << "unknown test mode\n"; 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(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(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"\""; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; + LPWCH environment = GetEnvironmentStringsW(); 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'; 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( process.hProcess, process.dwProcessId, nullptr, @@ -628,13 +707,75 @@ int wmain(const int argument_count, wchar_t** arguments) { if (main_window == nullptr) { 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) { Sleep(1000); if (!PostMessageW(main_window, kWmCommand, kFileSaveAs, 0)) { return fail(process, 69, "could not send File Save As"); } const HWND save_as_dialog = wait_for_window( - process.hProcess, process.dwProcessId, L"OpusSdmDialog", + process.hProcess, process.dwProcessId, L"#32770", L"Save As", 5000); if (save_as_dialog == nullptr) { std::cerr << "Save As stage=" @@ -642,17 +783,32 @@ int wmain(const int argument_count, wchar_t** arguments) { main_window, "OpusX64SaveAsStage")) << '\n'; 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") || !window_is_responsive(process.hProcess, save_as_dialog)) { return fail(process, 71, "File Save As dialog did not finish initializing"); } - if (!PostMessageW(save_as_dialog, kWmCommand, 2, 0) || - !wait_for_window_to_close(process.hProcess, process.dwProcessId, - L"OpusSdmDialog", 5000) || - !window_is_responsive(process.hProcess, main_window)) { + const bool cancel_posted = + PostMessageW(save_as_dialog, kWmCommand, 2, 0) != FALSE; + const bool dialog_closed = cancel_posted && + 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, "File Save As dialog did not cancel cleanly"); } @@ -662,6 +818,45 @@ int wmain(const int argument_count, wchar_t** arguments) { CloseHandle(process.hProcess); 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(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) { Sleep(1000); if (!PostMessageW(main_window, kWmCommand, kHelpAbout, 0)) {