Better Docx support.

This commit is contained in:
Justin Marshall
2026-08-10 14:57:08 -07:00
parent e8c9260ad7
commit 2b1165d869
9 changed files with 1481 additions and 140 deletions
+19
View File
@@ -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);
+141
View File
@@ -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:
+70 -51
View File
@@ -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;
}
+27 -1
View File
@@ -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;
{
+62
View File
@@ -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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <fstream>
#include <iostream>
#include <string>
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<DWORD>(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<char>(input), {});
}
std::string pdf_data;
if (pdf_written) {
std::ifstream input(pdf, std::ios::binary);
pdf_data.assign(std::istreambuf_iterator<char>(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;
}
+67
View File
@@ -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, &current, 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);
+158 -7
View File
@@ -4,7 +4,9 @@
#include <array>
#include <cstring>
#include <cwchar>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <utility>
#include <vector>
@@ -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<unsigned char> pixels(
static_cast<std::size_t>(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<DWORD>(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<DWORD>(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<DWORD>(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<DWORD>(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<DWORD>(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<char>(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) {