Repository navigation
Conversation
ikashapov
force-pushed
the
issue-151-pgborders-docgrid
branch
5 times, most recently
from
September 16, 2026 09:38
da75014 to
6a48f63
Compare
ikashapov
force-pushed
the
issue-151-pgborders-docgrid
branch
from
September 18, 2026 02:49
6a48f63 to
ca5ada5
Compare
Two section-level features that change how a page looks (issue nerdy-pro#151), independent halves of one w:sectPr. w:pgBorders (§17.6.10) was not parsed at all. Now: schema + model (attributes optional, mirrored as written; [MS-OE376] defaults text/front/allPages applied at render), and a painter pass beside header/footer rendering — the one point where a finished page and its section's PageConfig coexist, which @w:zOrder needs since painting order is z-order (front = after everything, back = before). Geometry per the spec prose and the Word↔Writer interop notes: space measures to the border edge nearest the reference — page edge inward, or text margin outward; horizontal edges own the corners, verticals run between the bands (the table painter's junction convention). Edges resolve through the tables' convert_model_border, so double draws its §17.4.38 two-thirds band and the other line styles approximate solid, warn-once. The @w:val here spans all of ST_Border including the ~165 art names no other border may carry — those parse losslessly (points, not eighths, for sz) and decline to draw, the LibreOffice reading, warn-once. The endnotes page continues the last section's flow (§17.11.2), so it wears that section's borders at the page index after the section's own — display="firstPage" does not re-fire there. A page shared across a continuous break takes the owning section's borders, the same ownership rule the header selection applies (module doc records the open Word-reference-render question). shadow/frame are accepted-and-dropped; bordersDoNotSurroundHeader/Footer are not read. w:docGrid (§17.6.5) was parsed and ignored. The pitch now applies — but only when @w:type is lines/linesAndChars/snapToChars: every Word document carries <w:docGrid w:linePitch="360"/> with no type, and §17.18.14 spells out that default/absent means no grid (the settings survive for re-enabling). A gridded line consumes ceil(h/pitch) whole slots — Word's signature spacing jump for tall text — with its single-spaced box (max of natural and text height, leading included) centered in them, per LibreOffice's Word-conformed CalcRealHeight: lineRule="exact" escapes (the override §17.6.5 itself names), atLeast takes max(slot, min) except the MSO atLeast-0 quirk, auto multiplies the slotted height. Registration is by quantized heights alone, position-independent, which is what lets pagination sum precomputed line heights and still agree with emission — the grid rides resolve_line_metrics at the four sites that must agree, and the keepNext group pre-measure stamps the same pitch, or a gridded chain would be measured short and torn at page bottoms. Scope is the spec's: body flow only, stamped onto ParagraphStyle at the body page layout and nowhere else, so table cells (no adjustLineHeightInTable support), headers/footers and footnotes stay off by construction; pPr w:snapToGrid (§17.3.1.32, newly parsed, cascading) opts a paragraph out. Character pitch is not applied. Fixtures (scripts/make_issue151_fixtures.py, byte-reproducible, strict CT_SectPr child order — the issue-165 lesson): page-borders.docx runs four deliberately distinct edges through both offset modes; doc-grid.docx carries identical content under type="lines" and under the type-less grid — the ungridded control the assertions measure against, so no font metric is pinned. tests/page_borders.rs pins each edge's band by its own colour in both frames plus display/zOrder/art and the endnotes page; tests/doc_grid.rs pins pitch deltas, slot centering as a relation between the two sections, both escapes, the two-slot jump, the empty-paragraph slot, the table-cell exclusion, and the keepNext measure. Six deliberate implementation breaks (grid off, snap ignored, typeless applied, display ignored, centering dropped, keepNext measure unstamped) each fail their own tests. The 28 existing corpus documents render byte-identically before and after (modulo the DATE-field fixture rendering the render date). Closes nerdy-pro#151
ikashapov
force-pushed
the
issue-151-pgborders-docgrid
branch
from
September 22, 2026 09:39
ca5ada5 to
27204b3
Compare
ikashapov
added a commit
to ikashapov/dxpdf
that referenced
this pull request
Oct 14, 2026
One squashed commit carrying the tree of integration/2026-10-14, which was previously a chain of ten merges ending at ac5787e (kept locally as archive/integration-2026-10-14-merges). Base: upstream main 3a5c7ca. Integrated, each at the head of its open upstream pull request: - nerdy-pro#252 fix-layout-defects-228-232 (a40d5ce) — issues nerdy-pro#228–nerdy-pro#232: bookmark-only paragraphs, nested-table width, list instances over one abstract numbering, table width resolution (pct and tcW autofit), column balancing and separators - nerdy-pro#183 issue-150-wmf-svg (6e5aa99) — WMF/EMF and SVG images - nerdy-pro#184 issue-151-pgborders-docgrid (27204b3) — page borders, document grid - nerdy-pro#186 issue-153-shaped-clusters (65739f6) — shaped text clusters - nerdy-pro#187 issue-154-tracked-changes (6982b34) — tracked changes and comments - nerdy-pro#185 issue-155-smartart-charts (3683223) — SmartArt and charts - nerdy-pro#256 issue-253-del-instr-text (0656a52) — w:delInstrText tolerated - nerdy-pro#257 issue-250-image-hyperlinks (cb5ce2b) — a:hlinkClick on pictures - nerdy-pro#258 issue-188-font-absent-cost (9a63bf0) — no deep catalogue tier before the substitution table, no theme script-font preload - nerdy-pro#259 issue-158-auto-hyphenation (9bfbae2) — w:autoHyphenation Beyond the union of those branches, the integration itself changed: - README keeps one row per feature after the merges; the matrix tally is recomputed from the rows. - nerdy-pro#253's tests assert that a deleted field code is never drawn and never run text, because nerdy-pro#154 keeps w:del runs instead of dropping them at parse. - nerdy-pro#250's push_link_over sits after nerdy-pro#151's resolve_line_metrics; nerdy-pro#158's HyphenationCtx.measure and rejoin_hyphenated take &dyn SplitMeasure (nerdy-pro#153). Verified on this exact tree: cargo fmt, clippy and doc clean; cargo test 2977 passed / 0 failed (skipping the capi library freshness guards); --no-default-features and --features capi build; the twelve-document harness passes 12/12.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Apply
w:pgBorderspage borders and thew:docGridline gridCloses #151. Based on
main(b2c0174, v0.5.1); one commit, branchissue-151-pgborders-docgrid. Independent of the other open branches.Two section-level features that change how a page looks, filed together
because both are
w:sectPrchildren; implemented as the issue suggested —independently — and landing together.
w:pgBorders— §17.6.10 (was: not parsed at all)Parse + model. New
PageBordersonSectionProperties(Dup, like itssiblings), attributes kept optional so the document is mirrored as written;
the renderer applies Word's defaults from [MS-OE376] §2.6.10
(
offsetFrom="text",zOrder="front") and §17.6.10's own(
display="allPages"). One parsing subtlety: page borders are the onlyplace
@w:valspans all of §17.18.2ST_Border— the 27 line stylesplus ~165 art names no other border may carry. The val lands as a string
and classifies afterwards, so
val="apples"cannot take down the whole<w:sectPr>; an art edge is carried losslessly (szreinterpreted as thewhole points the spec gives art borders, not the line styles' eighths).
Render. A new
layout::page_borderspass runs in phase 2 besideheader/footer rendering — the only point where a finished page's command
list and its section's
PageConfigcoexist, and@w:zOrderneeds exactlythat pair: painting order is z-order, so
frontappends aftereverything (headers, body, footers) and
backprepends. Geometry per theECMA prose and the Word↔Writer interop notes (
editeng:: BorderDistanceFromWord):spacemeasures to the border edge nearestthe reference — from the page edge inward, or from the text margin
outward; horizontal edges own the corner squares, vertical edges run
between the bands, the same junction convention the table painter uses.
Edges resolve through the tables' existing
convert_model_border, sodoubledraws its §17.4.38 two-thirds band via the same(
pub(crate)-lifted)emit_border_rect, the other 24 line stylesapproximate solid warn-once, and a
doublepage border can never driftfrom a
doubletable border.Declined, and recorded where: art borders draw nothing (the reading
LibreOffice takes) with a warn-once;
@shadow/@frameaccepted-and-dropped;
w:bordersDoNotSurroundHeader/Footernot read — atextborderalways references the configured margins (module doc says why the
configured margin, not the per-page pushed-down content, is the
reference).
w:docGrid— §17.6.5 (was: parsed, then ignored)The gate that matters most: the pitch applies only when
@w:typeislines/linesAndChars/snapToChars. Every Word document on earthcarries
<w:docGrid w:linePitch="360"/>with no type, and §17.18.14spells out that absent/
defaultmeans "no document grid" (kept so thesettings survive re-enabling). A test pins this directly, and the 28
existing corpus documents render byte-identically before and after.
The algorithm (LibreOffice's Word-conformed
CalcRealHeight, plus§17.6.5's own overrides): a gridded line consumes
ceil(h/pitch)wholeslots — tall text jumps straight to two, Word's signature behavior — with
its natural box vertically centered in them, which is the visible
half-gap above the first line of a Japanese page.
lineRule="exact"escapes (the one override §17.6.5 names);
atLeasttakesmax(slot, min)except the MSOatLeast-0 quirk (grid off for theline);
automultiplies the slotted height, clamped below at 1.0 — undera grid, "1.5 lines" is 1.5 pitches. Registration is by quantized heights
alone (no absolute re-snap), which keeps the advance
position-independent — that is what lets pagination sum precomputed line
heights and still agree with emission to the point.
Where it lives: one function,
resolve_line_metrics, feeding the foursites the module doc already required to agree (fitting, emission, the
border box, the empty-paragraph path — an empty gridded paragraph holds a
slot open, like Word's empty 行). The pitch reaches lines only by being
stamped onto
ParagraphStylein the body page flow, so the spec's scopecomes free: table cells (no
w:adjustLineHeightInTablesupport),headers/footers, footnotes and shape text never pass the stamp point.
w:pPr/w:snapToGrid(§17.3.1.32) is newly parsed, cascades through thestyle hierarchy like its sibling toggles, and opts a paragraph out.
Character pitch (
linesAndChars/snapToChars's horizontal half) is notapplied; the module doc records the boundary.
Tests and fixtures
test-files/page-borders.docx— the same four deliberately distinctedges (each its own colour, width, space) through both offset modes,
one section each; every edge's band is pinned by colour, and the two
pages differ only in reference frame.
test-files/doc-grid.docx—identical content twice:
type="lines" linePitch="360"vs thetype-less grid, making section 2 the ungridded control the assertions
measure against so no font metric is pinned. Both by
scripts/make_issue151_fixtures.py, verified withverify_docx.py.tests/page_borders.rs(8): both fixture pages edge-by-edge,displayselection,
zOrderfront/back as command order, art/nil/nonedeclines, the
textdefault.tests/doc_grid.rs(9): pitch deltas, slot centering as a relationbetween the fixture's two sections, both escapes (
exact,snapToGrid=0), the type-less no-op, the two-slot jump for 20pt text,the empty-paragraph slot, the table-cell exclusion.
escapes,
atLeast/autointeractions, leading-inclusive slot base),PageConfig::from_sectiongating, the pgBorders schema (art vals,defaults-as-None, all four edges), the
snapToGridtoggle and cascade.type-less applied,
displayignored, centering dropped, keepNextmeasure unstamped) each fail their own tests.
against
main; the two new fixtures verified visually (CoreGraphicsraster) — page borders in both frames, and the gridded page showing
the even 18pt pitch against its compact control.
Review
An adversarial multi-agent review of the diff (3 lenses, every finding
independently re-verified against the code) confirmed and led to fixing,
before this PR was opened:
group pre-measure laid paragraphs out with the unstamped style, so a
gridded chain was measured at natural heights, held at a page bottom it
could not fit, and torn. The measure now stamps the same pitch the
placement path does; a regression test builds the exact page geometry
where the two disagree.
now wears that section's borders — at the page index after the
section's own pages, so
display="firstPage"does not re-fire on it(both asserted).
xsd:sequence; thefixtures originally wrote
pgBorders/docGridbeforepgSz— exactlythe class of package this parser tolerates and Word refuses (the
issue-165 lesson). Regenerated in schema order, and the ZIPs are now
byte-reproducible (pinned timestamps, like the sibling scripts).
ST_PageBorder*§ citations were off by three(§17.18.62–64, cross-checked against the repo's own
ST_PageOrientation= §17.18.65); two pre-existing wrong § numbers on
header_refs/footer_refsthat collided with the new correct ones weredropped per the AGENTS rule; the README coverage tallies were updated;
a shared-continuous-page border ownership note (with its open
Word-reference-render question) landed in the module doc.
Gates
cargo fmt --check,clippy --all-targets -- -D warnings,RUSTDOCFLAGS="-D warnings" cargo doc --no-deps,cargo build --no-default-featuresand the full 46-target suite pass.No new dependencies; public API unchanged.