Skip to content

Fix DOCX font measurement and table layout to match Word - #206

Merged
shps951023 merged 2 commits into
mainfrom
fix/docx-price-list-font-layout
Nov 1, 2026
Merged

shps951023 merged 2 commits into
mainfrom
fix/docx-price-list-font-layout

Conversation

@shps951023

@shps951023 shps951023 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

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."

  • Use preferred Cambria advances consistently for table wrapping/row-height calculation and mixed-format paragraph words, spaces, bold/italic runs, positioning, alignment, and shading.
  • Distinguish explicitly sized paragraph marks from inherited cell defaults; account for border strokes in natural row heights, text insets, and vertical alignment.
  • Preserve paragraph baseline flow through large first runs, empty marks, inline-image hosts, and paragraph-to-table transitions. Collapse spacing across hard section starts.
  • Use verified Courier and Segoe fallback vertical metrics where needed.
  • Add focused regression tests and update FEATURE_PARITY.md. There are no public API changes or new dependencies.

Price-list output now matches Word's note line breaks:

Note: Volume discounts are applied automatically. Contact sales for enterprise agreements
over 100 seats.

No linked issue exists; this addresses a user-reported visual mismatch. LibreOffice behavior research used SwTextGuess::Guess, SwFlowFrame::CalcUpperSpace, and SwTextFormatter::CalcRealHeight; no LibreOffice source or proprietary fonts were copied.

Validation

Commands run on Windows:

dotnet test tests\MiniPdf.Tests\MiniPdf.Tests.csproj -c Release --filter 'FullyQualifiedName~Docx' --verbosity quiet
& .\scripts\Run-DotNet-VisualBenchmark.ps1 -Suite classic -Format docx -Filter 'docx_classic115_price_list' -ArtifactRoot artifacts\price-note-focused
& .\scripts\Run-DotNet-VisualBenchmark.ps1 -Suite classic -Format docx
git diff --check
  • DOCX tests: 124 passed, 0 failed, reconfirmed before publication.
  • Focused benchmark: passed; an explicit PDF text/geometry check confirms the second note line is exactly over 100 seats., matching the Word reference.
  • Price list, original baseline to final: overall 0.9585 -> 0.9915, visual 0.9254 -> 0.9787, text similarity 0.9709 -> 1.0000; candidate/reference pages remain 1/1.
  • Full benchmark: regenerated 180 candidates and verified 1,090 report images, with no conversion failures, missing/invalid PDFs, or missing references/images.
  • Full runner exits nonzero solely because three pre-existing cases remain below its default 0.95 score floor: docx_classic38_table_column_widths, docx_classic68_sidebar_layout, and docx_classic140_rotated_text_table. They are not conversion failures.
  • Mean visual score against the original baseline: 0.9649517 -> 0.9870061. No case loses more than 0.002 visual score. Only classic50 changes page count, correcting 1 -> 2 pages to match Word.
  • Against the immediately preceding validated implementation, the note fix improves 21 cases, leaves 158 unchanged, and decreases one by only 0.0005; no page-count changes or text-similarity losses over 0.01.
  • Whitespace validation: passed. Full solution tests and non-.NET implementations were not run for this DOCX-only change.

Generated evidence is retained locally, not committed:

  • artifacts/dotnet-benchmark/classic/docx/report/
  • artifacts/price-list-baseline/
  • artifacts/price-note-baseline/full-after-audit.json

The 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

  • The change is focused and does not include unrelated formatting or generated files.
  • New or changed behavior has automated test coverage where practical.
  • Relevant .NET and/or Rust checks pass.
  • Public API and compatibility effects are documented.
  • Security implications and untrusted-input behavior were considered.
  • I have the right to submit every added source file, fixture, font, image, and other asset under its stated license.
  • Third-party and generated material includes its source and license.
  • Test documents contain no confidential information or personal data.
  • User-facing documentation is updated, including translated README files when the English README changed.
  • Large benchmark outputs are attached or stored as CI artifacts unless they are required canonical evidence.

