Repository navigation
Fix DOCX font measurement and table layout to match Word - #206
Conversation
Correct Cambria table and mixed-run wrapping, paragraph baseline flow, explicit mark sizing, border-aware row geometry, and image/section transitions. Add focused DOCX regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Report explicit skips when required fonts are unavailable on portable runners, and run the DOCX suite on Windows with mandatory font prerequisite checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
CI portability follow-up: a22ecb2 adds explicit skips for unavailable system-font prerequisites and a dedicated Windows DOCX CI job that verifies Cambria/Segoe fonts before running tests. Ubuntu keeps running the portable suite. No renderer code changed. Local validation: |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical issues include tests that depend on fonts unavailable in Ubuntu CI and an image page-break bug that can place the following table above the image bottom.
10 open issues
Reclaim only the image offset retained after a page break · New Skip font-dependent tests when Cambria is unavailable · New Skip Segoe UI Emoji fallback tests when the font is unavailable · New Include paragraph mark font size in baseline ascent tracking · New Update baseline ascent in the image-only paragraph branch · New Adjust paragraph border start for the baseline offset · New Use nominal baseline ratio after the first header/footer paragraph · New Clear pending section spacing when a table starts the section · New Add each cell’s border insets before taking the row maximum · New Include terminal merged-cell borders in alignment and height · New
What changed in this PR
This PR improves .NET DOCX rendering to better match Word, focusing on Cambria text wrapping and the related table and paragraph geometry exposed by the price-list benchmark.
Changes:
- Uses preferred Cambria measurements for mixed-format wrapping, positioning, alignment, and shading.
- Refines table border insets, paragraph-mark sizing, baseline flow, image spacing, and section transitions.
- Adds focused regression tests and documents the updated rendering behavior.
| File | Description |
|---|---|
| tests/MiniPdf.Tests/DocxToPdfConverterTests.cs | Adds regression coverage for font measurements and layout geometry. |
| src/MiniPdf/PdfWriter.cs | Exposes internal Unicode helpers for fallback-font metric selection. |
| src/MiniPdf/DocxToPdfConverter.cs | Updates font measurement, baseline flow, table sizing, and spacing. |
| src/MiniPdf/DocxReader.cs | Tracks explicitly sized paragraph marks separately from inherited defaults. |
| FEATURE_PARITY.md | Documents the improved .NET DOCX rendering behavior. |
Review effort: Balanced
| if (followedByTable && options.GridLinePitch <= 0 && !paragraph.LineSpacingAbsolute | ||
| && paragraph.Images.Any(image => !image.IsAnchor)) | ||
| state.CurrentY += baselineFontSize * topOfPageAscentRatio; |
| Assert.True(PdfWriter.TryMeasurePreferredFontWidth("Cambria", prefixText, 11f, | ||
| true, italicPrefix, 0f, out var prefixWidth)); | ||
| Assert.True(PdfWriter.TryMeasurePreferredFontWidth("Cambria", firstLineTail, 11f, | ||
| false, false, charSpacing, out var tailWidth)); | ||
| Assert.True(PdfWriter.TryMeasurePreferredFontWidth("Cambria", firstLineTail + " over", 11f, | ||
| false, false, charSpacing, out var overflowingTailWidth)); |
| [InlineData("ftr", true)] | ||
| public void Convert_SymbolTable_UsesSymbolAscentAndLineHeight(string location, bool useFallbackFont) | ||
| { | ||
| using var stream = CreateDocxWithCambriaTable("\u2713", location); |
| // spaces) DO contribute — Word renders space glyphs at the run's font | ||
| // size, so they keep their full vertical metrics. | ||
| var effectiveFs = fontSize; | ||
| var baselineFontSize = 0f; |
| var usedLatinBaselineAdjustment = !state.IsTopOfPage && options.GridLinePitch <= 0 | ||
| && !paragraph.LineSpacingAbsolute | ||
| && latinBaselineAscent.HasValue && state.LastBaselineAscent.HasValue; |
| RenderParagraphBorders(state, paragraph, paragraphStartY + firstLineAscent, state.CurrentY + firstLineAscent); | ||
| var borderAscent = currentGridAscent > 0 ? firstLineAscent | ||
| : effectiveFs * topOfPageAscentRatio; | ||
| RenderParagraphBorders(state, paragraph, paragraphStartY + borderAscent, state.CurrentY + borderAscent); |
| var lines = WordWrap(text, textWidth, textWidth, runFontSize, null, | ||
| firstRun?.Bold ?? false, firstRun?.CharSpacing ?? 0f, cellUseCalibri); | ||
|
|
||
| var firstLineAscentRatio = GetCellFirstLineAscentRatio(metricFontName); |
| state.LastSpacingAfter = 0; | ||
| state.LastBaselineAscent = GetLatinBaselineAscent(s_defaultFontName, options.FontSize); | ||
| state.LastLineHeight = 0; |
| height = Math.Max(height, GetCellTopBorderInset(table, cell, rowIndex) | ||
| + GetCellBottomBorderInset(table, cell, rowIndex)); |
| if (table.CellSpacing > 0 || cell.IsVMergeContinue || cell.IsVMergeRestart) | ||
| return 0; |
|
Caution Review failedThe pull request is closed. Walkthrough
Merge Risk:
|


Summary
Fix .NET DOCX font measurement and table geometry exposed by
docx_classic115_price_list, including the remaining line-break difference before "over 100 seats."FEATURE_PARITY.md. There are no public API changes or new dependencies.Price-list output now matches Word's note line breaks:
No linked issue exists; this addresses a user-reported visual mismatch. LibreOffice behavior research used
SwTextGuess::Guess,SwFlowFrame::CalcUpperSpace, andSwTextFormatter::CalcRealHeight; no LibreOffice source or proprietary fonts were copied.Validation
Commands run on Windows:
over 100 seats., matching the Word reference.docx_classic38_table_column_widths,docx_classic68_sidebar_layout, anddocx_classic140_rotated_text_table. They are not conversion failures.Generated evidence is retained locally, not committed:
artifacts/dotnet-benchmark/classic/docx/report/artifacts/price-list-baseline/artifacts/price-note-baseline/full-after-audit.jsonThe price list is not pixel-identical overall; this change fixes the reported wrapping and coupled geometry without adding document-specific conditions. No new input acceptance or external network behavior is introduced.
Checklist
Summary by CodeRabbit