diff --git a/src/Opus/disp1.c b/src/Opus/disp1.c index 0d1f723..8c07ff7 100644 --- a/src/Opus/disp1.c +++ b/src/Opus/disp1.c @@ -517,9 +517,28 @@ LSplat2: rcw.ywBottom = max(rcw.ywTop, rcw.ywBottom - dyFrameLine); } + #ifdef OPUS_X64 + { + int iPageClip = 0; + /* The native continuous-page shell keeps rcePage in client pixel + coordinates. Some legacy opaque-line erases widen their rectangle + after ClipRectFromDr, which can otherwise paint white bands into the + gray workspace. A real DC clip is the final safety boundary. */ + if (pwwd->fPageView && (iPageClip = SaveDC(hdc)) != 0) + IntersectClipRect(hdc, pwwd->rcePage.xeLeft, + pwwd->rcePage.yeTop, pwwd->rcePage.xeRight, + pwwd->rcePage.yeBottom); + DisplayFliCore(ww, rcw, + dxpToXw + /* aesthetic fudge */ (fWin ? 0 : vsci.dxpBorder), + rcwErase.ywBottom); + if (iPageClip != 0) + RestoreDC(hdc, iPageClip); + } + #else DisplayFliCore(ww, rcw, dxpToXw + /* aesthetic fudge */ (fWin ? 0 : vsci.dxpBorder), rcwErase.ywBottom); + #endif /* edl may have changed in drawing picture in displayflicore. Update. */ GetPlc(hplcedl, dl, &edl); diff --git a/src/Opus/openrare.c b/src/Opus/openrare.c index cfcbd28..ea618d1 100644 --- a/src/Opus/openrare.c +++ b/src/Opus/openrare.c @@ -34,6 +34,7 @@ DEBUGASSERTSZ /* WIN - bogus macro for assert string */ #include "dlbenum.h" #include "error.h" #include "prompt.h" +#include "border.h" #ifdef DEBUG #ifdef PCJ @@ -85,6 +86,13 @@ extern KMP ** hkmpCur; extern KMP ** vhkmpUser; #ifdef OPUS_X64 extern int OpusWin95OpenAliasIsDocx(); +extern int OpusModernPendingDocxRunCount(); +extern int OpusModernPendingDocxParagraphCount(); +extern int OpusModernGetPendingDocxRun(); +extern int OpusModernGetPendingDocxParagraph(); +extern int OpusModernGetPendingDocxPage(); +extern void OpusModernClearPendingDocxFormatting(); +extern int OpusX64FtcFromFontNameForDoc(); #endif struct DTTM DttmCur(); @@ -94,6 +102,133 @@ static csconst char rgchRTF[] = { '{', '\\','r','t','f'}; +#ifdef OPUS_X64 +/* Apply the OOXML properties recorded while the temporary plain-text file was + created. Importing the generated RTF through the 16-bit-era parser is not + reliable on x64, but the normal Word property engine is: this keeps the + stable text-file open path and restores formatting to the resulting doc. */ +static int CbAppendDocxPrl(grpprl, cb, cbMax, sprm, val) +char *grpprl; +int cb, cbMax, sprm, val; +{ + int cch; + if (cb > cbMax - 3) + return cb; + cch = CchPrlFromSprmVal(grpprl + cb, sprm, val); + return cch == valNil ? cb : cb + cch; +} + + +static void ApplyPendingDocxFormatting(doc) +int doc; +{ + int i, c, cb, ftc; + long cpFirst, cpLim; + CP cpMac; + struct CA ca; + char grpprl[96]; + char szFont[LF_FACESIZE]; + int bold, italic, underline, strike, smallCaps, allCaps, hidden; + int hps, ico; + int jc, dxaLeft, dxaRight, dxaLeft1, dyaBefore, dyaAfter, dyaLine; + int fKeep, fKeepFollow, fPageBreakBefore, fBottomBorder; + int xaPage, yaPage, dxaLeftPage, dxaRightPage, dyaTopPage, dyaBottomPage; + struct DOD *pdod; + + cpMac = CpMacDocEdit(doc); + if (OpusModernGetPendingDocxPage(&xaPage, &yaPage, &dxaLeftPage, + &dxaRightPage, &dyaTopPage, &dyaBottomPage)) + { + pdod = PdodDoc(doc); + pdod->dop.xaPage = xaPage; + pdod->dop.yaPage = yaPage; + pdod->dop.dxaLeft = dxaLeftPage; + pdod->dop.dxaRight = dxaRightPage; + pdod->dop.dyaTop = dyaTopPage; + pdod->dop.dyaBottom = dyaBottomPage; + pdod->fEnvDirty = fTrue; + pdod->fLRDirty = fTrue; + InvalPageView(doc); + InvalDoc(doc); + InvalFli(); + } + c = OpusModernPendingDocxParagraphCount(); + for (i = 0; i < c; ++i) + { + if (!OpusModernGetPendingDocxParagraph(i, &cpFirst, &cpLim, + &jc, &dxaLeft, &dxaRight, &dxaLeft1, &dyaBefore, + &dyaAfter, &dyaLine, &fKeep, &fKeepFollow, + &fPageBreakBefore, &fBottomBorder)) + continue; + if (cpFirst < cp0 || cpFirst > cpMac) + continue; + if (cpLim > cpMac) + cpLim = cpMac; + if (cpLim <= cpFirst) + cpLim = min(cpMac, cpFirst + ccpEop); + if (cpLim <= cpFirst) + continue; + cb = 0; + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPJc, jc); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDxaLeft, dxaLeft); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDxaRight, dxaRight); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDxaLeft1, dxaLeft1); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDyaBefore, dyaBefore); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDyaAfter, dyaAfter); + if (dyaLine != 0) + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPDyaLine, dyaLine); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPFKeep, fKeep); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmPFKeepFollow, fKeepFollow); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), + sprmPFPageBreakBefore, fPageBreakBefore); + if (fBottomBorder) + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), + sprmPBrcBottom, brcSingle | (dptBrcSpace << shiftDxpSpaceBrc)); + ca.doc = doc; + ca.cpFirst = cpFirst; + ca.cpLim = cpLim; + ApplyGrpprlCa(grpprl, cb, &ca); + } + + c = OpusModernPendingDocxRunCount(); + for (i = 0; i < c; ++i) + { + if (!OpusModernGetPendingDocxRun(i, &cpFirst, &cpLim, &bold, + &italic, &underline, &strike, &smallCaps, &allCaps, + &hidden, &hps, &ico, szFont, sizeof(szFont))) + continue; + if (cpFirst < cp0 || cpFirst >= cpMac) + continue; + if (cpLim > cpMac) + cpLim = cpMac; + if (cpLim <= cpFirst) + continue; + cb = 0; + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFBold, bold); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFItalic, italic); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCKul, + underline ? kulSingle : kulNone); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFStrike, strike); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFSmallCaps, smallCaps); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFCaps, allCaps); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFVanish, hidden); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCHps, hps); + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCIco, ico); + ftc = OpusX64FtcFromFontNameForDoc(doc, szFont); + if (ftc >= 0) + cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFtc, ftc); + ca.doc = doc; + ca.cpFirst = cpFirst; + ca.cpLim = cpLim; + ApplyGrpprlCa(grpprl, cb, &ca); + } + + PdodDoc(doc)->fFormatted = fTrue; + OpusModernClearPendingDocxFormatting(); +} +#endif + + /* used to convert between versions of the fib */ ConvertFib (pfib) struct FIB *pfib; @@ -529,10 +664,16 @@ LConvert: case dffOpenText: if ((doc = DocReadPlainFn (fn)) != docNil) + { if (dff == dffOpenText) PfcbFn(fn)->fForeignFormat = fFalse; else PdodDoc(doc)->fn = fnT; +#ifdef OPUS_X64 + if (OpusWin95OpenAliasIsDocx(PfcbFn(fn)->stFile)) + ApplyPendingDocxFormatting(doc); +#endif + } break; default: diff --git a/src/Opus/rulerdrw.c b/src/Opus/rulerdrw.c index dc87a38..4d5b418 100644 --- a/src/Opus/rulerdrw.c +++ b/src/Opus/rulerdrw.c @@ -50,7 +50,9 @@ extern RRF rrf; extern struct WWD ** hwwdCur; extern struct MWD ** hmwdCur; extern struct SEL selCur; +extern struct CHP vchpFetch; extern struct CHP vchpGraySelCur; +extern struct PAP vpapFetch; extern struct PAP vpapSelCur; extern struct PAP vpapGraySelCur; extern struct SCI vsci; @@ -106,17 +108,17 @@ static int vWin95BaseDysInch = 0; int OpusExportCurrentDocumentPdf() { int doc; - HANDLE hText; - LPCH lpch; int result; - int cchText = 0; - int fAfterReturn = fFalse; - CP cp; + CP cpPara; CP cpMac; + struct DOD *pdod; extern int vccpFetch; extern CHAR HUGE *vhpchFetch; - extern int N_FetchCp(); - extern int OpusExportTextToPdfDialog(); + extern int OpusPdfSnapshotBegin(); + extern int OpusPdfSnapshotAddParagraph(); + extern int OpusPdfSnapshotAddRun(); + extern int OpusPdfSnapshotExportDialog(); + extern void OpusX64FontNameFromFtc(); if (selCur.doc == docNil) return fFalse; @@ -124,61 +126,78 @@ int OpusExportCurrentDocumentPdf() 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); + pdod = PdodMother(doc); + if (!OpusPdfSnapshotBegin(pdod->dop.xaPage, pdod->dop.yaPage, + pdod->dop.dxaLeft, pdod->dop.dxaRight, + pdod->dop.dyaTop < 0 ? -pdod->dop.dyaTop : pdod->dop.dyaTop, + pdod->dop.dyaBottom < 0 ? -pdod->dop.dyaBottom : pdod->dop.dyaBottom)) return fFalse; - } - for (cp = cp0; cp < cpMac; ) + for (cpPara = cp0; cpPara < cpMac; ) { - int ich; - int cch; - FetchCp(doc, cp, fcmChars); - if (vccpFetch <= 0 || vhpchFetch == NULL) - { - GlobalUnlock(hText); - GlobalFree(hText); + CP cpParaLim; + CP cpRun; + CachePara(doc, cpPara); + cpParaLim = CpMin(caPara.cpLim, cpMac); + if (cpParaLim <= cpPara) + cpParaLim = cpPara + 1; + if (!OpusPdfSnapshotAddParagraph(vpapFetch.jc, + vpapFetch.dxaLeft, vpapFetch.dxaRight, + vpapFetch.dxaLeft1, vpapFetch.dyaBefore, + vpapFetch.dyaAfter, vpapFetch.dyaLine, + vpapFetch.fKeep, vpapFetch.fKeepFollow, + vpapFetch.fPageBreakBefore, + vpapFetch.brcBottom != 0)) return fFalse; - } - cch = (int)CpMin((CP)vccpFetch, cpMac - cp); - for (ich = 0; ich < cch; ++ich) + + for (cpRun = cpPara; cpRun < cpParaLim; ) { - int ch = (unsigned char)vhpchFetch[ich]; - if (ch == chReturn) + int ich; + int cch; + int cchOutput; + char rgch[1024]; + char szFont[LF_FACESIZE]; + FetchCp(doc, cpRun, fcmChars + fcmProps); + if (vccpFetch <= 0 || vhpchFetch == NULL) + return fFalse; + cch = (int)CpMin((CP)vccpFetch, cpParaLim - cpRun); + cch = min(cch, sizeof(rgch) - 1); + cchOutput = 0; + for (ich = 0; ich < cch; ++ich) { - 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; + int ch = (unsigned char)vhpchFetch[ich]; + if (ch == chReturn || ch == chEop) + continue; if (ch == chTable || ch == chTab) - lpch[cchText++] = '\t'; + rgch[cchOutput++] = '\t'; else if (ch == chSect || ch == chColumnBreak) - lpch[cchText++] = '\f'; - else if (ch >= chSpace || ch == chNonReqHyphen) - lpch[cchText++] = (char)ch; + rgch[cchOutput++] = '\f'; + else if (ch == chNonReqHyphen) + rgch[cchOutput++] = '-'; + /* fSpec can cover imported field/list runs whose bytes are still + ordinary visible text. Filter actual control codes above, not + the whole run, or leading words disappear from PDF output. */ + else if (ch >= chSpace) + rgch[cchOutput++] = (char)ch; } + if (cchOutput > 0) + { + rgch[cchOutput] = '\0'; + OpusX64FontNameFromFtc(vchpFetch.ftc, + szFont, sizeof(szFont)); + if (!OpusPdfSnapshotAddRun(rgch, cchOutput, szFont, + vchpFetch.hps, vchpFetch.fBold, + vchpFetch.fItalic, vchpFetch.kul != kulNone, + vchpFetch.fStrike, vchpFetch.fSmallCaps, + vchpFetch.fCaps, vchpFetch.fVanish, + vchpFetch.ico)) + return fFalse; + } + cpRun += cch; } - cp += cch; + cpPara = cpParaLim; } - lpch[cchText] = '\0'; - result = OpusExportTextToPdfDialog(vhwndApp, lpch, cchText); - GlobalUnlock(hText); - GlobalFree(hText); + result = OpusPdfSnapshotExportDialog(vhwndApp); return result; } diff --git a/src/Opus/wordtech/fetch2.c b/src/Opus/wordtech/fetch2.c index 04b8c54..975f412 100644 --- a/src/Opus/wordtech/fetch2.c +++ b/src/Opus/wordtech/fetch2.c @@ -980,7 +980,7 @@ const char *sz; { int cch; struct FFN *pffn; - char rgch[cbFfnLast]; + char rgch[cbFfnLast] = {0}; pffn = (struct FFN *)rgch; if (sz == NULL || *sz == '\0') @@ -999,6 +999,32 @@ const char *sz; return FtcValidateFont(pffn); } +/* Variant used while a DOCX is being opened. The new document has not yet + become selCur.doc, so the normal ribbon adapter would add the font to the + document that was active before File Open. */ +EXPORT int OpusX64FtcFromFontNameForDoc(doc, sz) +int doc; +const char *sz; +{ + if (sz == NULL || *sz == '\0') + return wNinch; + /* Word 1.x keeps only three guaranteed GDI font slots in a new plain-text + document: Times, Symbol, and Helvetica. Adding Office-era fonts before + the imported document becomes current can alias the new ftc to Symbol. + Preserve the requested family safely; Helvetica is the closest available + substitute for Aptos/Calibri/Arial and retains ANSI character mapping. */ + if (!FNeNcSz(sz, "Symbol") || !FNeNcSz(sz, "Wingdings")) + return 1; + if (!FNeNcSz(sz, "Times New Roman") || !FNeNcSz(sz, "Times") || + !FNeNcSz(sz, "Georgia") || !FNeNcSz(sz, "Garamond") || + !FNeNcSz(sz, "Cambria")) + return ftcDefault; + if (!FNeNcSz(sz, "Courier") || !FNeNcSz(sz, "Courier New") || + !FNeNcSz(sz, "Consolas")) + return FtcFromDocIbst(doc, ibstCourier); + return 2; +} + EXPORT int OpusX64HpsFromFontSize(sz) const char *sz; { diff --git a/src/Opus/wproc.c b/src/Opus/wproc.c index 32dc161..4bab5e4 100644 --- a/src/Opus/wproc.c +++ b/src/Opus/wproc.c @@ -95,6 +95,7 @@ struct CA caTable; extern struct FTI vfti; extern char (**vhgrpchr)[]; extern int vbchrMac; +extern struct STTB **vhsttbFont; #endif extern struct FCB **mpfnhfcb[]; extern int vfnPreload; @@ -1767,6 +1768,67 @@ LONG lParam; return (LRESULT)(pkme != 0 && pkme->kt == ktMacro ? pkme->bcm : -1); } + case 87: + case 88: + case 89: + case 90: + { + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDocEdit(selCur.doc)) + return (LRESULT)-1; + FetchCp(selCur.doc, cp, fcmProps); + if (LOWORD(wParam) == 87) return (LRESULT)(vchpFetch.fBold != 0); + if (LOWORD(wParam) == 88) return (LRESULT)(vchpFetch.fItalic != 0); + if (LOWORD(wParam) == 89) return (LRESULT)vchpFetch.kul; + return (LRESULT)vchpFetch.ico; + } + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + { + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDocEdit(selCur.doc)) + return (LRESULT)-1; + CachePara(selCur.doc, cp); + if (LOWORD(wParam) == 91) return (LRESULT)vpapFetch.jc; + if (LOWORD(wParam) == 92) return (LRESULT)vpapFetch.dxaLeft; + if (LOWORD(wParam) == 93) return (LRESULT)vpapFetch.dyaBefore; + if (LOWORD(wParam) == 94) return (LRESULT)vpapFetch.dyaAfter; + if (LOWORD(wParam) == 95) return (LRESULT)vpapFetch.brcBottom; + return (LRESULT)(vpapFetch.fKeepFollow != 0); + } + case 97: + return (LRESULT)PdodMother(selCur.doc)->dop.xaPage; + case 98: + return (LRESULT)PdodMother(selCur.doc)->dop.yaPage; + case 99: + return (LRESULT)PdodMother(selCur.doc)->dop.dxaLeft; + case 100: + return (LRESULT)PdodMother(selCur.doc)->dop.dxaRight; + case 101: + return (LRESULT)PdodMother(selCur.doc)->dop.dyaTop; + case 102: + return (LRESULT)PdodMother(selCur.doc)->dop.dyaBottom; + case 103: + case 104: + { + int ibst; + struct FFN *pffn; + CP cp = (CP)lParam; + if (cp < cp0 || cp >= CpMacDocEdit(selCur.doc)) + return (LRESULT)-1; + FetchCp(selCur.doc, cp, fcmProps); + ibst = IbstFontFromFtcDoc(vchpFetch.ftc, selCur.doc); + if (LOWORD(wParam) == 103) + return (LRESULT)ibst; + if (ibst < 0) + return (LRESULT)-1; + pffn = (struct FFN *)PstFromSttb(vhsttbFont, ibst); + return (LRESULT)(unsigned char)ChsPffn(pffn); + } } return (LRESULT) -1; #endif diff --git a/src/port/original/opus_modern_formats.cpp b/src/port/original/opus_modern_formats.cpp index 22555ed..1c83f90 100644 --- a/src/port/original/opus_modern_formats.cpp +++ b/src/port/original/opus_modern_formats.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -94,6 +96,20 @@ std::string wide_to_ansi(std::wstring_view value) { return result; } +std::wstring ansi_to_wide(std::string_view value) { + if (value.empty()) return {}; + const int count = MultiByteToWideChar( + 1252, 0, value.data(), static_cast(value.size()), nullptr, 0); + std::wstring result(count > 0 ? static_cast(count) : 0, + L'\0'); + if (count > 0) { + MultiByteToWideChar(1252, 0, value.data(), + static_cast(value.size()), result.data(), + count); + } + return result; +} + std::string xml_escape(std::wstring_view text) { std::string result; for (const wchar_t character : text) { @@ -250,6 +266,10 @@ struct RunStyle { bool bold = false; bool italic = false; bool underline = false; + bool strike = false; + bool small_caps = false; + bool all_caps = false; + bool hidden = false; int half_points = 20; COLORREF color = RGB(0, 0, 0); bool auto_color = true; @@ -258,9 +278,54 @@ struct RunStyle { }; struct TextRun { RunStyle style; std::wstring text; }; -struct Paragraph { int alignment = PFA_LEFT; std::vector runs; }; +struct DocumentSettings { + bool valid = false; + int page_width = 0; + int page_height = 0; + int margin_left = 0; + int margin_right = 0; + int margin_top = 0; + int margin_bottom = 0; +}; +struct Paragraph { + int alignment = PFA_LEFT; + int left_indent = 0; + int right_indent = 0; + int first_line_indent = 0; + int space_before = 0; + int space_after = 0; + int line_spacing = 0; + bool keep_together = false; + bool keep_with_next = false; + bool page_break_before = false; + bool bottom_border = false; + std::vector runs; +}; -bool property_enabled(std::string_view properties, const char* local_name) { +std::string_view element_block(std::string_view xml, + std::string_view local_name) { + const std::string opening = "' || xml[boundary] == '/' || + std::isspace(static_cast(xml[boundary])))) { + const std::size_t open_end = xml.find('>', boundary); + if (open_end == std::string_view::npos) return {}; + if (open_end > start && xml[open_end - 1] == '/') + return xml.substr(start, open_end - start + 1); + const std::string closing = ""; + const std::size_t close = xml.find(closing, open_end + 1); + if (close == std::string_view::npos) return {}; + return xml.substr(start, close + closing.size() - start); + } + start = boundary; + } + return {}; +} + +int property_state(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); @@ -268,12 +333,15 @@ bool property_enabled(std::string_view properties, const char* local_name) { 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"); + return value == "0" || value == "false" || value == "none" ? 0 : 1; } position = end + 1; } - return false; + return -1; +} + +bool property_enabled(std::string_view properties, const char* local_name) { + return property_state(properties, local_name) == 1; } std::string first_property_value(std::string_view properties, @@ -290,16 +358,18 @@ std::string first_property_value(std::string_view properties, 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"); +void apply_run_properties(RunStyle& style, std::string_view props) { + const auto apply_switch = [&](const char* name, bool& target) { + const int state = property_state(props, name); + if (state >= 0) target = state != 0; + }; + apply_switch("b", style.bold); + apply_switch("i", style.italic); + apply_switch("u", style.underline); + apply_switch("strike", style.strike); + apply_switch("smallCaps", style.small_caps); + apply_switch("caps", style.all_caps); + apply_switch("vanish", style.hidden); if (const std::string size = first_property_value(props, "sz"); !size.empty()) { style.half_points = (std::max)(2, std::atoi(size.c_str())); } @@ -312,7 +382,131 @@ RunStyle parse_run_style(std::string_view run) { 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; + } else if (first_property_value(props, "color") == "auto") { + style.auto_color = true; + style.color = RGB(0, 0, 0); } +} + +void apply_paragraph_properties(Paragraph& paragraph, std::string_view props) { + if (const std::string alignment = first_property_value(props, "jc"); + !alignment.empty()) { + 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; + else paragraph.alignment = PFA_LEFT; + } + if (const std::string_view indent = element_block(props, "ind"); !indent.empty()) { + const std::string left = tag_attribute(indent, "left"); + const std::string right = tag_attribute(indent, "right"); + const std::string first = tag_attribute(indent, "firstLine"); + const std::string hanging = tag_attribute(indent, "hanging"); + if (!left.empty()) paragraph.left_indent = std::atoi(left.c_str()); + if (!right.empty()) paragraph.right_indent = std::atoi(right.c_str()); + if (!first.empty()) paragraph.first_line_indent = std::atoi(first.c_str()); + if (!hanging.empty()) paragraph.first_line_indent = -std::atoi(hanging.c_str()); + } + if (const std::string_view spacing = element_block(props, "spacing"); + !spacing.empty()) { + const std::string before = tag_attribute(spacing, "before"); + const std::string after = tag_attribute(spacing, "after"); + const std::string line = tag_attribute(spacing, "line"); + if (!before.empty()) paragraph.space_before = std::atoi(before.c_str()); + if (!after.empty()) paragraph.space_after = std::atoi(after.c_str()); + if (!line.empty()) paragraph.line_spacing = std::atoi(line.c_str()); + } + const auto apply_switch = [&](const char* name, bool& target) { + const int state = property_state(props, name); + if (state >= 0) target = state != 0; + }; + apply_switch("keepLines", paragraph.keep_together); + apply_switch("keepNext", paragraph.keep_with_next); + apply_switch("pageBreakBefore", paragraph.page_break_before); + if (const std::string_view borders = element_block(props, "pBdr"); + !borders.empty()) { + const std::string_view bottom = element_block(borders, "bottom"); + const std::string value = tag_attribute(bottom, "val"); + paragraph.bottom_border = !bottom.empty() && value != "nil" && + value != "none"; + } +} + +struct StyleDefinition { + std::string based_on; + std::string run_properties; + std::string paragraph_properties; +}; + +struct StyleCatalog { + RunStyle default_run; + Paragraph default_paragraph; + std::map definitions; +}; + +StyleCatalog parse_style_catalog(std::string_view xml) { + StyleCatalog catalog; + if (const std::string_view defaults = element_block(xml, "docDefaults"); + !defaults.empty()) { + const std::string_view run_default = element_block(defaults, "rPrDefault"); + apply_run_properties(catalog.default_run, + element_block(run_default, "rPr")); + const std::string_view paragraph_default = + element_block(defaults, "pPrDefault"); + apply_paragraph_properties(catalog.default_paragraph, + element_block(paragraph_default, "pPr")); + } + std::size_t position = 0; + while ((position = xml.find("= xml.size() || + (!std::isspace(static_cast(xml[boundary])) && + xml[boundary] != '>')) { + position = boundary; + continue; + } + const std::size_t open_end = xml.find('>', boundary); + 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 opening = xml.substr(position, open_end - position + 1); + const std::string id = tag_attribute(opening, "styleId"); + const std::string_view block = xml.substr(position, close + 10 - position); + if (!id.empty()) { + StyleDefinition definition; + definition.based_on = first_property_value(block, "basedOn"); + if (const std::string_view props = element_block(block, "rPr"); + !props.empty()) definition.run_properties.assign(props); + if (const std::string_view props = element_block(block, "pPr"); + !props.empty()) definition.paragraph_properties.assign(props); + catalog.definitions[id] = std::move(definition); + } + position = close + 10; + } + return catalog; +} + +void apply_style(const StyleCatalog& catalog, const std::string& id, + RunStyle& run, Paragraph& paragraph, const int depth = 0) { + if (id.empty() || depth > 16) return; + const auto found = catalog.definitions.find(id); + if (found == catalog.definitions.end()) return; + if (!found->second.based_on.empty() && found->second.based_on != id) { + apply_style(catalog, found->second.based_on, run, paragraph, depth + 1); + } + apply_run_properties(run, found->second.run_properties); + apply_paragraph_properties(paragraph, found->second.paragraph_properties); +} + +RunStyle parse_run_style(std::string_view run, RunStyle style, + const StyleCatalog& catalog) { + const std::string_view props = element_block(run, "rPr"); + if (const std::string character_style = + first_property_value(props, "rStyle"); !character_style.empty()) { + Paragraph ignored; + apply_style(catalog, character_style, style, ignored); + } + apply_run_properties(style, props); return style; } @@ -331,14 +525,20 @@ std::wstring parse_run_text(std::string_view run) { position = close + 6; } else { if (name == "tab") text.push_back(L'\t'); - else if (name == "br" || name == "cr") text.push_back(L'\n'); + else if (name == "br") { + text.push_back(tag_attribute(tag, "type") == "page" ? + L'\f' : L'\n'); + } else if (name == "cr") text.push_back(L'\n'); + else if (name == "noBreakHyphen") text.push_back(L'-'); + else if (name == "softHyphen") text.push_back(L'\x00ad'); position = tag_end + 1; } } return text; } -std::vector parse_document_xml(std::string_view xml) { +std::vector parse_paragraphs_flat( + std::string_view xml, const StyleCatalog& catalog = {}) { std::vector paragraphs; std::size_t position = 0; while ((position = xml.find(" parse_document_xml(std::string_view xml) { 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; + Paragraph paragraph = catalog.default_paragraph; + RunStyle paragraph_run = catalog.default_run; + apply_style(catalog, "Normal", paragraph_run, paragraph); + const std::string_view paragraph_properties = element_block(block, "pPr"); + if (const std::string paragraph_style = + first_property_value(paragraph_properties, "pStyle"); + !paragraph_style.empty() && paragraph_style != "Normal") { + apply_style(catalog, paragraph_style, paragraph_run, paragraph); + } + apply_paragraph_properties(paragraph, paragraph_properties); std::size_t run_position = 0; while ((run_position = block.find(" parse_document_xml(std::string_view xml) { 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)}); + if (!text.empty()) paragraph.runs.push_back( + {parse_run_style(run, paragraph_run, catalog), std::move(text)}); run_position = run_close + 6; } paragraphs.push_back(std::move(paragraph)); @@ -383,17 +588,282 @@ std::vector parse_document_xml(std::string_view xml) { return paragraphs; } -std::string paragraphs_to_text(const std::vector& paragraphs) { +struct TableLayout { + std::size_t first_paragraph = 0; + std::size_t paragraph_count = 0; + int rows = 0; + int columns = 0; +}; + +std::size_t find_word_tag(std::string_view xml, std::string_view name, + std::size_t position) { + const std::string opening = "' || xml[boundary] == '/' || + std::isspace(static_cast(xml[boundary])))) { + return position; + } + position = boundary; + } + return std::string_view::npos; +} + +std::vector parse_document_xml( + std::string_view xml, const StyleCatalog& catalog = {}, + std::vector* table_layouts = nullptr) { + std::vector paragraphs; + if (table_layouts != nullptr) table_layouts->clear(); + std::size_t cursor = 0; + while (cursor < xml.size()) { + const std::size_t table_start = find_word_tag(xml, "tbl", cursor); + if (table_start == std::string_view::npos) { + const std::string_view remainder = xml.substr(cursor); + if (find_word_tag(remainder, "p", 0) != std::string_view::npos) { + std::vector tail = + parse_paragraphs_flat(remainder, catalog); + paragraphs.insert(paragraphs.end(), + std::make_move_iterator(tail.begin()), + std::make_move_iterator(tail.end())); + } + break; + } + const std::string_view prefix = + xml.substr(cursor, table_start - cursor); + if (find_word_tag(prefix, "p", 0) != std::string_view::npos) { + std::vector before = + parse_paragraphs_flat(prefix, catalog); + paragraphs.insert(paragraphs.end(), + std::make_move_iterator(before.begin()), + std::make_move_iterator(before.end())); + } + const std::size_t table_close = xml.find("", table_start); + if (table_close == std::string_view::npos) break; + const std::size_t table_end = table_close + std::strlen(""); + const std::string_view table = + xml.substr(table_start, table_end - table_start); + TableLayout layout; + layout.first_paragraph = paragraphs.size(); + std::size_t row_cursor = 0; + while (true) { + const std::size_t row_start = find_word_tag(table, "tr", row_cursor); + if (row_start == std::string_view::npos) break; + const std::size_t row_close = table.find("", row_start); + if (row_close == std::string_view::npos) break; + const std::size_t row_end = row_close + std::strlen(""); + const std::string_view row = table.substr(row_start, + row_end - row_start); + std::vector> cells; + std::size_t cell_cursor = 0; + while (true) { + const std::size_t cell_start = find_word_tag(row, "tc", cell_cursor); + if (cell_start == std::string_view::npos) break; + const std::size_t cell_close = row.find("", cell_start); + if (cell_close == std::string_view::npos) break; + const std::size_t cell_end = cell_close + std::strlen(""); + cells.push_back(parse_paragraphs_flat( + row.substr(cell_start, cell_end - cell_start), catalog)); + cell_cursor = cell_end; + } + std::size_t line_count = 0; + for (const auto& cell : cells) + line_count = (std::max)(line_count, cell.size()); + if (!cells.empty()) { + layout.columns = (std::max)(layout.columns, + static_cast(cells.size())); + for (std::size_t line = 0; line < line_count; ++line) { + for (auto& cell : cells) { + if (line < cell.size()) + paragraphs.push_back(std::move(cell[line])); + else + paragraphs.push_back({}); + } + ++layout.rows; + } + } + row_cursor = row_end; + } + layout.paragraph_count = paragraphs.size() - layout.first_paragraph; + if (layout.rows > 0 && layout.columns > 1 && + layout.paragraph_count == + static_cast(layout.rows * layout.columns)) { + if (table_layouts != nullptr) table_layouts->push_back(layout); + } else { + paragraphs.resize(layout.first_paragraph); + std::vector flat = parse_paragraphs_flat(table, catalog); + paragraphs.insert(paragraphs.end(), + std::make_move_iterator(flat.begin()), + std::make_move_iterator(flat.end())); + } + cursor = table_end; + } + if (paragraphs.empty()) paragraphs.push_back({}); + return paragraphs; +} + +DocumentSettings parse_document_settings(std::string_view xml) { + DocumentSettings settings; + const std::string_view section = element_block(xml, "sectPr"); + const std::string_view size = element_block(section, "pgSz"); + const std::string_view margins = element_block(section, "pgMar"); + const auto integer_attribute = [](std::string_view tag, + const char* name) { + const std::string value = tag_attribute(tag, name); + return value.empty() ? 0 : std::atoi(value.c_str()); + }; + settings.page_width = integer_attribute(size, "w"); + settings.page_height = integer_attribute(size, "h"); + settings.margin_left = integer_attribute(margins, "left"); + settings.margin_right = integer_attribute(margins, "right"); + settings.margin_top = integer_attribute(margins, "top"); + settings.margin_bottom = integer_attribute(margins, "bottom"); + settings.valid = settings.page_width > 0 && settings.page_height > 0 && + settings.margin_left >= 0 && settings.margin_right >= 0 && + settings.margin_top >= 0 && settings.margin_bottom >= 0; + return settings; +} + +struct PendingRunFormat { + long cp_first = 0; + long cp_lim = 0; + RunStyle style; +}; + +struct PendingParagraphFormat { + long cp_first = 0; + long cp_lim = 0; + Paragraph paragraph; +}; + +struct PendingTableFormat { + long cp_first = 0; + long cp_lim = 0; + int first_paragraph = 0; + int rows = 0; + int columns = 0; +}; + +struct PendingDocxImport { + std::vector runs; + std::vector paragraphs; + std::vector tables; + DocumentSettings settings; +}; + +PendingDocxImport pending_docx_import; + +struct PendingPdfExport { + std::vector paragraphs; + DocumentSettings settings; +}; + +PendingPdfExport pending_pdf_export; + +std::string ansi_run_text(std::wstring_view text) { + std::string result; + for (const wchar_t character : text) { + if (character == L'\n') { + result += "\r\n"; + } else if (character == L'\r') { + if (result.empty() || result.back() != '\r') result.push_back('\r'); + } else { + result += wide_to_ansi(std::wstring_view(&character, 1)); + } + } + return result; +} + +std::string paragraphs_to_text(const std::vector& paragraphs, + PendingDocxImport* pending = nullptr, + const std::vector* tables = nullptr) { std::string text; + if (pending != nullptr) *pending = {}; for (std::size_t index = 0; index < paragraphs.size(); ++index) { + PendingParagraphFormat paragraph_format; + paragraph_format.cp_first = static_cast(text.size()); + paragraph_format.paragraph = paragraphs[index]; + paragraph_format.paragraph.runs.clear(); for (const auto& run : paragraphs[index].runs) { - text += wide_to_ansi(run.text); + PendingRunFormat run_format; + run_format.cp_first = static_cast(text.size()); + run_format.style = run.style; + text += ansi_run_text(run.text); + run_format.cp_lim = static_cast(text.size()); + if (pending != nullptr && run_format.cp_lim > run_format.cp_first) + pending->runs.push_back(std::move(run_format)); } if (index + 1 < paragraphs.size()) text += "\r\n"; + paragraph_format.cp_lim = static_cast(text.size()); + if (pending != nullptr) + pending->paragraphs.push_back(std::move(paragraph_format)); + } + if (pending != nullptr && tables != nullptr) { + for (const TableLayout& table : *tables) { + if (table.paragraph_count == 0 || + table.first_paragraph >= pending->paragraphs.size() || + table.first_paragraph + table.paragraph_count > + pending->paragraphs.size()) continue; + PendingTableFormat format; + format.cp_first = + pending->paragraphs[table.first_paragraph].cp_first; + format.cp_lim = pending->paragraphs[ + table.first_paragraph + table.paragraph_count - 1].cp_lim; + format.first_paragraph = static_cast(table.first_paragraph); + format.rows = table.rows; + format.columns = table.columns; + pending->tables.push_back(format); + } } return text; } +bool load_docx_paragraphs(const char* path, + std::vector& paragraphs, + DocumentSettings* settings = nullptr, + std::vector* tables = nullptr) { + std::string document; + std::string styles; + if (!read_opc_part(wide_path(path), L"/word/document.xml", document)) + return false; + read_opc_part(wide_path(path), L"/word/styles.xml", styles); + paragraphs = parse_document_xml(document, parse_style_catalog(styles), + tables); + if (settings != nullptr) *settings = parse_document_settings(document); + return true; +} + +int legacy_color_index(const RunStyle& style) { + if (style.auto_color) return 0; + const int red = GetRValue(style.color); + const int green = GetGValue(style.color); + const int blue = GetBValue(style.color); + const int maximum = (std::max)({red, green, blue}); + const int minimum = (std::min)({red, green, blue}); + if (maximum < 64 || maximum - minimum < 32) { + return maximum > 208 ? 8 : 1; + } + const bool high_red = red * 5 >= maximum * 3; + const bool high_green = green * 5 >= maximum * 3; + const bool high_blue = blue * 5 >= maximum * 3; + if (high_red && high_green && !high_blue) return 7; + if (high_red && high_blue && !high_green) return 5; + if (high_green && high_blue && !high_red) return 3; + if (maximum == red) return 6; + if (maximum == green) return 4; + return 2; +} + +void apply_legacy_color(const int color_index, RunStyle& style) { + static constexpr std::array colors = { + RGB(0, 0, 0), RGB(0, 0, 0), RGB(0, 0, 255), + RGB(0, 255, 255), RGB(0, 128, 0), RGB(255, 0, 255), + RGB(255, 0, 0), RGB(255, 255, 0), RGB(255, 255, 255)}; + style.auto_color = color_index <= 0 || + color_index >= static_cast(colors.size()); + style.color = style.auto_color ? RGB(0, 0, 0) : colors[color_index]; +} + std::string ansi_text_to_rtf(std::string_view text) { std::string rtf = "{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Arial;}}\\f0\\fs24 "; @@ -793,7 +1263,9 @@ struct PdfLineRun { struct PdfLine { std::vector runs; double width = 0.0; - double largest_font = 12.0; + double largest_font = 0.0; + double left = 72.0; + double writable_width = 468.0; int alignment = PFA_LEFT; }; @@ -870,13 +1342,11 @@ std::string pdf_encoded_text(std::wstring_view text) { 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; + double x = line.left; if (line.alignment == PFA_CENTER) { - x += ((std::max)(0.0, writable_width - line.width)) / 2.0; + x += ((std::max)(0.0, line.writable_width - line.width)) / 2.0; } else if (line.alignment == PFA_RIGHT) { - x += (std::max)(0.0, writable_width - line.width); + x += (std::max)(0.0, line.writable_width - line.width); } std::string underlines; content += "BT\n1 0 0 1 " + pdf_number(x) + " " + @@ -909,49 +1379,107 @@ void append_pdf_line(std::string& content, const PdfLine& line, } 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; + const std::vector& paragraphs, + const DocumentSettings& settings) { + const double page_width = settings.valid ? + static_cast(settings.page_width) / 20.0 : 612.0; + const double page_height = settings.valid ? + static_cast(settings.page_height) / 20.0 : 792.0; + const double margin_left = settings.valid ? + static_cast(settings.margin_left) / 20.0 : 72.0; + const double margin_right = settings.valid ? + static_cast(settings.margin_right) / 20.0 : 72.0; + const double margin_top = settings.valid ? + static_cast(settings.margin_top) / 20.0 : 72.0; + const double margin_bottom = settings.valid ? + static_cast(settings.margin_bottom) / 20.0 : 72.0; + const double top = (std::max)(margin_bottom + 36.0, + page_height - margin_top); + const double bottom = (std::max)(18.0, margin_bottom); 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)); - }; + bool first_paragraph = true; for (const Paragraph& paragraph : paragraphs) { - line.alignment = paragraph.alignment; + if (paragraph.page_break_before && !first_paragraph) begin_new_page(); + first_paragraph = false; + cursor -= (std::max)(0.0, + static_cast(paragraph.space_before) / 20.0); + if (cursor <= bottom + 12.0) begin_new_page(); + + PdfLine line; + bool first_visual_line = true; + const auto reset_line = [&]() { + const double first_indent = first_visual_line ? + static_cast(paragraph.first_line_indent) / 20.0 : 0.0; + line.alignment = paragraph.alignment; + line.left = margin_left + + static_cast(paragraph.left_indent) / 20.0 + + first_indent; + const double right = page_width - margin_right - + static_cast(paragraph.right_indent) / 20.0; + line.writable_width = (std::max)(36.0, right - line.left); + }; + reset_line(); + const auto emit_line = [&](const bool force) { + if (!force && line.runs.empty()) return; + const double largest_font = line.largest_font > 0.0 ? + line.largest_font : 12.0; + double line_height = (std::max)(largest_font * 1.20, 4.8); + if (paragraph.line_spacing < 0) { + line_height = (std::max)(4.8, + -static_cast(paragraph.line_spacing) / 20.0); + } else if (paragraph.line_spacing > 0) { + line_height = (std::max)(line_height, + static_cast(paragraph.line_spacing) / 20.0); + } + if (cursor - line_height < bottom) begin_new_page(); + const double baseline = cursor - largest_font; + append_pdf_line(pages.back(), line, baseline); + cursor -= line_height; + first_visual_line = false; + line = PdfLine{}; + reset_line(); + }; + 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 > line.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)); + }; + const auto add_word = [&](std::wstring_view word, + const RunStyle& style) { + double word_width = 0.0; + for (const wchar_t character : word) + word_width += pdf_character_width(character, style); + if (!line.runs.empty() && + line.width + word_width > line.writable_width) { + emit_line(false); + } + for (const wchar_t character : word) + add_character(character, style); + }; + for (const TextRun& run : paragraph.runs) { - for (const wchar_t character : run.text) { + if (run.style.hidden) continue; + for (std::size_t position = 0; position < run.text.size();) { + const wchar_t character = run.text[position]; if (character == L'\f') { emit_line(false); begin_new_page(); @@ -960,25 +1488,52 @@ std::vector pdf_page_contents( } else if (character == L'\t') { for (int index = 0; index < 4; ++index) add_character(L' ', run.style); + } else if (character == L' ') { + if (!line.runs.empty()) add_character(character, run.style); } else if (character >= 0x20) { - add_character(character, run.style); + std::size_t end = position + 1; + while (end < run.text.size() && run.text[end] > L' ' && + run.text[end] != L'\f') ++end; + add_word(std::wstring_view(run.text).substr( + position, end - position), run.style); + position = end; + continue; } + ++position; } } emit_line(true); + if (paragraph.bottom_border) { + const double border_left = margin_left + + static_cast(paragraph.left_indent) / 20.0; + const double border_right = page_width - margin_right - + static_cast(paragraph.right_indent) / 20.0; + pages.back() += "q\n0 0 0 RG\n0.5 w\n" + + pdf_number(border_left) + " " + pdf_number(cursor + 2.0) + + " m\n" + pdf_number(border_right) + " " + + pdf_number(cursor + 2.0) + " l\nS\nQ\n"; + } + cursor -= (std::max)(0.0, + static_cast(paragraph.space_after) / 20.0); } if (pages.empty()) pages.emplace_back(); return pages; } bool write_pdf(const std::wstring& path, - const std::vector& paragraphs) { + const std::vector& paragraphs, + const DocumentSettings& settings = {}) { 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); + const std::vector contents = + pdf_page_contents(paragraphs, settings); + const double page_width = settings.valid ? + static_cast(settings.page_width) / 20.0 : 612.0; + const double page_height = settings.valid ? + static_cast(settings.page_height) / 20.0 : 792.0; constexpr int catalog_object = 1; constexpr int pages_object = 2; constexpr int first_font_object = 3; @@ -1013,7 +1568,8 @@ bool write_pdf(const std::wstring& path, 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] " + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " + + pdf_number(page_width) + " " + pdf_number(page_height) + "] " "/Resources " + resources + " /Contents " + std::to_string(content_object) + " 0 R >>"; objects[content_object] = @@ -1049,7 +1605,9 @@ bool rtf_to_pdf(std::string_view rtf, const std::wstring& path) { write_pdf(path, paragraphs_from_rich_edit(rich)); } -int export_rtf_to_pdf_dialog(HWND owner, std::string_view rtf) { +int export_paragraphs_to_pdf_dialog( + HWND owner, const std::vector& paragraphs, + const DocumentSettings& settings = {}) { wchar_t path[32768] = L"Document.pdf"; static const wchar_t filter[] = L"PDF Files (*.pdf)\0*.pdf\0\0"; OPENFILENAMEW dialog{}; @@ -1071,7 +1629,7 @@ int export_rtf_to_pdf_dialog(HWND owner, std::string_view rtf) { if (!accepted) { return CommDlgExtendedError() == 0 ? -1 : false; } - if (!rtf_to_pdf(rtf, path)) { + if (!write_pdf(path, paragraphs, settings)) { MessageBoxW(owner, L"Word could not create the PDF. Check that the selected folder is writable.", L"Export as PDF", MB_OK | MB_ICONEXCLAMATION); @@ -1080,6 +1638,13 @@ int export_rtf_to_pdf_dialog(HWND owner, std::string_view rtf) { return true; } +int export_rtf_to_pdf_dialog(HWND owner, std::string_view rtf) { + RichEditDocument rich; + if (!rich.load(rtf)) return false; + return export_paragraphs_to_pdf_dialog( + owner, paragraphs_from_rich_edit(rich)); +} + } // namespace extern "C" int OpusModernPathIsDocx(const char* path) { @@ -1087,19 +1652,224 @@ extern "C" int OpusModernPathIsDocx(const char* path) { } 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))); + const char* rtf_path) { + std::vector paragraphs; + return load_docx_paragraphs(docx_path, paragraphs) && + write_bytes(wide_path(rtf_path), paragraphs_to_rtf(paragraphs)); } 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))); + const char* text_path) { + std::vector paragraphs; + std::vector tables; + DocumentSettings settings; + if (!load_docx_paragraphs(docx_path, paragraphs, &settings, &tables)) { + pending_docx_import = {}; + return false; + } + const std::string text = paragraphs_to_text(paragraphs, + &pending_docx_import, &tables); + pending_docx_import.settings = settings; + if (!write_bytes(wide_path(text_path), text)) { + pending_docx_import = {}; + return false; + } + return true; +} + +extern "C" int OpusModernPendingDocxRunCount() { + return static_cast(pending_docx_import.runs.size()); +} + +extern "C" int OpusModernPendingDocxParagraphCount() { + return static_cast(pending_docx_import.paragraphs.size()); +} + +extern "C" int OpusModernPendingDocxTableCount() { + return static_cast(pending_docx_import.tables.size()); +} + +extern "C" int OpusModernGetPendingDocxTable( + const int index, long* cp_first, long* cp_lim, int* first_paragraph, + int* rows, int* columns) { + if (index < 0 || static_cast(index) >= + pending_docx_import.tables.size()) return false; + const PendingTableFormat& record = pending_docx_import.tables[index]; + if (cp_first != nullptr) *cp_first = record.cp_first; + if (cp_lim != nullptr) *cp_lim = record.cp_lim; + if (first_paragraph != nullptr) + *first_paragraph = record.first_paragraph; + if (rows != nullptr) *rows = record.rows; + if (columns != nullptr) *columns = record.columns; + return true; +} + +extern "C" int OpusModernGetPendingDocxParagraphRange( + const int index, long* cp_first, long* cp_lim) { + if (index < 0 || static_cast(index) >= + pending_docx_import.paragraphs.size()) return false; + const PendingParagraphFormat& record = + pending_docx_import.paragraphs[index]; + if (cp_first != nullptr) *cp_first = record.cp_first; + if (cp_lim != nullptr) *cp_lim = record.cp_lim; + return true; +} + +extern "C" int OpusModernGetPendingDocxPage( + int* page_width, int* page_height, int* margin_left, int* margin_right, + int* margin_top, int* margin_bottom) { + const DocumentSettings& settings = pending_docx_import.settings; + if (!settings.valid) return false; + if (page_width != nullptr) *page_width = settings.page_width; + if (page_height != nullptr) *page_height = settings.page_height; + if (margin_left != nullptr) *margin_left = settings.margin_left; + if (margin_right != nullptr) *margin_right = settings.margin_right; + if (margin_top != nullptr) *margin_top = settings.margin_top; + if (margin_bottom != nullptr) *margin_bottom = settings.margin_bottom; + return true; +} + +extern "C" int OpusModernGetPendingDocxRun( + const int index, long* cp_first, long* cp_lim, int* bold, int* italic, + int* underline, int* strike, int* small_caps, int* all_caps, int* hidden, + int* half_points, int* color_index, char* font, const int font_capacity) { + if (index < 0 || static_cast(index) >= + pending_docx_import.runs.size()) return false; + const PendingRunFormat& record = pending_docx_import.runs[index]; + if (cp_first != nullptr) *cp_first = record.cp_first; + if (cp_lim != nullptr) *cp_lim = record.cp_lim; + if (bold != nullptr) *bold = record.style.bold; + if (italic != nullptr) *italic = record.style.italic; + if (underline != nullptr) *underline = record.style.underline; + if (strike != nullptr) *strike = record.style.strike; + if (small_caps != nullptr) *small_caps = record.style.small_caps; + if (all_caps != nullptr) *all_caps = record.style.all_caps; + if (hidden != nullptr) *hidden = record.style.hidden; + if (half_points != nullptr) *half_points = record.style.half_points; + if (color_index != nullptr) *color_index = legacy_color_index(record.style); + if (font != nullptr && font_capacity > 0) { + const std::string ansi_font = wide_to_ansi(record.style.font); + lstrcpynA(font, ansi_font.c_str(), font_capacity); + } + return true; +} + +extern "C" int OpusModernGetPendingDocxParagraph( + const int index, long* cp_first, long* cp_lim, int* alignment, + int* left_indent, int* right_indent, int* first_line_indent, + int* space_before, int* space_after, int* line_spacing, + int* keep_together, int* keep_with_next, int* page_break_before, + int* bottom_border) { + if (index < 0 || static_cast(index) >= + pending_docx_import.paragraphs.size()) return false; + const PendingParagraphFormat& record = + pending_docx_import.paragraphs[index]; + if (cp_first != nullptr) *cp_first = record.cp_first; + if (cp_lim != nullptr) *cp_lim = record.cp_lim; + if (alignment != nullptr) { + *alignment = record.paragraph.alignment == PFA_CENTER ? 1 : + record.paragraph.alignment == PFA_RIGHT ? 2 : + record.paragraph.alignment == PFA_JUSTIFY ? 3 : 0; + } + if (left_indent != nullptr) *left_indent = record.paragraph.left_indent; + if (right_indent != nullptr) *right_indent = record.paragraph.right_indent; + if (first_line_indent != nullptr) + *first_line_indent = record.paragraph.first_line_indent; + if (space_before != nullptr) *space_before = record.paragraph.space_before; + if (space_after != nullptr) *space_after = record.paragraph.space_after; + if (line_spacing != nullptr) *line_spacing = record.paragraph.line_spacing; + if (keep_together != nullptr) *keep_together = record.paragraph.keep_together; + if (keep_with_next != nullptr) *keep_with_next = record.paragraph.keep_with_next; + if (page_break_before != nullptr) + *page_break_before = record.paragraph.page_break_before; + if (bottom_border != nullptr) *bottom_border = record.paragraph.bottom_border; + return true; +} + +extern "C" void OpusModernClearPendingDocxFormatting() { + pending_docx_import = {}; +} + +extern "C" int OpusPdfSnapshotBegin( + const int page_width, const int page_height, const int margin_left, + const int margin_right, const int margin_top, const int margin_bottom) { + pending_pdf_export = {}; + DocumentSettings& settings = pending_pdf_export.settings; + settings.page_width = page_width; + settings.page_height = page_height; + settings.margin_left = (std::max)(0, margin_left); + settings.margin_right = (std::max)(0, margin_right); + settings.margin_top = (std::max)(0, margin_top); + settings.margin_bottom = (std::max)(0, margin_bottom); + settings.valid = page_width > 0 && page_height > 0 && + settings.margin_left + settings.margin_right < page_width && + settings.margin_top + settings.margin_bottom < page_height; + return true; +} + +extern "C" int OpusPdfSnapshotAddParagraph( + const int alignment, const int left_indent, const int right_indent, + const int first_line_indent, const int space_before, + const int space_after, const int line_spacing, + const int keep_together, const int keep_with_next, + const int page_break_before, const int bottom_border) { + Paragraph paragraph; + paragraph.alignment = alignment == 1 ? PFA_CENTER : + alignment == 2 ? PFA_RIGHT : + alignment == 3 ? PFA_JUSTIFY : PFA_LEFT; + paragraph.left_indent = left_indent; + paragraph.right_indent = right_indent; + paragraph.first_line_indent = first_line_indent; + paragraph.space_before = space_before; + paragraph.space_after = space_after; + paragraph.line_spacing = line_spacing; + paragraph.keep_together = keep_together != 0; + paragraph.keep_with_next = keep_with_next != 0; + paragraph.page_break_before = page_break_before != 0; + paragraph.bottom_border = bottom_border != 0; + pending_pdf_export.paragraphs.push_back(std::move(paragraph)); + return true; +} + +extern "C" int OpusPdfSnapshotAddRun( + const char* text, const int length, const char* font, + const int half_points, const int bold, const int italic, + const int underline, const int strike, const int small_caps, + const int all_caps, const int hidden, const int color_index) { + if (text == nullptr || length < 0 || + pending_pdf_export.paragraphs.empty()) return false; + RunStyle style; + style.bold = bold != 0; + style.italic = italic != 0; + style.underline = underline != 0; + style.strike = strike != 0; + style.small_caps = small_caps != 0; + style.all_caps = all_caps != 0; + style.hidden = hidden != 0; + style.half_points = half_points >= 8 ? half_points : 20; + if (font != nullptr && *font != '\0') style.font = ansi_to_wide(font); + apply_legacy_color(color_index, style); + std::wstring run_text = ansi_to_wide( + std::string_view(text, static_cast(length))); + if (style.all_caps || style.small_caps) { + for (wchar_t& character : run_text) { + character = static_cast(std::towupper(character)); + } + } + if (style.small_caps && !style.all_caps) { + style.half_points = (std::max)(8, style.half_points * 4 / 5); + } + pending_pdf_export.paragraphs.back().runs.push_back( + {style, std::move(run_text)}); + return true; +} + +extern "C" int OpusPdfSnapshotExportDialog(HWND owner) { + if (pending_pdf_export.paragraphs.empty()) return false; + const int result = export_paragraphs_to_pdf_dialog( + owner, pending_pdf_export.paragraphs, pending_pdf_export.settings); + pending_pdf_export = {}; + return result; } extern "C" int OpusModernRtfFileToDocx(const char* rtf_path, diff --git a/src/port/original/opus_modern_formats_test.cpp b/src/port/original/opus_modern_formats_test.cpp new file mode 100644 index 0000000..ba42c51 --- /dev/null +++ b/src/port/original/opus_modern_formats_test.cpp @@ -0,0 +1,86 @@ +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include + +extern "C" int OpusModernDocxToRtfFile(const char*, const char*); +extern "C" int OpusModernRtfFileToDocx(const char*, const char*); +extern "C" int OpusModernRtfFileToPdf(const char*, const char*); + +int main(const int argument_count, char** arguments) { + if (argument_count == 3) { + const bool converted = + OpusModernDocxToRtfFile(arguments[1], arguments[2]) != 0; + std::cout << "DOCX import " << (converted ? "passed" : "failed") + << '\n'; + return converted ? 0 : 3; + } + if (argument_count != 1) { + std::cerr << "usage: opus_modern_formats_test [input.docx output.rtf]\n"; + return 1; + } + char temporary[MAX_PATH]{}; + char seed[MAX_PATH]{}; + if (GetTempPathA(MAX_PATH, temporary) == 0 || + GetTempFileNameA(temporary, "OWF", 0, seed) == 0) { + return 1; + } + DeleteFileA(seed); + const std::string base = seed; + const std::string rtf = base + ".rtf"; + const std::string docx = base + ".docx"; + const std::string roundtrip = base + ".roundtrip.rtf"; + char requested_pdf[32768]{}; + const DWORD requested_pdf_length = GetEnvironmentVariableA( + "WORD1_TEST_KEEP_PDF", requested_pdf, + static_cast(std::size(requested_pdf))); + const bool keep_pdf = requested_pdf_length > 0 && + requested_pdf_length < std::size(requested_pdf); + const std::string pdf = keep_pdf ? requested_pdf : base + ".pdf"; + DeleteFileA(pdf.c_str()); + { + std::ofstream output(rtf, std::ios::binary); + output << "{\\rtf1\\ansi{\\fonttbl{\\f0 Arial;}}" + "{\\colortbl;\\red255\\green0\\blue0;}" + "\\f0\\fs24 Plain {\\b Bold} {\\i Italic} " + "{\\ul Underline} {\\cf1 Red}\\par Second paragraph}"; + } + const bool written = OpusModernRtfFileToDocx(rtf.c_str(), docx.c_str()) != 0; + const bool read = written && + OpusModernDocxToRtfFile(docx.c_str(), roundtrip.c_str()) != 0; + const bool pdf_written = OpusModernRtfFileToPdf(rtf.c_str(), pdf.c_str()) != 0; + std::string result; + if (read) { + std::ifstream input(roundtrip, std::ios::binary); + result.assign(std::istreambuf_iterator(input), {}); + } + std::string pdf_data; + if (pdf_written) { + std::ifstream input(pdf, std::ios::binary); + pdf_data.assign(std::istreambuf_iterator(input), {}); + } + DeleteFileA(rtf.c_str()); + DeleteFileA(docx.c_str()); + DeleteFileA(roundtrip.c_str()); + if (!keep_pdf) DeleteFileA(pdf.c_str()); + if (!written || !read || result.find("Bold") == std::string::npos || + result.find("Second paragraph") == std::string::npos || + result.find("\\b") == std::string::npos || + result.find("\\cf1") == std::string::npos || !pdf_written || + !pdf_data.starts_with("%PDF-") || + pdf_data.find("Plain") == std::string::npos || + pdf_data.find("Bold") == std::string::npos || + pdf_data.find("Second paragraph") == std::string::npos || + pdf_data.find("/Helvetica-Bold") == std::string::npos || + pdf_data.find("xref") == std::string::npos || + pdf_data.find("%%EOF") == std::string::npos) { + std::cerr << "DOCX round trip failed: write=" << written + << " read=" << read << " pdf=" << pdf_written + << " bytes=" << result.size() << '\n'; + return 2; + } + std::cout << "DOCX round trip passed (" << result.size() << " RTF bytes)\n"; + return 0; +} diff --git a/src/port/original/opus_win95_chrome.cpp b/src/port/original/opus_win95_chrome.cpp index 8e07c17..63c061e 100644 --- a/src/port/original/opus_win95_chrome.cpp +++ b/src/port/original/opus_win95_chrome.cpp @@ -1582,6 +1582,72 @@ void draw_neighbor_sheet(HDC dc, const RECT& sheet, const RECT& client, DeleteObject(border_brush); } +void draw_continuous_page_workspace(HWND pane, HDC dc) { + int page_left = 0; + int page_top = 0; + int page_right = 0; + int page_bottom = 0; + int page_gap = 0; + int has_previous = 0; + int has_next = 0; + if (!OpusGetWin95ContinuousPageMetrics( + pane, &page_left, &page_top, &page_right, &page_bottom, + &page_gap, &has_previous, &has_next)) { + return; + } + + RECT client{}; + GetClientRect(pane, &client); + const int page_height = page_bottom - page_top; + if (page_right <= page_left || page_height <= 0) { + return; + } + + /* The original Page View eraser assumes that the page consumes the + window width. In the continuous-page shell that can leave white + invalidation bands across the gray inter-page gap. Restore the + workspace after the legacy paint, excluding only real sheets, before + drawing cached neighbor pages. */ + const int saved = SaveDC(dc); + IntersectClipRect(dc, client.left, client.top, + client.right, client.bottom); + const int page_step = page_height + page_gap; + const int offsets[] = { + 0, + has_previous ? -page_step : 0, + has_next ? page_step : 0}; + const int first = has_previous ? 0 : 1; + const int last = has_next ? 3 : 2; + for (int index = first; index < last; ++index) { + const RECT sheet{ + page_left, page_top + offsets[index], + page_right, page_bottom + offsets[index]}; + ExcludeClipRect(dc, sheet.left, sheet.top, + sheet.right, sheet.bottom); + } + FillRect(dc, &client, GetSysColorBrush(COLOR_APPWORKSPACE)); + RestoreDC(dc, saved); + + /* Rebuild the current sheet's shadow after cleaning the surrounding + workspace. Clip the shadow away from the page so it cannot cover + document pixels that the legacy formatter just drew. */ + const RECT current{page_left, page_top, page_right, page_bottom}; + RECT shadow = current; + OffsetRect(&shadow, 3, 3); + const int shadow_saved = SaveDC(dc); + IntersectClipRect(dc, shadow.left, shadow.top, + shadow.right, shadow.bottom); + ExcludeClipRect(dc, current.left, current.top, + current.right, current.bottom); + HBRUSH shadow_brush = CreateSolidBrush(RGB(96, 96, 96)); + FillRect(dc, &shadow, shadow_brush); + DeleteObject(shadow_brush); + RestoreDC(dc, shadow_saved); + HBRUSH border_brush = CreateSolidBrush(RGB(96, 96, 96)); + FrameRect(dc, ¤t, border_brush); + DeleteObject(border_brush); +} + void draw_continuous_page_neighbors(HWND pane, HDC dc) { int page_left = 0; int page_top = 0; @@ -2404,6 +2470,7 @@ LRESULT CALLBACK document_pane_proc(HWND pane, UINT message, HDC dc = GetDC(pane); if (dc != nullptr) { update_current_page_snapshot(pane, dc); + draw_continuous_page_workspace(pane, dc); draw_continuous_page_neighbors(pane, dc); draw_vertical_ruler(pane, dc); ReleaseDC(pane, dc); diff --git a/src/port/original/opus_word1_ui_test.cpp b/src/port/original/opus_word1_ui_test.cpp index 5791ab6..056531f 100644 --- a/src/port/original/opus_word1_ui_test.cpp +++ b/src/port/original/opus_word1_ui_test.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -569,6 +571,62 @@ bool window_is_responsive(const HANDLE process, const HWND window) { 2000, &message_result) != 0; } +bool capture_window_bmp(const HWND window, const wchar_t* path) { + RECT bounds{}; + if (window == nullptr || path == nullptr || !GetWindowRect(window, &bounds)) + return false; + const int width = bounds.right - bounds.left; + const int height = bounds.bottom - bounds.top; + if (width <= 0 || height <= 0) return false; + HDC screen = GetDC(nullptr); + HDC memory = screen != nullptr ? CreateCompatibleDC(screen) : nullptr; + HBITMAP bitmap = memory != nullptr ? + CreateCompatibleBitmap(screen, width, height) : nullptr; + HGDIOBJ previous = bitmap != nullptr ? SelectObject(memory, bitmap) : nullptr; + const bool copied = bitmap != nullptr && + BitBlt(memory, 0, 0, width, height, screen, bounds.left, bounds.top, + SRCCOPY | CAPTUREBLT) != FALSE; + BITMAPINFO info{}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = width; + info.bmiHeader.biHeight = -height; + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + std::vector pixels( + static_cast(width) * height * 4); + const bool read = copied && GetDIBits( + memory, bitmap, 0, height, pixels.data(), &info, DIB_RGB_COLORS) != 0; + bool written = false; + if (read) { + BITMAPFILEHEADER file_header{}; + file_header.bfType = 0x4d42; + file_header.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); + file_header.bfSize = file_header.bfOffBits + + static_cast(pixels.size()); + HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file != INVALID_HANDLE_VALUE) { + DWORD count = 0; + written = WriteFile(file, &file_header, sizeof(file_header), + &count, nullptr) && + count == sizeof(file_header) && + WriteFile(file, &info.bmiHeader, + sizeof(info.bmiHeader), &count, nullptr) && + count == sizeof(info.bmiHeader) && + WriteFile(file, pixels.data(), + static_cast(pixels.size()), &count, + nullptr) && count == pixels.size(); + CloseHandle(file); + } + } + if (previous != nullptr) SelectObject(memory, previous); + if (bitmap != nullptr) DeleteObject(bitmap); + if (memory != nullptr) DeleteDC(memory); + if (screen != nullptr) ReleaseDC(nullptr, screen); + return written; +} + int fail(PROCESS_INFORMATION& process, const int code, const char* message) { std::cerr << message << '\n'; @@ -596,7 +654,8 @@ int wmain(const int argument_count, wchar_t** arguments) { "usage: opus_word1_ui_test WORD1.exe " "[--typing|--interaction|--selection|--caret|--formatting|--color|" "--font-typing|--clipboard|--about|--save-as|--pdf-export|" - "--docx-open FILE]\n"; + "--docx-open FILE|--docx-format-open FILE|" + "--docx-pdf-export FILE]\n"; return 1; } const bool typing_mode = @@ -631,9 +690,17 @@ int wmain(const int argument_count, wchar_t** arguments) { const bool pdf_export_mode = argument_count == 3 && std::wcscmp(arguments[2], L"--pdf-export") == 0; + const bool docx_pdf_export_mode = + argument_count == 4 && + std::wcscmp(arguments[2], L"--docx-pdf-export") == 0; + const bool formatted_docx_open_mode = + argument_count == 4 && + (std::wcscmp(arguments[2], L"--docx-format-open") == 0 || + docx_pdf_export_mode); const bool docx_open_mode = argument_count == 4 && - std::wcscmp(arguments[2], L"--docx-open") == 0; + (std::wcscmp(arguments[2], L"--docx-open") == 0 || + formatted_docx_open_mode); if (argument_count == 3 && !typing_mode && !interaction_mode && !selection_mode && !caret_mode && !formatting_mode && !color_mode && !font_typing_mode && !clipboard_mode && !about_mode && @@ -648,6 +715,7 @@ int wmain(const int argument_count, wchar_t** arguments) { std::string docx_path; std::string pdf_path; + bool keep_pdf = false; if (docx_open_mode) { const int path_size = WideCharToMultiByte( CP_ACP, 0, arguments[3], -1, nullptr, 0, nullptr, nullptr); @@ -668,7 +736,18 @@ int wmain(const int argument_count, wchar_t** arguments) { SetEnvironmentVariableA("WORD1_TEST_FILE_DIALOG_PATH", docx_path.c_str()); } - if (pdf_export_mode) { + if (pdf_export_mode || docx_pdf_export_mode) { + char requested_pdf[32768]{}; + const DWORD requested_length = GetEnvironmentVariableA( + "WORD1_TEST_KEEP_PDF", requested_pdf, + static_cast(std::size(requested_pdf))); + keep_pdf = requested_length > 0 && + requested_length < std::size(requested_pdf); + if (keep_pdf) { + pdf_path = requested_pdf; + DeleteFileA(pdf_path.c_str()); + SetEnvironmentVariableA("WORD1_TEST_PDF_PATH", pdf_path.c_str()); + } else { char temporary_directory[MAX_PATH]{}; char temporary_seed[MAX_PATH]{}; if (GetTempPathA(static_cast(std::size(temporary_directory)), @@ -681,6 +760,7 @@ int wmain(const int argument_count, wchar_t** arguments) { 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"\""; @@ -733,7 +813,7 @@ int wmain(const int argument_count, wchar_t** arguments) { << (pane != nullptr ? SendMessageW( pane, kWmOpusX64QuerySelection, 41, 0) : 0) << '\n'; - DeleteFileA(pdf_path.c_str()); + if (!keep_pdf) DeleteFileA(pdf_path.c_str()); return fail(process, 85, "PDF export test could not prepare its document"); } @@ -761,7 +841,7 @@ int wmain(const int argument_count, wchar_t** arguments) { WaitForSingleObject(process.hProcess, 2000); CloseHandle(process.hThread); CloseHandle(process.hProcess); - DeleteFileA(pdf_path.c_str()); + if (!keep_pdf) DeleteFileA(pdf_path.c_str()); if (!pdf_written) { std::cerr << "PDF export failed or crashed: exit=0x" << std::hex << exit_code << std::dec << '\n'; @@ -845,16 +925,87 @@ int wmain(const int argument_count, wchar_t** arguments) { SendMessageW(pane, kWmOpusX64QuerySelection, 41, 0) : 0; const LRESULT displayed_lines = pane != nullptr ? SendMessageW(pane, kWmOpusX64QuerySelection, 30, 0) : 0; + const LRESULT title_hps = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 52, 0) : -1; + const LRESULT title_bold = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 87, 0) : -1; + const LRESULT title_color = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 90, 0) : -1; + const LRESULT title_alignment = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 91, 0) : -1; + const LRESULT page_width = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 97, 0) : -1; + const LRESULT page_height = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 98, 0) : -1; + const LRESULT left_margin = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 99, 0) : -1; + const LRESULT top_margin = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 101, 0) : -1; + const LRESULT body_font_index = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 103, 20) : -1; + const LRESULT body_font_charset = pane != nullptr ? + SendMessageW(pane, kWmOpusX64QuerySelection, 104, 20) : -1; + wchar_t capture_path[32768]{}; + const DWORD capture_length = GetEnvironmentVariableW( + L"WORD1_TEST_CAPTURE_PATH", capture_path, + static_cast(std::size(capture_path))); + if (capture_length > 0 && capture_length < std::size(capture_path)) { + ShowWindow(main_window, SW_RESTORE); + SetForegroundWindow(main_window); + UpdateWindow(main_window); + Sleep(2000); + if (!capture_window_bmp(main_window, capture_path)) { + return fail(process, 84, "DOCX capture failed"); + } + } + bool rich_pdf_valid = !docx_pdf_export_mode; + if (docx_pdf_export_mode && + PostMessageW(main_window, kWmCommand, kExportPdf, 0)) { + const ULONGLONG pdf_deadline = GetTickCount64() + 15000; + do { + std::ifstream input(pdf_path, std::ios::binary); + if (input) { + const std::string pdf_data( + std::istreambuf_iterator(input), {}); + rich_pdf_valid = pdf_data.starts_with("%PDF-") && + pdf_data.find("JUSTIN MARSHALL") != std::string::npos && + pdf_data.find("/Helvetica-Bold") != std::string::npos && + pdf_data.find("0 0 1 rg") != std::string::npos && + pdf_data.find("/MediaBox [0 0 612 792]") != + std::string::npos; + } + if (!rich_pdf_valid) Sleep(100); + } while (!rich_pdf_valid && GetTickCount64() < pdf_deadline && + WaitForSingleObject(process.hProcess, 0) != WAIT_OBJECT_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) { + displayed_lines < 1 || + (formatted_docx_open_mode && + (title_hps < 40 || title_bold != 1 || title_color != 2 || + title_alignment != 1 || page_width != 12240 || + page_height != 15840 || left_margin != 835 || + top_margin != 691 || body_font_index != 2 || + body_font_charset != ANSI_CHARSET)) || !rich_pdf_valid) { std::cerr << "DOCX open state: length=" << imported_length - << " displayedLines=" << displayed_lines << '\n'; + << " displayedLines=" << displayed_lines + << " title={hps:" << title_hps + << ",bold:" << title_bold + << ",color:" << title_color + << ",alignment:" << title_alignment << "}" + << " page={" << page_width << "x" << page_height + << ",left:" << left_margin << ",top:" << top_margin + << "} bodyFont={index:" << body_font_index + << ",charset:" << body_font_charset << "}\n"; + if (docx_pdf_export_mode) + std::cerr << "richPdf=" << rich_pdf_valid << '\n'; + if (!keep_pdf && !pdf_path.empty()) DeleteFileA(pdf_path.c_str()); return 84; } + if (!keep_pdf && !pdf_path.empty()) DeleteFileA(pdf_path.c_str()); return 0; } if (about_mode) {