Summary by CodeRabbit

  • Bug Fixes
    • Improved DOCX-to-PDF text wrapping, alignment, and spacing across paragraphs, tables, and section breaks.
    • Improved table layout around borders and vertical alignment, including in headers and footers.
    • Improved rendering of mixed-font text, symbols, emoji, and inline images.
    • Refined line-height and baseline positioning for more consistent layouts across supported fonts, including Cambria, Calibri, Courier, and Segoe UI.
    • Improved spacing around images and transitions between paragraphs, tables, and sections.

shps951023 and others added 2 commits November 1, 2026 16:54
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>
@shps951023

Copy link
Copy Markdown
Member Author

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: dotnet test tests\MiniPdf.Tests\MiniPdf.Tests.csproj -c Release --filter "FullyQualifiedName~Docx|FullyQualifiedName~SystemFontTheoryAttributeTests" --verbosity quiet passed (125 tests, zero failures/skips); git diff --check passed. Waiting for the new GitHub checks before merging.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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
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

Comment on lines +1799 to +1801
if (followedByTable && options.GridLinePitch <= 0 && !paragraph.LineSpacingAbsolute
&& paragraph.Images.Any(image => !image.IsAnchor))
state.CurrentY += baselineFontSize * topOfPageAscentRatio;
Comment on lines +29 to +34
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;
Comment on lines +1218 to +1220
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);
Comment on lines 4764 to +4766
state.LastSpacingAfter = 0;
state.LastBaselineAscent = GetLatinBaselineAscent(s_defaultFontName, options.FontSize);
state.LastLineHeight = 0;
Comment on lines +4991 to +4992
height = Math.Max(height, GetCellTopBorderInset(table, cell, rowIndex)
+ GetCellBottomBorderInset(table, cell, rowIndex));
Comment on lines +4999 to +5000
if (table.CellSpacing > 0 || cell.IsVMergeContinue || cell.IsVMergeRestart)
return 0;
@shps951023
shps951023 merged commit 3fa3268 into main Nov 1, 2026
3 checks passed
Copilot AI balanced review requested due to automatic review settings November 1, 2026 09:55
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Walkthrough

The DOCX converter now uses font-specific metrics and spacing calculations for paragraph flow, text wrapping, and tables. The changes include paragraph-mark font-size data, table-border insets, emoji font fallback metrics, rendering regression tests, and Windows CI checks for required fonts.

Changes: DOCX layout rendering

Layer / File(s) Summary
Paragraph baselines and spacing
src/MiniPdf/DocxToPdfConverter.cs, tests/MiniPdf.Tests/DocxToPdfConverterTests.cs, FEATURE_PARITY.md
Paragraph layout tracks ascent and section spacing across paragraphs, empty paragraphs, and inline images. Tests cover paragraph spacing, font transitions, image spacing, and section breaks.
Font-aware text measurement
src/MiniPdf/DocxToPdfConverter.cs, src/MiniPdf/PdfWriter.cs, tests/MiniPdf.Tests/DocxToPdfConverterTests.cs
Text-width estimation and wrapping use font and italic information, with preferred-font measurements when available. Tests cover mixed-format Cambria runs and table text wrapping.
Table metrics, borders, and cell positioning
src/MiniPdf/DocxReader.cs, src/MiniPdf/DocxToPdfConverter.cs, src/MiniPdf/PdfWriter.cs, tests/MiniPdf.Tests/DocxToPdfConverterTests.cs
The reader stores paragraph-mark font size. Table layout uses font metrics for cell line placement and height, accounts for resolved border insets, and selects Segoe UI Emoji metrics for eligible runs. Tests cover tables in the body, headers, and footers.
Font-dependent test support
.github/workflows/ci.yml, CONTRIBUTING.md, tests/MiniPdf.Tests/SystemFontTheoryAttribute.cs
Font-dependent theories skip when required fonts cannot be measured. Windows CI verifies required font files and runs the selected DOCX and system-font tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Merge Risk: Moderate · up to a22ec

The layout changes can misplace paragraphs, overlap final text with a following table, and shift centered merged-cell content. Resolve these localized rendering regressions before merging; the additional test suggestions improve confidence but do not establish production failures.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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