Skip to content

feat: add private DOCX-to-PDF export - #707

Draft
TimurKr wants to merge 15 commits into
mainfrom
feat/docx-to-pdf
Draft

feat: add private DOCX-to-PDF export#707
TimurKr wants to merge 15 commits into
mainfrom
feat/docx-to-pdf

Conversation

@TimurKr

@TimurKr TimurKr commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a private one-shot exportPdf API over Core semantic layouts.
  • Paint pages, text, list markers, links, and metadata through a bounded PDFKit writer.
  • Report unsupported fidelity, enforce strict refusal, and bound assets, commands, diagnostics, and output bytes.

Test plan

  • bun run typecheck
  • bun run lint
  • bun test ./packages/docx-to-pdf/test
  • bun run api:check
  • openspec validate docx-to-pdf-export --strict
  • Full suite: 11,393 tests passed; the aggregate benchmark timed out and passed alone in 21 seconds.

Made with Cursor


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Keep export sessions authoritative for resource ownership and geometry.
Confine packaged asset reads to host-selected roots and byte limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docx-editor Ready Ready Preview Sep 8, 2026 9:05am UTC

Request Review

@eigenpal-release-pal

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅

Posted by the CLA bot.

@TimurKr
TimurKr marked this pull request as draft September 2, 2026 20:19

@jedrazb jedrazb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not ready to merge

I ran exportPdf over every document in e2e/fixtures (97 files). 34 of them fail to export, and the exported text is wrong for any script outside Windows-1252. Fonts are not embedded, which is the core ask of #657 and the P0 in #697. Details are inline; the summary is here.

Crashes (reproduced)

Cause Fixtures Error
Underline or strike on any run 10 (demo.docx, hyperlink-demo.docx, toc-hyperlink-tabs.docx, styled-content.docx, ...) Error: unsupported number: NaN
Hard page break (w:br w:type="page") 24 (typing-perf-521pp.docx, footer-page-number.docx, titlePg-header-footer.docx, ...) PdfPaintValidationError: box.width must be greater than zero

Both reproduce with a one-paragraph document built from test/fixture.ts.

Fonts and text (reproduced)

  • The writer paints every span with a PDF standard-14 font. Those fonts are WinAnsi only. Zażółć gęślą jaźń exports as ZaÏ1Brg‘[lR ja¡D, Cyrillic and Greek become byte salad, and a Symbol bullet becomes ð·. No diagnostic names this.
  • Core lays out with the packaged metric-compatible faces, then the writer paints Helvetica. On issue-68-large.docx Helvetica is 8.7% wider in aggregate (Discussion 82.3 pt vs 101.0 pt). Spans overlap their neighbors and overrun the right margin.
  • The session already publishes admittedFontFace and shapeLaidOutText (base PR #706). This PR calls shapeLaidOutText for every span, discards the result, and uses it only to pick a diagnostic string. That costs 834 ms on the 307-page fixture and makes fidelityPolicy: 'strict' throw for every document that contains text.

Contract drift from #651 and #657

  • #651 and #657 put this package under the EigenPal Pro License, following editor-api. The PR ships Apache-2.0 in package.json and LICENSE, with no license-check.json.
  • #657 asks to remove the unused pdf-lib optional peer from packages/core/package.json. It is still there. Deferring is fine, but say so in tasks.md.
  • #657 lists tables, images, headers and footers, and note areas as MVP scope. This slice paints body, header, and footer text only. The README states this, so I read it as an intentional first slice, not an MVP. Rename the PR or the task list so the two agree.

Stacking

The base branch is PR #706, which has its own open blockers. Rebase after #706 lands. The packaged-font trustedRoot symlink finding from that review applies to pdf-export.ts too (inline).

What passes

  • bun test ./packages/docx-to-pdf/test: 84 pass.
  • bun run typecheck: pass.
  • bun run lint: 0 errors.
  • The paint-command model, bounds, cancellation, and diagnostics collector look solid. The planner and writer split is the right shape.

Suggested order

  1. Fix the two crash classes and add both to export-pdf.test.ts.
  2. Embed admitted face bytes through doc.font(buffer) so widths and code points come from the same face Core measured. Gate strict mode on that, not on a per-span diagnostic.
  3. Settle the license before anything publishes.
  4. Add a fixture sweep test over e2e/fixtures so the export never regresses to throwing on a supported document.

Comment thread packages/docx-to-pdf/src/pdfkit-paint-writer.ts Outdated
Comment thread packages/docx-to-pdf/src/pdf-page-planner.ts
Comment thread packages/docx-to-pdf/src/pdfkit-paint-writer.ts
Comment thread packages/docx-to-pdf/src/pdfkit-paint-writer.ts Outdated
Comment thread packages/docx-to-pdf/src/pdf-export.ts Outdated
Comment thread packages/docx-to-pdf/package.json Outdated
Comment thread packages/docx-to-pdf/src/pdfkit-paint-writer.ts Outdated
Comment thread packages/docx-to-pdf/src/pdf-page-planner.ts Outdated
Comment thread packages/docx-to-pdf/src/pdf-export.ts Outdated
TimurKr and others added 2 commits September 3, 2026 09:36
Co-authored-by: Cursor <cursoragent@cursor.com>
Build PDF bytes from Core semantic pages with bounded planning,
encoding, and asset reads. Report unsupported fidelity explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
Embed admitted fonts safely, preserve Unicode, and refuse unsupported
font encodings without corrupting output.

Co-authored-by: Cursor <cursoragent@cursor.com>
Base automatically changed from feat/export-foundation-resources to main September 3, 2026 16:16
jedrazb and others added 11 commits September 3, 2026 09:22
Build PDF bytes from Core semantic pages with bounded planning,
encoding, and asset reads. Report unsupported fidelity explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
Embed admitted fonts safely, preserve Unicode, and refuse unsupported
font encodings without corrupting output.

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Paint published paragraph borders, embed selected TTC faces, and keep planner traversal cancellable.
Remaining: Compare EP_ZMVZ_MULTI_v4 against Word with macOS Garamond and fix confirmed residual layout differences.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Treat the paragraph text start as the first-line implicit tab for non-list hanging indents.
Remaining: Resolve the first fixture footer, italic wrap, centered title, and border extent differences.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Hang collapsible boundary spaces during wrapping and center/right alignment; shrink justified spaces to a metric floor; apply Word's paragraph-border side gutter.
Remaining: Regenerate and compare only EP_ZMVZ_MULTI_v4 page 1, then trace residual differences.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Apply the same hanging-space and justified-shrink test when one word spans multiple runs.
Remaining: Regenerate and compare only EP_ZMVZ_MULTI_v4 page 1.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Derive underline thickness and offset from font post metrics; merge compatible adjacent underline fills across spaces.
Remaining: Restore the cross-run hanging-tail fix and add the opt-in macOS Word grid profile.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Derive justified used width from the full trailing U+0020 chain and exclude that chain from slack distribution.
Remaining: Add the opt-in macOS Word 300 dpi compatibility profile and recompare EP_ZMVZ_MULTI_v4 page 1.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
[gstack-context]
Decisions: Keep default export twip-exact; expose an opt-in PDF profile backed by a neutral Core grid policy; apply role-aware grid phases to page, furniture, border, text, links, and underline geometry.
Remaining: Rebase the branch, run final PR-wide verification, and update the comparison corpus only after EP_ZMVZ_MULTI_v4 remains stable.
Skill: investigate
[/gstack-context]

Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve the remote review commits while keeping the newer local implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants