diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01566aaf9..bfe39695e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,29 @@ follow semantic versioning; release dates are ISO 8601.
### Public API
+- **`MidnightNavy`'s sidebar stands where the page sets it in Word.** Three causes, each its own:
+ - Its monogram centres two initials in a 76pt ring, two layers of one shape container. Only a
+ container of one layer was set in from its outline, so the initials were written from the
+ ring's top: they stood 9.6pt high, and the name and the whole column under the ring 19.3pt.
+ A container's paragraph layers standing one under another are now set in as one, from the
+ top of the first and the foot of the last.
+ - Each skill is a row: its label, and a meter drawn where the page draws it, so nothing is
+ written in the meter's cell. The paragraph a cell must end with was a line of the document's
+ font, and each row stood 2.1pt taller than the page's. A cell left with nothing in it now
+ holds a hairline.
+ - Each language's five dots are inline shapes, written as pictures wider than their boxes on
+ either side — a quarter point, and a ring's half stroke more — so each pushed the next 0.5pt
+ or more further along: the fifth broke onto a second line, each language row 9.7pt taller.
+ The text beside a shape now gives that room back, its letters set closer in whole tenths of
+ a point, and each dot's picture stands where the page puts its box. The page draws half the
+ last ring's stroke past its cell, where Word cuts a cell's content; a row's last cell is
+ widened by as far as the line ending in a shape reaches past it.
+
+ In Word, lines more than 2pt off fall from 30 to 0, and the p90 drift from 19.86pt to 0.58.
+ `CharcoalGold` finds 83 of its 85 lines in Word where it found 77. In LibreOffice the p90 falls
+ from 17.11pt to 3.24; two skills, which the rows' growth had carried onto the page's line,
+ now stand at the 2.7pt every line of the sheet stands at there.
+
- **`SlateOrange`'s sidebar stands where the page sets it in Word.** Each skill is a row with a
12.4pt icon on a line of its own, no text in it, and Word sizes that line itself. The
paragraph's mark, set in the icon cell's style at 10.5pt, reached below the icon, so each row
@@ -1521,6 +1544,12 @@ follow semantic versioning; release dates are ISO 8601.
### Tests
+- **The DOCX fidelity corpus measures a line from its first letter.** A space opened a line
+ (`PdfLines`), and Word writes one for an empty paragraph: drawn just before a line of text
+ beside it, a few points off its baseline, it gave that line its own. The lines measured past
+ 2pt in Word fall from 9 to 0 in `SlateOrange`, from 14 to 2 in `VioletGrid` and from 2 to 0
+ in `OrangeOps`; the text did not move. LibreOffice's measurement is unchanged.
+
- **The DOCX fidelity corpus reports which presets are set as the page sets them.** Each run
writes `measured--acceptance.md` (`FidelityAcceptance`): a preset passes when the editor
sets it on the page's pages, finds 95% of its lines, sets them a median of 1pt or less from
diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md
index c660a63f8..ae7d3c722 100644
--- a/docs/recipes/docx-export.md
+++ b/docs/recipes/docx-export.md
@@ -595,7 +595,20 @@ tint it was flattened to. Recorded, like the other two.
containers round it, and a container's one layer set in from its
content's top and bottom — a
photo centred in a ring wider than it — keeps that much space above and
- below it. What a table cell composes
+ below it; so do its paragraph layers written one under another — each
+ starting at or below the one before, or held as a stack of lines — as one,
+ from the top of the first and the foot of the last: a monogram's two
+ initials centred in a ring. A row's cell left with nothing written in it, its
+ content all drawn where the page draws it — a skill's meter beside its
+ label — ends with a hairline paragraph, not a line of the document's font.
+ An inline shape is written as a picture a quarter point wider than its box
+ on either side, and its ink's overhang wider still, and Word sets a picture
+ at its width whatever its run's spacing: the text before it gives back the
+ room on the left and the text after it the room on the right, its letters set
+ closer in whole tenths of a point, so the shape and what follows stand where
+ the page puts them. A run too long for a tenth a letter gives back nothing.
+ A row's last cell whose line ends in a shape reaching past the cell is
+ widened by that much, as Word cuts a cell's content at its edge. What a table cell composes
(`DocumentTableCell.node(...)`) has no place of its own in the layout:
its drawing belongs to the table, and the table draws it — an icon, a
tile, a disc under a number — anchored to the page where the page draws
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/DocxFidelityGateTest.java b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/DocxFidelityGateTest.java
index 96acae759..d9a63dc6e 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/DocxFidelityGateTest.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/DocxFidelityGateTest.java
@@ -194,6 +194,34 @@ void linesOfTheSameLettersArePairedNearestFirst() {
assertThat(found.getValue().drift()).isEqualTo(0.5);
}
+ @Test
+ void aLineStandsWhereItsFirstLetterDoesNotWhereASpaceBeforeItDoes() throws Exception {
+ // An empty paragraph's space, drawn just before a label 4pt lower beside it.
+ Path page = Files.createTempFile("first-letter", ".pdf");
+ try (org.apache.pdfbox.pdmodel.PDDocument document = new org.apache.pdfbox.pdmodel.PDDocument()) {
+ org.apache.pdfbox.pdmodel.PDPage sheet = new org.apache.pdfbox.pdmodel.PDPage();
+ document.addPage(sheet);
+ var font = new org.apache.pdfbox.pdmodel.font.PDType1Font(
+ org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName.HELVETICA);
+ try (var content = new org.apache.pdfbox.pdmodel.PDPageContentStream(document, sheet)) {
+ content.beginText();
+ content.setFont(font, 10);
+ content.newLineAtOffset(50, 700);
+ content.showText(" ");
+ content.newLineAtOffset(2, -4);
+ content.showText("Label");
+ content.endText();
+ }
+ document.save(page.toFile());
+ }
+ double height = 792;
+
+ PdfLines.Line line = PdfLines.of(page).lines().get(0);
+ assertThat(line.key()).isEqualTo("label");
+ assertThat(line.baseline()).isCloseTo(height - 696, org.assertj.core.data.Offset.offset(0.01));
+ assertThat(line.x()).isCloseTo(52, org.assertj.core.data.Offset.offset(0.01));
+ }
+
@Test
void twoColumnsOnOneBaselineAreTwoLines() throws Exception {
Path page = pdf("columns", flow -> flow.addRow("Row", row -> row
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/PdfLines.java b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/PdfLines.java
index 3324823b5..d34f25544 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/fidelity/PdfLines.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/fidelity/PdfLines.java
@@ -84,16 +84,24 @@ protected void processTextPosition(TextPosition glyph) {
double y = glyph.getYDirAdj();
double left = glyph.getXDirAdj();
double size = Math.max(1, glyph.getFontSizeInPt());
+ String letters = glyph.getUnicode();
+ // A space opens no line: Word writes one for an empty paragraph, and one drawn just
+ // before a line of text beside it, on a baseline a few points off, gave that line
+ // the space's baseline instead of its letters'.
+ boolean blank = letters == null
+ || letters.codePoints().allMatch(c -> Character.isWhitespace(c) || Character.isSpaceChar(c));
boolean sameLine = !Double.isNaN(baseline)
&& Math.abs(y - baseline) < size * 0.5
&& left >= lastX - 1
&& left - lastEnd < size * 1.5;
if (!sameLine) {
endLine();
+ if (blank) {
+ return;
+ }
baseline = y;
x = left;
}
- String letters = glyph.getUnicode();
if (letters != null) {
letters.codePoints()
.filter(c -> !Character.isWhitespace(c) && !Character.isSpaceChar(c))
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 5e03861dd..64ad7aa61 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv
@@ -559,41 +559,41 @@ 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-midnight_navy 0:21d2636ad3df -16.37 alexmorgan
-cv-midnight_navy 0:5518402feccc -17.05 marketingmanager
+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
-cv-midnight_navy 0:093e7d5fdbaa -17.12 contact
-cv-midnight_navy 0:01dcad830244 -16.44 +1(555)123-4567
-cv-midnight_navy 0:3eef38c97138 -16.45 alex.morgan@email.com
-cv-midnight_navy 0:d5a0d0abdc64 -16.45 newyork,ny,usa
-cv-midnight_navy 0:84308bea4540 -16.46 linkedin
-cv-midnight_navy 0:0e6a8e0b849e -16.47 portfolio
-cv-midnight_navy 0:68bf0109c6f8 -17.11 education
-cv-midnight_navy 0:6e1119579227 -17.18 masterofbusiness
-cv-midnight_navy 0:512afb6c7631 -17.18 administration
-cv-midnight_navy 0:09b0e2130c1e -16.68 universityofchicagoboothschoolof
-cv-midnight_navy 0:11d404179596 -16.69 business
-cv-midnight_navy 0:3b8048983ecc -17.07 2016–2018
+cv-midnight_navy 0:093e7d5fdbaa 2.58 contact
+cv-midnight_navy 0:01dcad830244 3.26 +1(555)123-4567
+cv-midnight_navy 0:3eef38c97138 3.25 alex.morgan@email.com
+cv-midnight_navy 0:d5a0d0abdc64 3.25 newyork,ny,usa
+cv-midnight_navy 0:84308bea4540 3.24 linkedin
+cv-midnight_navy 0:0e6a8e0b849e 3.23 portfolio
+cv-midnight_navy 0:68bf0109c6f8 2.59 education
+cv-midnight_navy 0:6e1119579227 2.52 masterofbusiness
+cv-midnight_navy 0:512afb6c7631 2.52 administration
+cv-midnight_navy 0:09b0e2130c1e 3.02 universityofchicagoboothschoolof
+cv-midnight_navy 0:11d404179596 3.01 business
+cv-midnight_navy 0:3b8048983ecc 2.63 2016–2018
cv-midnight_navy 0:3b8048983ecc#1 2.76 2016–2018
-cv-midnight_navy 0:84c2243138f7 -17.20 bachelorofsciencein
-cv-midnight_navy 0:e2a530e251d3 -17.19 marketing
-cv-midnight_navy 0:1f676d444ffb -16.99 bostonuniversity
-cv-midnight_navy 0:8789bd1a8f91 -17.07 2012–2016
-cv-midnight_navy 0:32c1cf49a2fe -17.15 skills
-cv-midnight_navy 0:4a3750fc1d48 -15.95 strategicplanning
-cv-midnight_navy 0:0d676ed982b4 -13.85 digitalmarketing
-cv-midnight_navy 0:c29cfa799c8d -11.74 marketresearch
-cv-midnight_navy 0:2c5646de026b -9.64 brandmanagement
-cv-midnight_navy 0:2f594f65f16c -7.53 dataanalysis
-cv-midnight_navy 0:340455fbac25 -5.43 projectmanagement
-cv-midnight_navy 0:96fa20c5c60d -3.33 googleanalytics
-cv-midnight_navy 0:9bc298c12dc3 -1.22 seo/sem
-cv-midnight_navy 0:a95ee8d8cf67 0.88 microsoftofficesuite
-cv-midnight_navy 0:d4e16994df90 2.99 communication
-cv-midnight_navy 0:6cb574fa10b6 3.88 languages
-cv-midnight_navy 0:b6234d2ea0d6 8.86 english
-cv-midnight_navy 0:9c9b13446cd9 18.59 spanish
-cv-midnight_navy 0:21bb36ad0638 28.32 french
+cv-midnight_navy 0:84c2243138f7 2.50 bachelorofsciencein
+cv-midnight_navy 0:e2a530e251d3 2.51 marketing
+cv-midnight_navy 0:1f676d444ffb 2.71 bostonuniversity
+cv-midnight_navy 0:8789bd1a8f91 2.63 2012–2016
+cv-midnight_navy 0:32c1cf49a2fe 2.55 skills
+cv-midnight_navy 0:4a3750fc1d48 2.70 strategicplanning
+cv-midnight_navy 0:0d676ed982b4 2.70 digitalmarketing
+cv-midnight_navy 0:c29cfa799c8d 2.71 marketresearch
+cv-midnight_navy 0:2c5646de026b 2.71 brandmanagement
+cv-midnight_navy 0:2f594f65f16c 2.72 dataanalysis
+cv-midnight_navy 0:340455fbac25 2.72 projectmanagement
+cv-midnight_navy 0:96fa20c5c60d 2.72 googleanalytics
+cv-midnight_navy 0:9bc298c12dc3 2.73 seo/sem
+cv-midnight_navy 0:a95ee8d8cf67 2.73 microsoftofficesuite
+cv-midnight_navy 0:d4e16994df90 2.74 communication
+cv-midnight_navy 0:6cb574fa10b6 2.58 languages
+cv-midnight_navy 0:b6234d2ea0d6 2.71 english
+cv-midnight_navy 0:9c9b13446cd9 2.74 spanish
+cv-midnight_navy 0:21bb36ad0638 2.77 french
cv-midnight_navy 0:b944906ed84c 2.44 professionalsummary
cv-midnight_navy 0:05b2b441ce7b 3.20 results-drivenmarketingmanagerwi
cv-midnight_navy 0:66d5ff95c72a 3.18 executingdata-drivenmarketingstr
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
index 4f2c85933..5d42ea2b0 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv
@@ -9,7 +9,7 @@ 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-engineering_resume 1 1 57 57 0.60 1.62 0
cv-executive 2 2 60 60 0.24 3.10 15
-cv-midnight_navy 1 1 86 80 3.12 17.11 74
+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-mint_editorial 2 2 59 59 0.27 0.60 0
cv-modern_professional 2 2 61 59 0.12 0.59 5
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 aa874c87f..ff334efe1 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv
@@ -558,41 +558,41 @@ 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-midnight_navy 0:21d2636ad3df -16.37 alexmorgan
-cv-midnight_navy 0:5518402feccc -17.05 marketingmanager
+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
-cv-midnight_navy 0:093e7d5fdbaa -17.12 contact
-cv-midnight_navy 0:01dcad830244 -16.44 +1(555)123-4567
-cv-midnight_navy 0:3eef38c97138 -16.45 alex.morgan@email.com
-cv-midnight_navy 0:d5a0d0abdc64 -16.45 newyork,ny,usa
-cv-midnight_navy 0:84308bea4540 -16.46 linkedin
-cv-midnight_navy 0:0e6a8e0b849e -16.47 portfolio
-cv-midnight_navy 0:68bf0109c6f8 -17.11 education
-cv-midnight_navy 0:6e1119579227 -17.18 masterofbusiness
-cv-midnight_navy 0:512afb6c7631 -17.18 administration
-cv-midnight_navy 0:09b0e2130c1e -16.68 universityofchicagoboothschoolof
-cv-midnight_navy 0:11d404179596 -16.69 business
-cv-midnight_navy 0:3b8048983ecc -17.07 2016–2018
+cv-midnight_navy 0:093e7d5fdbaa 2.58 contact
+cv-midnight_navy 0:01dcad830244 3.26 +1(555)123-4567
+cv-midnight_navy 0:3eef38c97138 3.25 alex.morgan@email.com
+cv-midnight_navy 0:d5a0d0abdc64 3.25 newyork,ny,usa
+cv-midnight_navy 0:84308bea4540 3.24 linkedin
+cv-midnight_navy 0:0e6a8e0b849e 3.23 portfolio
+cv-midnight_navy 0:68bf0109c6f8 2.59 education
+cv-midnight_navy 0:6e1119579227 2.52 masterofbusiness
+cv-midnight_navy 0:512afb6c7631 2.52 administration
+cv-midnight_navy 0:09b0e2130c1e 3.02 universityofchicagoboothschoolof
+cv-midnight_navy 0:11d404179596 3.01 business
+cv-midnight_navy 0:3b8048983ecc 2.63 2016–2018
cv-midnight_navy 0:3b8048983ecc#1 2.76 2016–2018
-cv-midnight_navy 0:84c2243138f7 -17.20 bachelorofsciencein
-cv-midnight_navy 0:e2a530e251d3 -17.19 marketing
-cv-midnight_navy 0:1f676d444ffb -16.99 bostonuniversity
-cv-midnight_navy 0:8789bd1a8f91 -17.07 2012–2016
-cv-midnight_navy 0:32c1cf49a2fe -17.15 skills
-cv-midnight_navy 0:4a3750fc1d48 -15.95 strategicplanning
-cv-midnight_navy 0:0d676ed982b4 -13.85 digitalmarketing
-cv-midnight_navy 0:c29cfa799c8d -11.74 marketresearch
-cv-midnight_navy 0:2c5646de026b -9.64 brandmanagement
-cv-midnight_navy 0:2f594f65f16c -7.53 dataanalysis
-cv-midnight_navy 0:340455fbac25 -5.43 projectmanagement
-cv-midnight_navy 0:96fa20c5c60d -3.33 googleanalytics
-cv-midnight_navy 0:9bc298c12dc3 -1.22 seo/sem
-cv-midnight_navy 0:a95ee8d8cf67 0.88 microsoftofficesuite
-cv-midnight_navy 0:d4e16994df90 2.99 communication
-cv-midnight_navy 0:6cb574fa10b6 3.88 languages
-cv-midnight_navy 0:b6234d2ea0d6 8.86 english
-cv-midnight_navy 0:9c9b13446cd9 18.59 spanish
-cv-midnight_navy 0:21bb36ad0638 28.32 french
+cv-midnight_navy 0:84c2243138f7 2.50 bachelorofsciencein
+cv-midnight_navy 0:e2a530e251d3 2.51 marketing
+cv-midnight_navy 0:1f676d444ffb 2.71 bostonuniversity
+cv-midnight_navy 0:8789bd1a8f91 2.63 2012–2016
+cv-midnight_navy 0:32c1cf49a2fe 2.55 skills
+cv-midnight_navy 0:4a3750fc1d48 2.70 strategicplanning
+cv-midnight_navy 0:0d676ed982b4 2.70 digitalmarketing
+cv-midnight_navy 0:c29cfa799c8d 2.71 marketresearch
+cv-midnight_navy 0:2c5646de026b 2.71 brandmanagement
+cv-midnight_navy 0:2f594f65f16c 2.72 dataanalysis
+cv-midnight_navy 0:340455fbac25 2.72 projectmanagement
+cv-midnight_navy 0:96fa20c5c60d 2.72 googleanalytics
+cv-midnight_navy 0:9bc298c12dc3 2.73 seo/sem
+cv-midnight_navy 0:a95ee8d8cf67 2.73 microsoftofficesuite
+cv-midnight_navy 0:d4e16994df90 2.74 communication
+cv-midnight_navy 0:6cb574fa10b6 2.58 languages
+cv-midnight_navy 0:b6234d2ea0d6 2.71 english
+cv-midnight_navy 0:9c9b13446cd9 2.74 spanish
+cv-midnight_navy 0:21bb36ad0638 2.77 french
cv-midnight_navy 0:b944906ed84c 2.44 professionalsummary
cv-midnight_navy 0:05b2b441ce7b 3.20 results-drivenmarketingmanagerwi
cv-midnight_navy 0:66d5ff95c72a 3.18 executingdata-drivenmarketingstr
diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
index dc5c968c3..2e8f8ded2 100644
--- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv
@@ -9,7 +9,7 @@ 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-midnight_navy 1 1 86 80 3.12 17.11 74
+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-mint_editorial 2 2 59 59 0.27 0.60 0
cv-modern_professional 2 2 61 59 0.12 0.59 5
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 53e9cbaae..81fef0e11 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
@@ -201,20 +201,26 @@ cv-charcoal_gold 0:66cb4ff79ee1 0.12 bscbusinessadministration
cv-charcoal_gold 0:8826393fc0f0 0.08 king'scollegelondon
cv-charcoal_gold 0:c7d18ae8c50d 0.18 2014–2017
cv-charcoal_gold 0:3acba0d7b5dc -0.17 projectmanager
+cv-charcoal_gold 0:3acba0d7b5dc#1 0.17 projectmanager
cv-charcoal_gold 0:f54e0195601a 0.14 results-drivenprojectmanagerwith
cv-charcoal_gold 0:7a0497cef9c6 0.20 teamsanddeliveringcomplexproject
cv-charcoal_gold 0:c38f1902ece8 0.56 skilledinagileandwaterfallmethod
cv-charcoal_gold 0:ca94787c6c7a -0.22 management.passionateaboutdrivin
cv-charcoal_gold 0:53e5e7c5a884 -0.14 experience
+cv-charcoal_gold 0:086c0e036e67 0.01 2021–present
+cv-charcoal_gold 0:eddc2d311872 -0.01 seniorprojectmanager
cv-charcoal_gold 0:243953e20dc4 0.06 digitalsolutionsltd·london,uk
cv-charcoal_gold 0:7fde5861c84b 0.09 •managecross-functionalteamsofup
cv-charcoal_gold 0:80c76af1fe2a 0.17 •implementagilepracticesthatimpr
cv-charcoal_gold 0:b6bb33b79f44 0.13 •buildstrongclientrelationshipsa
+cv-charcoal_gold 0:6c39082d9294 0.19 2019–2021
cv-charcoal_gold 0:46df57ef8321 0.24 innovatechsystems·london,uk
cv-charcoal_gold 0:271b01626043 0.17 •delivered12+projectsacrossitand
cv-charcoal_gold 0:77dd7c2ad6ae 0.25 •monitoredbudgets,timelines,andr
cv-charcoal_gold 0:bf8278cd4edc 0.23 •identifiedrisksandimplementedmi
cv-charcoal_gold 0:2773f92777d7 0.31 •achieved98%on-timeprojectdelive
+cv-charcoal_gold 0:ad564a6ee91d 0.25 2017–2019
+cv-charcoal_gold 0:e085ad3a0d32 0.23 projectcoordinator
cv-charcoal_gold 0:6ac382583801 0.30 globalbusinessservices·london,uk
cv-charcoal_gold 0:77aec5bb6b7f 0.35 •supportedprojectmanagersinplann
cv-charcoal_gold 0:38dafe4a1765 0.31 •preparedreports,dashboards,andd
@@ -552,41 +558,41 @@ 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-midnight_navy 0:21d2636ad3df -19.32 alexmorgan
-cv-midnight_navy 0:5518402feccc -19.90 marketingmanager
+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
-cv-midnight_navy 0:093e7d5fdbaa -19.83 contact
-cv-midnight_navy 0:01dcad830244 -19.25 +1(555)123-4567
-cv-midnight_navy 0:3eef38c97138 -19.28 alex.morgan@email.com
-cv-midnight_navy 0:d5a0d0abdc64 -19.30 newyork,ny,usa
-cv-midnight_navy 0:84308bea4540 -19.33 linkedin
-cv-midnight_navy 0:0e6a8e0b849e -19.33 portfolio
-cv-midnight_navy 0:68bf0109c6f8 -19.88 education
-cv-midnight_navy 0:6e1119579227 -19.86 masterofbusiness
-cv-midnight_navy 0:512afb6c7631 -19.88 administration
-cv-midnight_navy 0:09b0e2130c1e -19.57 universityofchicagoboothschoolof
-cv-midnight_navy 0:11d404179596 -19.53 business
-cv-midnight_navy 0:3b8048983ecc -19.80 2016–2018
+cv-midnight_navy 0:093e7d5fdbaa -0.15 contact
+cv-midnight_navy 0:01dcad830244 0.43 +1(555)123-4567
+cv-midnight_navy 0:3eef38c97138 0.40 alex.morgan@email.com
+cv-midnight_navy 0:d5a0d0abdc64 0.41 newyork,ny,usa
+cv-midnight_navy 0:84308bea4540 0.38 linkedin
+cv-midnight_navy 0:0e6a8e0b849e 0.47 portfolio
+cv-midnight_navy 0:68bf0109c6f8 -0.08 education
+cv-midnight_navy 0:6e1119579227 -0.18 masterofbusiness
+cv-midnight_navy 0:512afb6c7631 -0.20 administration
+cv-midnight_navy 0:09b0e2130c1e 0.11 universityofchicagoboothschoolof
+cv-midnight_navy 0:11d404179596 0.15 business
+cv-midnight_navy 0:3b8048983ecc -0.12 2016–2018
cv-midnight_navy 0:3b8048983ecc#1 -0.01 2016–2018
-cv-midnight_navy 0:84c2243138f7 -19.92 bachelorofsciencein
-cv-midnight_navy 0:e2a530e251d3 -19.93 marketing
-cv-midnight_navy 0:1f676d444ffb -19.60 bostonuniversity
-cv-midnight_navy 0:8789bd1a8f91 -19.87 2012–2016
-cv-midnight_navy 0:32c1cf49a2fe -19.89 skills
-cv-midnight_navy 0:4a3750fc1d48 -18.72 strategicplanning
-cv-midnight_navy 0:0d676ed982b4 -16.54 digitalmarketing
-cv-midnight_navy 0:c29cfa799c8d -14.47 marketresearch
-cv-midnight_navy 0:2c5646de026b -12.41 brandmanagement
-cv-midnight_navy 0:2f594f65f16c -10.22 dataanalysis
-cv-midnight_navy 0:340455fbac25 -8.16 projectmanagement
-cv-midnight_navy 0:96fa20c5c60d -6.07 googleanalytics
-cv-midnight_navy 0:9bc298c12dc3 -3.88 seo/sem
-cv-midnight_navy 0:a95ee8d8cf67 -1.82 microsoftofficesuite
-cv-midnight_navy 0:d4e16994df90 0.25 communication
-cv-midnight_navy 0:6cb574fa10b6 1.26 languages
-cv-midnight_navy 0:b6234d2ea0d6 6.14 english
-cv-midnight_navy 0:9c9b13446cd9 15.94 spanish
-cv-midnight_navy 0:21bb36ad0638 25.64 french
+cv-midnight_navy 0:84c2243138f7 -0.22 bachelorofsciencein
+cv-midnight_navy 0:e2a530e251d3 -0.23 marketing
+cv-midnight_navy 0:1f676d444ffb 0.08 bostonuniversity
+cv-midnight_navy 0:8789bd1a8f91 -0.07 2012–2016
+cv-midnight_navy 0:32c1cf49a2fe -0.09 skills
+cv-midnight_navy 0:4a3750fc1d48 0.00 strategicplanning
+cv-midnight_navy 0:0d676ed982b4 0.02 digitalmarketing
+cv-midnight_navy 0:c29cfa799c8d 0.05 marketresearch
+cv-midnight_navy 0:2c5646de026b -0.05 brandmanagement
+cv-midnight_navy 0:2f594f65f16c -0.02 dataanalysis
+cv-midnight_navy 0:340455fbac25 0.00 projectmanagement
+cv-midnight_navy 0:96fa20c5c60d 0.05 googleanalytics
+cv-midnight_navy 0:9bc298c12dc3 0.08 seo/sem
+cv-midnight_navy 0:a95ee8d8cf67 0.10 microsoftofficesuite
+cv-midnight_navy 0:d4e16994df90 0.01 communication
+cv-midnight_navy 0:6cb574fa10b6 -0.06 languages
+cv-midnight_navy 0:b6234d2ea0d6 0.02 english
+cv-midnight_navy 0:9c9b13446cd9 0.10 spanish
+cv-midnight_navy 0:21bb36ad0638 0.06 french
cv-midnight_navy 0:b944906ed84c -0.44 professionalsummary
cv-midnight_navy 0:05b2b441ce7b 0.28 results-drivenmarketingmanagerwi
cv-midnight_navy 0:66d5ff95c72a 0.32 executingdata-drivenmarketingstr
@@ -1070,8 +1076,8 @@ cv-orange_ops 0:9806038f6123 0.49 •assistedwithkpireportingandperf
cv-orange_ops 0:c2ec5b8c794d 0.61 •ensuredcompliancewithcompanypol
cv-orange_ops 0:a691a9aab3fc 0.61 keykpisnapshot(recent12months)
cv-orange_ops 0:b5f6dbae04bd 0.46 15%
-cv-orange_ops 0:e918ea42a538 -6.02 99.2%
-cv-orange_ops 0:16906b6f3827 -6.02 98.4%
+cv-orange_ops 0:e918ea42a538 0.46 99.2%
+cv-orange_ops 0:16906b6f3827 0.46 98.4%
cv-orange_ops 0:bd1b16004d22 0.71 productivity
cv-orange_ops 0:b11a85b296a9 0.71 inventory
cv-orange_ops 0:b1e25044bf23 0.71 on-time
@@ -1233,7 +1239,7 @@ cv-serif_headline 0:0a52c5f62d73 0.01 github.com/alexmorgan
cv-serif_headline 0:ff00f99bc03c -0.25 softwareengineerwith5+yearsofexp
cv-serif_headline 0:7fd0d2d0fde8 -0.27 backendservices.proficientinjava
cv-serif_headline 0:81bb94512f0b -0.29 systemdesign,anddeliveringhigh-q
-cv-serif_headline 0:53e5e7c5a884 -1.33 experience
+cv-serif_headline 0:53e5e7c5a884 0.11 experience
cv-serif_headline 0:b37c6a2d5d11 0.07 seniorsoftwareengineer
cv-serif_headline 0:e7227bbe0c40 0.14 jan2022–present
cv-serif_headline 0:f4e5af202d87 0.12 techsolutionsinc.|newyork,ny
@@ -1255,7 +1261,7 @@ cv-serif_headline 0:2198cdea3aba 0.50 devsphere|newyork,ny
cv-serif_headline 0:173ed9f7b6b1 0.55 •assistedindevelopingandmaintain
cv-serif_headline 0:0a1fbe0c94d9 0.52 •wroteunitandintegrationteststoe
cv-serif_headline 0:bd5eec545fa8 0.50 •participatedinagileceremoniesan
-cv-serif_headline 0:2577c0f557b2 -0.79 projects
+cv-serif_headline 0:2577c0f557b2 0.65 projects
cv-serif_headline 0:c03ade0f8985 0.67 e-commerceplatform
cv-serif_headline 0:f726495cb8e6 0.64 java,springboot,react,postgresql
cv-serif_headline 0:09271909c2c5 0.40 developedafull-stacke-commerce
@@ -1393,15 +1399,15 @@ cv-slate_orange 0:88cc29c2fda2 0.56 microsoftoffice&googlesuite
cv-slate_orange 0:e37d86244e99 0.57 crmsystems(salesforce,hubspot)
cv-slate_orange 0:d70e5edb4685 0.58 timemanagement&prioritization
cv-slate_orange 0:8bab53aa1bee 0.04 selectedachievements
-cv-slate_orange 0:b08c44dac044 4.50 processimprovement
+cv-slate_orange 0:b08c44dac044 0.54 processimprovement
cv-slate_orange 0:c7966365f508 1.29 designedanewtrackingsystemthat
cv-slate_orange 0:3a5ad5d0dc8d 0.79 reducedreportingtimeby30%and
cv-slate_orange 0:74821a85e7b9 0.29 improvedaccuracy.
-cv-slate_orange 0:dd9532e202a3 4.52 customersatisfaction
+cv-slate_orange 0:dd9532e202a3 0.56 customersatisfaction
cv-slate_orange 0:dfc9a4fd0657 1.19 maintaineda96%customer
cv-slate_orange 0:5e04b5e4dbf9 0.81 satisfactionratingthroughrespons
cv-slate_orange 0:c119f318eaae 0.31 supportandissueresolution.
-cv-slate_orange 0:365853229c97 4.54 revenuecontribution
+cv-slate_orange 0:365853229c97 0.58 revenuecontribution
cv-slate_orange 0:292a456730c2 1.21 consistentlyexceededmonthlysales
cv-slate_orange 0:8eb387deb3e1 0.83 goalsby15–20%throughrelationship
cv-slate_orange 0:77706bc9ed09 0.33 buildingandconsultativeservice.
@@ -1410,8 +1416,8 @@ cv-slate_orange 0:890744623577 0.99 spanishprofessionalworking
cv-slate_orange 0:21bb36ad0638 1.15 french
cv-slate_orange 0:fbb7b5bf4ca3 1.18 basic
cv-slate_orange 0:676dfdd56ad2 0.92 additionalinformation
-cv-slate_orange 0:ef68dac1790b 4.15 availability:full-time
-cv-slate_orange 0:6795aec0e50a 4.13 willingtorelocate:yes
+cv-slate_orange 0:ef68dac1790b 1.39 availability:full-time
+cv-slate_orange 0:6795aec0e50a 1.37 willingtorelocate:yes
cv-slate_orange 0:b8eeb230ca32 1.48 remotework:opento
cv-slate_orange 0:b73078ca247c 1.42 hybrid/remote
cv-slate_orange 0:3c69d6bb4892 0.04 professionalprofile
@@ -1421,21 +1427,21 @@ cv-slate_orange 0:41221430e3f8 0.09 organizationalskills,andtheabili
cv-slate_orange 0:324c4509a3e4 0.06 managingpriorities,buildingrelat
cv-slate_orange 0:57271827af78 0.03 customersatisfaction,andbusiness
cv-slate_orange 0:c2f296ef30a3 0.21 professionalexperience
-cv-slate_orange 0:da2ea6509bec -3.46 operationscoordinator
+cv-slate_orange 0:da2ea6509bec 0.74 operationscoordinator
cv-slate_orange 0:e7227bbe0c40 0.63 jan2022–present
cv-slate_orange 0:9f7cecddf317 0.57 brightstartlogistics,austin,tx
cv-slate_orange 0:e0bbd6c9782e 0.62 •coordinatedailyoperationsandsch
cv-slate_orange 0:dc9a542f8db9 0.66 •developandmaintaindashboardsand
cv-slate_orange 0:66acc2e5b891 0.58 •streamlinecommunicationbetweend
cv-slate_orange 0:8bd3fd672f95 0.62 •managevendorrelationshipsandass
-cv-slate_orange 0:dd7872236383 -3.58 customersupportspecialist
+cv-slate_orange 0:dd7872236383 0.62 customersupportspecialist
cv-slate_orange 0:1734c189cfe2 0.63 jun2020–dec2021
cv-slate_orange 0:b9dfc8902d16 0.57 techwavesolutions,remote
cv-slate_orange 0:56391e23e83a 0.52 •providedtechnicalsupporttocusto
cv-slate_orange 0:7eb50a39cf50 0.56 •resolvedanaverageof40+inquiries
cv-slate_orange 0:46f632748ba8 0.60 •createdknowledgebasearticlesand
cv-slate_orange 0:02b44f46125c 0.52 •collaboratedwithproductandengin
-cv-slate_orange 0:a1707ef6164e -3.68 salesassociate
+cv-slate_orange 0:a1707ef6164e 0.64 salesassociate
cv-slate_orange 0:2bc613d4e516 0.53 mar2019–may2020
cv-slate_orange 0:bb7253ff7002 0.47 urbanoutfitters,austin,tx
cv-slate_orange 0:aaa52372c317 -0.08 •consistentlyexceededsalestarget
@@ -1443,7 +1449,7 @@ cv-slate_orange 0:198e638fe8f3 0.13 personalizedcustomerservice.
cv-slate_orange 0:dabead95ee7e 0.53 •builtandmaintainedstrongcustome
cv-slate_orange 0:ff3f5f9ca7a0 0.45 •merchandisedfloordisplaysandcon
cv-slate_orange 0:05a39f2908ba 0.52 •recognizedas“topperformer”forq4
-cv-slate_orange 0:832235539465 -3.68 administrativeassistant/projecta
+cv-slate_orange 0:832235539465 0.52 administrativeassistant/projecta
cv-slate_orange 0:2ccfc04a3ae7 0.53 may2018–feb2019
cv-slate_orange 0:cfc5fd80ae80 0.47 greenfieldnonprofitinitiative,au
cv-slate_orange 0:b8aecb03e7ed 0.40 •supportedprojectmanagerswithsch
@@ -1612,10 +1618,10 @@ cv-terracotta_rail 0:bdc5f821f737 -0.42 sustainablemasterplan;achievedhi
cv-terracotta_rail 0:208ed62d3df6 -0.70 environmentalstandardsandstrongs
cv-terracotta_rail 0:ac5e63220124 -0.98 performance.
cv-terracotta_rail 0:68bf0109c6f8 -0.81 education
-cv-terracotta_rail 0:1d1dcf789873 -1.87 marcharchitecture
+cv-terracotta_rail 0:1d1dcf789873 -0.91 marcharchitecture
cv-terracotta_rail 0:00583a6bed38 -0.91 2014–2016
cv-terracotta_rail 0:5761434c99d8 -0.81 universityofsheffield
-cv-terracotta_rail 0:e3f2f6a4e375 -1.88 ba(hons)architecture
+cv-terracotta_rail 0:e3f2f6a4e375 -0.92 ba(hons)architecture
cv-terracotta_rail 0:2920a7fb42fc -0.92 2011–2014
cv-terracotta_rail 0:af625dd0aa20 -0.82 universityofthewestofengland
cv-timeline_minimal 0:8f1010120ed6 -0.20 jordanrivera
@@ -1742,23 +1748,23 @@ cv-violet_grid 0:5a0774260e0c 0.48 2022-present
cv-violet_grid 0:204f460767ff 0.43 seniorux/uidesigner|novafin(fint
cv-violet_grid 0:318bba211bdc 0.60 sanfrancisco,ca
cv-violet_grid 0:318bba211bdc#1 0.44 sanfrancisco,ca
-cv-violet_grid 0:3883e88c63c3 2.43 ledend-to-enddesignforapersonalf
-cv-violet_grid 0:ccc00663f089 2.38 conducteduserresearch,builtperso
-cv-violet_grid 0:7d0f908c7e5f 2.33 designedwireframes,prototypes,an
-cv-violet_grid 0:9b57c4a85aad 2.41 establishedandmaintainedascalabl
-cv-violet_grid 0:6894eafb99c2 2.38 collaboratedwithproductmanagers,
+cv-violet_grid 0:3883e88c63c3 0.51 ledend-to-enddesignforapersonalf
+cv-violet_grid 0:ccc00663f089 0.46 conducteduserresearch,builtperso
+cv-violet_grid 0:7d0f908c7e5f 0.41 designedwireframes,prototypes,an
+cv-violet_grid 0:9b57c4a85aad 0.49 establishedandmaintainedascalabl
+cv-violet_grid 0:6894eafb99c2 0.46 collaboratedwithproductmanagers,
cv-violet_grid 0:01e4455cec51 0.44 2020-2022
cv-violet_grid 0:24d722f0cfbe 0.39 uxdesigner|brighthealth(healthte
-cv-violet_grid 0:530707fb0897 2.39 redesignedpatientportalexperienc
-cv-violet_grid 0:b2e27a1d9270 2.34 plannedandfacilitatedusabilityte
-cv-violet_grid 0:ab80a52d70fe 2.29 partneredwithengineerstoensurefe
-cv-violet_grid 0:11cbf615189b 2.37 createdreusablecomponentsandstyl
+cv-violet_grid 0:530707fb0897 0.47 redesignedpatientportalexperienc
+cv-violet_grid 0:b2e27a1d9270 0.42 plannedandfacilitatedusabilityte
+cv-violet_grid 0:ab80a52d70fe 0.37 partneredwithengineerstoensurefe
+cv-violet_grid 0:11cbf615189b 0.45 createdreusablecomponentsandstyl
cv-violet_grid 0:7e5081a3ed48 0.31 2018-2020
cv-violet_grid 0:73f7bbf7cd60 0.26 juniorux/uidesigner|lumendigital
cv-violet_grid 0:c6b06045332f 0.43 austin,tx
-cv-violet_grid 0:5ea8234925c5 2.26 supporteddiscoveryworkshops,user
-cv-violet_grid 0:46f00c76528c 2.33 designedresponsivewebsitesandlan
-cv-violet_grid 0:f3cea36a9d83 2.28 collaboratedwithcopywritersandde
+cv-violet_grid 0:5ea8234925c5 0.34 supporteddiscoveryworkshops,user
+cv-violet_grid 0:46f00c76528c 0.41 designedresponsivewebsitesandlan
+cv-violet_grid 0:f3cea36a9d83 0.36 collaboratedwithcopywritersandde
cv-violet_grid 0:d94eccbb1fbe 0.31 selectedprojects
cv-violet_grid 0:45dc8bf81211 0.35 novafinmobileapp|personalfinance
cv-violet_grid 0:d398b29d3dbb 0.43 2023
@@ -3332,16 +3338,16 @@ invoice-subscription 0:2474519f77a9#2 -0.63 unitedkingdom
invoice-subscription 0:bfdec553b85a 0.32 billing@halstead.example
invoice-subscription 0:78acb3cb907b 0.32 www.halstead.example
invoice-subscription 0:f1c030eb4329 0.43 vatnogb418227603
-invoice-subscription 0:77ead61b5875 -0.11 invoiceno
+invoice-subscription 0:77ead61b5875 0.37 invoiceno
invoice-subscription 0:0b0c0ee24de8 0.34 hs-inv-2026-0417
invoice-subscription 0:0b0c0ee24de8#1 0.56 hs-inv-2026-0417
-invoice-subscription 0:8feaf8016bab -0.04 issuedate
+invoice-subscription 0:8feaf8016bab 0.32 issuedate
invoice-subscription 0:4d49cb78b810 0.41 27august2026
-invoice-subscription 0:ac10f8c16320 0.03 duedate
+invoice-subscription 0:ac10f8c16320 0.39 duedate
invoice-subscription 0:b55571aaabac 0.49 10september2026
-invoice-subscription 0:fb854bac39c8 0.11 paymentterms
+invoice-subscription 0:fb854bac39c8 0.47 paymentterms
invoice-subscription 0:56a2e019b8aa 0.44 14days
-invoice-subscription 0:466e5b9cd57f 0.06 currency
+invoice-subscription 0:466e5b9cd57f 0.54 currency
invoice-subscription 0:54f8e1b3fe44 0.51 gbp
invoice-subscription 0:aeba3a7b3598 0.59 billto
invoice-subscription 0:7bfc2b791f41 -0.66 northlineconsultingltd
@@ -3786,8 +3792,8 @@ proposal-editorial 1:06ebb5670fa8 3.16 ownershipofallfinaldeliverables
proposal-editorial 1:c1514c6cdad1 2.62 acceptance
proposal-editorial 1:893663b06769 2.26 bysigningbelow,youagreetothescop
proposal-editorial 1:df16232de5b9 2.41 acceptedby:
-proposal-editorial 1:f089d24d7cf3 4.57 signature:
-proposal-editorial 1:c74c9cd82f13 4.57 date:
+proposal-editorial 1:f089d24d7cf3 2.41 signature:
+proposal-editorial 1:c74c9cd82f13 2.41 date:
proposal-editorial 1:026890c58de9 0.12 northlinestudio|northlinestudio.
proposal-indigo 0:07f15cde5b18 0.22 kestrel
proposal-indigo 0:ecd1378bc9dc 0.18 proposal
@@ -4165,7 +4171,7 @@ rota-cobalt 0:e594a1e64b7d 0.52 l22/d48
rota-cobalt 0:7062fafe1dab 0.52 l65/d112
rota-cobalt 0:778cb57b62fa 0.52 l73/d112
rota-cobalt 0:1b4471920f01 0.52 l72/d83
-rota-cobalt 0:288965a1f2c8 -0.17 management
+rota-cobalt 0:288965a1f2c8 0.31 management
rota-cobalt 0:6a61eb7383e8 0.42 rosa
rota-cobalt 0:3bb0f0bb5552 0.53 09:00-18:00
rota-cobalt 0:3bb0f0bb5552#1 0.68 09:00-18:00
@@ -4210,7 +4216,7 @@ rota-cobalt 0:6d43df86fe4b#11 1.60 16:00-00:00
rota-cobalt 0:6d43df86fe4b#12 1.60 16:00-00:00
rota-cobalt 0:6d43df86fe4b#13 0.97 16:00-00:00
rota-cobalt 0:5a5f719aae11 -0.02 11:00-16:00
-rota-cobalt 0:df9ba122242c 0.01 bartenders
+rota-cobalt 0:df9ba122242c 0.49 bartenders
rota-cobalt 0:dbaf86dd80e3 0.58 nadia
rota-cobalt 0:0cdd9d9e75dc -0.12 09:00-16:00
rota-cobalt 0:0cdd9d9e75dc#1 0.14 09:00-16:00
@@ -4240,7 +4246,7 @@ rota-cobalt 0:86846e84f9bf 1.13 sofia
rota-cobalt 0:effcc54ba75f 1.14 karol
rota-cobalt 0:5c1e134d2d34 1.28 henrik
rota-cobalt 0:6326e0e8cfda 1.42 lucia
-rota-cobalt 0:c235e502637c 0.87 barbacks
+rota-cobalt 0:c235e502637c 1.35 barbacks
rota-cobalt 0:bd558de5f794 1.35 oskar
rota-cobalt 0:4941d6993ee9 1.36 teodor
rota-cobalt 0:1d3d3d6a07bd 1.62 vesna
diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv
index 5971c4d59..fe09d08a7 100644
--- a/qa/src/test/resources/docx-fidelity/word-windows.tsv
+++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv
@@ -3,29 +3,29 @@ 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-charcoal_gold 1 1 85 77 0.21 0.58 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-midnight_navy 1 1 86 80 0.64 19.86 30
+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-mint_editorial 2 2 59 59 0.13 0.31 0
cv-modern_professional 2 2 61 59 0.20 0.77 5
cv-monogram_sidebar 1 1 53 50 0.79 2.95 17
cv-navy_sidebar 1 1 71 69 0.13 0.22 0
cv-nordic_clean 1 1 63 58 0.22 0.61 0
-cv-orange_ops 1 1 108 99 0.42 0.71 2
+cv-orange_ops 1 1 108 99 0.42 0.71 0
cv-panel 1 1 79 73 0.43 0.94 0
cv-professional_sidebar 1 1 68 64 0.11 0.27 0
-cv-serif_headline 1 1 103 103 0.51 1.05 0
+cv-serif_headline 1 1 103 103 0.50 1.03 0
cv-sidebar_portrait 1 1 49 49 0.08 0.27 0
-cv-slate_orange 1 1 87 85 0.54 3.46 9
+cv-slate_orange 1 1 87 85 0.53 1.18 0
cv-teal_pulse 1 1 57 57 0.11 0.38 0
cv-terracotta_rail 1 1 102 102 0.42 0.87 0
cv-timeline_minimal 2 2 89 86 1.44 1.57 0
-cv-violet_grid 1 1 80 77 0.35 2.34 14
+cv-violet_grid 1 1 80 77 0.35 0.81 2
invoice-classic 1 1 56 56 0.48 1.22 0
invoice-classic_long 2 2 166 166 0.51 0.88 0
invoice-consulting 1 1 92 92 1.10 1.65 3
@@ -39,7 +39,7 @@ invoice-modern_long 2 2 162 162 0.44 0.60 0
invoice-obsidian 1 1 81 81 0.26 0.62 0
invoice-payments 1 1 104 100 0.50 1.10 2
invoice-platform 1 1 111 111 0.19 0.41 0
-invoice-subscription 1 1 81 81 0.45 0.64 0
+invoice-subscription 1 1 81 81 0.47 0.64 0
invoice-workspace 1 1 90 90 0.44 1.26 0
letter-blue_banner 1 1 10 10 0.39 0.50 0
letter-boxed_sections 1 1 10 10 0.55 0.63 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 e9e969214..57e65468b 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
@@ -4959,10 +4959,10 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
// edges are no space in the flow, as writeContainerBody hands them back.
double carriedFromOutside = carriedSpacingBefore;
long blocksBefore = blocksWritten;
- // A single layer the container sets in from its edges — centred, smaller than its
- // outline — stands that far in from them on the page: NavySidebar's photo sits 1.6pt
- // inside its ring, top and bottom, and written flush with the ring's top the column
- // under it stood twice the ring's width high.
+ // What the container sets in from its edges — a layer centred, smaller than its
+ // outline, or lines written one under another — stands that far in from them on the
+ // page: NavySidebar's photo sits 1.6pt inside its ring, top and bottom, and written
+ // flush with the ring's top the column under it stood twice the ring's width high.
double[] setIn = layerSetIn(node);
carriedSpacingBefore += node.margin().top() + node.padding().top() + setIn[0];
for (DocumentNode child : node.children()) {
@@ -4986,27 +4986,53 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node,
}
/**
- * How far a shape container sets its one layer in from the top and the bottom of its content,
- * where the layout placed both on one page; {@code {0, 0}} for a container of more layers,
- * or one the layout did not place.
+ * How far a shape container sets its layers in from the top and the bottom of its content,
+ * where the layout placed them all on its page; {@code {0, 0}} for one the layout did not
+ * place, or one of several layers that do not stand one under another.
+ *
+ *
Several layers are set in as one where they are paragraphs written one under another:
+ * each starting at or below the foot of the one before, or the two held as a stack of lines
+ * ({@link #holdStackedLines}), whose lines meet. The set-in runs from the top of the first to
+ * the foot of the last. {@code MidnightNavy}'s monogram centres its two initials in a 76pt
+ * ring; written from the ring's top, the initials stood 9.6pt high in Word, and the name
+ * under the ring 19.3pt, with the whole column under it. Layers set side by side, or
+ * overlapping without being a stack, are written taller than the page has them, and are
+ * not set in.
*/
private double[] layerSetIn(ShapeContainerNode node) {
- if (node.children().size() != 1) {
+ List layers = node.children();
+ if (layers.isEmpty()) {
return new double[]{0, 0};
}
com.demcha.compose.document.layout.PlacedNode box = layout.placement(node);
- com.demcha.compose.document.layout.PlacedNode layer = layout.placement(node.children().get(0));
- if (box == null || layer == null || box.startPage() != box.endPage()
- || layer.startPage() != box.startPage() || layer.endPage() != box.startPage()) {
+ if (box == null || box.startPage() != box.endPage()) {
return new double[]{0, 0};
}
+ DocumentNode previous = null;
+ double previousFoot = Double.POSITIVE_INFINITY;
+ for (DocumentNode child : layers) {
+ com.demcha.compose.document.layout.PlacedNode placed = layout.placement(child);
+ if (placed == null || placed.startPage() != box.startPage() || placed.endPage() != box.startPage()
+ || layers.size() > 1 && !(child instanceof ParagraphNode)) {
+ return new double[]{0, 0};
+ }
+ double top = placed.placementY() + placed.placementHeight();
+ boolean stacked = previous instanceof ParagraphNode above && stackedLineHeights.containsKey(above)
+ && child instanceof ParagraphNode below && stackedLineHeights.containsKey(below);
+ if (previous != null && !(top <= previousFoot + 0.01) && !stacked) {
+ return new double[]{0, 0};
+ }
+ previous = child;
+ previousFoot = placed.placementY();
+ }
+ com.demcha.compose.document.layout.PlacedNode first = layout.placement(layers.get(0));
+ com.demcha.compose.document.layout.PlacedNode last = layout.placement(layers.get(layers.size() - 1));
// Measured up from the foot of the page: the box's content runs from its padding up.
double contentTop = box.placementY() + box.placementHeight() - node.padding().top();
double contentFoot = box.placementY() + node.padding().bottom();
- // The layer's own margins are its own edges, written with it.
- DocumentInsets margin = node.children().get(0).margin();
- double above = contentTop - (layer.placementY() + layer.placementHeight()) - margin.top();
- double below = layer.placementY() - contentFoot - margin.bottom();
+ // A layer's own margins are its own edges, written with it.
+ double above = contentTop - (first.placementY() + first.placementHeight()) - layers.get(0).margin().top();
+ double below = last.placementY() - contentFoot - layers.get(layers.size() - 1).margin().bottom();
// A layer moved past either edge leaves the other side no more than the two hold together.
// A paragraph's line past the foot is written where the page puts it: hangBelowItsBox
// takes its overhang from the gap under the container.
@@ -5527,6 +5553,13 @@ private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node, boolean
PictureReach pictures = PictureReach.NONE;
// The mark closes the last line, so it is sized as the text that ends it.
DocumentTextStyle markStyle = node.textStyle();
+ // A shape's picture is wider than its box on either side: the text just before it gives
+ // back the room on the left, so its ink starts where the page starts it, and the text
+ // just after it the room on the right (giveBackWidth).
+ double widthOwed = 0;
+ XWPFRun textBefore = null;
+ java.util.Map givenBack = new java.util.LinkedHashMap<>();
+ java.util.Map lettersOf = new java.util.HashMap<>();
for (InlineRun run : node.inlineRuns()) {
InlineTextRun text = textOf(run);
if (text == null) {
@@ -5534,7 +5567,14 @@ private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node, boolean
if (reach != null) {
wroteARun = true;
pictures = pictures.max(reach);
+ if (run instanceof InlineShapeRun shape && !rightToLeft) {
+ if (textBefore != null) {
+ givenBack.merge(textBefore, DocxShapePictures.widthBeforeItsBox(shape), Double::sum);
+ }
+ widthOwed += DocxShapePictures.widthAfterItsBox(shape);
+ }
}
+ textBefore = null;
continue;
}
// A run's own link wins over the paragraph's: a sentence with one linked phrase
@@ -5547,8 +5587,15 @@ private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node, boolean
applyRunDirection(docRun, rightToLeft);
applyInlineBackground(docRun, backgroundOf(run), path);
setTextBrokenAtLines(docRun, text.text());
+ if (widthOwed > 0) {
+ givenBack.merge(docRun, widthOwed, Double::sum);
+ }
+ widthOwed = 0;
+ textBefore = docRun;
+ lettersOf.put(docRun, text.text());
wroteARun = true;
}
+ givenBack.forEach((textRun, room) -> giveBackWidth(textRun, lettersOf.get(textRun), room));
if (!wroteARun) {
XWPFRun docRun = newRun(para, node.linkTarget());
applyStyle(docRun, node.textStyle());
@@ -5787,6 +5834,35 @@ private double shiftToThePagesBaseline(XWPFParagraph para, ParagraphNode node, d
return 0;
}
+ /**
+ * Sets a run's letters closer by the room the shapes beside it take past their boxes.
+ *
+ *
A shape is written as a picture wider than its box on either side, by its ink's
+ * overhang and an edge ({@link DocxShapePictures#widthBeforeItsBox},
+ * {@link DocxShapePictures#widthAfterItsBox}). Word sets a picture at its width whatever
+ * spacing its run states, and letters closer when theirs is condensed: a run gives back the
+ * room the shape before it takes on its right and the shape after it on its left, spread
+ * over its letters. Word sets a letter's spacing after every letter in tenths of a point —
+ * measured, two quarter points on two letters of a gap came out three tenths each — and
+ * LibreOffice moved nothing for a space condensed in a run of its own; so each letter of the
+ * run is set closer by whole tenths, as near the room as they come. A run too long for a
+ * tenth a letter gives back nothing. {@code MidnightNavy}'s language dots, five to a line
+ * with four spaces between each, stood 2.5pt wider than the page's in Word, and the fifth
+ * broke onto a second line. A run holding a line break is left as it is, its letters on two
+ * lines.
+ *
+ * @param points the room the shapes beside the run take past their boxes, in points
+ */
+ private static void giveBackWidth(XWPFRun run, String text, double points) {
+ if (text == null || text.isEmpty() || LINE_BREAK.matcher(text).find()) {
+ return;
+ }
+ long tenths = Math.round(points * 10 / text.codePointCount(0, text.length()));
+ if (tenths != 0) {
+ run.setCharacterSpacing((int) (run.getCharacterSpacing() - tenths * 2));
+ }
+ }
+
/** A line break in text, as the page breaks lines at it (see {@code ParagraphWrapping}). */
private static final java.util.regex.Pattern LINE_BREAK = java.util.regex.Pattern.compile("\r\n|\r|\n");
@@ -8163,6 +8239,7 @@ private void writeRow(XWPFDocument document, RowNode node) throws Exception {
}
applyRowVerticalAlign(cell, node.verticalAlign());
}
+ giveTheLastCellItsInk(table, node, placedColumns);
// Anywhere, a row whose tallest child is one Word holds nothing of in its cell — a
// picture drawn where the page puts it, a badge beside a heading — is only as tall there
// as its text: WorkspaceInvoice's bill-to heading stood beside a 20pt badge, and the
@@ -8176,6 +8253,86 @@ private void writeRow(XWPFDocument document, RowNode node) throws Exception {
indentTable(table);
}
+ /**
+ * Widens a row's last cell by as far as the shape ending its line is drawn past it.
+ *
+ *
The page draws a shape's ink past its box — half a ring's stroke — and past the cell
+ * the box ends at. Word cuts a cell's content at the cell's edge, the transparent edge
+ * round a shape's picture with it: {@code MidnightNavy}'s fifth language dot, a ring at its
+ * column's right edge, lost the right of its stroke. Its column, and the table, are made as
+ * much wider; the row's other columns keep their widths. Only a paragraph of one line set
+ * from the left is measured: its line ends where the page ends it, wider by the room its
+ * shapes take where no text beside them gives it back (see {@link #giveBackWidth}).
+ *
+ * @param placedColumns whether the row's columns are the layout's, its children's margins
+ * already outside them
+ */
+ private void giveTheLastCellItsInk(XWPFTable table, RowNode node, boolean placedColumns) {
+ int last = node.children().size() - 1;
+ if (last < 0 || !(node.children().get(last) instanceof ParagraphNode paragraph)
+ || paragraph.align() == TextAlign.CENTER || paragraph.align() == TextAlign.RIGHT
+ || ParagraphDirection.resolve(paragraph) == TextDirection.RTL
+ || layout.lineCount(paragraph) != 1 || paragraph.inlineRuns().isEmpty()
+ || !(paragraph.inlineRuns().get(paragraph.inlineRuns().size() - 1) instanceof InlineShapeRun)) {
+ return;
+ }
+ java.util.Optional line = layout.firstLine(paragraph);
+ XWPFTableCell cell = table.getRow(0).getCell(last);
+ double room = usableWidthOf(cell, last, 1)
+ - paragraph.padding().left() - paragraph.padding().right()
+ - (placedColumns ? 0 : paragraph.margin().left() + paragraph.margin().right());
+ if (line.isEmpty() || !Double.isFinite(room)) {
+ return;
+ }
+ double past = line.get().width() + roomNotGivenBack(paragraph.inlineRuns()) - room;
+ if (!(past > 0)) {
+ return;
+ }
+ long more = (long) Math.ceil(past * POINT_TO_TWIP);
+ var column = table.getCTTbl().getTblGrid().getGridColArray(last);
+ Long columnTwips = writtenTwips(column.getW());
+ if (columnTwips == null) {
+ return;
+ }
+ column.setW(BigInteger.valueOf(columnTwips + more));
+ CTTcPr properties = cellProperties(cell);
+ if (properties.isSetTcW() && writtenTwips(properties.getTcW().getW()) != null) {
+ properties.getTcW().setW(BigInteger.valueOf(writtenTwips(properties.getTcW().getW()) + more));
+ }
+ CTTblPr tableProperties = table.getCTTbl().getTblPr();
+ if (tableProperties != null && tableProperties.isSetTblW() && writtenTwips(tableProperties.getTblW().getW()) != null) {
+ tableProperties.getTblW().setW(BigInteger.valueOf(writtenTwips(tableProperties.getTblW().getW()) + more));
+ }
+ }
+
+ /**
+ * The room a paragraph's shapes take past their boxes that no text beside them gives back, in
+ * points: a shape opening its line, the side of one standing against another, the end of
+ * one closing the line (see {@link #giveBackWidth}).
+ */
+ private double roomNotGivenBack(List runs) {
+ double room = 0;
+ for (int index = 0; index < runs.size(); index++) {
+ if (!(runs.get(index) instanceof InlineShapeRun shape)) {
+ continue;
+ }
+ if (index == 0 || !givesBackWidth(runs.get(index - 1))) {
+ room += DocxShapePictures.widthBeforeItsBox(shape);
+ }
+ if (index == runs.size() - 1 || !givesBackWidth(runs.get(index + 1))) {
+ room += DocxShapePictures.widthAfterItsBox(shape);
+ }
+ }
+ return room;
+ }
+
+ /** Whether a run is text that sets its letters closer for a shape beside it (giveBackWidth). */
+ private boolean givesBackWidth(InlineRun run) {
+ InlineTextRun text = textOf(run);
+ return text != null && text.text() != null && !text.text().isEmpty()
+ && !LINE_BREAK.matcher(text.text()).find();
+ }
+
/**
* Whether a layer of a stack holds only what is drawn where the page puts it, and may be
* left out of the stack's columns (see {@link DocxLayerColumns#of}): drawing, or a picture
@@ -9269,11 +9426,20 @@ private static void hideTableGrid(XWPFTable table) {
* node that contributes nothing — a wrapper that ended up with no children — would
* otherwise leave the cell with no block child at all. Word tolerates less of that than
* the schema validator notices.
+ *
+ *
A cell left with nothing in it at all — its content drawn where the page draws it, as
+ * a skill's meter beside its label — has that paragraph a hairline tall: left to Word's
+ * own line it was a line of the document's font, and {@code MidnightNavy}'s skills each
+ * stood 2.1pt taller than the page's.
*/
private void writeCellBody(XWPFTableCell cell, DocumentNode child) throws Exception {
writeCellNode(cell, child);
if (cell.getParagraphs().isEmpty()) {
- cell.addParagraph();
+ boolean empty = cell.getBodyElements().isEmpty();
+ XWPFParagraph closing = cell.addParagraph();
+ if (empty) {
+ holdToHairline(closing);
+ }
}
}
diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxShapePictures.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxShapePictures.java
index 61de050a0..b8db82264 100644
--- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxShapePictures.java
+++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxShapePictures.java
@@ -82,6 +82,36 @@ Overhang max(Overhang other) {
}
}
+ /**
+ * How far an inline shape's picture reaches past its box on the left, in points: its ink's
+ * overhang there and {@link #EDGE}.
+ *
+ * @param run the shape
+ * @return the width the picture takes in its line before the run's box
+ */
+ static double widthBeforeItsBox(InlineShapeRun run) {
+ return inkOf(run).left() + EDGE;
+ }
+
+ /**
+ * How far an inline shape's picture reaches past its box on the right, in points: its ink's
+ * overhang there and {@link #EDGE}.
+ *
+ * @param run the shape
+ * @return the width the picture takes in its line after the run's box
+ */
+ static double widthAfterItsBox(InlineShapeRun run) {
+ return inkOf(run).right() + EDGE;
+ }
+
+ private static Overhang inkOf(InlineShapeRun run) {
+ Overhang ink = Overhang.NONE;
+ for (ShapeLayer layer : run.layers()) {
+ ink = ink.max(overhang(layer, run.width(), run.height()));
+ }
+ return ink;
+ }
+
/**
* Draws an inline shape as a picture.
*
@@ -91,10 +121,7 @@ Overhang max(Overhang other) {
static Picture of(InlineShapeRun run) {
double width = run.width();
double height = run.height();
- Overhang ink = Overhang.NONE;
- for (ShapeLayer layer : run.layers()) {
- ink = ink.max(overhang(layer, width, height));
- }
+ Overhang ink = inkOf(run);
double left = ink.left() + EDGE;
double right = ink.right() + EDGE;
double top = ink.top() + EDGE;
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlinePictureTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlinePictureTest.java
index a91520622..911b5776f 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlinePictureTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxInlinePictureTest.java
@@ -41,6 +41,103 @@ class DocxInlinePictureTest {
private static final SvgIcon ICON = SvgIcon.parse("");
+ @Test
+ void theTextBesideADotGivesBackTheRoomItsPictureTakesPastItsBox() throws Exception {
+ // A dot is written as a picture a quarter point wider than its box on either side: the
+ // text before it gives back the left quarter, so the dot starts where the page starts it,
+ // and the four spaces between two dots the first's right quarter and the second's left.
+ com.demcha.compose.document.style.DocumentColor black = com.demcha.compose.document.style.DocumentColor.rgb(0, 0, 0);
+ try (XWPFDocument document = export(page -> page.addParagraph(p -> p
+ .inlineText("Level ")
+ .dot(6, black).inlineText(" ")
+ .dot(6, black)
+ .inlineText(" done")))) {
+ // Spread over the letters in whole tenths: half a point over four spaces is a tenth a
+ // letter, a quarter over five letters a tenth, and over six letters less than one.
+ assertThat(lettersAndSpacing(document.getParagraphs().get(0)))
+ .as("each run's room given back by its letters")
+ .containsExactly("Level :0", "[picture]", " :-2", "[picture]", " done:-2");
+ }
+ }
+
+ @Test
+ void aLineBreakAfterADotKeepsItsLetters() throws Exception {
+ com.demcha.compose.document.style.DocumentColor black = com.demcha.compose.document.style.DocumentColor.rgb(0, 0, 0);
+ try (XWPFDocument document = export(page -> page.addParagraph(p -> p
+ .dot(6, black).inlineText(" one\ntwo")))) {
+ assertThat(lettersAndSpacing(document.getParagraphs().get(0)))
+ .as("its letters stand on two lines; none is set closer")
+ .allMatch(piece -> piece.equals("[picture]") || piece.endsWith(":0"));
+ }
+ }
+
+ /** Each run of a paragraph as its letters and its character spacing in twips, a picture as such. */
+ private static List lettersAndSpacing(XWPFParagraph paragraph) {
+ List pieces = new java.util.ArrayList<>();
+ for (var run : paragraph.getCTP().getRList()) {
+ if (run.sizeOfDrawingArray() > 0) {
+ pieces.add("[picture]");
+ continue;
+ }
+ StringBuilder letters = new StringBuilder();
+ for (var text : run.getTArray()) {
+ letters.append(text.getStringValue());
+ }
+ long spacing = run.isSetRPr() && run.getRPr().sizeOfSpacingArray() > 0
+ ? DocxTwips.of(run.getRPr().getSpacingArray(0).getVal()) : 0;
+ pieces.add(letters + ":" + spacing);
+ }
+ return pieces;
+ }
+
+ @Test
+ void aRowsLastCellIsWidenedToTheRingEndingItsLine() throws Exception {
+ // The page draws half a ring's stroke past the cell its line ends at; Word cuts a cell's
+ // content at its edge, so the column is made that much wider.
+ com.demcha.compose.document.style.DocumentColor white = com.demcha.compose.document.style.DocumentColor.rgb(255, 255, 255);
+ com.demcha.compose.document.style.DocumentStroke ring =
+ com.demcha.compose.document.style.DocumentStroke.of(com.demcha.compose.document.style.DocumentColor.rgb(0, 0, 0), 0.8);
+ assertThat(lastColumnTwips(30.2, p -> {
+ for (int dot = 0; dot < 5; dot++) {
+ p.dot(6, white, ring);
+ }
+ })).as("five rings against each other, none of their edges given back: each a point wider")
+ .isGreaterThan(Math.round(30.2 * 20) + 5 * 20);
+ assertThat(lastColumnTwips(60, p -> {
+ p.dot(6, white, ring);
+ for (int dot = 1; dot < 5; dot++) {
+ p.inlineText(" ").dot(6, white, ring);
+ }
+ })).as("rings with room to spare, the spaces between them giving their edges back")
+ .isEqualTo(Math.round(60 * 20));
+ }
+
+ private static long lastColumnTwips(double column,
+ Consumer dots) throws Exception {
+ try (XWPFDocument document = export(page -> page.addRow(row -> row
+ .spacing(0)
+ .columns(com.demcha.compose.document.style.DocumentRowColumn.weight(1),
+ com.demcha.compose.document.style.DocumentRowColumn.fixed(column))
+ .addParagraph("Level")
+ .addParagraph(dots::accept)))) {
+ var grid = document.getTables().get(0).getCTTbl().getTblGrid();
+ return DocxTwips.of(grid.getGridColArray(1).getW());
+ }
+ }
+
+ @Test
+ void textWithNoShapeBesideItKeepsItsSpacing() throws Exception {
+ try (XWPFDocument document = export(page -> page.addParagraph(p -> p
+ .inlineText("Before ")
+ .inlineImage(DocumentImageData.fromBytes(png(20, 20)), 20, 20)
+ .inlineText(" after")))) {
+ List runs = document.getParagraphs().get(0).getRuns();
+
+ assertThat(runs.get(0).getCharacterSpacing()).as("a picture is set at its own width").isZero();
+ assertThat(runs.get(2).getCharacterSpacing()).isZero();
+ }
+ }
+
@Test
void aPictureInALineIsARunBetweenItsWords() throws Exception {
try (XWPFDocument document = export(page -> page.addParagraph(p -> p
diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxOverlayBandTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxOverlayBandTest.java
index c8db032f2..0dfd04619 100644
--- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxOverlayBandTest.java
+++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxOverlayBandTest.java
@@ -178,6 +178,78 @@ void aDrawingThatIsNotWrittenStillTakesItsRoom() throws Exception {
}
}
+ @Test
+ void aCellOfDrawingAloneHoldsAHairline() throws Exception {
+ // Nothing in the cell is written, and no space is carried into it: the paragraph a cell
+ // ends with is a hairline, not a line of the document's font beside a shorter label.
+ // MidnightNavy's skill row: held in a layer, so it can sit in its column's section.
+ com.demcha.compose.document.dsl.SectionBuilder skill = new com.demcha.compose.document.dsl.SectionBuilder();
+ skill.name("SkillHolder").addRow("SkillRow", row -> row
+ .verticalAlign(com.demcha.compose.document.node.RowVerticalAlign.CENTER)
+ .columns(com.demcha.compose.document.style.DocumentRowColumn.weight(1),
+ com.demcha.compose.document.style.DocumentRowColumn.fixed(60))
+ .addParagraph(p -> p.text("Beside"))
+ .addSection("MeterCell", cell -> cell.addLayerStack(stack -> stack.name("Meter")
+ .layer(new com.demcha.compose.document.dsl.LineBuilder().name("Track").horizontal(60)
+ .thickness(1).color(DocumentColor.rgb(120, 120, 120)).build(), LayerAlign.CENTER_LEFT, 0)
+ .position(new com.demcha.compose.document.dsl.ShapeBuilder().name("Knob").size(6, 4)
+ .fillColor(DocumentColor.rgb(0, 0, 0)).build(), 30, 0, LayerAlign.CENTER_LEFT, 1))));
+ com.demcha.compose.document.node.DocumentNode held = skill.build();
+ try (XWPFDocument document = DocxExports.withLayout(300, 500, 20, page -> page
+ .addSection("Column", column -> column.addLayerStack(stack -> stack.name("SkillLayer")
+ .layer(held, LayerAlign.TOP_LEFT, 0))))) {
+ XWPFParagraph holder = document.getTables().get(0).getRow(0).getCell(1).getParagraphs().get(0);
+ CTPPr properties = holder.getCTP().getPPr();
+
+ assertThat(holder.getRuns()).as("nothing of the meter is written in its cell").isEmpty();
+ assertThat(properties).as("the cell's paragraph is written").isNotNull();
+ assertThat(properties.getSpacing().getLineRule())
+ .isEqualTo(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.EXACT);
+ assertThat(DocxTwips.of(properties.getSpacing().getLine())).isEqualTo(2L);
+ }
+ }
+
+ @Test
+ void aShapeContainersStackedLinesStandWhereTheOutlineSetsThem() throws Exception {
+ // Two initials centred in a ring, one above the other: the space above the first and
+ // below the last is the ring's, as for a single layer.
+ DocumentSession session = GraphCompose.document().pageSize(300, 500).margin(DocumentInsets.of(20)).create();
+ session.pageFlow(page -> page.add(new com.demcha.compose.document.dsl.ShapeContainerBuilder()
+ .name("Ring").circle(76)
+ .stroke(DocumentStroke.of(DocumentColor.rgb(0, 0, 0), 1))
+ .position(new ParagraphBuilder().name("First").text("A")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(24)).build(), 0, -13, LayerAlign.CENTER)
+ .position(new ParagraphBuilder().name("Second").text("M")
+ .textStyle(DocumentTextStyle.DEFAULT.withSize(24)).build(), 0, 13, LayerAlign.CENTER)
+ .build())
+ .addParagraph(p -> p.name("Below").text("Below")));
+ try (session; XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(
+ session.export(new DocxSemanticBackend())))) {
+ PlacedNode ring = placed(session, "Ring");
+ PlacedNode first = placed(session, "First");
+ PlacedNode second = placed(session, "Second");
+ PlacedNode next = placed(session, "Below");
+ double above = (ring.placementY() + ring.placementHeight()) - (first.placementY() + first.placementHeight());
+ double below = second.placementY() - ring.placementY();
+ double gap = ring.placementY() - (next.placementY() + next.placementHeight());
+ XWPFParagraph initial = document.getParagraphs().stream()
+ .filter(paragraph -> "A".equals(paragraph.getText())).findFirst().orElseThrow();
+ XWPFParagraph after = document.getParagraphs().stream()
+ .filter(paragraph -> "Below".equals(paragraph.getText())).findFirst().orElseThrow();
+
+ assertThat(above).as("the initials stand inside the ring").isGreaterThan(5);
+ assertThat(before(initial)).as("written from where the ring sets the first initial")
+ .isCloseTo(Math.round(above * 20), org.assertj.core.data.Offset.offset(2L));
+ assertThat(before(after)).as("and the ring's foot as far under the last as the page has it")
+ .isCloseTo(Math.round((below + gap) * 20), org.assertj.core.data.Offset.offset(2L));
+ }
+ }
+
+ private static PlacedNode placed(DocumentSession session, String name) {
+ return session.layoutGraph().nodes().stream()
+ .filter(node -> name.equals(node.semanticName())).findFirst().orElseThrow();
+ }
+
@Test
void aCellOfSpaceAndDrawingKeepsItsHeight() throws Exception {
// A masthead's hairline column: padding round a vertical line. Nothing in it is