diff --git a/CHANGELOG.md b/CHANGELOG.md index 5966eac4c..08b1b6175 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A body paragraph or spacer the layout moves to a new page keeps the space above it there + in DOCX.** The page starts a paragraph a page break moves down at its own top edge, with the + containers opening there, and keeps the gap before it too where the gap did not fit at the + foot of the page above. Word drops a paragraph's space above at the top of a page: + `ModernProfessional`'s second page opens with a heading its section pads 8pt down, and in + Word it stood 7.7pt high with everything under it; `Executive`'s opens with the spacer + between two entries 3pt down, and stood 3.2pt high. That space is now a line's height, which + Word keeps, the gap that stays at the foot of the page above left as the space above that + line, so Word breaks the page where it did. Lines more than 2pt off fall from 15 to none for + `Executive` and from 5 to none for `ModernProfessional`, in both editors. - **What follows a line holding only a drawn shape stands where the page sets it in DOCX.** A shape is written as a picture a transparent frame larger than its ink on every side, and Word makes a line of pictures and no text, grown to them, as tall as the pictures, frame diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 1d2d60fc7..0d6d78b05 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -67,16 +67,16 @@ creation date is real metadata. | Document node | DOCX output | |---|---| -| Paragraphs | Word paragraphs with alignment, font, size, colour, bold/italic/underline; inline runs preserved; a `\n` in the text is a line break (`w:br`), which Word would otherwise read as a space; a table cell's `text("a\nb")` stays one line, as the page sets it; a `bulletOffset` of spaces is an indent — the wrapped lines (`FROM_SECOND_LINE`), the first (`FIRST_LINE`) or all of them start that far in, measured in the paragraph's style as the page measures it; a prefix with letters in it is not written, but the wrapped lines still start after the spaces the page covers it with; no prefix is applied to a paragraph written over the flow, as one of an overlay's left-and-right pair, or in a header or footer | +| Paragraphs | Word paragraphs with alignment, font, size, colour, bold/italic/underline; inline runs preserved; a `\n` in the text is a line break (`w:br`), which Word would otherwise read as a space; a table cell's `text("a\nb")` stays one line, as the page sets it; a `bulletOffset` of spaces is an indent — the wrapped lines (`FROM_SECOND_LINE`), the first (`FIRST_LINE`) or all of them start that far in, measured in the paragraph's style as the page measures it; a prefix with letters in it is not written, but the wrapped lines still start after the spaces the page covers it with; no prefix is applied to a paragraph written over the flow, as one of an overlay's left-and-right pair, or in a header or footer. A body paragraph the layout moves to a new page keeps the space the layout leaves above its text there — its own top edge, the edges of the containers opening with it, and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, since Word drops a paragraph's space above at the top of a page and keeps a line's height; the rest of the space owed stays above that line, so Word breaks the page where it did, and the gap between the paragraph's lines comes off the line as it would off its space above. A paragraph in a table cell, an overlay or a list is left to its container | | Lists | Real Word lists: a `numbering.xml` definition per list, `w:numPr` on each item, and the authored marker as the level's text. Nesting is a list level, so Enter continues the list and Tab demotes an item. See "What a list becomes" below for the kinds that stay plain paragraphs | -| Tables | Word tables, one cell per cell. Each cell states its own padding, on all four sides, so a row is as tall as the page draws it: as `w:tcMar`, and above and below partly in its paragraphs. Word and LibreOffice give every cell of a row the largest top and bottom margin of any cell in it, so a row's cells are written with its smallest, and the rest of a cell's padding above and below is space above its first paragraph and below its last (measured: a row whose day cells were padded 5.5pt above and 10.25pt below beside a label padded 0.75pt stood 60.3pt tall in both editors, where its tallest cell came to 46). A cell opening with a table has no paragraph above it to hold its padding, and a cell in a vertical merge has its bottom edge in another row: these keep their margins, and the row's comes down no lower than the largest of them. Its padding above and below gives up the room Word makes for the table's horizontal rules — half of a rule between two rows to each, the lower row's rule where the two differ, the rule above the table and the one below it whole to their row — which the page does not (measured: a 0.75pt rule made each row 0.75pt taller). A cell that holds nothing but an empty line — a row that is only a rule, its thickness the empty cell's font — has that line cut to the room its row leaves it, the page's row less the cell's own margins and border: the page draws the rule's borders across the line, and Word and LibreOffice keep them outside it and grow the row (measured: `CobaltRota`'s two rules under a 0.9pt border stood 0.9pt taller each). A line with letters, a picture or a paragraph border of its own keeps its height. A table that states no rule is written with the engine's default 1pt black rule, as the page draws it, not left on Word's thinner grid. Its `textAnchor` becomes `w:vAlign` and the paragraph's `w:jc`, with the engine's default — the vertical middle, on the left — where Word's is the top, so a line beside a taller neighbour sits where the page puts it and an amount column stays right-aligned. A cell with no style of its own is set in the engine's default cell face rather than the document's Normal. A cell's lines are one paragraph with line breaks; when its style's `lineSpacing(...)` is above zero and it has more than one line, they are a paragraph each, with the spacing after every line but the last, since Word has no space between the lines of one paragraph but a taller line. A column sized to its content gets a point more than the page gives it, so the editor's font substitute cannot wrap its widest cell. The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back". A table breaks across pages where the layout breaks it: every row the layout placed is kept whole (`w:cantSplit`), `repeatHeader(n)` rows repeat on each page (`w:tblHeader`) and stay with the row under them. Two tables in a row — rows included, since a row is carried as a table — are kept apart by a paragraph a tenth of a point tall, holding the rest of the gap between them: an editor joins two tables with nothing between them into one. A table or a row the layout moves to a new page keeps its own top edge there, as the page does — written as a line that tall, kept with it, since Word drops a paragraph's space above at the top of a page — while the gap between it and the block before stays at the foot of the page above, where it fits there; a paragraph moved to a new page does not yet keep its own | +| Tables | Word tables, one cell per cell. Each cell states its own padding, on all four sides, so a row is as tall as the page draws it: as `w:tcMar`, and above and below partly in its paragraphs. Word and LibreOffice give every cell of a row the largest top and bottom margin of any cell in it, so a row's cells are written with its smallest, and the rest of a cell's padding above and below is space above its first paragraph and below its last (measured: a row whose day cells were padded 5.5pt above and 10.25pt below beside a label padded 0.75pt stood 60.3pt tall in both editors, where its tallest cell came to 46). A cell opening with a table has no paragraph above it to hold its padding, and a cell in a vertical merge has its bottom edge in another row: these keep their margins, and the row's comes down no lower than the largest of them. Its padding above and below gives up the room Word makes for the table's horizontal rules — half of a rule between two rows to each, the lower row's rule where the two differ, the rule above the table and the one below it whole to their row — which the page does not (measured: a 0.75pt rule made each row 0.75pt taller). A cell that holds nothing but an empty line — a row that is only a rule, its thickness the empty cell's font — has that line cut to the room its row leaves it, the page's row less the cell's own margins and border: the page draws the rule's borders across the line, and Word and LibreOffice keep them outside it and grow the row (measured: `CobaltRota`'s two rules under a 0.9pt border stood 0.9pt taller each). A line with letters, a picture or a paragraph border of its own keeps its height. A table that states no rule is written with the engine's default 1pt black rule, as the page draws it, not left on Word's thinner grid. Its `textAnchor` becomes `w:vAlign` and the paragraph's `w:jc`, with the engine's default — the vertical middle, on the left — where Word's is the top, so a line beside a taller neighbour sits where the page puts it and an amount column stays right-aligned. A cell with no style of its own is set in the engine's default cell face rather than the document's Normal. A cell's lines are one paragraph with line breaks; when its style's `lineSpacing(...)` is above zero and it has more than one line, they are a paragraph each, with the spacing after every line but the last, since Word has no space between the lines of one paragraph but a taller line. A column sized to its content gets a point more than the page gives it, so the editor's font substitute cannot wrap its widest cell. The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back". A table breaks across pages where the layout breaks it: every row the layout placed is kept whole (`w:cantSplit`), `repeatHeader(n)` rows repeat on each page (`w:tblHeader`) and stay with the row under them. Two tables in a row — rows included, since a row is carried as a table — are kept apart by a paragraph a tenth of a point tall, holding the rest of the gap between them: an editor joins two tables with nothing between them into one. A table or a row the layout moves to a new page keeps its own top edge there, as the page does — written as a line that tall, kept with it, since Word drops a paragraph's space above at the top of a page — while the gap between it and the block before stays at the foot of the page above, where it fits there; a gap the layout carries onto the new page, because it did not fit at the foot of the page above, is not yet held above a table (body paragraphs and spacers: see their rows) | | Composed cells (`DocumentTableCell.node(...)`) | Written by the same writers that write that node anywhere else, so a cell built from an image, a list or a table carries it. A nested table is a real `w:tbl` followed by the paragraph Word requires a cell to end with — a hairline, which the paragraph written next in the cell takes over, so no empty line opens under the table, and whose mark is hidden where it is left at the cell's end holding nothing and no space, since LibreOffice lays it out — and takes the width of the column it sits in — the column's, not the one the page gives it, because the layout reports a composed cell's content under the owner's path | | Inline chips (`inlineCode(...)`, `inlineChip(...)`, `highlight(...)`) | The chip's fill becomes the run's own `w:shd`, in a paragraph and in a list item alike. Its shape does not travel — see "What a chip keeps and loses" below | | Images | Embedded pictures at the node's declared size | | Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line | | Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel) | -| Spacers | An empty paragraph a tenth of a point tall; the spacer's height is the space above the next block, or below this paragraph when a table follows | +| Spacers | An empty paragraph a tenth of a point tall; the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above | | Page breaks | Explicit Word page breaks | Page geometry (size, margins and orientation — a page wider than it is tall is stated as diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv index d0fa2ddb3..e9970c8a4 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -92,23 +92,23 @@ cv-boxed_sections 0:c2f296ef30a3 0.20 professionalexperience cv-boxed_sections 0:a76f47480818 0.19 seniorplatformengineer cv-boxed_sections 0:060300b18051 0.16 2024-present cv-boxed_sections 0:f890faef907f 0.18 northwindsystems -cv-boxed_sections 1:49faf14f1f56 -0.73 ledthereusabledocument-generatio -cv-boxed_sections 1:6bd3f168e89e -0.73 maintenancetimeby70%byretiringpe -cv-boxed_sections 1:daf65306b3b6 -0.30 softwareengineer -cv-boxed_sections 1:e7cc1822c6dc -0.33 2021-2024 -cv-boxed_sections 1:998c1a985022 -0.31 brightleaflabs -cv-boxed_sections 1:e55664011cda -0.59 builtbackendservicesandproductio -cv-boxed_sections 1:e80236b6d2b1 -0.59 itexttoacustomlayoutengine,elimi -cv-boxed_sections 1:41bb54fbae8d -0.16 backendengineer -cv-boxed_sections 1:3fa772117260 -0.19 2019-2021 -cv-boxed_sections 1:ef148f221e31 -0.17 helixprintco -cv-boxed_sections 1:9d31a6bfbc69 -0.45 maintainedahigh-volumeinvoice-pr -cv-boxed_sections 1:03af8a628309 -0.45 change. -cv-boxed_sections 1:676dfdd56ad2 -0.13 additionalinformation -cv-boxed_sections 1:f20997652453 -0.16 languages:english(fluent),german -cv-boxed_sections 1:ec9d3622395e -0.16 workeligibility:eligibletoworkin -cv-boxed_sections 1:dd20ae01c3cf -0.15 opensource:maintainerofgraphcomp -cv-boxed_sections 1:33c35e9d236d -0.15 speaking:jvmsummit2024,devoxxuk2 +cv-boxed_sections 1:49faf14f1f56 -0.13 ledthereusabledocument-generatio +cv-boxed_sections 1:6bd3f168e89e -0.13 maintenancetimeby70%byretiringpe +cv-boxed_sections 1:daf65306b3b6 0.30 softwareengineer +cv-boxed_sections 1:e7cc1822c6dc 0.27 2021-2024 +cv-boxed_sections 1:998c1a985022 0.29 brightleaflabs +cv-boxed_sections 1:e55664011cda 0.01 builtbackendservicesandproductio +cv-boxed_sections 1:e80236b6d2b1 0.01 itexttoacustomlayoutengine,elimi +cv-boxed_sections 1:41bb54fbae8d 0.44 backendengineer +cv-boxed_sections 1:3fa772117260 0.41 2019-2021 +cv-boxed_sections 1:ef148f221e31 0.43 helixprintco +cv-boxed_sections 1:9d31a6bfbc69 0.15 maintainedahigh-volumeinvoice-pr +cv-boxed_sections 1:03af8a628309 0.15 change. +cv-boxed_sections 1:676dfdd56ad2 0.47 additionalinformation +cv-boxed_sections 1:f20997652453 0.44 languages:english(fluent),german +cv-boxed_sections 1:ec9d3622395e 0.44 workeligibility:eligibletoworkin +cv-boxed_sections 1:dd20ae01c3cf 0.45 opensource:maintainerofgraphcomp +cv-boxed_sections 1:33c35e9d236d 0.45 speaking:jvmsummit2024,devoxxuk2 cv-centered_headline 0:8f1010120ed6 -0.50 jordanrivera cv-centered_headline 0:87ca614d0eab -0.18 professionaltitle cv-centered_headline 0:8d7a2cca3060 -0.23 +442055551000|jordan.rivera@exam @@ -158,13 +158,13 @@ cv-centered_headline 0:f0e2046536d3 0.94 builtbackendservicesandproductio cv-centered_headline 0:4eb608992328 0.95 toacustomlayoutengine,eliminatin cv-centered_headline 0:41bb54fbae8d 0.72 backendengineer cv-centered_headline 0:ef148f221e31 0.75 helixprintco -cv-centered_headline 1:9d31a6bfbc69 0.31 maintainedahigh-volumeinvoice-pr -cv-centered_headline 1:03af8a628309 0.32 change. -cv-centered_headline 1:676dfdd56ad2 -0.01 additionalinformation -cv-centered_headline 1:f20997652453 0.03 languages:english(fluent),german -cv-centered_headline 1:ec9d3622395e 0.04 workeligibility:eligibletoworkin -cv-centered_headline 1:dd20ae01c3cf 0.05 opensource:maintainerofgraphcomp -cv-centered_headline 1:33c35e9d236d 0.06 speaking:jvmsummit2024,devoxxuk2 +cv-centered_headline 1:9d31a6bfbc69 0.36 maintainedahigh-volumeinvoice-pr +cv-centered_headline 1:03af8a628309 0.37 change. +cv-centered_headline 1:676dfdd56ad2 0.04 additionalinformation +cv-centered_headline 1:f20997652453 0.08 languages:english(fluent),german +cv-centered_headline 1:ec9d3622395e 0.09 workeligibility:eligibletoworkin +cv-centered_headline 1:dd20ae01c3cf 0.10 opensource:maintainerofgraphcomp +cv-centered_headline 1:33c35e9d236d 0.11 speaking:jvmsummit2024,devoxxuk2 cv-charcoal_gold 0:73423c7707c4 3.21 anastasia cv-charcoal_gold 0:6627835f988e 2.75 smith cv-charcoal_gold 0:093e7d5fdbaa 2.72 contact @@ -433,15 +433,15 @@ cv-editorial_blue 0:998c1a985022 0.55 brightleaflabs cv-editorial_blue 0:e86691b0219f 0.26 builtbackendservicesandproductio cv-editorial_blue 0:4ec57434564a 0.26 migrationfromitexttoacustomlayou cv-editorial_blue 0:2427b23ddf4e 0.67 backendengineer2019-2021 -cv-editorial_blue 1:ef148f221e31 -1.80 helixprintco -cv-editorial_blue 1:f811fbee6901 -2.09 maintainedahigh-volumeinvoice-pr -cv-editorial_blue 1:cbfca5b2a6d7 -2.09 templatechange. -cv-editorial_blue 1:7b476727faf3 -1.78 additional -cv-editorial_blue 1:f20997652453 -1.81 languages:english(fluent),german -cv-editorial_blue 1:ec9d3622395e -1.80 workeligibility:eligibletoworkin -cv-editorial_blue 1:dd20ae01c3cf -1.80 opensource:maintainerofgraphcomp -cv-editorial_blue 1:33c35e9d236d -1.79 speaking:jvmsummit2024,devoxxuk2 -cv-editorial_blue 1:0b516aa076e0 -1.82 referencesavailableuponrequest. +cv-editorial_blue 1:ef148f221e31 0.20 helixprintco +cv-editorial_blue 1:f811fbee6901 -0.09 maintainedahigh-volumeinvoice-pr +cv-editorial_blue 1:cbfca5b2a6d7 -0.09 templatechange. +cv-editorial_blue 1:7b476727faf3 0.22 additional +cv-editorial_blue 1:f20997652453 0.19 languages:english(fluent),german +cv-editorial_blue 1:ec9d3622395e 0.20 workeligibility:eligibletoworkin +cv-editorial_blue 1:dd20ae01c3cf 0.20 opensource:maintainerofgraphcomp +cv-editorial_blue 1:33c35e9d236d 0.21 speaking:jvmsummit2024,devoxxuk2 +cv-editorial_blue 1:0b516aa076e0 0.18 referencesavailableuponrequest. cv-engineering_resume 0:8f1010120ed6 0.15 jordanrivera cv-engineering_resume 0:2d58f0cadc2d -0.25 platformengineer cv-engineering_resume 0:fbeea547d99e -0.21 london,uk @@ -544,21 +544,21 @@ cv-executive 0:060300b18051 0.20 2024-present cv-executive 0:f890faef907f 0.20 northwindsystems cv-executive 0:1af5650959fe 0.50 ledthereusabledocument-generatio cv-executive 0:27c01c228bd8 0.50 templatemaintenancetimeby70%byre -cv-executive 1:daf65306b3b6 -3.18 softwareengineer -cv-executive 1:e7cc1822c6dc -3.18 2021-2024 -cv-executive 1:998c1a985022 -3.18 brightleaflabs -cv-executive 1:ff8303967fa4 -2.88 builtbackendservicesandproductio -cv-executive 1:bd1fc9a4d815 -2.88 fromitexttoacustomlayoutengine,e -cv-executive 1:41bb54fbae8d -3.08 backendengineer -cv-executive 1:3fa772117260 -3.08 2019-2021 -cv-executive 1:ef148f221e31 -3.08 helixprintco -cv-executive 1:f811fbee6901 -2.78 maintainedahigh-volumeinvoice-pr -cv-executive 1:cbfca5b2a6d7 -2.78 templatechange. -cv-executive 1:676dfdd56ad2 -2.89 additionalinformation -cv-executive 1:f20997652453 -3.10 languages:english(fluent),german -cv-executive 1:ec9d3622395e -3.10 workeligibility:eligibletoworkin -cv-executive 1:dd20ae01c3cf -3.10 opensource:maintainerofgraphcomp -cv-executive 1:33c35e9d236d -3.10 speaking:jvmsummit2024,devoxxuk2 +cv-executive 1:daf65306b3b6 -0.18 softwareengineer +cv-executive 1:e7cc1822c6dc -0.18 2021-2024 +cv-executive 1:998c1a985022 -0.18 brightleaflabs +cv-executive 1:ff8303967fa4 0.12 builtbackendservicesandproductio +cv-executive 1:bd1fc9a4d815 0.12 fromitexttoacustomlayoutengine,e +cv-executive 1:41bb54fbae8d -0.08 backendengineer +cv-executive 1:3fa772117260 -0.08 2019-2021 +cv-executive 1:ef148f221e31 -0.08 helixprintco +cv-executive 1:f811fbee6901 0.22 maintainedahigh-volumeinvoice-pr +cv-executive 1:cbfca5b2a6d7 0.22 templatechange. +cv-executive 1:676dfdd56ad2 0.11 additionalinformation +cv-executive 1:f20997652453 -0.10 languages:english(fluent),german +cv-executive 1:ec9d3622395e -0.10 workeligibility:eligibletoworkin +cv-executive 1:dd20ae01c3cf -0.10 opensource:maintainerofgraphcomp +cv-executive 1:33c35e9d236d -0.10 speaking:jvmsummit2024,devoxxuk2 cv-midnight_navy 0:21d2636ad3df 3.33 alexmorgan cv-midnight_navy 0:5518402feccc 2.65 marketingmanager cv-midnight_navy 0:5518402feccc#1 2.33 marketingmanager @@ -679,23 +679,23 @@ cv-minimal_underlined 0:c2f296ef30a3 -13.36 professionalexperience cv-minimal_underlined 0:a76f47480818 -13.65 seniorplatformengineer cv-minimal_underlined 0:060300b18051 -13.69 2024-present cv-minimal_underlined 0:f890faef907f -13.66 northwindsystems -cv-minimal_underlined 1:49faf14f1f56 -0.73 ledthereusabledocument-generatio -cv-minimal_underlined 1:6bd3f168e89e -0.73 maintenancetimeby70%byretiringpe -cv-minimal_underlined 1:daf65306b3b6 -0.30 softwareengineer -cv-minimal_underlined 1:e7cc1822c6dc -0.33 2021-2024 -cv-minimal_underlined 1:998c1a985022 -0.31 brightleaflabs -cv-minimal_underlined 1:e55664011cda -0.59 builtbackendservicesandproductio -cv-minimal_underlined 1:e80236b6d2b1 -0.59 itexttoacustomlayoutengine,elimi -cv-minimal_underlined 1:41bb54fbae8d -0.16 backendengineer -cv-minimal_underlined 1:3fa772117260 -0.19 2019-2021 -cv-minimal_underlined 1:ef148f221e31 -0.17 helixprintco -cv-minimal_underlined 1:9d31a6bfbc69 -0.45 maintainedahigh-volumeinvoice-pr -cv-minimal_underlined 1:03af8a628309 -0.45 change. -cv-minimal_underlined 1:676dfdd56ad2 -0.12 additionalinformation -cv-minimal_underlined 1:f20997652453 -0.43 languages:english(fluent),german -cv-minimal_underlined 1:ec9d3622395e -0.43 workeligibility:eligibletoworkin -cv-minimal_underlined 1:dd20ae01c3cf -0.42 opensource:maintainerofgraphcomp -cv-minimal_underlined 1:33c35e9d236d -0.42 speaking:jvmsummit2024,devoxxuk2 +cv-minimal_underlined 1:49faf14f1f56 -0.13 ledthereusabledocument-generatio +cv-minimal_underlined 1:6bd3f168e89e -0.13 maintenancetimeby70%byretiringpe +cv-minimal_underlined 1:daf65306b3b6 0.30 softwareengineer +cv-minimal_underlined 1:e7cc1822c6dc 0.27 2021-2024 +cv-minimal_underlined 1:998c1a985022 0.29 brightleaflabs +cv-minimal_underlined 1:e55664011cda 0.01 builtbackendservicesandproductio +cv-minimal_underlined 1:e80236b6d2b1 0.01 itexttoacustomlayoutengine,elimi +cv-minimal_underlined 1:41bb54fbae8d 0.44 backendengineer +cv-minimal_underlined 1:3fa772117260 0.41 2019-2021 +cv-minimal_underlined 1:ef148f221e31 0.43 helixprintco +cv-minimal_underlined 1:9d31a6bfbc69 0.15 maintainedahigh-volumeinvoice-pr +cv-minimal_underlined 1:03af8a628309 0.15 change. +cv-minimal_underlined 1:676dfdd56ad2 0.48 additionalinformation +cv-minimal_underlined 1:f20997652453 0.17 languages:english(fluent),german +cv-minimal_underlined 1:ec9d3622395e 0.17 workeligibility:eligibletoworkin +cv-minimal_underlined 1:dd20ae01c3cf 0.18 opensource:maintainerofgraphcomp +cv-minimal_underlined 1:33c35e9d236d 0.18 speaking:jvmsummit2024,devoxxuk2 cv-mint_editorial 0:8f1010120ed6 -1.30 jordanrivera cv-mint_editorial 0:2d58f0cadc2d -0.27 platformengineer cv-mint_editorial 0:093e7d5fdbaa -0.71 contact @@ -809,11 +809,11 @@ cv-modern_professional 0:41bb54fbae8d 0.59 backendengineer cv-modern_professional 0:ef148f221e31 0.54 helixprintco cv-modern_professional 0:a32c7f2401d9 0.30 maintainedahigh-volumeinvoice-pr cv-modern_professional 0:7068c1d7034b 0.30 everytemplatechange. -cv-modern_professional 1:676dfdd56ad2 -7.64 additionalinformation -cv-modern_professional 1:f20997652453 -7.77 languages:english(fluent),german -cv-modern_professional 1:ec9d3622395e -7.77 workeligibility:eligibletoworkin -cv-modern_professional 1:dd20ae01c3cf -7.77 opensource:maintainerofgraphcomp -cv-modern_professional 1:33c35e9d236d -7.77 speaking:jvmsummit2024,devoxxuk2 +cv-modern_professional 1:676dfdd56ad2 0.36 additionalinformation +cv-modern_professional 1:f20997652453 0.23 languages:english(fluent),german +cv-modern_professional 1:ec9d3622395e 0.23 workeligibility:eligibletoworkin +cv-modern_professional 1:dd20ae01c3cf 0.23 opensource:maintainerofgraphcomp +cv-modern_professional 1:33c35e9d236d 0.23 speaking:jvmsummit2024,devoxxuk2 cv-monogram_sidebar 0:093e7d5fdbaa 2.70 contact cv-monogram_sidebar 0:89674e6f130e 2.85 +442055551000 cv-monogram_sidebar 0:e0f850305ded 2.87 jordan.rivera@example.com diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv index 50b3722e6..7e1325ac8 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv @@ -1,18 +1,18 @@ # libreoffice build 420(Build:2) on linux document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.14 0.46 0 -cv-boxed_sections 2 2 58 58 0.30 0.59 0 +cv-boxed_sections 2 2 58 58 0.30 0.46 0 cv-centered_headline 2 2 58 56 0.23 0.72 0 cv-charcoal_gold 1 1 85 83 2.94 3.29 83 cv-classic_serif 2 2 57 54 0.24 0.55 0 cv-compact_mono 1 1 58 58 0.27 0.93 0 -cv-editorial_blue 2 2 82 82 0.11 1.78 2 +cv-editorial_blue 2 2 82 82 0.10 0.45 0 cv-engineering_resume 1 1 57 57 0.60 1.62 0 -cv-executive 2 2 60 60 0.24 3.10 15 +cv-executive 2 2 60 60 0.18 0.30 0 cv-midnight_navy 1 1 86 80 2.72 3.24 76 -cv-minimal_underlined 2 2 59 57 0.69 13.40 9 +cv-minimal_underlined 2 2 59 57 0.68 13.40 9 cv-mint_editorial 2 2 59 59 0.27 0.60 0 -cv-modern_professional 2 2 61 59 0.12 0.59 5 +cv-modern_professional 2 2 61 59 0.12 0.44 0 cv-monogram_sidebar 1 1 53 50 2.90 3.57 50 cv-navy_sidebar 1 1 71 71 2.81 3.03 71 cv-nordic_clean 1 1 63 58 0.22 0.52 0 diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv index e59943004..63347d8d1 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -92,23 +92,23 @@ cv-boxed_sections 0:c2f296ef30a3 0.20 professionalexperience cv-boxed_sections 0:a76f47480818 0.19 seniorplatformengineer cv-boxed_sections 0:060300b18051 0.16 2024-present cv-boxed_sections 0:f890faef907f 0.18 northwindsystems -cv-boxed_sections 1:49faf14f1f56 -0.73 ledthereusabledocument-generatio -cv-boxed_sections 1:6bd3f168e89e -0.73 maintenancetimeby70%byretiringpe -cv-boxed_sections 1:daf65306b3b6 -0.30 softwareengineer -cv-boxed_sections 1:e7cc1822c6dc -0.33 2021-2024 -cv-boxed_sections 1:998c1a985022 -0.31 brightleaflabs -cv-boxed_sections 1:e55664011cda -0.59 builtbackendservicesandproductio -cv-boxed_sections 1:e80236b6d2b1 -0.59 itexttoacustomlayoutengine,elimi -cv-boxed_sections 1:41bb54fbae8d -0.16 backendengineer -cv-boxed_sections 1:3fa772117260 -0.19 2019-2021 -cv-boxed_sections 1:ef148f221e31 -0.17 helixprintco -cv-boxed_sections 1:9d31a6bfbc69 -0.45 maintainedahigh-volumeinvoice-pr -cv-boxed_sections 1:03af8a628309 -0.45 change. -cv-boxed_sections 1:676dfdd56ad2 -0.13 additionalinformation -cv-boxed_sections 1:f20997652453 -0.16 languages:english(fluent),german -cv-boxed_sections 1:ec9d3622395e -0.16 workeligibility:eligibletoworkin -cv-boxed_sections 1:dd20ae01c3cf -0.15 opensource:maintainerofgraphcomp -cv-boxed_sections 1:33c35e9d236d -0.15 speaking:jvmsummit2024,devoxxuk2 +cv-boxed_sections 1:49faf14f1f56 -0.13 ledthereusabledocument-generatio +cv-boxed_sections 1:6bd3f168e89e -0.13 maintenancetimeby70%byretiringpe +cv-boxed_sections 1:daf65306b3b6 0.30 softwareengineer +cv-boxed_sections 1:e7cc1822c6dc 0.27 2021-2024 +cv-boxed_sections 1:998c1a985022 0.29 brightleaflabs +cv-boxed_sections 1:e55664011cda 0.01 builtbackendservicesandproductio +cv-boxed_sections 1:e80236b6d2b1 0.01 itexttoacustomlayoutengine,elimi +cv-boxed_sections 1:41bb54fbae8d 0.44 backendengineer +cv-boxed_sections 1:3fa772117260 0.41 2019-2021 +cv-boxed_sections 1:ef148f221e31 0.43 helixprintco +cv-boxed_sections 1:9d31a6bfbc69 0.15 maintainedahigh-volumeinvoice-pr +cv-boxed_sections 1:03af8a628309 0.15 change. +cv-boxed_sections 1:676dfdd56ad2 0.47 additionalinformation +cv-boxed_sections 1:f20997652453 0.44 languages:english(fluent),german +cv-boxed_sections 1:ec9d3622395e 0.44 workeligibility:eligibletoworkin +cv-boxed_sections 1:dd20ae01c3cf 0.45 opensource:maintainerofgraphcomp +cv-boxed_sections 1:33c35e9d236d 0.45 speaking:jvmsummit2024,devoxxuk2 cv-centered_headline 0:8f1010120ed6 -0.50 jordanrivera cv-centered_headline 0:87ca614d0eab -0.18 professionaltitle cv-centered_headline 0:8d7a2cca3060 -0.23 +442055551000|jordan.rivera@exam @@ -158,13 +158,13 @@ cv-centered_headline 0:f0e2046536d3 0.94 builtbackendservicesandproductio cv-centered_headline 0:4eb608992328 0.95 toacustomlayoutengine,eliminatin cv-centered_headline 0:41bb54fbae8d 0.72 backendengineer cv-centered_headline 0:ef148f221e31 0.75 helixprintco -cv-centered_headline 1:9d31a6bfbc69 0.31 maintainedahigh-volumeinvoice-pr -cv-centered_headline 1:03af8a628309 0.32 change. -cv-centered_headline 1:676dfdd56ad2 -0.01 additionalinformation -cv-centered_headline 1:f20997652453 0.03 languages:english(fluent),german -cv-centered_headline 1:ec9d3622395e 0.04 workeligibility:eligibletoworkin -cv-centered_headline 1:dd20ae01c3cf 0.05 opensource:maintainerofgraphcomp -cv-centered_headline 1:33c35e9d236d 0.06 speaking:jvmsummit2024,devoxxuk2 +cv-centered_headline 1:9d31a6bfbc69 0.36 maintainedahigh-volumeinvoice-pr +cv-centered_headline 1:03af8a628309 0.37 change. +cv-centered_headline 1:676dfdd56ad2 0.04 additionalinformation +cv-centered_headline 1:f20997652453 0.08 languages:english(fluent),german +cv-centered_headline 1:ec9d3622395e 0.09 workeligibility:eligibletoworkin +cv-centered_headline 1:dd20ae01c3cf 0.10 opensource:maintainerofgraphcomp +cv-centered_headline 1:33c35e9d236d 0.11 speaking:jvmsummit2024,devoxxuk2 cv-charcoal_gold 0:73423c7707c4 3.21 anastasia cv-charcoal_gold 0:6627835f988e 2.75 smith cv-charcoal_gold 0:093e7d5fdbaa 2.72 contact @@ -543,21 +543,21 @@ cv-executive 0:060300b18051 0.20 2024-present cv-executive 0:f890faef907f 0.20 northwindsystems cv-executive 0:1af5650959fe 0.50 ledthereusabledocument-generatio cv-executive 0:27c01c228bd8 0.50 templatemaintenancetimeby70%byre -cv-executive 1:daf65306b3b6 -3.18 softwareengineer -cv-executive 1:e7cc1822c6dc -3.18 2021-2024 -cv-executive 1:998c1a985022 -3.18 brightleaflabs -cv-executive 1:ff8303967fa4 -2.88 builtbackendservicesandproductio -cv-executive 1:bd1fc9a4d815 -2.88 fromitexttoacustomlayoutengine,e -cv-executive 1:41bb54fbae8d -3.08 backendengineer -cv-executive 1:3fa772117260 -3.08 2019-2021 -cv-executive 1:ef148f221e31 -3.08 helixprintco -cv-executive 1:f811fbee6901 -2.78 maintainedahigh-volumeinvoice-pr -cv-executive 1:cbfca5b2a6d7 -2.78 templatechange. -cv-executive 1:676dfdd56ad2 -2.89 additionalinformation -cv-executive 1:f20997652453 -3.10 languages:english(fluent),german -cv-executive 1:ec9d3622395e -3.10 workeligibility:eligibletoworkin -cv-executive 1:dd20ae01c3cf -3.10 opensource:maintainerofgraphcomp -cv-executive 1:33c35e9d236d -3.10 speaking:jvmsummit2024,devoxxuk2 +cv-executive 1:daf65306b3b6 -0.18 softwareengineer +cv-executive 1:e7cc1822c6dc -0.18 2021-2024 +cv-executive 1:998c1a985022 -0.18 brightleaflabs +cv-executive 1:ff8303967fa4 0.12 builtbackendservicesandproductio +cv-executive 1:bd1fc9a4d815 0.12 fromitexttoacustomlayoutengine,e +cv-executive 1:41bb54fbae8d -0.08 backendengineer +cv-executive 1:3fa772117260 -0.08 2019-2021 +cv-executive 1:ef148f221e31 -0.08 helixprintco +cv-executive 1:f811fbee6901 0.22 maintainedahigh-volumeinvoice-pr +cv-executive 1:cbfca5b2a6d7 0.22 templatechange. +cv-executive 1:676dfdd56ad2 0.11 additionalinformation +cv-executive 1:f20997652453 -0.10 languages:english(fluent),german +cv-executive 1:ec9d3622395e -0.10 workeligibility:eligibletoworkin +cv-executive 1:dd20ae01c3cf -0.10 opensource:maintainerofgraphcomp +cv-executive 1:33c35e9d236d -0.10 speaking:jvmsummit2024,devoxxuk2 cv-midnight_navy 0:21d2636ad3df 3.33 alexmorgan cv-midnight_navy 0:5518402feccc 2.65 marketingmanager cv-midnight_navy 0:5518402feccc#1 2.33 marketingmanager @@ -678,23 +678,23 @@ cv-minimal_underlined 0:c2f296ef30a3 -13.36 professionalexperience cv-minimal_underlined 0:a76f47480818 -13.65 seniorplatformengineer cv-minimal_underlined 0:060300b18051 -13.69 2024-present cv-minimal_underlined 0:f890faef907f -13.66 northwindsystems -cv-minimal_underlined 1:49faf14f1f56 -0.73 ledthereusabledocument-generatio -cv-minimal_underlined 1:6bd3f168e89e -0.73 maintenancetimeby70%byretiringpe -cv-minimal_underlined 1:daf65306b3b6 -0.30 softwareengineer -cv-minimal_underlined 1:e7cc1822c6dc -0.33 2021-2024 -cv-minimal_underlined 1:998c1a985022 -0.31 brightleaflabs -cv-minimal_underlined 1:e55664011cda -0.59 builtbackendservicesandproductio -cv-minimal_underlined 1:e80236b6d2b1 -0.59 itexttoacustomlayoutengine,elimi -cv-minimal_underlined 1:41bb54fbae8d -0.16 backendengineer -cv-minimal_underlined 1:3fa772117260 -0.19 2019-2021 -cv-minimal_underlined 1:ef148f221e31 -0.17 helixprintco -cv-minimal_underlined 1:9d31a6bfbc69 -0.45 maintainedahigh-volumeinvoice-pr -cv-minimal_underlined 1:03af8a628309 -0.45 change. -cv-minimal_underlined 1:676dfdd56ad2 -0.12 additionalinformation -cv-minimal_underlined 1:f20997652453 -0.43 languages:english(fluent),german -cv-minimal_underlined 1:ec9d3622395e -0.43 workeligibility:eligibletoworkin -cv-minimal_underlined 1:dd20ae01c3cf -0.42 opensource:maintainerofgraphcomp -cv-minimal_underlined 1:33c35e9d236d -0.42 speaking:jvmsummit2024,devoxxuk2 +cv-minimal_underlined 1:49faf14f1f56 -0.13 ledthereusabledocument-generatio +cv-minimal_underlined 1:6bd3f168e89e -0.13 maintenancetimeby70%byretiringpe +cv-minimal_underlined 1:daf65306b3b6 0.30 softwareengineer +cv-minimal_underlined 1:e7cc1822c6dc 0.27 2021-2024 +cv-minimal_underlined 1:998c1a985022 0.29 brightleaflabs +cv-minimal_underlined 1:e55664011cda 0.01 builtbackendservicesandproductio +cv-minimal_underlined 1:e80236b6d2b1 0.01 itexttoacustomlayoutengine,elimi +cv-minimal_underlined 1:41bb54fbae8d 0.44 backendengineer +cv-minimal_underlined 1:3fa772117260 0.41 2019-2021 +cv-minimal_underlined 1:ef148f221e31 0.43 helixprintco +cv-minimal_underlined 1:9d31a6bfbc69 0.15 maintainedahigh-volumeinvoice-pr +cv-minimal_underlined 1:03af8a628309 0.15 change. +cv-minimal_underlined 1:676dfdd56ad2 0.48 additionalinformation +cv-minimal_underlined 1:f20997652453 0.17 languages:english(fluent),german +cv-minimal_underlined 1:ec9d3622395e 0.17 workeligibility:eligibletoworkin +cv-minimal_underlined 1:dd20ae01c3cf 0.18 opensource:maintainerofgraphcomp +cv-minimal_underlined 1:33c35e9d236d 0.18 speaking:jvmsummit2024,devoxxuk2 cv-mint_editorial 0:8f1010120ed6 -1.30 jordanrivera cv-mint_editorial 0:2d58f0cadc2d -0.27 platformengineer cv-mint_editorial 0:093e7d5fdbaa -0.71 contact @@ -808,11 +808,11 @@ cv-modern_professional 0:41bb54fbae8d 0.59 backendengineer cv-modern_professional 0:ef148f221e31 0.54 helixprintco cv-modern_professional 0:a32c7f2401d9 0.30 maintainedahigh-volumeinvoice-pr cv-modern_professional 0:7068c1d7034b 0.30 everytemplatechange. -cv-modern_professional 1:676dfdd56ad2 -7.64 additionalinformation -cv-modern_professional 1:f20997652453 -7.77 languages:english(fluent),german -cv-modern_professional 1:ec9d3622395e -7.77 workeligibility:eligibletoworkin -cv-modern_professional 1:dd20ae01c3cf -7.77 opensource:maintainerofgraphcomp -cv-modern_professional 1:33c35e9d236d -7.77 speaking:jvmsummit2024,devoxxuk2 +cv-modern_professional 1:676dfdd56ad2 0.36 additionalinformation +cv-modern_professional 1:f20997652453 0.23 languages:english(fluent),german +cv-modern_professional 1:ec9d3622395e 0.23 workeligibility:eligibletoworkin +cv-modern_professional 1:dd20ae01c3cf 0.23 opensource:maintainerofgraphcomp +cv-modern_professional 1:33c35e9d236d 0.23 speaking:jvmsummit2024,devoxxuk2 cv-monogram_sidebar 0:093e7d5fdbaa 2.70 contact cv-monogram_sidebar 0:89674e6f130e 2.85 +442055551000 cv-monogram_sidebar 0:e0f850305ded 2.87 jordan.rivera@example.com diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv index 8df7d81d1..7b2a6ec01 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv @@ -1,18 +1,18 @@ # libreoffice build bce0998afefd on windows document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.14 0.46 0 -cv-boxed_sections 2 2 58 58 0.30 0.59 0 +cv-boxed_sections 2 2 58 58 0.30 0.46 0 cv-centered_headline 2 2 58 56 0.23 0.72 0 cv-charcoal_gold 1 1 85 83 2.94 3.29 83 cv-classic_serif 2 2 57 54 0.24 0.55 0 cv-compact_mono 1 1 58 58 0.27 0.93 0 cv-editorial_blue 2 2 82 81 0.41 10.06 9 cv-engineering_resume 1 1 57 57 0.60 1.62 0 -cv-executive 2 2 60 60 0.24 3.10 15 +cv-executive 2 2 60 60 0.18 0.30 0 cv-midnight_navy 1 1 86 80 2.72 3.24 76 -cv-minimal_underlined 2 2 59 57 0.69 13.40 9 +cv-minimal_underlined 2 2 59 57 0.68 13.40 9 cv-mint_editorial 2 2 59 59 0.27 0.60 0 -cv-modern_professional 2 2 61 59 0.12 0.59 5 +cv-modern_professional 2 2 61 59 0.12 0.44 0 cv-monogram_sidebar 1 1 53 50 2.90 3.57 50 cv-navy_sidebar 1 1 71 71 2.81 3.03 71 cv-nordic_clean 1 1 63 58 0.20 0.57 0 diff --git a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv index 84a46b6d5..d21a5c685 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -92,23 +92,23 @@ cv-boxed_sections 0:c2f296ef30a3 0.28 professionalexperience cv-boxed_sections 0:a76f47480818 0.30 seniorplatformengineer cv-boxed_sections 0:060300b18051 0.35 2024-present cv-boxed_sections 0:f890faef907f 0.20 northwindsystems -cv-boxed_sections 1:49faf14f1f56 -0.66 ledthereusabledocument-generatio -cv-boxed_sections 1:6bd3f168e89e -0.61 maintenancetimeby70%byretiringpe -cv-boxed_sections 1:daf65306b3b6 -0.23 softwareengineer -cv-boxed_sections 1:e7cc1822c6dc -0.29 2021-2024 -cv-boxed_sections 1:998c1a985022 -0.33 brightleaflabs -cv-boxed_sections 1:e55664011cda -0.51 builtbackendservicesandproductio -cv-boxed_sections 1:e80236b6d2b1 -0.46 itexttoacustomlayoutengine,elimi -cv-boxed_sections 1:41bb54fbae8d -0.08 backendengineer -cv-boxed_sections 1:3fa772117260 -0.14 2019-2021 -cv-boxed_sections 1:ef148f221e31 -0.20 helixprintco -cv-boxed_sections 1:9d31a6bfbc69 -0.38 maintainedahigh-volumeinvoice-pr -cv-boxed_sections 1:03af8a628309 -0.45 change. -cv-boxed_sections 1:676dfdd56ad2 -0.08 additionalinformation -cv-boxed_sections 1:f20997652453 -0.13 languages:english(fluent),german -cv-boxed_sections 1:ec9d3622395e -0.09 workeligibility:eligibletoworkin -cv-boxed_sections 1:dd20ae01c3cf -0.09 opensource:maintainerofgraphcomp -cv-boxed_sections 1:33c35e9d236d -0.08 speaking:jvmsummit2024,devoxxuk2 +cv-boxed_sections 1:49faf14f1f56 -0.06 ledthereusabledocument-generatio +cv-boxed_sections 1:6bd3f168e89e -0.01 maintenancetimeby70%byretiringpe +cv-boxed_sections 1:daf65306b3b6 0.37 softwareengineer +cv-boxed_sections 1:e7cc1822c6dc 0.31 2021-2024 +cv-boxed_sections 1:998c1a985022 0.27 brightleaflabs +cv-boxed_sections 1:e55664011cda 0.09 builtbackendservicesandproductio +cv-boxed_sections 1:e80236b6d2b1 0.14 itexttoacustomlayoutengine,elimi +cv-boxed_sections 1:41bb54fbae8d 0.52 backendengineer +cv-boxed_sections 1:3fa772117260 0.46 2019-2021 +cv-boxed_sections 1:ef148f221e31 0.40 helixprintco +cv-boxed_sections 1:9d31a6bfbc69 0.22 maintainedahigh-volumeinvoice-pr +cv-boxed_sections 1:03af8a628309 0.15 change. +cv-boxed_sections 1:676dfdd56ad2 0.52 additionalinformation +cv-boxed_sections 1:f20997652453 0.47 languages:english(fluent),german +cv-boxed_sections 1:ec9d3622395e 0.51 workeligibility:eligibletoworkin +cv-boxed_sections 1:dd20ae01c3cf 0.51 opensource:maintainerofgraphcomp +cv-boxed_sections 1:33c35e9d236d 0.52 speaking:jvmsummit2024,devoxxuk2 cv-centered_headline 0:8f1010120ed6 0.12 jordanrivera cv-centered_headline 0:87ca614d0eab 0.09 professionaltitle cv-centered_headline 0:8d7a2cca3060 -0.29 +442055551000|jordan.rivera@exam @@ -158,12 +158,12 @@ cv-centered_headline 0:f0e2046536d3 0.93 builtbackendservicesandproductio cv-centered_headline 0:4eb608992328 0.92 toacustomlayoutengine,eliminatin cv-centered_headline 0:41bb54fbae8d 0.82 backendengineer cv-centered_headline 0:ef148f221e31 0.82 helixprintco -cv-centered_headline 1:9d31a6bfbc69 0.13 maintainedahigh-volumeinvoice-pr -cv-centered_headline 1:03af8a628309 0.12 change. -cv-centered_headline 1:676dfdd56ad2 -0.03 additionalinformation +cv-centered_headline 1:9d31a6bfbc69 0.25 maintainedahigh-volumeinvoice-pr +cv-centered_headline 1:03af8a628309 0.24 change. +cv-centered_headline 1:676dfdd56ad2 0.09 additionalinformation cv-centered_headline 1:f20997652453 0.10 languages:english(fluent),german cv-centered_headline 1:ec9d3622395e 0.16 workeligibility:eligibletoworkin -cv-centered_headline 1:dd20ae01c3cf 0.10 opensource:maintainerofgraphcomp +cv-centered_headline 1:dd20ae01c3cf 0.22 opensource:maintainerofgraphcomp cv-centered_headline 1:33c35e9d236d 0.16 speaking:jvmsummit2024,devoxxuk2 cv-charcoal_gold 0:73423c7707c4 0.14 anastasia cv-charcoal_gold 0:6627835f988e -0.03 smith @@ -543,21 +543,21 @@ cv-executive 0:060300b18051 0.33 2024-present cv-executive 0:f890faef907f 0.34 northwindsystems cv-executive 0:1af5650959fe 0.63 ledthereusabledocument-generatio cv-executive 0:27c01c228bd8 0.58 templatemaintenancetimeby70%byre -cv-executive 1:daf65306b3b6 -3.18 softwareengineer -cv-executive 1:e7cc1822c6dc -3.18 2021-2024 -cv-executive 1:998c1a985022 -3.16 brightleaflabs -cv-executive 1:ff8303967fa4 -2.90 builtbackendservicesandproductio -cv-executive 1:bd1fc9a4d815 -2.93 fromitexttoacustomlayoutengine,e -cv-executive 1:41bb54fbae8d -3.05 backendengineer -cv-executive 1:3fa772117260 -3.05 2019-2021 -cv-executive 1:ef148f221e31 -3.03 helixprintco -cv-executive 1:f811fbee6901 -2.77 maintainedahigh-volumeinvoice-pr -cv-executive 1:cbfca5b2a6d7 -2.82 templatechange. -cv-executive 1:676dfdd56ad2 -2.54 additionalinformation -cv-executive 1:f20997652453 -3.10 languages:english(fluent),german -cv-executive 1:ec9d3622395e -3.06 workeligibility:eligibletoworkin -cv-executive 1:dd20ae01c3cf -3.02 opensource:maintainerofgraphcomp -cv-executive 1:33c35e9d236d -3.10 speaking:jvmsummit2024,devoxxuk2 +cv-executive 1:daf65306b3b6 -0.18 softwareengineer +cv-executive 1:e7cc1822c6dc -0.18 2021-2024 +cv-executive 1:998c1a985022 -0.16 brightleaflabs +cv-executive 1:ff8303967fa4 0.10 builtbackendservicesandproductio +cv-executive 1:bd1fc9a4d815 0.07 fromitexttoacustomlayoutengine,e +cv-executive 1:41bb54fbae8d -0.05 backendengineer +cv-executive 1:3fa772117260 -0.05 2019-2021 +cv-executive 1:ef148f221e31 -0.03 helixprintco +cv-executive 1:f811fbee6901 0.23 maintainedahigh-volumeinvoice-pr +cv-executive 1:cbfca5b2a6d7 0.18 templatechange. +cv-executive 1:676dfdd56ad2 0.46 additionalinformation +cv-executive 1:f20997652453 -0.10 languages:english(fluent),german +cv-executive 1:ec9d3622395e -0.06 workeligibility:eligibletoworkin +cv-executive 1:dd20ae01c3cf -0.02 opensource:maintainerofgraphcomp +cv-executive 1:33c35e9d236d -0.10 speaking:jvmsummit2024,devoxxuk2 cv-midnight_navy 0:21d2636ad3df 0.36 alexmorgan cv-midnight_navy 0:5518402feccc -0.10 marketingmanager cv-midnight_navy 0:5518402feccc#1 -0.34 marketingmanager @@ -680,23 +680,23 @@ cv-minimal_underlined 0:c2f296ef30a3 -0.46 professionalexperience cv-minimal_underlined 0:a76f47480818 -0.69 seniorplatformengineer cv-minimal_underlined 0:060300b18051 -0.75 2024-present cv-minimal_underlined 0:f890faef907f -0.78 northwindsystems -cv-minimal_underlined 1:49faf14f1f56 -0.70 ledthereusabledocument-generatio -cv-minimal_underlined 1:6bd3f168e89e -0.65 maintenancetimeby70%byretiringpe -cv-minimal_underlined 1:daf65306b3b6 -0.27 softwareengineer -cv-minimal_underlined 1:e7cc1822c6dc -0.21 2021-2024 -cv-minimal_underlined 1:998c1a985022 -0.37 brightleaflabs -cv-minimal_underlined 1:e55664011cda -0.55 builtbackendservicesandproductio -cv-minimal_underlined 1:e80236b6d2b1 -0.50 itexttoacustomlayoutengine,elimi -cv-minimal_underlined 1:41bb54fbae8d -0.12 backendengineer -cv-minimal_underlined 1:3fa772117260 -0.06 2019-2021 -cv-minimal_underlined 1:ef148f221e31 -0.24 helixprintco -cv-minimal_underlined 1:9d31a6bfbc69 -0.30 maintainedahigh-volumeinvoice-pr -cv-minimal_underlined 1:03af8a628309 -0.25 change. -cv-minimal_underlined 1:676dfdd56ad2 -0.03 additionalinformation -cv-minimal_underlined 1:f20997652453 -0.44 languages:english(fluent),german -cv-minimal_underlined 1:ec9d3622395e -0.40 workeligibility:eligibletoworkin -cv-minimal_underlined 1:dd20ae01c3cf -0.40 opensource:maintainerofgraphcomp -cv-minimal_underlined 1:33c35e9d236d -0.39 speaking:jvmsummit2024,devoxxuk2 +cv-minimal_underlined 1:49faf14f1f56 -0.10 ledthereusabledocument-generatio +cv-minimal_underlined 1:6bd3f168e89e -0.05 maintenancetimeby70%byretiringpe +cv-minimal_underlined 1:daf65306b3b6 0.33 softwareengineer +cv-minimal_underlined 1:e7cc1822c6dc 0.39 2021-2024 +cv-minimal_underlined 1:998c1a985022 0.23 brightleaflabs +cv-minimal_underlined 1:e55664011cda 0.05 builtbackendservicesandproductio +cv-minimal_underlined 1:e80236b6d2b1 0.10 itexttoacustomlayoutengine,elimi +cv-minimal_underlined 1:41bb54fbae8d 0.48 backendengineer +cv-minimal_underlined 1:3fa772117260 0.54 2019-2021 +cv-minimal_underlined 1:ef148f221e31 0.36 helixprintco +cv-minimal_underlined 1:9d31a6bfbc69 0.30 maintainedahigh-volumeinvoice-pr +cv-minimal_underlined 1:03af8a628309 0.35 change. +cv-minimal_underlined 1:676dfdd56ad2 0.57 additionalinformation +cv-minimal_underlined 1:f20997652453 0.16 languages:english(fluent),german +cv-minimal_underlined 1:ec9d3622395e 0.20 workeligibility:eligibletoworkin +cv-minimal_underlined 1:dd20ae01c3cf 0.20 opensource:maintainerofgraphcomp +cv-minimal_underlined 1:33c35e9d236d 0.21 speaking:jvmsummit2024,devoxxuk2 cv-mint_editorial 0:8f1010120ed6 -0.18 jordanrivera cv-mint_editorial 0:2d58f0cadc2d 0.01 platformengineer cv-mint_editorial 0:093e7d5fdbaa -0.13 contact @@ -810,11 +810,11 @@ cv-modern_professional 0:41bb54fbae8d 0.77 backendengineer cv-modern_professional 0:ef148f221e31 0.66 helixprintco cv-modern_professional 0:a32c7f2401d9 0.45 maintainedahigh-volumeinvoice-pr cv-modern_professional 0:7068c1d7034b 0.54 everytemplatechange. -cv-modern_professional 1:676dfdd56ad2 -7.65 additionalinformation -cv-modern_professional 1:f20997652453 -7.80 languages:english(fluent),german -cv-modern_professional 1:ec9d3622395e -7.77 workeligibility:eligibletoworkin -cv-modern_professional 1:dd20ae01c3cf -7.74 opensource:maintainerofgraphcomp -cv-modern_professional 1:33c35e9d236d -7.69 speaking:jvmsummit2024,devoxxuk2 +cv-modern_professional 1:676dfdd56ad2 0.39 additionalinformation +cv-modern_professional 1:f20997652453 0.24 languages:english(fluent),german +cv-modern_professional 1:ec9d3622395e 0.27 workeligibility:eligibletoworkin +cv-modern_professional 1:dd20ae01c3cf 0.32 opensource:maintainerofgraphcomp +cv-modern_professional 1:33c35e9d236d 0.35 speaking:jvmsummit2024,devoxxuk2 cv-monogram_sidebar 0:093e7d5fdbaa 0.00 contact cv-monogram_sidebar 0:89674e6f130e 0.15 +442055551000 cv-monogram_sidebar 0:e0f850305ded 0.15 jordan.rivera@example.com diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv index 15a8a8e0f..557237bc5 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv @@ -1,18 +1,18 @@ # word 16.0 (16.0.20430) on windows document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.14 0.49 0 -cv-boxed_sections 2 2 58 58 0.30 0.51 0 -cv-centered_headline 2 2 58 56 0.20 0.82 0 +cv-boxed_sections 2 2 58 58 0.31 0.52 0 +cv-centered_headline 2 2 58 56 0.24 0.82 0 cv-charcoal_gold 1 1 85 83 0.20 0.58 0 cv-classic_serif 2 2 57 54 0.29 0.68 0 cv-compact_mono 1 1 58 58 0.26 1.01 0 cv-editorial_blue 2 2 82 81 0.47 10.00 9 cv-engineering_resume 1 1 57 57 0.58 1.54 0 -cv-executive 2 2 60 60 0.25 3.05 15 +cv-executive 2 2 60 60 0.16 0.38 0 cv-midnight_navy 1 1 86 80 0.15 0.58 0 -cv-minimal_underlined 2 2 59 59 0.62 0.89 0 +cv-minimal_underlined 2 2 59 59 0.57 0.89 0 cv-mint_editorial 2 2 59 59 0.13 0.31 0 -cv-modern_professional 2 2 61 59 0.20 0.77 5 +cv-modern_professional 2 2 61 59 0.20 0.56 0 cv-monogram_sidebar 1 1 53 50 0.29 0.79 0 cv-navy_sidebar 1 1 71 69 0.13 0.22 0 cv-nordic_clean 1 1 63 58 0.22 0.61 0 diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java index 35dd2d0a8..97aadd8f4 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java @@ -279,6 +279,18 @@ public final class DocxSemanticBackend implements SemanticBackend { /** The page the last block written in the flow ended on, -1 before the first. */ private int lastEndPage = -1; + + /** A paragraph whose own top edge a line above it holds: {@link #holdAParagraphsTopEdgeOnItsPage}. */ + private DocumentNode topEdgeHeldAbove; + + /** How much of that paragraph's own top edge the line holds, in points. */ + private double topEdgeHeld; + + /** The line holding that edge, until the paragraph's line gap is written. */ + private XWPFParagraph topEdgeLine; + + /** The Word paragraph that line holds the edge of, once written. */ + private XWPFParagraph topEdgeLineOver; private int sectionIndex; // Where the section being written starts among the body's elements. private int sectionFirstElement; @@ -357,6 +369,9 @@ public final class DocxSemanticBackend implements SemanticBackend { /** The page's height in points, or {@code NaN} when the export has no canvas. */ private double canvasHeight = Double.NaN; + /** The page's top margin in points, or {@code NaN} when the export has no canvas. */ + private double canvasTopMargin = Double.NaN; + /** Whether this export writes more than one section — see {@link #exportSections}. */ private boolean sectioned; @@ -659,6 +674,10 @@ private byte[] write(List sections, Path outputFile) throws Exc anchors.reset(); currentPage = 0; lastEndPage = -1; + topEdgeHeldAbove = null; + topEdgeHeld = 0; + topEdgeLine = null; + topEdgeLineOver = null; clipContainer = null; cellDrawing = CellDrawing.NONE; tableDrawings = null; @@ -788,6 +807,7 @@ private void beginSection(SemanticSection section, int index) { lastWrittenParagraph = null; contentWidth = context.canvas() == null ? Double.MAX_VALUE : context.canvas().innerWidth(); canvasHeight = context.canvas() == null ? Double.NaN : context.canvas().height(); + canvasTopMargin = context.canvas() == null ? Double.NaN : context.canvas().margin().top(); } /** @@ -5230,6 +5250,9 @@ private void writeParagraph(XWPFDocument document, ParagraphNode node) { writeTextOverTheFlow(document, node); return; } + if (currentCell == null && overlayDepth == 0 && startsAPageOfItsOwn(node)) { + holdAParagraphsTopEdgeOnItsPage(document, node); + } // Its own sides hold its text in, as a container's do: SerifHeadline's summary stops at // the column divider through its right margin, and without it ran the page's width in // Word — a line short, and everything under it that much high. @@ -5408,6 +5431,8 @@ private boolean applyParagraphProperties(XWPFParagraph target, ParagraphNode sou applyLineHeight(target, stacked != null ? java.util.OptionalDouble.of(stacked.height()) : layout.lineHeight(source)); applyVerticalSpacing(target, source); applyLineGap(target, layout.lineGap(source), layout.lineCount(source), layout.linePitch(source)); + topEdgeLine = null; + topEdgeLineOver = null; if (stacked == null) { oweWhatTheLinesFallShort(target, source); } @@ -5558,6 +5583,10 @@ private void applyLineGap(XWPFParagraph target, double gap, int lines, java.util if (taken > 0) { spacing.setBefore(BigInteger.valueOf(before - taken)); } + // The paragraph's top edge held by a line above it is the space above it all the same. + if (target == topEdgeLineOver && taken < twips) { + taken += takeFromTheLineHoldingItsEdge(twips - taken); + } // The cell's padding gives no more than steps the lines the page's distance apart: n lines // of (line + extra) are that distance apart where extra = pitch - line. Lines of // different heights can stand closer than their tallest and the gap. @@ -5665,6 +5694,13 @@ private long takeFromTheTopOfItsCell(XWPFParagraph target, long twips) { */ private void applyVerticalSpacing(XWPFParagraph target, DocumentNode source) { double before = source.margin().top() + source.padding().top(); + if (source == topEdgeHeldAbove) { + // Held by the line above it, on the page the layout starts it on. + before -= topEdgeHeld; + topEdgeHeldAbove = null; + topEdgeHeld = 0; + topEdgeLineOver = target; + } if (target == hangingOver) { before = Math.max(0, before - hangingOverBy); } @@ -10074,13 +10110,20 @@ private void holdTheSpaceAboveOnItsPage(XWPFDocument document) { if (!(carriedSpacingBefore + pendingSpacingAfter - borderBelow - pullBelow > 0.01)) { return; } - XWPFParagraph spacer = newBodyParagraph(document); - CTPPr properties = spacer.getCTP().isSetPPr() ? spacer.getCTP().getPPr() : spacer.getCTP().addNewPPr(); + XWPFParagraph line = newBodyParagraph(document); + CTPPr properties = line.getCTP().isSetPPr() ? line.getCTP().getPPr() : line.getCTP().addNewPPr(); CTSpacing spacing = properties.isSetSpacing() ? properties.getSpacing() : properties.addNewSpacing(); long held = twipsOf(spacing.isSetBefore() ? spacing.getBefore() : null); spacing.setBefore(BigInteger.ZERO); + holdInALineKeptWithTheNext(line, held); + } + + /** Makes a paragraph a line of exactly {@code twips} with no text, kept with what follows. */ + private static void holdInALineKeptWithTheNext(XWPFParagraph line, long twips) { + CTPPr properties = line.getCTP().isSetPPr() ? line.getCTP().getPPr() : line.getCTP().addNewPPr(); + CTSpacing spacing = properties.isSetSpacing() ? properties.getSpacing() : properties.addNewSpacing(); spacing.setLineRule(STLineSpacingRule.EXACT); - spacing.setLine(BigInteger.valueOf(Math.max(1, held))); + spacing.setLine(BigInteger.valueOf(Math.max(1, twips))); if (!properties.isSetKeepNext()) { properties.addNewKeepNext(); } @@ -10091,6 +10134,125 @@ private void holdTheSpaceAboveOnItsPage(XWPFDocument document) { (mark.sizeOfSzArray() > 0 ? mark.getSzArray(0) : mark.addNewSz()).setVal(BigInteger.valueOf(ONE_POINT_IN_HALF_POINTS)); } + /** + * Holds the top edge of a paragraph the layout moves to a new page, on that page. + * + *

