diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bd45655..0f05267 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -865,7 +865,7 @@ target_compile_features(WORD1 PRIVATE cxx_std_20) target_compile_definitions(WORD1 PRIVATE WIN WIN23 CRLF NONATIVE OPUS_X64 NOMINMAX UNICODE _UNICODE ) -target_link_libraries(WORD1 PRIVATE opus_original_engine opus_x64_runtime user32 dbghelp) +target_link_libraries(WORD1 PRIVATE opus_original_engine opus_x64_runtime user32 imm32 dbghelp) if(MSVC) target_compile_options(WORD1 PRIVATE /J /W3 /wd4005 /utf-8 /guard:cf /sdl) target_link_options(WORD1 PRIVATE /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT /guard:cf /CETCOMPAT /MANIFEST:NO) @@ -1006,6 +1006,10 @@ add_test(NAME opus_word1_clipboard_shortcut_test COMMAND $ $ --clipboard ) set_tests_properties(opus_word1_clipboard_shortcut_test PROPERTIES TIMEOUT 20) +add_test(NAME opus_word1_unicode_test + COMMAND $ $ --unicode +) +set_tests_properties(opus_word1_unicode_test PROPERTIES TIMEOUT 20) add_test(NAME opus_word1_typing_test COMMAND $ $ --typing ) diff --git a/src/Opus/CLIPBORD.C b/src/Opus/CLIPBORD.C index 656ff55..c4fd744 100644 --- a/src/Opus/CLIPBORD.C +++ b/src/Opus/CLIPBORD.C @@ -479,6 +479,7 @@ ChangeClipboard() if (vsab.fOwnClipboard) { /* only set handles if we really have something in docScrap */ SetClipboardData( CF_OWNERDISPLAY, NULL ); + SetClipboardData( CF_UNICODETEXT, NULL ); /* don't put out cf_tiff or other rejected pic types, or rtf or link for any of those types */ diff --git a/src/Opus/CLIPBRD2.C b/src/Opus/CLIPBRD2.C index b7cc134..adfb743 100644 --- a/src/Opus/CLIPBRD2.C +++ b/src/Opus/CLIPBRD2.C @@ -70,6 +70,11 @@ extern char szEmpty[]; extern struct SCI vsci; extern int vwWinVersion; extern int wwCur; +#ifdef OPUS_X64 +extern HANDLE OpusUnicodeCreateClipboardHandle(); +extern int OpusUnicodeClipboardToLegacy(); +extern int OpusUnicodeBindPendingClipboard(); +#endif extern LPCH LpchIncr(); @@ -279,6 +284,21 @@ int *pfBlankPic; /* set true if an empty picture; else ignored */ AssureLegalSel (PcaSet(&ca, doc, cpFirst, cpLim)); switch (cf) { + case CF_UNICODETEXT: +#ifdef OPUS_X64 + { + HANDLE hAnsi; + HANDLE hUnicode; + hAnsi = HWriteText(ca.doc, ca.cpFirst, ca.cpLim, cbInitial); + if (hAnsi == NULL) + return NULL; + hUnicode = OpusUnicodeCreateClipboardHandle(ca.doc, ca.cpFirst, hAnsi); + GlobalFree(hAnsi); + return hUnicode; + } +#else + return NULL; +#endif case CF_TEXT: return HWriteText (ca.doc, ca.cpFirst, ca.cpLim, cbInitial); @@ -845,7 +865,7 @@ int FReadExtScrap() { vsab.fMayHavePic = vsab.fPict = fTrue; } - if (cf != CF_TEXT) + if (cf != CF_TEXT && cf != CF_UNICODETEXT) vsab.fFormatted = fTrue; break; } @@ -884,6 +904,8 @@ int cbInitial; { int docDest; BOOL fOk; + BOOL fUnicode = fFalse; + HANDLE hConverted = NULL; struct CA ca; #ifdef BZ @@ -892,6 +914,18 @@ int cbInitial; switch (cf) { + case CF_UNICODETEXT: +#ifdef OPUS_X64 + if (!OpusUnicodeClipboardToLegacy(hData, &hConverted) || + hConverted == NULL) + return fFalse; + hData = hConverted; + cf = CF_TEXT; + fUnicode = fTrue; + break; +#else + return fFalse; +#endif case CF_TEXT: case CF_DIB: case CF_BITMAP: @@ -917,7 +951,11 @@ int cbInitial; docDest = docScrap; /* otherwise use vdocScratch */ else if ((docDest = DocCreateScratch (pca->doc)) == docNil) + { + if (hConverted != NULL) + GlobalFree(hConverted); return fFalse; + } /* empty the destination doc and set styles, fonts to standard */ SetWholeDoc (docDest, PcaSetNil(&ca)); @@ -939,7 +977,11 @@ int cbInitial; break; case CF_TEXT: fOk = FReadText (docDest, cf, hData, cbInitial); + if (fOk && fUnicode) + fOk = OpusUnicodeBindPendingClipboard(docDest); } + if (hConverted != NULL) + GlobalFree(hConverted); fOk &= (!vmerr.fMemFail && !vmerr.fDiskFail); @@ -1843,7 +1885,8 @@ int cf; BOOL fPrPic; { - /* Desired order (win3 word: Notice text before picture formats): + /* Desired order (Unicode text precedes legacy formats): + CF_UNICODETEXT cfRTF CF_TEXT cfPRPic @@ -1876,7 +1919,9 @@ BOOL fPrPic; goto LMetafile; /* 4th or 5th choice */ case cfNil: - return cfRTF; /* 1st choice */ + return CF_UNICODETEXT; /* 1st choice */ + case CF_UNICODETEXT: + return cfRTF; case CF_TEXT: if (fPrPic && cfPrPic != cfNil) return cfPrPic; /* 4th choice (Excel specific) */ @@ -1915,6 +1960,7 @@ BOOL fRestrictRTF; switch (cf) { + case CF_UNICODETEXT: case CF_TEXT: /* reject text for either a normal pic or an import field pic, since it has no text result and would show nothing */ diff --git a/src/Opus/CREATE2.C b/src/Opus/CREATE2.C index 0379b2f..fc4e459 100644 --- a/src/Opus/CREATE2.C +++ b/src/Opus/CREATE2.C @@ -813,6 +813,9 @@ int doc; int isels; struct DOD *pdod, dod; extern struct PL **vhplbmc; +#ifdef OPUS_X64 + extern void OpusUnicodeForgetDocument(); +#endif Assert(doc >= 0 && doc < docMac); Assert(doc == docNil || mpdochdod[doc] != hNil); @@ -848,6 +851,9 @@ int doc; vdocTemp = docNil; else if (doc == vdocScratch) vdocScratch = docNil; +#ifdef OPUS_X64 + OpusUnicodeForgetDocument(doc); +#endif DisposeDocPdod(doc, &dod ); Assert (vdocScratch == docNil || PdodDoc(vdocScratch)->doc diff --git a/src/Opus/EDIT.C b/src/Opus/EDIT.C index baf8ce7..a2d4c42 100644 --- a/src/Opus/EDIT.C +++ b/src/Opus/EDIT.C @@ -73,6 +73,8 @@ BOOL vfNoInval=0; /* can be > 1 */ #ifdef OPUS_X64 struct PLC **HplcCreateEdc(); +extern void OpusUnicodeOnReplace(); +extern void OpusUnicodeOnReplaceCps(); #else struct PLC *HplcCreateEdc(); #endif @@ -463,6 +465,9 @@ struct CA *pca; int fn; FC fc, dfc; { + int docUnicode = pca->doc; + CP cpFirstUnicode = pca->cpFirst; + CP cpLimUnicode = pca->cpLim; struct XBC xbc; struct XSR *pxsr; /* this should be the maximum number of calls to XReplace or XReplaceCps that @@ -489,6 +494,7 @@ should go off) */ XReplace(fFalse, &xbc, pxsr); EndCommit(); CloseTns(&xbc); + OpusUnicodeOnReplace(docUnicode, cpFirstUnicode, cpLimUnicode, dfc); return fTrue; } @@ -821,6 +827,12 @@ on failure (in which case document is unchanged) */ HANDNATIVE BOOL C_FReplaceCps(pcaDel, pcaIns) struct CA *pcaDel, *pcaIns; { + int docDelUnicode = pcaDel->doc; + int docInsUnicode = pcaIns->doc; + CP cpDelFirstUnicode = pcaDel->cpFirst; + CP cpDelLimUnicode = pcaDel->cpLim; + CP cpInsFirstUnicode = pcaIns->cpFirst; + CP cpInsLimUnicode = pcaIns->cpLim; struct XBC xbc; struct XSR *pxsr; /* this should be the maximum number of calls to XReplace or XReplaceCps that @@ -848,6 +860,9 @@ should go off). Win is greater than Mac because of annotations. */ XReplaceCps(fFalse, &xbc, pxsr); EndCommit(); CloseTns(&xbc); + OpusUnicodeOnReplaceCps(docDelUnicode, cpDelFirstUnicode, + cpDelLimUnicode, docInsUnicode, cpInsFirstUnicode, + cpInsLimUnicode); return fTrue; } diff --git a/src/Opus/command2.c b/src/Opus/command2.c index e533cc4..00b86ba 100644 --- a/src/Opus/command2.c +++ b/src/Opus/command2.c @@ -1808,6 +1808,7 @@ BOOL fPasteLink; { if ((fPasteLink && fmt == cfLink) || (!fPasteLink && (fmt == cfRTF || + fmt == CF_UNICODETEXT || fmt == CF_TEXT || fmt == CF_BITMAP || fmt == CF_METAFILEPICT))) diff --git a/src/Opus/disp1.c b/src/Opus/disp1.c index 8c07ff7..e9d30c1 100644 --- a/src/Opus/disp1.c +++ b/src/Opus/disp1.c @@ -128,6 +128,10 @@ extern struct DBS vdbs; extern struct PREF vpref; extern struct PRI vpri; +#ifdef OPUS_X64 +extern int OpusUnicodeExtTextOut(); +extern int OpusUnicodeHasRange(); +#endif extern int vlm; extern int vfPrvwDisp; @@ -1256,13 +1260,26 @@ LDacNop: else { LRealExtTextOut: - ExtTextOut(hdc, xp, yp, - eto, /* action bits */ - fRcAdjusted ? (LPRECT)&rcAdjusted : - (LPRECT)&rcOpaque, /* opaque rect */ - (LPCH)&vfli.rgch [ich], /* string */ - cch, /* length */ - lpdxp ); /* lpdx */ +#ifdef OPUS_X64 + ExtTextOut(hdc, xp, yp, eto, + fRcAdjusted ? (LPRECT)&rcAdjusted : + (LPRECT)&rcOpaque, + (LPCH)&vfli.rgch [ich], cch, lpdxp); + if (OpusUnicodeHasRange(vfli.doc, + vfli.cpMin + dcpVanish + ich, cch)) + OpusUnicodeExtTextOut(hdc, xp, yp, eto, + fRcAdjusted ? (LPRECT)&rcAdjusted : + (LPRECT)&rcOpaque, + vfli.doc, + vfli.cpMin + dcpVanish + ich, + (LPCH)&vfli.rgch [ich], cch, lpdxp); +#else + ExtTextOut(hdc, xp, yp, + eto, + fRcAdjusted ? (LPRECT)&rcAdjusted : + (LPRECT)&rcOpaque, + (LPCH)&vfli.rgch [ich], cch, lpdxp); +#endif } vrf.fInExternalCall = fFalse; (char *) pchr = (char *)*vhgrpchr + bchrCur; diff --git a/src/Opus/idle.c b/src/Opus/idle.c index 1729442..3dca694 100644 --- a/src/Opus/idle.c +++ b/src/Opus/idle.c @@ -1265,6 +1265,15 @@ LHaveEvent: return fFalse; } +#ifdef OPUS_X64 + /* End the legacy one-byte insertion batch before a UTF-16 WM_CHAR is + consumed. The outer message loop will pass it to the Unicode bridge + with the full code unit intact. */ + if (vmsgLast.message == WM_CHAR && + (unsigned int)vmsgLast.wParam >= 0x100) + return fFalse; +#endif + if (FIsKeyMessage((LPMSG) &vmsgLast)) { if (vmsgLast.wParam & 0x8000) /* indicates we did xlation */ diff --git a/src/Opus/openrare.c b/src/Opus/openrare.c index ea618d1..527028d 100644 --- a/src/Opus/openrare.c +++ b/src/Opus/openrare.c @@ -91,6 +91,7 @@ extern int OpusModernPendingDocxParagraphCount(); extern int OpusModernGetPendingDocxRun(); extern int OpusModernGetPendingDocxParagraph(); extern int OpusModernGetPendingDocxPage(); +extern int OpusModernBindPendingDocxUnicode(); extern void OpusModernClearPendingDocxFormatting(); extern int OpusX64FtcFromFontNameForDoc(); #endif @@ -128,8 +129,9 @@ int doc; struct CA ca; char grpprl[96]; char szFont[LF_FACESIZE]; + char szLanguage[32]; int bold, italic, underline, strike, smallCaps, allCaps, hidden; - int hps, ico; + int hps, ico, charset; int jc, dxaLeft, dxaRight, dxaLeft1, dyaBefore, dyaAfter, dyaLine; int fKeep, fKeepFollow, fPageBreakBefore, fBottomBorder; int xaPage, yaPage, dxaLeftPage, dxaRightPage, dyaTopPage, dyaBottomPage; @@ -195,7 +197,8 @@ int doc; { if (!OpusModernGetPendingDocxRun(i, &cpFirst, &cpLim, &bold, &italic, &underline, &strike, &smallCaps, &allCaps, - &hidden, &hps, &ico, szFont, sizeof(szFont))) + &hidden, &hps, &ico, szFont, sizeof(szFont), &charset, + szLanguage, sizeof(szLanguage))) continue; if (cpFirst < cp0 || cpFirst >= cpMac) continue; @@ -214,7 +217,7 @@ int doc; 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); + ftc = OpusX64FtcFromFontNameForDoc(doc, szFont, charset); if (ftc >= 0) cb = CbAppendDocxPrl(grpprl, cb, sizeof(grpprl), sprmCFtc, ftc); ca.doc = doc; @@ -224,6 +227,7 @@ int doc; } PdodDoc(doc)->fFormatted = fTrue; + OpusModernBindPendingDocxUnicode(doc); OpusModernClearPendingDocxFormatting(); } #endif diff --git a/src/Opus/rulerdrw.c b/src/Opus/rulerdrw.c index 9198485..2ad6188 100644 --- a/src/Opus/rulerdrw.c +++ b/src/Opus/rulerdrw.c @@ -105,6 +105,24 @@ static int vWin95ZoomPercent = 100; static int vWin95BaseDxsInch = 0; static int vWin95BaseDysInch = 0; int vOpusPdfExportStage = 0; +static int vOpusDocxSnapshotDoc = docNil; +static const char *vszOpusDocxSnapshotPath = NULL; + +int OpusGetUnicodeSelection(pdoc, pcpFirst, pcpLim) +int *pdoc; +long *pcpFirst; +long *pcpLim; +{ + if (selCur.doc == docNil) + return fFalse; + if (pdoc != NULL) + *pdoc = selCur.doc; + if (pcpFirst != NULL) + *pcpFirst = selCur.cpFirst; + if (pcpLim != NULL) + *pcpLim = selCur.cpLim; + return fTrue; +} int OpusExportCurrentDocumentPdf() { @@ -117,14 +135,19 @@ int OpusExportCurrentDocumentPdf() extern CHAR HUGE *vhpchFetch; extern int OpusPdfSnapshotBegin(); extern int OpusPdfSnapshotAddParagraph(); - extern int OpusPdfSnapshotAddRun(); + extern int OpusPdfSnapshotAddRunUtf8(); extern int OpusPdfSnapshotExportDialog(); + extern int OpusDocxSnapshotExportPath(); extern void OpusX64FontNameFromFtc(); + extern int OpusX64CharsetFromFtc(); + extern int OpusUnicodeTextToUtf8(); + extern int OpusUnicodeLanguageTagAt(); vOpusPdfExportStage = 1; - if (selCur.doc == docNil) + if (vOpusDocxSnapshotDoc == docNil && selCur.doc == docNil) return fFalse; - doc = DocMother(selCur.doc); + doc = vOpusDocxSnapshotDoc != docNil ? vOpusDocxSnapshotDoc : + DocMother(selCur.doc); cpMac = CpMacDocEdit(doc); if (cpMac < cp0 || cpMac > 0x3fffffffL) return fFalse; @@ -159,43 +182,59 @@ int OpusExportCurrentDocumentPdf() int ich; int cch; int cchOutput; - char rgch[1024]; + char rgch[4096]; char szFont[LF_FACESIZE]; + char szLanguage[32]; + int charset; 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); + cch = min(cch, (sizeof(rgch) - 1) / 4); + charset = OpusX64CharsetFromFtc(doc, vchpFetch.ftc); cchOutput = 0; for (ich = 0; ich < cch; ++ich) { int ch = (unsigned char)vhpchFetch[ich]; + char chOutput; + char rgchUtf8[8]; + int cchUtf8; if (ch == chReturn || ch == chEop) continue; if (ch == chTable || ch == chTab) - rgch[cchOutput++] = '\t'; + chOutput = '\t'; else if (ch == chSect || ch == chColumnBreak) - rgch[cchOutput++] = '\f'; + chOutput = '\f'; else if (ch == chNonReqHyphen) - rgch[cchOutput++] = '-'; + chOutput = '-'; /* 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; + chOutput = (char)ch; + else + continue; + cchUtf8 = OpusUnicodeTextToUtf8(doc, cpRun + ich, + &chOutput, 1, charset, rgchUtf8, sizeof(rgchUtf8)); + if (cchUtf8 < 0 || cchUtf8 > sizeof(rgch) - 1 - cchOutput) + return fFalse; + bltbyte(rgchUtf8, rgch + cchOutput, cchUtf8); + cchOutput += cchUtf8; } if (cchOutput > 0) { rgch[cchOutput] = '\0'; OpusX64FontNameFromFtc(vchpFetch.ftc, szFont, sizeof(szFont)); + OpusUnicodeLanguageTagAt(doc, cpRun, + szLanguage, sizeof(szLanguage)); vOpusPdfExportStage = 100000 + (int)cpRun; - if (!OpusPdfSnapshotAddRun(rgch, cchOutput, szFont, + if (!OpusPdfSnapshotAddRunUtf8(rgch, cchOutput, szFont, vchpFetch.hps, vchpFetch.fBold, vchpFetch.fItalic, vchpFetch.kul != kulNone, vchpFetch.fStrike, vchpFetch.fSmallCaps, vchpFetch.fCaps, vchpFetch.fVanish, - vchpFetch.ico)) + vchpFetch.ico, szLanguage, charset)) return fFalse; } cpRun += cch; @@ -203,11 +242,29 @@ int OpusExportCurrentDocumentPdf() cpPara = cpParaLim; } vOpusPdfExportStage = 3; - result = OpusPdfSnapshotExportDialog(vhwndApp); + result = vszOpusDocxSnapshotPath != NULL ? + OpusDocxSnapshotExportPath(vszOpusDocxSnapshotPath) : + OpusPdfSnapshotExportDialog(vhwndApp); vOpusPdfExportStage = result ? 4 : -4; return result; } +int OpusSaveDocumentAsDocx(doc, szPath) +int doc; +const char *szPath; +{ + int result; + if (doc == docNil || szPath == NULL || *szPath == '\0' || + vOpusDocxSnapshotDoc != docNil) + return fFalse; + vOpusDocxSnapshotDoc = DocMother(doc); + vszOpusDocxSnapshotPath = szPath; + result = OpusExportCurrentDocumentPdf(); + vOpusDocxSnapshotDoc = docNil; + vszOpusDocxSnapshotPath = NULL; + 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 99ce772..71ae39b 100644 --- a/src/Opus/save.c +++ b/src/Opus/save.c @@ -708,7 +708,7 @@ LCleanup: FWin95SaveAliasMatches(stFile) && PdodDoc(doc)->fn != fnNil) FCloseFn(PdodDoc(doc)->fn); if (!OpusFinishWin95SaveAlias(stFile, - cmd == cmdOK && pcmb->fAction) && cmd == cmdOK) + cmd == cmdOK && pcmb->fAction, doc) && cmd == cmdOK) cmd = cmdError; #endif if (cmd == cmdCancelled) diff --git a/src/Opus/wordtech/fetch2.c b/src/Opus/wordtech/fetch2.c index 975f412..04ca513 100644 --- a/src/Opus/wordtech/fetch2.c +++ b/src/Opus/wordtech/fetch2.c @@ -1002,27 +1002,50 @@ const char *sz; /* 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) +EXPORT int OpusX64FtcFromFontNameForDoc(doc, sz, charset) int doc; const char *sz; +int charset; { + int cch; + struct FFN *pffn; + char rgch[cbFfnLast] = {0}; + 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. */ + /* A new Word 1.x document exposes only the three guaranteed screen-font + slots while DOCX formatting is applied. Adding a normal ANSI font at + this point can alias it to slot 1 (Symbol), corrupting every Latin + character in the document. Keep the historical safe mappings for ANSI + faces; charset-specific runs still get real FFN records below. */ 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; + if (charset == ANSI_CHARSET || charset == DEFAULT_CHARSET) + { + 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; + } + pffn = (struct FFN *)rgch; + if (CchSz(sz) > LF_FACESIZE - 1) + { + bltbyte(sz, pffn->szFfn, LF_FACESIZE - 1); + pffn->szFfn[LF_FACESIZE - 1] = '\0'; + } + else + CchCopySz(sz, pffn->szFfn); + cch = CchStripString(pffn->szFfn, CchSz(pffn->szFfn) - 1) + 1; + if (cch <= 1) + return wNinch; + pffn->ffid = FF_DONTCARE; + pffn->cbFfnM1 = CbFfnFromCchSzFfn(cch) - 1; + ChsPffn(pffn) = (charset >= 0 && charset <= 255) ? charset : ANSI_CHARSET; + return FtcChkDocFfn(doc, pffn); } EXPORT int OpusX64HpsFromFontSize(sz) @@ -1067,6 +1090,22 @@ int cchMax; bltbyte(szFont, sz, min(CchSz(szFont), cchMax)); sz[cchMax - 1] = '\0'; } + +EXPORT int OpusX64CharsetFromFtc(doc, ftc) +int doc; +int ftc; +{ + int ibst; + struct FFN *pffn; + + if (doc == docNil || ftc == wNinch) + return ANSI_CHARSET; + ibst = IbstFontFromFtcDoc(ftc, doc); + if (ibst == iNil) + return ANSI_CHARSET; + pffn = (struct FFN *)PstFromSttb(vhsttbFont, ibst); + return (unsigned char)ChsPffn(pffn); +} #endif diff --git a/src/Opus/wproc.c b/src/Opus/wproc.c index 999ffa2..afe7b25 100644 --- a/src/Opus/wproc.c +++ b/src/Opus/wproc.c @@ -96,6 +96,8 @@ extern struct FTI vfti; extern char (**vhgrpchr)[]; extern int vbchrMac; extern struct STTB **vhsttbFont; +extern unsigned int OpusUnicodeScalarAt(); +extern int OpusQueueUnicodeWmChar(); #endif extern struct FCB **mpfnhfcb[]; extern int vfnPreload; @@ -1834,6 +1836,12 @@ LONG lParam; extern int vOpusPdfExportStage; return (LRESULT)vOpusPdfExportStage; } + case 106: + return (LRESULT)OpusUnicodeScalarAt( + selCur.doc, (CP)lParam); + case 107: + return (LRESULT)OpusQueueUnicodeWmChar( + hwnd, (unsigned int)lParam); } return (LRESULT) -1; #endif @@ -2501,6 +2509,17 @@ LPMSG lpmsg; wm = lpmsg->message; kc = lpmsg->wParam; /* key code or char */ +#ifdef OPUS_X64 + /* Unicode window classes deliver non-ANSI keyboard layouts as UTF-16 + WM_CHAR units. Queue those through the compatibility layer before the + legacy insertion loop truncates the character to one byte. */ + if (wm == WM_CHAR && !vfInsertMode && (unsigned int)kc >= 0x100) + { + OpusQueueUnicodeWmChar(lpmsg->hwnd, (unsigned int)kc); + return fTrue; + } +#endif + #ifdef COMING_SOON_TO_A_WORD_PROCESSOR_NEAR_YOU diff --git a/src/port/original/opus_modern_formats.cpp b/src/port/original/opus_modern_formats.cpp index b131f0f..5a4a2b9 100644 --- a/src/port/original/opus_modern_formats.cpp +++ b/src/port/original/opus_modern_formats.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include using Microsoft::WRL::ComPtr; @@ -349,9 +350,141 @@ struct RunStyle { COLORREF color = RGB(0, 0, 0); bool auto_color = true; std::wstring font = L"Arial"; + std::string language = "en-US"; + int code_page = 1252; + int charset = ANSI_CHARSET; bool operator==(const RunStyle&) const = default; }; +struct LanguageProfile { + const char* tag; + int code_page; + int charset; + LANGID language_id; +}; + +constexpr LanguageProfile kLanguageProfiles[] = { + {"en-US", 1252, ANSI_CHARSET, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)}, + {"es-ES", 1252, ANSI_CHARSET, MAKELANGID(LANG_SPANISH, SUBLANG_SPANISH_MODERN)}, + {"fr-FR", 1252, ANSI_CHARSET, MAKELANGID(LANG_FRENCH, SUBLANG_FRENCH)}, + {"de-DE", 1252, ANSI_CHARSET, MAKELANGID(LANG_GERMAN, SUBLANG_GERMAN)}, + {"pl-PL", 1250, EASTEUROPE_CHARSET, MAKELANGID(LANG_POLISH, SUBLANG_DEFAULT)}, + {"el-GR", 1253, GREEK_CHARSET, MAKELANGID(LANG_GREEK, SUBLANG_DEFAULT)}, + {"ru-RU", 1251, RUSSIAN_CHARSET, MAKELANGID(LANG_RUSSIAN, SUBLANG_DEFAULT)}, + {"tr-TR", 1254, TURKISH_CHARSET, MAKELANGID(LANG_TURKISH, SUBLANG_DEFAULT)}, + {"he-IL", 1255, HEBREW_CHARSET, MAKELANGID(LANG_HEBREW, SUBLANG_DEFAULT)}, + {"ar-SA", 1256, ARABIC_CHARSET, MAKELANGID(LANG_ARABIC, SUBLANG_ARABIC_SAUDI_ARABIA)}, + {"th-TH", 874, THAI_CHARSET, MAKELANGID(LANG_THAI, SUBLANG_DEFAULT)}, + {"vi-VN", 1258, VIETNAMESE_CHARSET, MAKELANGID(LANG_VIETNAMESE, SUBLANG_DEFAULT)}, + {"ja-JP", 932, SHIFTJIS_CHARSET, MAKELANGID(LANG_JAPANESE, SUBLANG_DEFAULT)}, + {"zh-CN", 936, GB2312_CHARSET, MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED)}, + {"zh-TW", 950, CHINESEBIG5_CHARSET, MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL)}, + {"ko-KR", 949, HANGEUL_CHARSET, MAKELANGID(LANG_KOREAN, SUBLANG_DEFAULT)}, +}; + +const LanguageProfile& default_language_profile() { + return kLanguageProfiles[0]; +} + +bool tag_prefix(std::string_view value, std::string_view prefix) { + if (value.size() < prefix.size()) return false; + for (std::size_t index = 0; index < prefix.size(); ++index) { + const unsigned char left = static_cast(value[index]); + const unsigned char right = static_cast(prefix[index]); + if (std::tolower(left) != std::tolower(right)) return false; + } + return value.size() == prefix.size() || value[prefix.size()] == '-'; +} + +const LanguageProfile& profile_for_tag(std::string_view tag) { + for (const LanguageProfile& profile : kLanguageProfiles) { + if (_stricmp(std::string(tag).c_str(), profile.tag) == 0) + return profile; + } + struct Alias { const char* prefix; std::size_t profile; }; + static constexpr Alias aliases[] = { + {"en", 0}, {"es", 1}, {"fr", 2}, {"de", 3}, {"pl", 4}, + {"cs", 4}, {"sk", 4}, {"hu", 4}, {"ro", 4}, {"el", 5}, + {"ru", 6}, {"uk", 6}, {"bg", 6}, {"sr", 6}, {"tr", 7}, + {"he", 8}, {"iw", 8}, {"ar", 9}, {"fa", 9}, {"ur", 9}, + {"th", 10}, {"vi", 11}, {"ja", 12}, {"zh-CN", 13}, + {"zh-SG", 13}, {"zh", 14}, {"ko", 15}, + }; + for (const Alias& alias : aliases) { + if (tag_prefix(tag, alias.prefix)) return kLanguageProfiles[alias.profile]; + } + return default_language_profile(); +} + +const LanguageProfile& profile_for_language_id(const LANGID language) { + const WORD primary = PRIMARYLANGID(language); + for (const LanguageProfile& profile : kLanguageProfiles) { + if (PRIMARYLANGID(profile.language_id) == primary) return profile; + } + return default_language_profile(); +} + +const LanguageProfile& profile_for_scalar(const std::uint32_t scalar) { + if (scalar >= 0x0370 && scalar <= 0x03ff) return kLanguageProfiles[5]; + if (scalar >= 0x0400 && scalar <= 0x052f) return kLanguageProfiles[6]; + if (scalar >= 0x0590 && scalar <= 0x05ff) return kLanguageProfiles[8]; + if ((scalar >= 0x0600 && scalar <= 0x08ff) || + (scalar >= 0xfb50 && scalar <= 0xfdff) || + (scalar >= 0xfe70 && scalar <= 0xfeff)) return kLanguageProfiles[9]; + if (scalar >= 0x0e00 && scalar <= 0x0e7f) return kLanguageProfiles[10]; + if ((scalar >= 0x3040 && scalar <= 0x30ff) || + (scalar >= 0x31f0 && scalar <= 0x31ff)) return kLanguageProfiles[12]; + if ((scalar >= 0x3400 && scalar <= 0x9fff) || + (scalar >= 0xf900 && scalar <= 0xfaff)) return kLanguageProfiles[13]; + if ((scalar >= 0x1100 && scalar <= 0x11ff) || + (scalar >= 0x3130 && scalar <= 0x318f) || + (scalar >= 0xac00 && scalar <= 0xd7af)) return kLanguageProfiles[15]; + return default_language_profile(); +} + +std::uint32_t scalar_at(std::wstring_view text, std::size_t& index) { + const std::uint32_t first = static_cast(text[index++]); + if (first >= 0xd800 && first <= 0xdbff && index < text.size()) { + const std::uint32_t second = static_cast(text[index]); + if (second >= 0xdc00 && second <= 0xdfff) { + ++index; + return 0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00); + } + } + if (first >= 0xd800 && first <= 0xdfff) return 0xfffd; + return first; +} + +std::wstring scalar_to_wide(const std::uint32_t scalar) { + if (scalar <= 0xffff && !(scalar >= 0xd800 && scalar <= 0xdfff)) + return std::wstring(1, static_cast(scalar)); + if (scalar <= 0x10ffff) { + const std::uint32_t value = scalar - 0x10000; + const wchar_t pair[] = { + static_cast(0xd800 + (value >> 10)), + static_cast(0xdc00 + (value & 0x3ff))}; + return std::wstring(pair, 2); + } + return std::wstring(1, L'\xfffd'); +} + +char legacy_byte_for_scalar(const std::uint32_t scalar, + const LanguageProfile& profile) { + if (scalar == 0) return '?'; + const std::wstring wide = scalar_to_wide(scalar); + char bytes[4]{}; + BOOL used_default = FALSE; + const int count = WideCharToMultiByte( + profile.code_page, WC_NO_BEST_FIT_CHARS, wide.data(), + static_cast(wide.size()), bytes, static_cast(sizeof(bytes)), + "?", &used_default); + if (count == 1 && !used_default && bytes[0] != '\0') return bytes[0]; + const bool wide_script = profile.code_page == 932 || + profile.code_page == 936 || profile.code_page == 949 || + profile.code_page == 950 || scalar > 0xffff; + return wide_script ? 'W' : '?'; +} + struct TextRun { RunStyle style; std::wstring text; }; struct DocumentSettings { bool valid = false; @@ -448,10 +581,23 @@ void apply_run_properties(RunStyle& style, std::string_view props) { if (const std::string size = first_property_value(props, "sz"); !size.empty()) { parse_bounded_int(size, 2, 254, style.half_points); } - if (const std::string font = first_property_value(props, "rFonts", "ascii"); - !font.empty()) { + std::string font = first_property_value(props, "rFonts", "ascii"); + if (font.empty()) font = first_property_value(props, "rFonts", "hAnsi"); + if (font.empty()) font = first_property_value(props, "rFonts", "eastAsia"); + if (font.empty()) font = first_property_value(props, "rFonts", "cs"); + if (!font.empty()) { style.font = xml_unescape(font); } + std::string language = first_property_value(props, "lang", "val"); + if (language.empty()) + language = first_property_value(props, "lang", "eastAsia"); + if (language.empty()) language = first_property_value(props, "lang", "bidi"); + if (!language.empty()) { + const LanguageProfile& profile = profile_for_tag(language); + style.language = language; + style.code_page = profile.code_page; + style.charset = profile.charset; + } if (const std::string color = first_property_value(props, "color"); !color.empty() && color != "auto" && color.size() == 6) { unsigned rgb = 0; @@ -854,14 +1000,27 @@ struct PendingTableFormat { int columns = 0; }; +struct UnicodeCell { + std::uint32_t scalar = 0; + LANGID language = 0; +}; + +struct UnicodeDocument { + std::vector cells; +}; + struct PendingDocxImport { std::vector runs; std::vector paragraphs; std::vector tables; + std::vector unicode_cells; DocumentSettings settings; }; PendingDocxImport pending_docx_import; +std::unordered_map unicode_documents; +std::vector pending_clipboard_cells; +std::string input_language = "auto"; struct PendingPdfExport { std::vector paragraphs; @@ -872,15 +1031,47 @@ struct PendingPdfExport { PendingPdfExport pending_pdf_export; -std::string ansi_run_text(std::wstring_view text) { +RunStyle effective_run_style(const TextRun& run) { + RunStyle style = run.style; + if (style.language.empty() || style.language == "en-US") { + for (std::size_t index = 0; index < run.text.size();) { + const std::uint32_t scalar = scalar_at(run.text, index); + const LanguageProfile& detected = profile_for_scalar(scalar); + if (detected.code_page != 1252) { + style.language = detected.tag; + style.code_page = detected.code_page; + style.charset = detected.charset; + break; + } + } + } + return style; +} + +void append_legacy_scalar(std::string& result, + std::vector* cells, + const std::uint32_t scalar, + const LanguageProfile& profile) { + result.push_back(legacy_byte_for_scalar(scalar, profile)); + if (cells != nullptr) cells->push_back({scalar, profile.language_id}); +} + +std::string legacy_run_text(std::wstring_view text, const RunStyle& style, + std::vector* cells) { 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'); + const LanguageProfile& configured = profile_for_tag(style.language); + for (std::size_t index = 0; index < text.size();) { + const std::uint32_t scalar = scalar_at(text, index); + const LanguageProfile& profile = style.language.empty() ? + profile_for_scalar(scalar) : configured; + if (scalar == L'\n') { + append_legacy_scalar(result, cells, L'\r', profile); + append_legacy_scalar(result, cells, L'\n', profile); + } else if (scalar == L'\r') { + if (result.empty() || result.back() != '\r') + append_legacy_scalar(result, cells, scalar, profile); } else { - result += wide_to_ansi(std::wstring_view(&character, 1)); + append_legacy_scalar(result, cells, scalar, profile); } } return result; @@ -900,8 +1091,11 @@ std::string paragraphs_to_text(const std::vector& paragraphs, for (const auto& run : paragraphs[index].runs) { PendingRunFormat run_format; run_format.cp_first = static_cast(text.size()); - run_format.style = run.style; - const std::string encoded = ansi_run_text(run.text); + run_format.style = effective_run_style(run); + std::vector* cells = pending != nullptr ? + &pending->unicode_cells : nullptr; + const std::string encoded = legacy_run_text( + run.text, run_format.style, cells); require_parse_limit(encoded.size() <= kMaxTextBytes - text.size()); text += encoded; run_format.cp_lim = static_cast(text.size()); @@ -911,6 +1105,11 @@ std::string paragraphs_to_text(const std::vector& paragraphs, if (index + 1 < paragraphs.size()) { require_parse_limit(text.size() <= kMaxTextBytes - 2); text += "\r\n"; + if (pending != nullptr) { + const LanguageProfile& profile = default_language_profile(); + pending->unicode_cells.push_back({L'\r', profile.language_id}); + pending->unicode_cells.push_back({L'\n', profile.language_id}); + } } paragraph_format.cp_lim = static_cast(text.size()); if (pending != nullptr) @@ -1029,14 +1228,8 @@ void append_rtf_text(std::string& rtf, std::wstring_view text) { 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]); + const auto unit = static_cast(character); + rtf += "\\u" + std::to_string(static_cast(unit)) + "?"; } } } @@ -1051,10 +1244,21 @@ std::string paragraphs_to_rtf(const std::vector& paragraphs) { std::find(colors.begin(), colors.end(), run.style.color) == colors.end()) colors.push_back(run.style.color); } - std::string rtf = "{\\rtf1\\ansi \\deff0"; + std::string rtf = "{\\rtf1\\ansi\\ansicpg1252\\uc1 \\deff0"; rtf += "{\\fonttbl"; for (std::size_t index = 0; index < fonts.size(); ++index) { - rtf += "{\\f" + std::to_string(index) + "\\fnil "; + int charset = ANSI_CHARSET; + for (const auto& paragraph : paragraphs) { + const auto found = std::find_if( + paragraph.runs.begin(), paragraph.runs.end(), + [&](const TextRun& run) { return run.style.font == fonts[index]; }); + if (found != paragraph.runs.end()) { + charset = found->style.charset; + break; + } + } + rtf += "{\\f" + std::to_string(index) + "\\fnil\\fcharset" + + std::to_string(charset) + " "; append_rtf_text(rtf, fonts[index]); rtf += ";}"; } @@ -1081,6 +1285,8 @@ std::string paragraphs_to_rtf(const std::vector& paragraphs) { 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)); + rtf += "\\lang" + std::to_string( + profile_for_tag(run.style.language).language_id); if (!run.style.auto_color) rtf += "\\cf" + std::to_string(std::distance(colors.begin(), color) + 1); rtf.push_back(' '); @@ -1261,6 +1467,13 @@ RunStyle rich_style_at(HWND rich, LONG position) { style.auto_color = (format.dwEffects & CFE_AUTOCOLOR) != 0; style.color = format.crTextColor; if (format.szFaceName[0] != L'\0') style.font = format.szFaceName; + if (format.lcid != 0) { + const LanguageProfile& profile = profile_for_language_id( + LANGIDFROMLCID(format.lcid)); + style.language = profile.tag; + style.code_page = profile.code_page; + style.charset = profile.charset; + } return style; } @@ -1306,25 +1519,99 @@ std::vector paragraphs_from_rich_edit(RichEditDocument& rich) { return paragraphs; } -std::string document_xml(const std::vector& paragraphs) { +std::string run_text_xml(std::wstring_view text) { + std::string xml; + std::wstring ordinary; + const auto flush = [&]() { + if (!ordinary.empty()) { + xml += "" + xml_escape(ordinary) + + ""; + ordinary.clear(); + } + }; + for (const wchar_t character : text) { + if (character == L'\t' || character == L'\n' || character == L'\r' || + character == L'\f') { + flush(); + if (character == L'\t') xml += ""; + else if (character == L'\f') xml += ""; + else if (character == L'\n') xml += ""; + } else { + ordinary.push_back(character); + } + } + flush(); + return xml; +} + +std::string document_xml(const std::vector& paragraphs, + const DocumentSettings& settings = {}) { std::string xml = "" "" ""; for (const auto& paragraph : paragraphs) { xml += ""; + xml += ""; if (paragraph.alignment != PFA_LEFT) { const char* value = paragraph.alignment == PFA_CENTER ? "center" : paragraph.alignment == PFA_RIGHT ? "right" : "both"; - xml += std::string(""; + xml += std::string(""; } + if (paragraph.left_indent != 0 || paragraph.right_indent != 0 || + paragraph.first_line_indent != 0) { + xml += " 0) { + xml += " w:line=\"" + + std::to_string(paragraph.line_spacing) + + "\" w:lineRule=\"atLeast\""; + } + xml += "/>"; + } + if (paragraph.keep_together) xml += ""; + if (paragraph.keep_with_next) xml += ""; + if (paragraph.page_break_before) xml += ""; + if (paragraph.bottom_border) + xml += ""; + xml += ""; for (const auto& run : paragraph.runs) { xml += ""; if (run.style.bold) xml += ""; if (run.style.italic) xml += ""; if (run.style.underline) xml += ""; + if (run.style.strike) xml += ""; + if (run.style.small_caps) xml += ""; + if (run.style.all_caps) xml += ""; + if (run.style.hidden) xml += ""; xml += ""; + "\" w:hAnsi=\"" + xml_escape(run.style.font) + + "\" w:eastAsia=\"" + xml_escape(run.style.font) + + "\" w:cs=\"" + xml_escape(run.style.font) + "\"/>"; + if (!run.style.language.empty()) { + xml += ""; + } xml += ""; if (!run.style.auto_color) { char color[7]{}; @@ -1332,14 +1619,23 @@ std::string document_xml(const std::vector& paragraphs) { GetGValue(run.style.color), GetBValue(run.style.color)); xml += std::string(""; } - xml += "" + - xml_escape(run.text) + ""; + xml += "" + run_text_xml(run.text) + ""; } xml += ""; } - xml += "" - "" - ""; + const int page_width = settings.valid ? settings.page_width : 12240; + const int page_height = settings.valid ? settings.page_height : 15840; + const int margin_left = settings.valid ? settings.margin_left : 1440; + const int margin_right = settings.valid ? settings.margin_right : 1440; + const int margin_top = settings.valid ? settings.margin_top : 1440; + const int margin_bottom = settings.valid ? settings.margin_bottom : 1440; + xml += ""; return xml; } @@ -1371,7 +1667,8 @@ bool add_relationship(IOpcRelationshipSet* set, IOpcFactory* factory, &relationship)); } -bool write_docx(const std::wstring& path, const std::vector& paragraphs) { +bool write_docx(const std::wstring& path, const std::vector& paragraphs, + const DocumentSettings& settings = {}) { ComApartment apartment; if (!apartment.usable()) return false; ComPtr factory; @@ -1387,14 +1684,14 @@ bool write_docx(const std::wstring& path, const std::vector& paragrap 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; + document_xml(paragraphs, settings), &main_part)) return false; const std::string styles = "" "" "" ""; - const std::string settings = + const std::string settings_xml = "" ""; const std::string core = @@ -1409,7 +1706,7 @@ bool write_docx(const std::wstring& path, const std::vector& paragrap 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) || + L"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", settings_xml) || !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", @@ -1517,6 +1814,150 @@ int pdf_font_resource(const RunStyle& style) { return family * 4 + face + 1; } +bool pdf_needs_unicode_font(const std::vector& paragraphs) { + for (const Paragraph& paragraph : paragraphs) { + for (const TextRun& run : paragraph.runs) { + for (const wchar_t character : run.text) { + if (character > 0xff) return true; + } + } + } + return false; +} + +std::wstring pdf_unicode_font_name(const std::vector& paragraphs) { + for (const Paragraph& paragraph : paragraphs) { + for (const TextRun& run : paragraph.runs) { + for (std::size_t index = 0; index < run.text.size();) { + const std::uint32_t scalar = scalar_at(run.text, index); + const LanguageProfile& profile = profile_for_scalar(scalar); + if (profile.code_page == 932) return L"Yu Gothic UI"; + if (profile.code_page == 936) return L"Microsoft YaHei UI"; + if (profile.code_page == 950) return L"Microsoft JhengHei UI"; + if (profile.code_page == 949) return L"Malgun Gothic"; + if (scalar > 0xffff) return L"Segoe UI Symbol"; + } + } + } + return L"Segoe UI"; +} + +std::string pdf_font_identifier(std::wstring_view value) { + std::string identifier; + for (const wchar_t character : value) { + if ((character >= L'A' && character <= L'Z') || + (character >= L'a' && character <= L'z') || + (character >= L'0' && character <= L'9')) + identifier.push_back(static_cast(character)); + } + return identifier.empty() ? "SegoeUI" : identifier; +} + +struct PdfUnicodeFont { + HDC dc = nullptr; + HFONT font = nullptr; + HGDIOBJ previous = nullptr; + std::wstring face; + std::vector file; + std::map unicode_by_glyph; + TEXTMETRICW metrics{}; + + ~PdfUnicodeFont() { + if (dc != nullptr && previous != nullptr) SelectObject(dc, previous); + if (font != nullptr) DeleteObject(font); + if (dc != nullptr) DeleteDC(dc); + } + + bool initialize(const std::wstring& requested) { + face = requested; + dc = CreateCompatibleDC(nullptr); + if (dc == nullptr) return false; + font = CreateFontW(-1000, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_TT_ONLY_PRECIS, + CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, + DEFAULT_PITCH | FF_DONTCARE, face.c_str()); + if (font == nullptr) return false; + previous = SelectObject(dc, font); + if (previous == nullptr || !GetTextMetricsW(dc, &metrics)) return false; + wchar_t actual[LF_FACESIZE]{}; + if (GetTextFaceW(dc, static_cast(std::size(actual)), actual) > 0) + face = actual; + const DWORD size = GetFontData(dc, 0, 0, nullptr, 0); + if (size == GDI_ERROR || size == 0 || size > kMaxGeneratedBytes) + return false; + file.resize(size); + return GetFontData(dc, 0, 0, file.data(), size) != GDI_ERROR; + } + + WORD glyph(std::wstring_view characters) { + WORD glyphs[2]{0xffff, 0xffff}; + const int count = (std::min)(2, static_cast(characters.size())); + if (count <= 0 || GetGlyphIndicesW(dc, characters.data(), count, + glyphs, GGI_MARK_NONEXISTING_GLYPHS) == + GDI_ERROR) return 0; + WORD selected = glyphs[0] == 0xffff ? 0 : glyphs[0]; + if (selected == 0 && count == 2 && glyphs[1] != 0xffff) + selected = glyphs[1]; + unicode_by_glyph[selected] = std::wstring(characters); + return selected; + } +}; + +std::string pdf_hex_word(const WORD value) { + static constexpr char digits[] = "0123456789ABCDEF"; + std::string result(4, '0'); + result[0] = digits[(value >> 12) & 0xf]; + result[1] = digits[(value >> 8) & 0xf]; + result[2] = digits[(value >> 4) & 0xf]; + result[3] = digits[value & 0xf]; + return result; +} + +std::string pdf_hex_utf16(std::wstring_view value) { + std::string result; + for (const wchar_t character : value) + result += pdf_hex_word(static_cast(character)); + return result; +} + +std::string pdf_unicode_encoded_text(std::wstring_view text, + PdfUnicodeFont& font) { + std::string encoded = "<"; + for (std::size_t index = 0; index < text.size();) { + const std::size_t first = index; + const std::uint32_t scalar = scalar_at(text, index); + (void)scalar; + const std::wstring_view characters = text.substr(first, index - first); + encoded += pdf_hex_word(font.glyph(characters)); + } + encoded += ">"; + return encoded; +} + +std::string pdf_to_unicode_cmap(const PdfUnicodeFont& font) { + std::string cmap = + "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n" + "/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n" + "/CMapName /Word1Unicode def\n/CMapType 2 def\n" + "1 begincodespacerange\n<0000> \nendcodespacerange\n"; + auto current = font.unicode_by_glyph.begin(); + while (current != font.unicode_by_glyph.end()) { + const std::size_t count = (std::min)( + static_cast(100), + static_cast(std::distance( + current, font.unicode_by_glyph.end()))); + cmap += std::to_string(count) + " beginbfchar\n"; + for (std::size_t index = 0; index < count; ++index, ++current) { + cmap += "<" + pdf_hex_word(current->first) + "> <" + + pdf_hex_utf16(current->second) + ">\n"; + } + cmap += "endbfchar\n"; + } + cmap += "endcmap\nCMapName currentdict /CMap defineresource pop\n" + "end\nend\n"; + return cmap; +} + std::string pdf_encoded_text(std::wstring_view text) { std::string encoded; for (const wchar_t character : text) { @@ -1535,7 +1976,7 @@ std::string pdf_encoded_text(std::wstring_view text) { } void append_pdf_line(std::string& content, const PdfLine& line, - const double baseline) { + const double baseline, PdfUnicodeFont* unicode_font) { double x = line.left; if (line.alignment == PFA_CENTER) { x += ((std::max)(0.0, line.writable_width - line.width)) / 2.0; @@ -1552,11 +1993,17 @@ void append_pdf_line(std::string& content, const PdfLine& line, 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"; + const bool unicode = unicode_font != nullptr && + std::any_of(run.text.begin(), run.text.end(), + [](const wchar_t character) { return character > 0xff; }); + content += unicode ? "/FU " : + "/F" + std::to_string(pdf_font_resource(run.style)) + " "; + content += 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"; + pdf_number(blue) + " rg\n"; + content += unicode ? pdf_unicode_encoded_text(run.text, *unicode_font) : + "(" + pdf_encoded_text(run.text) + ")"; + content += " 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) + @@ -1574,7 +2021,7 @@ void append_pdf_line(std::string& content, const PdfLine& line, std::vector pdf_page_contents( const std::vector& paragraphs, - const DocumentSettings& settings) { + const DocumentSettings& settings, PdfUnicodeFont* unicode_font) { const double page_width = settings.valid ? static_cast(settings.page_width) / 20.0 : 612.0; const double page_height = settings.valid ? @@ -1634,7 +2081,7 @@ std::vector pdf_page_contents( } if (cursor - line_height < bottom) begin_new_page(); const double baseline = cursor - largest_font; - append_pdf_line(pages.back(), line, baseline); + append_pdf_line(pages.back(), line, baseline, unicode_font); cursor -= line_height; first_visual_line = false; line = PdfLine{}; @@ -1722,8 +2169,13 @@ bool write_pdf(const std::wstring& path, "Helvetica-BoldOblique", "Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic", "Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"}; + PdfUnicodeFont embedded_unicode; + PdfUnicodeFont* unicode_font = nullptr; + if (pdf_needs_unicode_font(paragraphs) && + embedded_unicode.initialize(pdf_unicode_font_name(paragraphs))) + unicode_font = &embedded_unicode; const std::vector contents = - pdf_page_contents(paragraphs, settings); + pdf_page_contents(paragraphs, settings, unicode_font); const double page_width = settings.valid ? static_cast(settings.page_width) / 20.0 : 612.0; const double page_height = settings.valid ? @@ -1731,8 +2183,14 @@ bool write_pdf(const std::wstring& path, 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 unicode_font_object = first_font_object + + static_cast(font_names.size()); + const int unicode_cid_font_object = unicode_font_object + 1; + const int unicode_descriptor_object = unicode_font_object + 2; + const int unicode_file_object = unicode_font_object + 3; + const int unicode_cmap_object = unicode_font_object + 4; + const int first_page_object = unicode_font == nullptr ? + unicode_font_object : unicode_cmap_object + 1; const int object_count = first_page_object - 1 + static_cast(contents.size()) * 2; std::vector objects(static_cast(object_count + 1)); @@ -1750,6 +2208,50 @@ bool write_pdf(const std::wstring& path, "<< /Type /Font /Subtype /Type1 /BaseFont /" + std::string(font_names[index]) + " /Encoding /WinAnsiEncoding >>"; } + if (unicode_font != nullptr) { + const std::string base_font = pdf_font_identifier(unicode_font->face); + const int ascent = unicode_font->metrics.tmAscent; + const int descent = -unicode_font->metrics.tmDescent; + const int height = (std::max)(1L, unicode_font->metrics.tmHeight); + const auto scaled = [height](const int value) { + return static_cast((static_cast(value) * 1000) / + height); + }; + const int scaled_ascent = scaled(ascent); + const int scaled_descent = scaled(descent); + objects[unicode_font_object] = + "<< /Type /Font /Subtype /Type0 /BaseFont /" + base_font + + " /Encoding /Identity-H /DescendantFonts [" + + std::to_string(unicode_cid_font_object) + + " 0 R] /ToUnicode " + std::to_string(unicode_cmap_object) + + " 0 R >>"; + objects[unicode_cid_font_object] = + "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /" + base_font + + " /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) " + "/Supplement 0 >> /FontDescriptor " + + std::to_string(unicode_descriptor_object) + + " 0 R /DW 1000 /CIDToGIDMap /Identity >>"; + objects[unicode_descriptor_object] = + "<< /Type /FontDescriptor /FontName /" + base_font + + " /Flags 32 /FontBBox [-1000 " + + std::to_string(scaled_descent) + " 3000 " + + std::to_string(scaled_ascent + 500) + "] /ItalicAngle 0 " + "/Ascent " + std::to_string(scaled_ascent) + " /Descent " + + std::to_string(scaled_descent) + " /CapHeight " + + std::to_string(scaled_ascent) + " /StemV 80 /FontFile2 " + + std::to_string(unicode_file_object) + " 0 R >>"; + objects[unicode_file_object] = + "<< /Length " + std::to_string(unicode_font->file.size()) + + " /Length1 " + std::to_string(unicode_font->file.size()) + + " >>\nstream\n" + + std::string(reinterpret_cast(unicode_font->file.data()), + unicode_font->file.size()) + + "\nendstream"; + const std::string cmap = pdf_to_unicode_cmap(*unicode_font); + objects[unicode_cmap_object] = + "<< /Length " + std::to_string(cmap.size()) + + " >>\nstream\n" + cmap + "endstream"; + } std::string resources = "<< /Font << "; for (std::size_t index = 0; index < font_names.size(); ++index) { @@ -1757,6 +2259,8 @@ bool write_pdf(const std::wstring& path, std::to_string(first_font_object + static_cast(index)) + " 0 R "; } + if (unicode_font != nullptr) + resources += "/FU " + std::to_string(unicode_font_object) + " 0 R "; resources += ">> >>"; for (std::size_t index = 0; index < contents.size(); ++index) { const int page_object = first_page_object + static_cast(index) * 2; @@ -1939,7 +2443,8 @@ 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) try { + const int font_capacity, int* charset, char* language, + const int language_capacity) try { if (index < 0 || static_cast(index) >= pending_docx_import.runs.size()) return false; const PendingRunFormat& record = pending_docx_import.runs[index]; @@ -1958,9 +2463,13 @@ extern "C" int OpusModernGetPendingDocxRun( const std::string ansi_font = wide_to_ansi(record.style.font); lstrcpynA(font, ansi_font.c_str(), font_capacity); } + if (charset != nullptr) *charset = record.style.charset; + if (language != nullptr && language_capacity > 0) + lstrcpynA(language, record.style.language.c_str(), language_capacity); return true; } catch (...) { if (font != nullptr && font_capacity > 0) font[0] = '\0'; + if (language != nullptr && language_capacity > 0) language[0] = '\0'; return false; } @@ -2000,6 +2509,376 @@ extern "C" void OpusModernClearPendingDocxFormatting() { pending_docx_import = {}; } +extern "C" int OpusModernBindPendingDocxUnicode(const int doc) try { + if (doc < 0 || pending_docx_import.unicode_cells.size() > kMaxTextBytes) + return false; + unicode_documents[doc].cells = pending_docx_import.unicode_cells; + return true; +} catch (...) { + return false; +} + +extern "C" void OpusUnicodeForgetDocument(const int doc) { + unicode_documents.erase(doc); +} + +extern "C" unsigned int OpusUnicodeScalarAt(const int doc, const long cp) { + if (cp < 0) return 0; + const auto found = unicode_documents.find(doc); + if (found == unicode_documents.end() || + static_cast(cp) >= found->second.cells.size()) return 0; + return found->second.cells[static_cast(cp)].scalar; +} + +extern "C" unsigned int OpusUnicodeLanguageAt(const int doc, const long cp) { + if (cp < 0) return 0; + const auto found = unicode_documents.find(doc); + if (found == unicode_documents.end() || + static_cast(cp) >= found->second.cells.size()) return 0; + return found->second.cells[static_cast(cp)].language; +} + +extern "C" int OpusUnicodeHasRange( + const int doc, const long cp_first, const int length) { + if (cp_first < 0 || length <= 0) return false; + const auto found = unicode_documents.find(doc); + if (found == unicode_documents.end()) return false; + const std::vector& cells = found->second.cells; + const std::size_t first = static_cast(cp_first); + if (first >= cells.size()) return false; + const std::size_t count = (std::min)( + static_cast(length), cells.size() - first); + return std::any_of( + cells.begin() + static_cast(first), + cells.begin() + static_cast(first + count), + [](const UnicodeCell& cell) { return cell.scalar != 0; }); +} + +extern "C" void OpusUnicodeOnReplace( + const int doc, const long cp_first, const long cp_lim, + const long inserted_count) try { + if (cp_first < 0 || cp_lim < cp_first || inserted_count < 0 || + inserted_count > static_cast(kMaxTextBytes)) return; + const auto found = unicode_documents.find(doc); + if (found == unicode_documents.end()) return; + std::vector& cells = found->second.cells; + const std::size_t first = (std::min)( + static_cast(cp_first), cells.size()); + const std::size_t limit = (std::min)( + static_cast(cp_lim), cells.size()); + if (cells.size() - (limit - first) + + static_cast(inserted_count) > kMaxTextBytes) return; + cells.erase(cells.begin() + static_cast(first), + cells.begin() + static_cast(limit)); + cells.insert(cells.begin() + static_cast(first), + static_cast(inserted_count), UnicodeCell{}); +} catch (...) { +} + +extern "C" void OpusUnicodeOnReplaceCps( + const int destination_doc, const long destination_first, + const long destination_lim, const int source_doc, + const long source_first, const long source_lim) try { + if (destination_first < 0 || destination_lim < destination_first || + source_first < 0 || source_lim < source_first) return; + const auto source = unicode_documents.find(source_doc); + const auto destination = unicode_documents.find(destination_doc); + if (source == unicode_documents.end() && + destination == unicode_documents.end()) return; + const std::size_t requested = static_cast( + source_lim - source_first); + if (requested > kMaxTextBytes) return; + std::vector copied(requested); + if (source != unicode_documents.end()) { + const std::vector& source_cells = source->second.cells; + const std::size_t first = (std::min)( + static_cast(source_first), source_cells.size()); + const std::size_t available = (std::min)( + requested, source_cells.size() - first); + std::copy_n(source_cells.begin() + static_cast(first), + available, copied.begin()); + } + std::vector& cells = unicode_documents[destination_doc].cells; + if (static_cast(destination_first) > cells.size()) { + if (static_cast(destination_first) > kMaxTextBytes) return; + cells.resize(static_cast(destination_first)); + } + const std::size_t first = static_cast(destination_first); + const std::size_t limit = (std::min)( + static_cast(destination_lim), cells.size()); + if (cells.size() - (limit - first) + copied.size() > kMaxTextBytes) return; + cells.erase(cells.begin() + static_cast(first), + cells.begin() + static_cast(limit)); + cells.insert(cells.begin() + static_cast(first), + copied.begin(), copied.end()); +} catch (...) { +} + +const LanguageProfile& active_input_profile() { + if (input_language != "auto") return profile_for_tag(input_language); + const LANGID language = LOWORD(reinterpret_cast( + GetKeyboardLayout(0))); + return profile_for_language_id(language); +} + +extern "C" int OpusUnicodeSetInputLanguage(const char* language) { + if (language == nullptr || *language == '\0' || + _stricmp(language, "auto") == 0) { + input_language = "auto"; + return true; + } + const LanguageProfile& profile = profile_for_tag(language); + input_language = profile.tag; + return true; +} + +extern "C" int OpusUnicodeGetInputLanguage(char* language, + const int capacity) { + if (language == nullptr || capacity <= 0) return false; + const std::string value = input_language == "auto" ? "auto" : + active_input_profile().tag; + lstrcpynA(language, value.c_str(), capacity); + return true; +} + +extern "C" int OpusUnicodeLegacyByteForScalar(const unsigned int scalar) { + const LanguageProfile& selected = input_language == "auto" ? + profile_for_scalar(scalar) : active_input_profile(); + return static_cast(legacy_byte_for_scalar(scalar, selected)); +} + +extern "C" int OpusUnicodeSetScalar(const int doc, const long cp, + const unsigned int scalar) try { + if (doc < 0 || cp < 0 || static_cast(cp) >= kMaxTextBytes || + scalar == 0 || scalar > 0x10ffff || + (scalar >= 0xd800 && scalar <= 0xdfff)) return false; + std::vector& cells = unicode_documents[doc].cells; + if (cells.size() <= static_cast(cp)) + cells.resize(static_cast(cp) + 1); + const LanguageProfile& profile = input_language == "auto" ? + profile_for_scalar(scalar) : active_input_profile(); + cells[static_cast(cp)] = {scalar, profile.language_id}; + return true; +} catch (...) { + return false; +} + +int code_page_for_charset(const int charset) { + switch (charset) { + case EASTEUROPE_CHARSET: return 1250; + case RUSSIAN_CHARSET: return 1251; + case GREEK_CHARSET: return 1253; + case TURKISH_CHARSET: return 1254; + case HEBREW_CHARSET: return 1255; + case ARABIC_CHARSET: return 1256; + case BALTIC_CHARSET: return 1257; + case VIETNAMESE_CHARSET: return 1258; + case THAI_CHARSET: return 874; + case SHIFTJIS_CHARSET: return 932; + case GB2312_CHARSET: return 936; + case HANGEUL_CHARSET: return 949; + case CHINESEBIG5_CHARSET: return 950; + default: return 1252; + } +} + +std::uint32_t decode_legacy_byte(const unsigned char byte, + const int code_page) { + wchar_t decoded = 0; + if (byte < 0x80) return byte; + if (MultiByteToWideChar(code_page, 0, + reinterpret_cast(&byte), 1, + &decoded, 1) == 1) return decoded; + return 0xfffd; +} + +extern "C" int OpusUnicodeTextToUtf8( + const int doc, const long cp_first, const char* bytes, const int length, + const int charset, char* output, const int capacity) try { + if (cp_first < 0 || bytes == nullptr || length < 0 || capacity < 0 || + static_cast(length) > kMaxTextBytes) return -1; + std::wstring wide; + wide.reserve(static_cast(length)); + const int code_page = code_page_for_charset(charset); + for (int index = 0; index < length; ++index) { + std::uint32_t scalar = OpusUnicodeScalarAt(doc, cp_first + index); + if (scalar == 0) scalar = decode_legacy_byte( + static_cast(bytes[index]), code_page); + wide += scalar_to_wide(scalar); + } + const std::string utf8 = wide_to_utf8(wide); + if (utf8.size() >= static_cast(capacity)) return -1; + if (output != nullptr && capacity > 0) { + std::memcpy(output, utf8.data(), utf8.size()); + output[utf8.size()] = '\0'; + } + return static_cast(utf8.size()); +} catch (...) { + return -1; +} + +extern "C" int OpusUnicodeLanguageTagAt( + const int doc, const long cp, char* language, const int capacity) { + if (language == nullptr || capacity <= 0) return false; + const LANGID language_id = static_cast( + OpusUnicodeLanguageAt(doc, cp)); + const LanguageProfile& profile = language_id != 0 ? + profile_for_language_id(language_id) : default_language_profile(); + lstrcpynA(language, profile.tag, capacity); + return true; +} + +extern "C" int OpusUnicodeClipboardToLegacy( + HANDLE unicode_handle, HANDLE* legacy_handle) try { + if (legacy_handle == nullptr) return false; + *legacy_handle = nullptr; + pending_clipboard_cells.clear(); + if (unicode_handle == nullptr) return false; + const SIZE_T bytes = GlobalSize(unicode_handle); + if (bytes < sizeof(wchar_t) || bytes > kMaxTextBytes * sizeof(wchar_t)) + return false; + const auto* source = static_cast(GlobalLock(unicode_handle)); + if (source == nullptr) return false; + const std::size_t capacity = bytes / sizeof(wchar_t); + std::size_t length = 0; + while (length < capacity && source[length] != L'\0') ++length; + if (length == capacity) { + GlobalUnlock(unicode_handle); + return false; + } + std::string legacy; + legacy.reserve(length + 1); + pending_clipboard_cells.reserve(length); + const std::wstring_view text(source, length); + for (std::size_t index = 0; index < text.size();) { + const std::uint32_t scalar = scalar_at(text, index); + const LanguageProfile& profile = input_language == "auto" ? + profile_for_scalar(scalar) : active_input_profile(); + append_legacy_scalar(legacy, &pending_clipboard_cells, + scalar, profile); + } + GlobalUnlock(unicode_handle); + if (legacy.size() >= 0x10000 || legacy.size() > kMaxTextBytes) { + pending_clipboard_cells.clear(); + return false; + } + HANDLE converted = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, + legacy.size() + 1); + if (converted == nullptr) { + pending_clipboard_cells.clear(); + return false; + } + void* destination = GlobalLock(converted); + if (destination == nullptr) { + GlobalFree(converted); + pending_clipboard_cells.clear(); + return false; + } + if (!legacy.empty()) std::memcpy(destination, legacy.data(), legacy.size()); + GlobalUnlock(converted); + *legacy_handle = converted; + return true; +} catch (...) { + pending_clipboard_cells.clear(); + return false; +} + +extern "C" HANDLE OpusUnicodeCreateClipboardHandle( + const int doc, const long cp_first, HANDLE legacy_handle) try { + if (cp_first < 0 || legacy_handle == nullptr) return nullptr; + const SIZE_T bytes = GlobalSize(legacy_handle); + if (bytes == 0 || bytes > kMaxTextBytes) return nullptr; + const auto* source = static_cast(GlobalLock(legacy_handle)); + if (source == nullptr) return nullptr; + std::size_t length = 0; + while (length < bytes && source[length] != '\0') ++length; + if (length == bytes) { + GlobalUnlock(legacy_handle); + return nullptr; + } + std::wstring wide; + wide.reserve(length); + for (std::size_t index = 0; index < length; ++index) { + std::uint32_t scalar = OpusUnicodeScalarAt( + doc, cp_first + static_cast(index)); + if (scalar == 0) scalar = decode_legacy_byte( + static_cast(source[index]), 1252); + wide += scalar_to_wide(scalar); + } + GlobalUnlock(legacy_handle); + if (wide.size() > (kMaxTextBytes / sizeof(wchar_t)) - 1) return nullptr; + HANDLE result = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, + (wide.size() + 1) * sizeof(wchar_t)); + if (result == nullptr) return nullptr; + void* destination = GlobalLock(result); + if (destination == nullptr) { + GlobalFree(result); + return nullptr; + } + if (!wide.empty()) + std::memcpy(destination, wide.data(), wide.size() * sizeof(wchar_t)); + GlobalUnlock(result); + return result; +} catch (...) { + return nullptr; +} + +extern "C" int OpusUnicodeBindPendingClipboard(const int doc) try { + if (doc < 0 || pending_clipboard_cells.size() > kMaxTextBytes) return false; + unicode_documents[doc].cells = pending_clipboard_cells; + pending_clipboard_cells.clear(); + return true; +} catch (...) { + pending_clipboard_cells.clear(); + return false; +} + +extern "C" BOOL OpusUnicodeExtTextOut( + HDC dc, const int x, const int y, const UINT options, const RECT* rectangle, + const int doc, const long cp_first, const char* bytes, const UINT length, + const int* advances) try { + if (dc == nullptr || bytes == nullptr || length == 0) + return ExtTextOutA(dc, x, y, options, rectangle, bytes, length, advances); + const auto found = unicode_documents.find(doc); + bool has_unicode = false; + if (found != unicode_documents.end() && cp_first >= 0) { + const std::vector& cells = found->second.cells; + const std::size_t first = static_cast(cp_first); + if (first < cells.size()) { + const std::size_t count = (std::min)( + static_cast(length), cells.size() - first); + has_unicode = std::any_of( + cells.begin() + static_cast(first), + cells.begin() + static_cast(first + count), + [](const UnicodeCell& cell) { return cell.scalar != 0; }); + } + } + if (!has_unicode) + return ExtTextOutA(dc, x, y, options, rectangle, bytes, length, advances); + + const int code_page = code_page_for_charset(GetTextCharset(dc)); + std::wstring wide; + std::vector wide_advances; + wide.reserve(length); + if (advances != nullptr) wide_advances.reserve(length); + for (UINT index = 0; index < length; ++index) { + std::uint32_t scalar = OpusUnicodeScalarAt(doc, cp_first + index); + if (scalar == 0) scalar = decode_legacy_byte( + static_cast(bytes[index]), code_page); + const std::wstring encoded = scalar_to_wide(scalar); + wide += encoded; + if (advances != nullptr) { + wide_advances.push_back(advances[index]); + if (encoded.size() == 2) wide_advances.push_back(0); + } + } + return ExtTextOutW(dc, x, y, options, rectangle, wide.data(), + static_cast(wide.size()), + advances != nullptr ? wide_advances.data() : nullptr); +} catch (...) { + return ExtTextOutA(dc, x, y, options, rectangle, bytes, length, advances); +} + 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) { @@ -2107,6 +2986,61 @@ extern "C" int OpusPdfSnapshotAddRun( return false; } +extern "C" int OpusPdfSnapshotAddRunUtf8( + 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, + const char* language, const int charset) try { + if (text == nullptr || length < 0 || + static_cast(length) > kMaxTextBytes || + pending_pdf_export.paragraphs.empty() || + pending_pdf_export.run_count >= kMaxRuns || + static_cast(length) > + kMaxTextBytes - pending_pdf_export.text_bytes) 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 <= 254 ? + half_points : 20; + if (font != nullptr && *font != '\0') { + const std::size_t font_length = strnlen_s(font, 256); + if (font_length == 256) return false; + style.font = ansi_to_wide(std::string_view(font, font_length)); + } + if (language != nullptr && *language != '\0') { + const std::size_t language_length = strnlen_s(language, 32); + if (language_length == 32) return false; + const LanguageProfile& profile = profile_for_tag( + std::string_view(language, language_length)); + style.language = profile.tag; + style.code_page = profile.code_page; + style.charset = charset >= 0 && charset <= 255 ? charset : + profile.charset; + } + apply_legacy_color(color_index, style); + std::wstring run_text = utf8_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)}); + ++pending_pdf_export.run_count; + pending_pdf_export.text_bytes += static_cast(length); + return true; +} catch (...) { + return false; +} + extern "C" int OpusPdfSnapshotExportDialog(HWND owner) try { if (pending_pdf_export.paragraphs.empty()) return false; const int result = export_paragraphs_to_pdf_dialog( @@ -2118,6 +3052,19 @@ extern "C" int OpusPdfSnapshotExportDialog(HWND owner) try { return false; } +extern "C" int OpusDocxSnapshotExportPath(const char* path) try { + if (path == nullptr || *path == '\0' || + pending_pdf_export.paragraphs.empty()) return false; + const bool written = write_docx( + wide_path(path), pending_pdf_export.paragraphs, + pending_pdf_export.settings); + pending_pdf_export = {}; + return written; +} catch (...) { + pending_pdf_export = {}; + return false; +} + extern "C" int OpusModernRtfFileToDocx(const char* rtf_path, const char* docx_path) try { return rtf_to_docx(wide_path(rtf_path), wide_path(docx_path)); diff --git a/src/port/original/opus_modern_formats_test.cpp b/src/port/original/opus_modern_formats_test.cpp index 2c3a398..580c9e8 100644 --- a/src/port/original/opus_modern_formats_test.cpp +++ b/src/port/original/opus_modern_formats_test.cpp @@ -11,6 +11,8 @@ extern "C" int OpusModernDocxToRtfFile(const char*, const char*); extern "C" int OpusModernDocxToTextFile(const char*, const char*); extern "C" int OpusModernRtfFileToDocx(const char*, const char*); extern "C" int OpusModernRtfFileToPdf(const char*, const char*); +extern "C" int OpusModernBindPendingDocxUnicode(int); +extern "C" unsigned int OpusUnicodeScalarAt(int, long); extern "C" int OpusPdfSnapshotBegin(int, int, int, int, int, int); extern "C" int OpusPdfSnapshotAddParagraph(int, int, int, int, int, int, int, int, int, int, int); @@ -52,6 +54,7 @@ int main(const int argument_count, char** arguments) { const std::string rtf = base + ".rtf"; const std::string docx = base + ".docx"; const std::string roundtrip = base + ".roundtrip.rtf"; + const std::string imported_text = base + ".unicode.txt"; const std::string oversized = base + ".oversized.rtf"; const std::string preserved = base + ".preserved.pdf"; char requested_pdf[32768]{}; @@ -67,11 +70,33 @@ int main(const int argument_count, char** arguments) { output << "{\\rtf1\\ansi{\\fonttbl{\\f0 Arial;}}" "{\\colortbl;\\red255\\green0\\blue0;}" "\\f0\\fs24 Plain {\\b Bold} {\\i Italic} " - "{\\ul Underline} {\\cf1 Red}\\par Second paragraph}"; + "{\\ul Underline} {\\cf1 Red} " + "{\\lang1049 \\u1055?\\u1088?\\u1080?\\u1074?\\u1077?\\u1090?}" + " {\\lang1032 \\u915?\\u949?\\u953?\\u940?}" + " {\\lang1025 \\u1605?\\u1585?\\u1581?\\u1576?\\u1575?}" + " {\\lang1041 \\u12371?\\u12435?\\u12395?\\u12385?\\u12399?}" + "\\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 unicode_imported = written && + OpusModernDocxToTextFile(docx.c_str(), imported_text.c_str()) != 0 && + OpusModernBindPendingDocxUnicode(42) != 0; + bool cyrillic_sidecar = false; + bool greek_sidecar = false; + bool arabic_sidecar = false; + bool japanese_sidecar = false; + if (unicode_imported) { + const std::string imported_bytes = read_file(imported_text); + for (long cp = 0; cp < static_cast(imported_bytes.size()); ++cp) { + const unsigned int scalar = OpusUnicodeScalarAt(42, cp); + if (scalar == 1055) cyrillic_sidecar = true; + if (scalar == 915) greek_sidecar = true; + if (scalar == 1605) arabic_sidecar = true; + if (scalar == 12371) japanese_sidecar = true; + } + } const bool pdf_written = OpusModernRtfFileToPdf(rtf.c_str(), pdf.c_str()) != 0; { std::ofstream output(preserved, std::ios::binary); @@ -115,17 +140,26 @@ int main(const int argument_count, char** arguments) { DeleteFileA(rtf.c_str()); DeleteFileA(docx.c_str()); DeleteFileA(roundtrip.c_str()); + DeleteFileA(imported_text.c_str()); DeleteFileA(oversized.c_str()); DeleteFileA(preserved.c_str()); if (!keep_pdf) DeleteFileA(pdf.c_str()); - if (!written || !read || result.find("Bold") == std::string::npos || + if (!written || !read || !unicode_imported || !cyrillic_sidecar || + !greek_sidecar || !arabic_sidecar || !japanese_sidecar || + result.find("Bold") == std::string::npos || result.find("Second paragraph") == std::string::npos || + result.find("\\u1055") == std::string::npos || + result.find("\\u915") == std::string::npos || + result.find("\\u1605") == std::string::npos || + result.find("\\u12371") == 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("/Encoding /Identity-H") == std::string::npos || + pdf_data.find("/ToUnicode") == 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 || diff --git a/src/port/original/opus_sdm_runtime.cpp b/src/port/original/opus_sdm_runtime.cpp index c359e69..6ad9ecc 100644 --- a/src/port/original/opus_sdm_runtime.cpp +++ b/src/port/original/opus_sdm_runtime.cpp @@ -26,6 +26,7 @@ int FSetCabSz(void**, const char*, std::uint16_t); int OpusModernPathIsDocx(const char*); int OpusModernDocxToTextFile(const char*, const char*); int OpusModernRtfFileToDocx(const char*, const char*); +int OpusSaveDocumentAsDocx(int, const char*); } extern "C" void OpusX64TraceRibbon(const char*, int, int, int, int, @@ -2527,7 +2528,7 @@ int OpusWin95DisplayAlias(unsigned char* const st_file) { } int OpusFinishWin95SaveAlias(const unsigned char* st_file, - const int success) { + const int success, const int doc) { const std::string path = counted_path(st_file); if (path.empty()) { return !g_win95_save_alias.active; @@ -2541,9 +2542,8 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, if (copied) { 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 : + OpusSaveDocumentAsDocx( + doc, g_win95_save_alias.selected_path.c_str()) != 0 : atomic_copy_file(g_win95_save_alias.legacy_path, g_win95_save_alias.selected_path); } @@ -2565,7 +2565,7 @@ int OpusFinishWin95SaveAlias(const unsigned char* st_file, return false; } return OpusModernPathIsDocx(saved->second.c_str()) ? - OpusModernRtfFileToDocx(path.c_str(), saved->second.c_str()) != 0 : + OpusSaveDocumentAsDocx(doc, saved->second.c_str()) != 0 : atomic_copy_file(path, saved->second); } diff --git a/src/port/original/opus_win95_chrome.cpp b/src/port/original/opus_win95_chrome.cpp index 08c16a5..15aa9c4 100644 --- a/src/port/original/opus_win95_chrome.cpp +++ b/src/port/original/opus_win95_chrome.cpp @@ -1,10 +1,12 @@ #define WIN32_LEAN_AND_MEAN #include #include +#include #include #include #include +#include #include #include @@ -36,6 +38,14 @@ extern "C" int OpusAdjustWin95HorizontalMargin( HWND ruler, int left_margin, int delta_pixels); extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler); extern "C" int OpusExportCurrentDocumentPdf(void); +extern "C" int OpusGetUnicodeSelection( + int* doc, long* cp_first, long* cp_lim); +extern "C" int OpusUnicodeLegacyByteForScalar(unsigned int scalar); +extern "C" int OpusUnicodeSetScalar( + int doc, long cp, unsigned int scalar); +extern "C" int OpusUnicodeSetInputLanguage(const char* language); +extern "C" int OpusUnicodeGetInputLanguage(char* language, int capacity); +extern "C" int OpusQueueUnicodeWmChar(HWND pane, unsigned int code_unit); namespace { @@ -57,7 +67,10 @@ constexpr UINT kCmdToggleStandardToolbar = 0x7101; constexpr UINT kCmdToggleFormattingToolbar = 0x7102; constexpr UINT kCmdExportPdf = 0x7103; constexpr UINT kCmdTextColorBase = 0x7200; +constexpr UINT kCmdLanguageBase = 0x7300; constexpr UINT_PTR kSyncTimer = 0x951; +constexpr UINT kWmCommitUnicodeScalar = WM_APP + 0x452; +constexpr std::size_t kMaxPendingUnicodeInput = 0xffff; constexpr wchar_t kOriginalPaneProcProperty[] = L"OpusWord95OriginalPaneProc"; enum class FormatGlyph { @@ -168,8 +181,47 @@ struct ToolbarState { HBRUSH g_menu_brush = nullptr; HMENU g_table_menu = nullptr; HMENU g_toolbars_menu = nullptr; +HMENU g_language_menu = nullptr; WNDPROC g_original_app_proc = nullptr; bool g_word95_page_view_active = false; +wchar_t g_pending_high_surrogate = 0; + +struct LanguageChoice { + UINT command; + const wchar_t* label; + const char* tag; +}; + +struct PendingUnicodeInput { + int doc = -1; + long cp = 0; + std::uint32_t scalar = 0; + int retries = 0; +}; + +std::deque g_pending_unicode_inputs; +PendingUnicodeInput g_active_unicode_input; +bool g_unicode_input_active = false; + +constexpr std::array kLanguageChoices{{ + {kCmdLanguageBase, L"&Automatic (Keyboard)", "auto"}, + {kCmdLanguageBase + 1, L"&English (United States)", "en-US"}, + {kCmdLanguageBase + 2, L"&Spanish", "es-ES"}, + {kCmdLanguageBase + 3, L"&French", "fr-FR"}, + {kCmdLanguageBase + 4, L"&German", "de-DE"}, + {kCmdLanguageBase + 5, L"&Polish / Central European", "pl-PL"}, + {kCmdLanguageBase + 6, L"&Greek", "el-GR"}, + {kCmdLanguageBase + 7, L"&Russian / Cyrillic", "ru-RU"}, + {kCmdLanguageBase + 8, L"&Turkish", "tr-TR"}, + {kCmdLanguageBase + 9, L"&Hebrew", "he-IL"}, + {kCmdLanguageBase + 10, L"&Arabic", "ar-SA"}, + {kCmdLanguageBase + 11, L"&Thai", "th-TH"}, + {kCmdLanguageBase + 12, L"&Vietnamese", "vi-VN"}, + {kCmdLanguageBase + 13, L"&Japanese", "ja-JP"}, + {kCmdLanguageBase + 14, L"Chinese (&Simplified)", "zh-CN"}, + {kCmdLanguageBase + 15, L"Chinese (&Traditional)", "zh-TW"}, + {kCmdLanguageBase + 16, L"&Korean", "ko-KR"}, +}}; struct VerticalRulerDragState { HWND pane = nullptr; @@ -347,6 +399,33 @@ void configure_word95_menus(HWND window) { MF_BYPOSITION | MF_POPUP | MF_STRING, reinterpret_cast(tools), L"&Tools"); } + const int tools_index = find_named(L"Tools"); + HMENU tools_menu = tools_index >= 0 ? GetSubMenu(root, tools_index) : + (utilities_index >= 0 ? GetSubMenu(root, utilities_index) : nullptr); + if (g_language_menu == nullptr || !IsMenu(g_language_menu)) { + g_language_menu = CreatePopupMenu(); + if (g_language_menu != nullptr) { + for (const LanguageChoice& choice : kLanguageChoices) { + AppendMenuW(g_language_menu, MF_STRING, choice.command, + choice.label); + } + } + } + if (tools_menu != nullptr && g_language_menu != nullptr) { + bool present = false; + for (int index = 0; index < GetMenuItemCount(tools_menu); ++index) { + if (GetSubMenu(tools_menu, index) == g_language_menu) { + present = true; + break; + } + } + if (!present) { + AppendMenuW(tools_menu, MF_SEPARATOR, 0, nullptr); + AppendMenuW(tools_menu, MF_POPUP | MF_STRING, + reinterpret_cast(g_language_menu), + L"&Language"); + } + } if (g_table_menu == nullptr || !IsMenu(g_table_menu)) { g_table_menu = CreatePopupMenu(); @@ -2313,9 +2392,153 @@ void show_document_context_menu(HWND pane, POINT screen_point) { } } +void update_language_menu_check() { + if (g_language_menu == nullptr) return; + char selected[32]{}; + OpusUnicodeGetInputLanguage(selected, static_cast(std::size(selected))); + UINT command = kCmdLanguageBase; + for (const LanguageChoice& choice : kLanguageChoices) { + if (_stricmp(selected, choice.tag) == 0) { + command = choice.command; + break; + } + } + CheckMenuRadioItem(g_language_menu, kCmdLanguageBase, + kCmdLanguageBase + + static_cast(kLanguageChoices.size() - 1), + command, MF_BYCOMMAND); +} + +bool start_next_unicode_input(HWND pane) { + if (g_unicode_input_active || g_pending_unicode_inputs.empty()) + return true; + int doc_before = -1; + long cp_before = 0; + long cp_lim_before = 0; + if (!OpusGetUnicodeSelection(&doc_before, &cp_before, &cp_lim_before)) { + g_pending_unicode_inputs.pop_front(); + return false; + } + g_active_unicode_input = g_pending_unicode_inputs.front(); + g_pending_unicode_inputs.pop_front(); + g_active_unicode_input.doc = doc_before; + g_active_unicode_input.cp = cp_before; + g_active_unicode_input.retries = 0; + g_unicode_input_active = true; + const WPARAM legacy = g_active_unicode_input.scalar < 0x20 ? + static_cast(g_active_unicode_input.scalar) : + static_cast(OpusUnicodeLegacyByteForScalar( + g_active_unicode_input.scalar) & 0xff); + if (!PostMessageW(pane, WM_CHAR, legacy, 1) || + !PostMessageW(pane, WM_LBUTTONUP, 0, 0) || + !PostMessageW(pane, kWmCommitUnicodeScalar, 0, 0)) { + g_unicode_input_active = false; + return false; + } + return true; +} + +bool queue_unicode_input(HWND pane, const std::uint32_t scalar) { + if (pane == nullptr || scalar == 0 || scalar > 0x10ffff || + (scalar >= 0xd800 && scalar <= 0xdfff)) return false; + if (g_pending_unicode_inputs.size() >= kMaxPendingUnicodeInput) + return false; + try { + g_pending_unicode_inputs.push_back({-1, 0, scalar, 0}); + } catch (...) { + return false; + } + return start_next_unicode_input(pane); +} + +bool insert_unicode_scalar(HWND pane, const std::uint32_t scalar) { + return scalar >= 0x20 && queue_unicode_input(pane, scalar); +} + +void insert_unicode_text(HWND pane, const std::wstring& text) { + for (std::size_t index = 0; index < text.size();) { + std::uint32_t scalar = static_cast(text[index++]); + if (scalar >= 0xd800 && scalar <= 0xdbff && index < text.size()) { + const std::uint32_t low = static_cast(text[index]); + if (low >= 0xdc00 && low <= 0xdfff) { + ++index; + scalar = 0x10000 + ((scalar - 0xd800) << 10) + + (low - 0xdc00); + } + } + if (scalar == L'\r' || scalar == L'\n' || scalar == L'\t') + queue_unicode_input(pane, scalar); + else + insert_unicode_scalar(pane, scalar); + } +} + LRESULT CALLBACK document_pane_proc(HWND pane, UINT message, WPARAM w_param, LPARAM l_param) { WNDPROC original = original_pane_proc(pane); + if (message == kWmCommitUnicodeScalar) { + if (g_unicode_input_active) { + int doc_after = -1; + long cp_after = 0; + long cp_lim_after = 0; + if (OpusGetUnicodeSelection( + &doc_after, &cp_after, &cp_lim_after) && + doc_after == g_active_unicode_input.doc && + cp_after > g_active_unicode_input.cp) { + if (g_active_unicode_input.scalar >= 0x20) + OpusUnicodeSetScalar( + g_active_unicode_input.doc, + g_active_unicode_input.cp, + g_active_unicode_input.scalar); + g_unicode_input_active = false; + InvalidateRect(pane, nullptr, FALSE); + start_next_unicode_input(pane); + } else if (doc_after == g_active_unicode_input.doc && + g_active_unicode_input.retries++ < 8) { + PostMessageW(pane, WM_LBUTTONUP, 0, 0); + PostMessageW(pane, kWmCommitUnicodeScalar, 0, 0); + } else { + g_unicode_input_active = false; + start_next_unicode_input(pane); + } + } + return 0; + } + if (message == WM_UNICHAR) { + if (w_param == UNICODE_NOCHAR) return TRUE; + insert_unicode_scalar(pane, static_cast(w_param)); + return 0; + } + /* The original message loop marks already-translated byte input by + setting bit 15 (0x80xx). That is an internal Word flag, not a Unicode + code point, and must reach the legacy pane procedure unchanged. */ + if (message == WM_CHAR && (w_param & 0xff00) != 0x8000 && + w_param >= 0x80) { + OpusQueueUnicodeWmChar( + pane, static_cast(w_param)); + return 0; + } + if (message == WM_IME_COMPOSITION && + (l_param & GCS_RESULTSTR) != 0) { + HIMC context = ImmGetContext(pane); + if (context != nullptr) { + const LONG byte_count = ImmGetCompositionStringW( + context, GCS_RESULTSTR, nullptr, 0); + if (byte_count > 0 && byte_count <= 1024 * 1024 && + (byte_count % sizeof(wchar_t)) == 0) { + std::wstring result( + static_cast(byte_count) / sizeof(wchar_t), + L'\0'); + if (ImmGetCompositionStringW(context, GCS_RESULTSTR, + &result[0], static_cast(byte_count)) == + byte_count) { + insert_unicode_text(pane, result); + } + } + ImmReleaseContext(pane, context); + return 0; + } + } if (message == WM_MOUSEWHEEL) { if (g_document_wheel.pane != pane) { g_document_wheel = {}; @@ -2509,6 +2732,9 @@ LRESULT CALLBACK document_pane_proc(HWND pane, UINT message, if (g_document_wheel.pane == pane) { g_document_wheel = {}; } + g_pending_unicode_inputs.clear(); + g_unicode_input_active = false; + g_pending_high_surrogate = 0; clear_page_snapshots(pane); RemovePropW(pane, kOriginalPaneProcProperty); if (original != nullptr) { @@ -2590,6 +2816,15 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message, OpusExportCurrentDocumentPdf(); return 0; } + if (command >= kCmdLanguageBase && + command < kCmdLanguageBase + kLanguageChoices.size()) { + const LanguageChoice& choice = + kLanguageChoices[command - kCmdLanguageBase]; + OpusUnicodeSetInputLanguage(choice.tag); + update_language_menu_check(); + DrawMenuBar(app); + return 0; + } if (command == kCmdToggleStandardToolbar) { toggle_toolbar_row(app, toolbar, true); return 0; @@ -2599,6 +2834,7 @@ LRESULT CALLBACK app_window_proc(HWND app, UINT message, return 0; } } else if (message == WM_INITMENUPOPUP) { + update_language_menu_check(); HWND toolbar = FindWindowExW(app, nullptr, kToolbarClass, nullptr); ToolbarState* state = toolbar_state(toolbar); if (state != nullptr) { @@ -2866,6 +3102,27 @@ bool register_ruler_overlay_class() { } // namespace +extern "C" int OpusQueueUnicodeWmChar( + HWND pane, const unsigned int code_unit) { + if (code_unit >= 0xd800 && code_unit <= 0xdbff) { + g_pending_high_surrogate = static_cast(code_unit); + return TRUE; + } + std::uint32_t scalar = code_unit; + if (code_unit >= 0xdc00 && code_unit <= 0xdfff) { + if (g_pending_high_surrogate < 0xd800 || + g_pending_high_surrogate > 0xdbff) { + g_pending_high_surrogate = 0; + return FALSE; + } + scalar = 0x10000 + + ((static_cast(g_pending_high_surrogate) - + 0xd800) << 10) + (code_unit - 0xdc00); + } + g_pending_high_surrogate = 0; + return insert_unicode_scalar(pane, scalar); +} + extern "C" void OpusDrawWin95HorizontalRuler(HWND ruler) { HDC dc = GetDC(ruler); if (dc != nullptr) { diff --git a/src/port/original/opus_word1_ui_test.cpp b/src/port/original/opus_word1_ui_test.cpp index 9759eca..a81b9cb 100644 --- a/src/port/original/opus_word1_ui_test.cpp +++ b/src/port/original/opus_word1_ui_test.cpp @@ -653,7 +653,7 @@ int wmain(const int argument_count, wchar_t** arguments) { std::cerr << "usage: opus_word1_ui_test WORD1.exe " "[--typing|--interaction|--selection|--caret|--formatting|--color|" - "--font-typing|--clipboard|--about|--save-as|--pdf-export|" + "--font-typing|--unicode|--clipboard|--about|--save-as|--pdf-export|" "--docx-open FILE|--docx-format-open FILE|" "--docx-pdf-export FILE]\n"; return 1; @@ -678,6 +678,9 @@ int wmain(const int argument_count, wchar_t** arguments) { const bool font_typing_mode = argument_count == 3 && std::wcscmp(arguments[2], L"--font-typing") == 0; + const bool unicode_mode = + argument_count == 3 && + std::wcscmp(arguments[2], L"--unicode") == 0; const bool about_mode = argument_count == 3 && std::wcscmp(arguments[2], L"--about") == 0; @@ -703,7 +706,7 @@ int wmain(const int argument_count, wchar_t** arguments) { 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 && + !font_typing_mode && !unicode_mode && !clipboard_mode && !about_mode && !save_as_mode && !pdf_export_mode) { std::cerr << "unknown test mode\n"; return 1; @@ -1097,6 +1100,67 @@ int wmain(const int argument_count, wchar_t** arguments) { CloseHandle(process.hProcess); return 0; } + if (unicode_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); + if (pane == nullptr || + !make_foreground_and_focus(main_window, pane, thread_id)) { + return fail(process, 87, + "Unicode test could not focus the document pane"); + } + const LRESULT cp_first = SendMessageW( + pane, kWmOpusX64QuerySelection, 0, 0); + const std::array scalars{ + 0x041f, 0x03a9, 0x0645, 0x3053, 0x1f642}; + for (std::size_t index = 0; index < scalars.size(); ++index) { + const std::uint32_t scalar = scalars[index]; + bool accepted = false; + if (index == 0) { + DWORD_PTR result = 0; + accepted = SendMessageTimeoutW( + pane, WM_UNICHAR, static_cast(scalar), 1, + SMTO_ABORTIFHUNG | SMTO_BLOCK, 3000, &result) != FALSE; + } else { + accepted = SendMessageW( + pane, kWmOpusX64QuerySelection, 107, + static_cast(scalar)) != FALSE; + } + if (!accepted) { + return fail(process, 88, + "Unicode input message was not accepted"); + } + } + Sleep(500); + const LRESULT cp_after = SendMessageW( + pane, kWmOpusX64QuerySelection, 0, 0); + bool scalars_preserved = cp_after == + cp_first + static_cast(scalars.size()); + std::array actual_scalars{}; + for (std::size_t index = 0; + index < scalars.size(); ++index) { + actual_scalars[index] = SendMessageW( + pane, kWmOpusX64QuerySelection, 106, + cp_first + static_cast(index)); + scalars_preserved = scalars_preserved && + actual_scalars[index] == scalars[index]; + } + if (!scalars_preserved) { + std::cerr << "Unicode input range=" << cp_first << "->" + << cp_after << " scalars="; + for (const LRESULT actual : actual_scalars) + std::cerr << actual << ','; + std::cerr << '\n'; + return fail(process, 89, + "Unicode code points were not preserved in the document"); + } + TerminateProcess(process.hProcess, 0); + WaitForSingleObject(process.hProcess, 2000); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return 0; + } if (font_typing_mode) { const HWND pane = find_descendant_by_class(main_window, L"OpusWwd"); DWORD ignored_process_id = 0;