The page starts a block a page break moves down at its own top edge — the containers + * opening there with it. Word drops a paragraph's space above at the top of a page, the + * paragraph's own edge with the gap: {@code ModernProfessional}'s second page opens with a + * heading its section pads 8pt down, and in Word the heading and everything under it stood + * 7.7pt high. A line as tall as that edge, kept with the paragraph, holds it; Word keeps a + * line's height there.

+ * + *

The line is as tall as the layout leaves above the paragraph's text on that page: the + * space above its box ({@link #spaceAboveOnItsPage}) and its own padding. That is the gap + * too where the gap did not fit at the foot of the page above, and less than the edges + * where the layout spent some on the page above — a heading kept with what follows, hoisted + * to the next page out of a section begun on this one, leaves the section's padding behind. + * The rest of the space owed stays above the line, so where Word breaks the page is + * unchanged: at the foot of the page above, the line and that space together take what the + * paragraph's space above did. Written on the page above instead, the gap let + * {@code ClassicSerif}'s second page's first line fit on its first in Word.

+ */ + private void holdAParagraphsTopEdgeOnItsPage(XWPFDocument document, ParagraphNode node) { + double own = Math.max(0, node.margin().top() + node.padding().top()); + double edges = Math.max(0, carriedSpacingBefore); + double above = spaceAboveOnItsPage(node); + double kept = Double.isNaN(above) ? own + edges : Math.max(0, above) + Math.max(0, node.padding().top()); + if (!(kept > 0.01)) { + return; + } + // Text hanging below a band, a pull out of the block before and a card's border below + // it stay on the page above with them, as at a table moved to a new page: the layout + // starts the new page from its top. + forgetTheHang(); + pullBelow = 0; + borderBelow = 0; + carriedSpacingBefore += own; + XWPFParagraph line = newBodyParagraph(document); + pullLeftOn = null; + pullLeft = 0; + CTPPr properties = line.getCTP().isSetPPr() ? line.getCTP().getPPr() : line.getCTP().addNewPPr(); + CTSpacing spacing = properties.isSetSpacing() ? properties.getSpacing() : properties.addNewSpacing(); + long before = twipsOf(spacing.isSetBefore() ? spacing.getBefore() : null); + long held = Math.min(before, toTwips(kept)); + spacing.setBefore(BigInteger.valueOf(before - held)); + holdInALineKeptWithTheNext(line, held); + topEdgeHeldAbove = node; + topEdgeHeld = own; + topEdgeLine = line; + } + + /** + * Takes up to {@code twips} out of the space above a paragraph whose top edge a line holds, + * for the gap between the paragraph's lines (see {@link #applyLineGap}): out of that line, + * and what the line cannot give out of the space above the line, as it came out of the + * paragraph's own space above before the line held it. Returns how much it took. + * + *

Word drops that space above at the top of a page, so what comes out of it moves + * nothing there and the first line stands that much low; it keeps the paragraph's lines as + * tall as where the whole gap comes off the space above it on one page.

+ */ + private long takeFromTheLineHoldingItsEdge(long twips) { + CTSpacing spacing = topEdgeLine == null ? null : topEdgeLine.getCTP().getPPr().getSpacing(); + Long line = spacing == null ? null : writtenTwips(spacing.getLine()); + if (line == null) { + return 0; + } + long taken = Math.max(0, Math.min(twips, line - 1)); + spacing.setLine(BigInteger.valueOf(line - taken)); + long before = twipsOf(spacing.isSetBefore() ? spacing.getBefore() : null); + long rest = Math.min(before, twips - taken); + if (rest > 0) { + spacing.setBefore(BigInteger.valueOf(before - rest)); + taken += rest; + } + return taken; + } + + /** + * How far below the top of its page the layout starts a block it moves to a new page, in + * points: the space above the block's box on that page — its own top margin, the edges of + * the containers opening with it and any gap carried there; the box holds its padding. NaN + * when the export has no canvas, or the block runs over more than one page, whose placement + * pairs its first piece's place with the whole block's height. + */ + private double spaceAboveOnItsPage(DocumentNode node) { + com.demcha.compose.document.layout.PlacedNode placed = layout.placement(node); + if (placed == null || placed.startPage() != placed.endPage() + || Double.isNaN(canvasHeight) || Double.isNaN(canvasTopMargin)) { + return Double.NaN; + } + return canvasHeight - canvasTopMargin - placed.placementY() - placed.placementHeight(); + } + + /** + * Moves the space above a spacer's line the layout starts on a new page into the line, as + * far as the layout keeps that space on the page. + * + *

The layout carries the gap between two blocks onto the next page when the gap itself + * does not fit at the foot of the page: {@code Executive}'s spacer between two entries opens + * its second page 3pt below the margin. Word drops a paragraph's space above at the top of a + * page and keeps its line's height, so the entries under it stood 3pt high. The line and the + * space above it together are as tall as before, so Word breaks the page where it did.

+ */ + private void holdTheGapAboveInTheLine(XWPFParagraph para, DocumentNode node) { + double onItsPage = spaceAboveOnItsPage(node); + CTSpacing spacing = para.getCTP().isSetPPr() && para.getCTP().getPPr().isSetSpacing() + ? para.getCTP().getPPr().getSpacing() : null; + if (!(onItsPage > 0.01) || spacing == null || !spacing.isSetBefore() + || spacing.getLineRule() != STLineSpacingRule.EXACT) { + return; + } + long before = twipsOf(spacing.getBefore()); + long held = Math.min(before, toTwips(onItsPage)); + if (held <= 0) { + return; + } + spacing.setBefore(BigInteger.valueOf(before - held)); + spacing.setLine(BigInteger.valueOf(twipsOf(spacing.getLine()) + held)); + } + /** Makes a paragraph that holds no text as short as a separator between tables. */ private static void holdToHairline(XWPFParagraph para) { CTPPr properties = para.getCTP().isSetPPr() ? para.getCTP().getPPr() : para.getCTP().addNewPPr(); @@ -10305,6 +10467,9 @@ private void writeSpacer(XWPFDocument document, SpacerNode node) throws Exceptio // height is a line of text tall, and it stood on top of that height: between two CV // entries held apart by a 4.5pt spacer, the page shows 16pt and LibreOffice drew 23. holdToHairline(para); + if (currentCell == null && overlayDepth == 0 && startsAPageOfItsOwn(node)) { + holdTheGapAboveInTheLine(para, node); + } // Text that hung below the band above takes its place out of this height first. double height = node.height(); if (hangingOver == para) { diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java new file mode 100644 index 000000000..287b583e4 --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxSpaceAboveOnANewPageTest.java @@ -0,0 +1,275 @@ +package com.demcha.compose.document.backend.semantic.docx; + +import com.demcha.compose.document.style.DocumentInsets; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; +import org.junit.jupiter.api.Test; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A paragraph the layout moves to a new page keeps the space the layout leaves above it there, + * in Word. + * + *

Word drops a paragraph's space above at the top of a page and keeps its line's height. The + * layout keeps a block's top edge there — the containers opening with it — and the gap before + * it too when the gap itself did not fit at the foot of the page above. {@code Executive}'s + * second page opens with the spacer between two entries 3pt below the margin, and + * {@code ModernProfessional}'s with a heading its section pads 8pt down; in Word both pages + * stood that much high.

+ * + * @author Artem Demchyshyn + */ +class DocxSpaceAboveOnANewPageTest { + + private static final double GAP = 4; + private static final double SPACER = 3; + private static final double PADDING = 8; + + /** + * The page's content is 160pt tall: 300 x 200 with a 20pt margin. A spacer fills it to a + * height; its height is owed below it, and written above the paragraph that follows. + */ + private static final double CONTENT = 160; + + @Test + void aSpacerTheGapBeforeCarriesToANewPageHoldsTheGapInItsLine() throws Exception { + // 157pt leave 3, less than the gap: the gap and the spacer open the second page. + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 3) + .spacer(0, SPACER) + .addParagraph("After"))) { + CTSpacing spacing = spacing(spacerOnThePageAfter(document)); + + assertThat(spacing.getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + assertThat(before(spacing)).as("the filler's height, owed below it, and no gap") + .isEqualTo(Math.round((CONTENT - 3) * 20)); + assertThat(DocxTwips.of(spacing.getLine())).as("the gap, and the hairline's height") + .isEqualTo(Math.round(GAP * 20) + HAIRLINE); + } + } + + @Test + void aSpacerTheGapBeforeStaysAboveIsWrittenAsBefore() throws Exception { + // 150pt leave 10: the gap fits at the foot of the first page, the spacer does not. + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 10) + .spacer(0, 8) + .addParagraph("After"))) { + CTSpacing spacing = spacing(spacerOnThePageAfter(document)); + + assertThat(before(spacing)).as("the filler's height and the gap, which the page leaves above") + .isEqualTo(Math.round((CONTENT - 10 + GAP) * 20)); + assertThat(DocxTwips.of(spacing.getLine())).isEqualTo(HAIRLINE); + } + } + + @Test + void aParagraphItsSectionPadsDownOnANewPageHoldsThePaddingInALineKeptWithIt() throws Exception { + // 150pt leave 10: the gap fits at the foot of the first page, the section does not. + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 10) + .addSection("Title", section -> section + .padding(new DocumentInsets(PADDING, 0, 0, 0)) + .addParagraph("Heading")))) { + XWPFParagraph heading = paragraphWith(document, "Heading"); + XWPFParagraph line = paragraphBefore(document, heading); + CTSpacing spacing = spacing(line); + + assertThat(line.getText()).isEmpty(); + assertThat(spacing.getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + assertThat(DocxTwips.of(spacing.getLine())).as("the section's padding, a line's height") + .isEqualTo(Math.round(PADDING * 20)); + assertThat(line.getCTP().getPPr().isSetKeepNext()).as("kept with the heading").isTrue(); + assertThat(before(spacing)).as("the filler's height and the gap stay above the line") + .isEqualTo(Math.round((CONTENT - 10 + GAP) * 20)); + assertThat(before(spacing(heading))).as("its edge is the line's").isZero(); + } + } + + @Test + void aParagraphWhoseGapCarriesToANewPageHoldsTheGapWithItsEdge() throws Exception { + // 157pt leave 3, less than the gap: the gap and the section open the second page. + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 3) + .addSection("Title", section -> section + .padding(new DocumentInsets(PADDING, 0, 0, 0)) + .addParagraph("Heading")))) { + CTSpacing spacing = spacing(paragraphBefore(document, paragraphWith(document, "Heading"))); + + assertThat(DocxTwips.of(spacing.getLine())).as("the gap and the padding") + .isEqualTo(Math.round((GAP + PADDING) * 20)); + assertThat(before(spacing)).as("the filler's height alone").isEqualTo(Math.round((CONTENT - 3) * 20)); + } + } + + @Test + void aParagraphWithNoEdgeOfItsOwnHoldsAGapCarriedToANewPage() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 3) + .addParagraph("After"))) { + XWPFParagraph after = paragraphWith(document, "After"); + CTSpacing spacing = spacing(paragraphBefore(document, after)); + + assertThat(DocxTwips.of(spacing.getLine())).as("the gap").isEqualTo(Math.round(GAP * 20)); + assertThat(spacing.getLineRule()).isEqualTo(STLineSpacingRule.EXACT); + assertThat(before(spacing(after))).isZero(); + } + } + + @Test + void aHeadingHoistedOutOfASectionBegunOnThePageAboveHoldsNothing() throws Exception { + // 126pt and the gap leave 30: the section and its 8pt padding begin on the first page, + // and the heading, kept with the body's first line, moves to the second without them. + java.util.function.Consumer content = page -> page + .spacing(GAP) + .spacer(0, CONTENT - 30 - GAP) + .addSection("Experience", section -> section + .padding(new DocumentInsets(PADDING, 0, 0, 0)) + .addSection("Title", title -> title.keepWithNext().addParagraph("Heading")) + .addParagraph("Body")); + try (com.demcha.compose.document.api.DocumentSession session = com.demcha.compose.GraphCompose.document() + .pageSize(300, 200).margin(DocumentInsets.of(20)).create()) { + session.pageFlow(content::accept); + List nodes = session.layoutGraph().nodes(); + assertThat(nodes).filteredOn(n -> "Experience".equals(n.semanticName())) + .singleElement().extracting(n -> n.startPage()).as("the section begins on the first page") + .isEqualTo(0); + assertThat(nodes).filteredOn(n -> "Title".equals(n.semanticName())) + .singleElement().extracting(n -> n.startPage()).as("the heading opens the second") + .isEqualTo(1); + } + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, content)) { + XWPFParagraph heading = paragraphWith(document, "Heading"); + + assertThat(document.getParagraphs()).as("the filler, the heading and the body, no line") + .hasSize(3); + assertThat(before(spacing(heading))).as("the padding, left for Word to drop") + .isEqualTo(Math.round((CONTENT - 30 - GAP + GAP + PADDING) * 20)); + } + } + + @Test + void aPullOutOfTheBlockOnThePageAboveLeavesTheLineWhole() throws Exception { + // The paragraph above pulls what follows it 5pt up, on its own page; the section opens + // the next page at its padding all the same. + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacer(0, CONTENT - 20) + .addParagraph(p -> p.text("Above").margin(new DocumentInsets(0, 0, -5, 0))) + .addSection("Title", section -> section + .padding(new DocumentInsets(PADDING, 0, 0, 0)) + .addParagraph("Heading")))) { + CTSpacing spacing = spacing(paragraphBefore(document, paragraphWith(document, "Heading"))); + + assertThat(DocxTwips.of(spacing.getLine())).as("the padding, none of it pulled") + .isEqualTo(Math.round(PADDING * 20)); + } + } + + @Test + void aParagraphInATableCellIsLeftToItsTable() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(300, 200, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 3) + .addRow(row -> row.addParagraph(p -> p.text("In a cell") + .margin(new DocumentInsets(PADDING, 0, 0, 0)))))) { + XWPFParagraph first = document.getTables().get(0).getRow(0).getCell(0).getParagraphs().get(0); + + assertThat(first.getText()).as("no line in the cell before it").isEqualTo("In a cell"); + } + } + + @Test + void theGapBetweenItsLinesComesOffTheLineHoldingItsEdge() throws Exception { + // A paragraph of n lines has n - 1 gaps on the page and n in Word; the one too many + // comes off the space above it, which on the new page is the line holding its edge. + try (XWPFDocument document = wrapped(2, 1.4)) { + XWPFParagraph text = paragraphWith(document, WRAPPED); + CTSpacing spacing = spacing(paragraphBefore(document, text)); + + assertThat(DocxTwips.of(spacing.getLine())).as("its 2pt edge, less the 1.4pt gap").isEqualTo(40 - 28); + assertThat(before(spacing)).isEqualTo(Math.round((CONTENT - 10 + GAP) * 20)); + assertThat(before(spacing(text))).isZero(); + } + } + + @Test + void whatTheLineCannotGiveOfTheGapComesOffTheSpaceAboveIt() throws Exception { + try (XWPFDocument document = wrapped(200, 1, 1.5); + XWPFDocument onOnePage = wrapped(600, 1, 1.5)) { + XWPFParagraph text = paragraphWith(document, WRAPPED); + CTSpacing spacing = spacing(paragraphBefore(document, text)); + + assertThat(DocxTwips.of(spacing.getLine())).as("a twip left of its 1pt edge").isEqualTo(1); + assertThat(before(spacing)).as("the 11 twips of the gap the line could not give") + .isEqualTo(Math.round((CONTENT - 10 + GAP) * 20) - 11); + assertThat(DocxTwips.of(spacing(text).getLine())) + .as("its lines as tall as where the whole gap comes off the space above it on one page") + .isEqualTo(DocxTwips.of(spacing(paragraphWith(onOnePage, WRAPPED)).getLine())); + } + } + + private static final String WRAPPED = "A paragraph long enough to wrap onto a second line in a " + + "column of two hundred and sixty points, and a third."; + + /** A wrapped paragraph with a top edge and a gap between its lines, moved to a new page. */ + private static XWPFDocument wrapped(double edge, double lineSpacing) throws Exception { + return wrapped(200, edge, lineSpacing); + } + + private static XWPFDocument wrapped(double pageHeight, double edge, double lineSpacing) throws Exception { + return DocxExports.withLayout(300, pageHeight, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 10) + .addParagraph(p -> p.text(WRAPPED).lineSpacing(lineSpacing) + .margin(new DocumentInsets(edge, 0, 0, 0)))); + } + + @Test + void aParagraphOnThePageOfTheBlockBeforeItIsWrittenAsBefore() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(300, 600, 20, page -> page + .spacing(GAP) + .spacer(0, CONTENT - 10) + .addSection("Title", section -> section + .padding(new DocumentInsets(PADDING, 0, 0, 0)) + .addParagraph("Heading")))) { + XWPFParagraph heading = paragraphWith(document, "Heading"); + + assertThat(document.getParagraphs()).as("the spacer and the heading, no line between them").hasSize(2); + assertThat(before(spacing(heading))).isEqualTo(Math.round((CONTENT - 10 + GAP + PADDING) * 20)); + } + } + + /** A spacer's paragraph is a tenth of a point tall, in twips (see holdToHairline). */ + private static final long HAIRLINE = 2; + + private static XWPFParagraph spacerOnThePageAfter(XWPFDocument document) { + return paragraphBefore(document, paragraphWith(document, "After")); + } + + private static XWPFParagraph paragraphWith(XWPFDocument document, String text) { + return document.getParagraphs().stream().filter(p -> p.getText().equals(text)).findFirst().orElseThrow(); + } + + private static XWPFParagraph paragraphBefore(XWPFDocument document, XWPFParagraph paragraph) { + List paragraphs = document.getParagraphs(); + return paragraphs.get(paragraphs.indexOf(paragraph) - 1); + } + + private static CTSpacing spacing(XWPFParagraph paragraph) { + return paragraph.getCTP().getPPr().getSpacing(); + } + + private static long before(CTSpacing spacing) { + return spacing.isSetBefore() ? DocxTwips.of(spacing.getBefore()) : 0; + } +}