From 32ac7816e0eadf0e56ddd9c2edec4fd2be989c42 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 5 Oct 2026 07:48:43 +0100 Subject: [PATCH 1/4] fix(docx): elect Normal from text in runs and table cells, each in the style it is written in --- CHANGELOG.md | 13 ++ docs/recipes/docx-export.md | 4 +- .../libreoffice-windows-lines.tsv | 131 +++++++++--------- .../docx-fidelity/libreoffice-windows.tsv | 2 +- .../docx-fidelity/word-windows-lines.tsv | 2 +- .../semantic/docx/DocxSemanticBackend.java | 76 ++++++++-- .../semantic/docx/DocxDocumentStyleTest.java | 34 +++++ .../semantic/docx/DocxParagraphMarkTest.java | 6 +- 8 files changed, 190 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d7053f23..1aca91777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **Restyling Normal reaches a DOCX's body text when the body is in runs or tables.** Normal is + elected from the style the most characters are set in, and the count left two kinds of text + out. A paragraph made of runs counted in its paragraph's style, which its runs set aside, and + table cells did not count at all. Where most of the text sits in runs or cells — invoices, + proposals, a rota — Normal came from the few paragraphs left, and the body carried a direct + size that restyling Normal did not reach: Normal 14pt over 10pt text in `ProposalEditorial`, + 12pt over 10.5pt in `CobaltRota`. Each piece of text now counts in the style it is written + in, a table cell's in the style its cascade resolves to. Word's editing protocol finds the + body following Normal in 61 of the 62 corpus documents, against 54; `ProposalNorthline` + still elects 10pt over its 9pt body. LibreOffice sets `VioletGrid` on one page, as the PDF, + instead of two, its 37 lines past 2pt down to none; 65 lines move in all, 50 nearer, and the + 15 further are `CobaltRota`'s by 0.05–0.4pt across. In Word one line moves: + `CobaltRota`'s "quayside bar" heading, 0.36pt further across. - **Enter in a DOCX continues the text it follows.** Word gives a new paragraph its mark's formatting, and the export left the mark of most paragraphs — every one written at an exact line height — in the document's own size and face. Pressing Enter at the end of a paragraph diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index eeeafb2db..97f381eb5 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -342,7 +342,9 @@ and a header's or footer's line keep the page's measure too. ## Named styles, so the document can be restyled The export writes a styles part whose `Normal` carries the document's own body text — -the style the most characters are set in, not the one the most nodes use. Runs that only +the style the most characters are set in, not the one the most nodes use. Each piece of text +counts in the style it is written in: a run in its own, a table cell's in the style its +cascade resolves to. Runs that only restate it stay silent, so changing `Normal` in Word changes the body the way a reader expects. A run whose font, size or colour differs keeps saying so, so headings, chips and accents are unaffected. 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 1d05dd45d..053e99799 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -1663,45 +1663,46 @@ cv-violet_grid 0:53e5e7c5a884 -0.20 0.10 experience cv-violet_grid 0:5a0774260e0c -0.08 0.10 2022-present cv-violet_grid 0:204f460767ff -0.12 0.02 seniorux/uidesigner|novafin(fint cv-violet_grid 0:318bba211bdc -0.07 -0.23 sanfrancisco,ca -cv-violet_grid 0:318bba211bdc#1 8.85 -0.23 sanfrancisco,ca +cv-violet_grid 0:318bba211bdc#1 -0.15 -0.23 sanfrancisco,ca cv-violet_grid 0:3883e88c63c3 -0.08 0.05 ledend-to-enddesignforapersonalf -cv-violet_grid 0:ccc00663f089 1.70 0.05 conducteduserresearch,builtperso -cv-violet_grid 0:7d0f908c7e5f 3.48 0.05 designedwireframes,prototypes,an -cv-violet_grid 0:9b57c4a85aad 5.27 0.05 establishedandmaintainedascalabl -cv-violet_grid 0:6894eafb99c2 7.05 0.05 collaboratedwithproductmanagers, -cv-violet_grid 0:01e4455cec51 8.84 0.10 2020-2022 -cv-violet_grid 0:24d722f0cfbe 8.80 0.02 uxdesigner|brighthealth(healthte -cv-violet_grid 0:530707fb0897 8.84 0.05 redesignedpatientportalexperienc -cv-violet_grid 0:b2e27a1d9270 10.62 0.05 plannedandfacilitatedusabilityte -cv-violet_grid 0:ab80a52d70fe 12.40 0.05 partneredwithengineerstoensurefe -cv-violet_grid 0:11cbf615189b 14.19 0.05 createdreusablecomponentsandstyl -cv-violet_grid 0:7e5081a3ed48 15.98 0.10 2018-2020 -cv-violet_grid 0:73f7bbf7cd60 15.94 0.02 juniorux/uidesigner|lumendigital -cv-violet_grid 0:c6b06045332f 15.99 -0.17 austin,tx -cv-violet_grid 0:5ea8234925c5 15.98 0.05 supporteddiscoveryworkshops,user -cv-violet_grid 0:46f00c76528c 17.76 0.05 designedresponsivewebsitesandlan -cv-violet_grid 0:f3cea36a9d83 19.54 0.05 collaboratedwithcopywritersandde -cv-violet_grid 0:d94eccbb1fbe 21.31 0.10 selectedprojects -cv-violet_grid 0:45dc8bf81211 21.26 0.08 novafinmobileapp|personalfinance -cv-violet_grid 0:d398b29d3dbb 21.31 0.61 2023 -cv-violet_grid 0:e66c1ce573d3 21.17 0.08 ledux/uidesignforbudgeting,goalt -cv-violet_grid 0:0ed8829e68d0 21.17 0.08 thatinformedasimplifiednavigatio -cv-violet_grid 0:ee9bd17ebccf 21.17 0.08 22%increaseinweeklyactiveusers. -cv-violet_grid 0:28b4603d784a 21.25 0.08 brighthealthpatientportal|health -cv-violet_grid 0:1bea20e1df19 21.30 0.61 2021 -cv-violet_grid 0:999ddb26bed6 21.16 0.08 redesignedkeypatientflowsincludi -cv-violet_grid 0:073d15633c0b 21.16 0.08 systemandcomponentlibraryusedacr -cv-violet_grid 0:68bf0109c6f8 21.41 0.10 education -cv-violet_grid 0:6cb574fa10b6 21.41 0.07 languages -cv-violet_grid 0:0cf7506cf94a 21.48 0.10 bacheloroffineartsingraphicdesig -cv-violet_grid 0:00ab7a6aa5f1 21.49 0.10 californiacollegeofthearts -cv-violet_grid 0:7e2c21bb41db 21.48 0.10 sanfrancisco,ca|2014-2018 -cv-violet_grid 0:b6234d2ea0d6 20.73 0.02 english -cv-violet_grid 0:bef32d2c315a 37.47 -0.03 native -cv-violet_grid 0:bef32d2c315a#1 5.21 -0.03 native -cv-violet_grid 0:9c9b13446cd9 21.96 0.02 spanish -cv-violet_grid 0:e35242c809c8 23.19 0.02 portuguese -cv-violet_grid 0:c5d1de4964d6 23.19 -0.03 conversational +cv-violet_grid 0:ccc00663f089 -0.10 0.05 conducteduserresearch,builtperso +cv-violet_grid 0:7d0f908c7e5f -0.12 0.05 designedwireframes,prototypes,an +cv-violet_grid 0:9b57c4a85aad -0.13 0.05 establishedandmaintainedascalabl +cv-violet_grid 0:6894eafb99c2 -0.15 0.05 collaboratedwithproductmanagers, +cv-violet_grid 0:01e4455cec51 -0.16 0.10 2020-2022 +cv-violet_grid 0:24d722f0cfbe -0.20 0.02 uxdesigner|brighthealth(healthte +cv-violet_grid 0:530707fb0897 -0.16 0.05 redesignedpatientportalexperienc +cv-violet_grid 0:b2e27a1d9270 -0.18 0.05 plannedandfacilitatedusabilityte +cv-violet_grid 0:ab80a52d70fe -0.20 0.05 partneredwithengineerstoensurefe +cv-violet_grid 0:11cbf615189b -0.21 0.05 createdreusablecomponentsandstyl +cv-violet_grid 0:7e5081a3ed48 -0.22 0.10 2018-2020 +cv-violet_grid 0:73f7bbf7cd60 -0.26 0.02 juniorux/uidesigner|lumendigital +cv-violet_grid 0:c6b06045332f -0.21 -0.17 austin,tx +cv-violet_grid 0:5ea8234925c5 -0.22 0.05 supporteddiscoveryworkshops,user +cv-violet_grid 0:46f00c76528c -0.24 0.05 designedresponsivewebsitesandlan +cv-violet_grid 0:f3cea36a9d83 -0.26 0.05 collaboratedwithcopywritersandde +cv-violet_grid 0:d94eccbb1fbe -0.29 0.10 selectedprojects +cv-violet_grid 0:45dc8bf81211 -0.34 0.08 novafinmobileapp|personalfinance +cv-violet_grid 0:d398b29d3dbb -0.29 0.61 2023 +cv-violet_grid 0:e66c1ce573d3 -0.43 0.08 ledux/uidesignforbudgeting,goalt +cv-violet_grid 0:0ed8829e68d0 -0.43 0.08 thatinformedasimplifiednavigatio +cv-violet_grid 0:ee9bd17ebccf -0.43 0.08 22%increaseinweeklyactiveusers. +cv-violet_grid 0:28b4603d784a -0.35 0.08 brighthealthpatientportal|health +cv-violet_grid 0:1bea20e1df19 -0.30 0.61 2021 +cv-violet_grid 0:999ddb26bed6 -0.44 0.08 redesignedkeypatientflowsincludi +cv-violet_grid 0:073d15633c0b -0.44 0.08 systemandcomponentlibraryusedacr +cv-violet_grid 0:68bf0109c6f8 -0.19 0.10 education +cv-violet_grid 0:6cb574fa10b6 -0.19 0.07 languages +cv-violet_grid 0:0cf7506cf94a -0.12 0.10 bacheloroffineartsingraphicdesig +cv-violet_grid 0:00ab7a6aa5f1 -0.11 0.10 californiacollegeofthearts +cv-violet_grid 0:7e2c21bb41db -0.12 0.10 sanfrancisco,ca|2014-2018 +cv-violet_grid 0:b6234d2ea0d6 -1.47 0.02 english +cv-violet_grid 0:bef32d2c315a -1.48 -0.03 native +cv-violet_grid 0:bef32d2c315a#1 -1.44 -0.03 native +cv-violet_grid 0:9c9b13446cd9 -1.44 0.02 spanish +cv-violet_grid 0:e35242c809c8 -1.41 0.02 portuguese +cv-violet_grid 0:c5d1de4964d6 -1.41 -0.03 conversational +cv-violet_grid 0:1de4fede4f63 -1.16 -0.17 idesignwithempathy,iteratewithpu invoice-classic 0:c26c22b94338 0.49 0.10 graphcomposestudio invoice-classic 0:c26c22b94338#1 0.30 0.06 graphcomposestudio invoice-classic 0:2a08ce5567fb 0.35 0.10 18layoutstreet @@ -4024,25 +4025,25 @@ receipt-modern 0:403a860bfc00 0.03 0.05 invented. receipt-modern 0:7b14b8d2c5c4 -0.04 0.24 page1of1 rota-cobalt 0:33888e306262 -0.17 -0.38 quay rota-cobalt 0:f5f481423c57 -0.32 -0.38 quaysidebar -rota-cobalt 0:d457d4238a16 0.08 0.38 monday31st -rota-cobalt 0:916a90a10237 0.08 0.44 tuesday1st -rota-cobalt 0:c602d130bfde 0.08 0.42 wednesday2nd -rota-cobalt 0:74cf6babb6f6 0.08 0.38 thursday3rd -rota-cobalt 0:ae6aa379ad0c 0.08 0.28 friday4th -rota-cobalt 0:a6643f169bfd 0.08 0.72 saturday5th -rota-cobalt 0:58752a3c0543 0.08 0.28 sunday6th +rota-cobalt 0:d457d4238a16 0.08 -0.12 monday31st +rota-cobalt 0:916a90a10237 0.08 -0.16 tuesday1st +rota-cobalt 0:c602d130bfde 0.08 -0.08 wednesday2nd +rota-cobalt 0:74cf6babb6f6 0.08 -0.17 thursday3rd +rota-cobalt 0:ae6aa379ad0c 0.08 -0.17 friday4th +rota-cobalt 0:a6643f169bfd 0.08 -0.18 saturday5th +rota-cobalt 0:58752a3c0543 0.08 -0.17 sunday6th rota-cobalt 0:c4aee96677b0 -0.23 -0.24 /cleancrushedice -rota-cobalt 0:2895e01ceae0 -0.24 -0.22 machine&area -rota-cobalt 0:f4e7df025c89 -0.23 -0.15 pianist18:30/clean +rota-cobalt 0:2895e01ceae0 -0.24 -0.27 machine&area +rota-cobalt 0:f4e7df025c89 -0.23 -0.20 pianist18:30/clean rota-cobalt 0:d3acaa767810 -0.24 -0.20 crushedicemachine& rota-cobalt 0:4a91ee5f0106 -0.25 -0.29 area -rota-cobalt 0:b913ee5e077a -0.23 -0.02 privatedinner44pax/ -rota-cobalt 0:d89987d22df9 -0.24 -0.16 soulgf/pianist18:30 -rota-cobalt 0:fb980282ae0a -0.23 -0.12 pianist18:30/partner -rota-cobalt 0:3c1fc038017e -0.24 -0.21 meeting3:30pm -rota-cobalt 0:cc96b22615e5 0.07 -0.25 pianist19:00 -rota-cobalt 0:bc14a4ce6407 -0.08 1.11 statuslegend -rota-cobalt 0:1f58b9145b24 -0.02 0.13 request +rota-cobalt 0:b913ee5e077a -0.23 -0.22 privatedinner44pax/ +rota-cobalt 0:d89987d22df9 -0.24 -0.21 soulgf/pianist18:30 +rota-cobalt 0:fb980282ae0a -0.23 -0.22 pianist18:30/partner +rota-cobalt 0:3c1fc038017e -0.24 -0.26 meeting3:30pm +rota-cobalt 0:cc96b22615e5 0.07 -0.30 pianist19:00 +rota-cobalt 0:bc14a4ce6407 -0.08 0.36 statuslegend +rota-cobalt 0:1f58b9145b24 -0.02 -0.02 request rota-cobalt 0:b4dc66dde806 -0.02 -0.17 off rota-cobalt 0:b4dc66dde806#1 -0.01 -0.40 off rota-cobalt 0:b4dc66dde806#2 -0.01 -0.49 off @@ -4080,11 +4081,11 @@ rota-cobalt 0:cdc2def9aec4#12 0.84 -0.50 hol rota-cobalt 0:cdc2def9aec4#13 0.85 -0.53 hol rota-cobalt 0:cdc2def9aec4#14 0.85 -0.48 hol rota-cobalt 0:cdc2def9aec4#15 0.85 -0.53 hol -rota-cobalt 0:08c4242b5f58 -0.02 0.03 stock -rota-cobalt 0:f8f8922da592 -0.02 -0.04 standby -rota-cobalt 0:c2fb788c7dee -0.02 0.25 training +rota-cobalt 0:08c4242b5f58 -0.02 -0.12 stock +rota-cobalt 0:f8f8922da592 -0.02 -0.09 standby +rota-cobalt 0:c2fb788c7dee -0.02 -0.05 training rota-cobalt 0:a18603086e5b -0.02 -0.04 support -rota-cobalt 0:17ae69cc7e46 -0.04 -0.23 covers +rota-cobalt 0:17ae69cc7e46 -0.04 -0.38 covers rota-cobalt 0:412a5c513322 -0.03 -0.21 l104/d50 rota-cobalt 0:304560bf3205 -0.03 -0.15 l33/d57 rota-cobalt 0:ca6ce47ada53 -0.03 -0.19 l32/d76 @@ -4093,7 +4094,7 @@ rota-cobalt 0:7062fafe1dab -0.03 -0.25 l65/d112 rota-cobalt 0:778cb57b62fa -0.03 -0.25 l73/d112 rota-cobalt 0:1b4471920f01 -0.03 -0.18 l72/d83 rota-cobalt 0:288965a1f2c8 -0.13 -0.37 management -rota-cobalt 0:6a61eb7383e8 -0.02 0.01 rosa +rota-cobalt 0:6a61eb7383e8 -0.02 -0.09 rosa rota-cobalt 0:3bb0f0bb5552 -0.01 -0.64 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#1 0.15 -0.68 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#2 0.15 -0.67 09:00-18:00 @@ -4121,7 +4122,7 @@ rota-cobalt 0:e669544e9895 -0.80 0.01 08:00-16:00 rota-cobalt 0:e669544e9895#1 -0.02 0.06 08:00-16:00 rota-cobalt 0:19c30663e8ef -0.74 0.81 16:00-21:00 rota-cobalt 0:19c30663e8ef#1 0.05 0.86 16:00-21:00 -rota-cobalt 0:c9affe7f7587 0.09 0.13 tomas +rota-cobalt 0:c9affe7f7587 0.09 -0.02 tomas rota-cobalt 0:6d43df86fe4b 0.10 -0.64 16:00-00:00 rota-cobalt 0:6d43df86fe4b#1 0.10 -0.59 16:00-00:00 rota-cobalt 0:6d43df86fe4b#2 0.15 -0.63 16:00-00:00 @@ -4164,12 +4165,12 @@ rota-cobalt 0:953cb9ba8128 0.55 0.00 mirela rota-cobalt 0:c1298a6e85c2 0.56 -0.63 12:00-18:00 rota-cobalt 0:c1298a6e85c2#1 0.67 -0.63 12:00-18:00 rota-cobalt 0:86846e84f9bf 0.66 -0.07 sofia -rota-cobalt 0:effcc54ba75f 0.66 -0.03 karol +rota-cobalt 0:effcc54ba75f 0.66 -0.08 karol rota-cobalt 0:5c1e134d2d34 0.77 -0.01 henrik -rota-cobalt 0:6326e0e8cfda 0.88 0.02 lucia +rota-cobalt 0:6326e0e8cfda 0.88 -0.08 lucia rota-cobalt 0:c235e502637c 0.81 -0.37 barbacks rota-cobalt 0:bd558de5f794 0.83 -0.06 oskar -rota-cobalt 0:4941d6993ee9 0.83 0.09 teodor -rota-cobalt 0:1d3d3d6a07bd 0.84 -0.02 vesna +rota-cobalt 0:4941d6993ee9 0.83 -0.01 teodor +rota-cobalt 0:1d3d3d6a07bd 0.84 -0.07 vesna rota-cobalt 0:59f2ff7c7c70 0.03 0.08 timesshownin24-hourformat.shiftc -rota-cobalt 0:594c41ec0506 0.03 -3.15 quaysidebar|31aug-6sep2026 +rota-cobalt 0:594c41ec0506 0.03 -3.55 quaysidebar|31aug-6sep2026 diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv index da7ee94ec..2b4a561b0 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv @@ -25,7 +25,7 @@ cv-slate_orange 1 1 87 86 0.42 1.02 0 cv-teal_pulse 1 1 57 57 0.22 0.64 0 cv-terracotta_rail 1 1 102 102 0.44 0.94 0 cv-timeline_minimal 2 2 89 86 0.45 0.50 0 -cv-violet_grid 1 2 80 77 0.36 21.41 37 +cv-violet_grid 1 1 80 78 0.22 0.44 0 invoice-classic 1 1 56 56 0.17 0.32 0 invoice-classic_long 2 2 166 166 0.22 0.27 0 invoice-consulting 1 1 92 92 0.67 1.01 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 600da53a7..946759b79 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -4279,4 +4279,4 @@ rota-cobalt 0:bd558de5f794 0.87 0.22 oskar rota-cobalt 0:4941d6993ee9 0.88 0.29 teodor rota-cobalt 0:1d3d3d6a07bd 0.90 0.32 vesna rota-cobalt 0:59f2ff7c7c70 0.08 -0.06 timesshownin24-hourformat.shiftc -rota-cobalt 0:594c41ec0506 0.08 -3.26 quaysidebar|31aug-6sep2026 +rota-cobalt 0:594c41ec0506 0.08 -3.62 quaysidebar|31aug-6sep2026 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 748a5e5ba..cd0634bdc 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 @@ -4352,21 +4352,81 @@ private static DocumentTextStyle dominantTextStyle(DocumentGraph graph) { .orElse(null); } + /** + * Weighs each piece of text by the style it is written in: a run in its own style, a + * paragraph's or list item's plain text in its node's, a table cell's in the style its + * cascade resolves to, and a composed cell's node as any other node. + * + *

A paragraph made of runs was weighed in its paragraph's style, which its runs set + * aside, and table cells were not weighed at all. Where most of a document's text is in + * runs or cells — invoices, proposals, a rota — Normal was elected from the few paragraphs + * left, and the body text carried a direct size that restyling Normal did not reach: Word's + * editing protocol found the body following Normal in none of {@code ProposalEditorial}'s or + * {@code CobaltRota}'s paragraphs, Normal 14pt or 12pt over body text of 10pt and 10.5pt.

+ */ private static void weighTextStyles(DocumentNode node, java.util.Map weights, java.util.Map byKey) { - if (node instanceof ParagraphNode paragraph && paragraph.textStyle() != null) { - weigh(paragraph.textStyle(), textWeight(paragraph.text()), weights, byKey); - } else if (node instanceof com.demcha.compose.document.node.ListNode list - && list.textStyle() != null) { - long weight = list.items().stream().mapToLong(DocxSemanticBackend::textWeight).sum(); - weigh(list.textStyle(), weight, weights, byKey); + if (node instanceof ParagraphNode paragraph) { + if (!paragraph.inlineRuns().isEmpty()) { + weighRuns(paragraph.inlineRuns(), paragraph.textStyle(), weights, byKey); + } else if (paragraph.textStyle() != null) { + weigh(paragraph.textStyle(), textWeight(paragraph.text()), weights, byKey); + } + } else if (node instanceof com.demcha.compose.document.node.ListNode list) { + if (!list.nestedItems().isEmpty()) { + weighItems(list.nestedItems(), list.textStyle(), weights, byKey); + } else if (list.textStyle() != null) { + long weight = list.items().stream().mapToLong(DocxSemanticBackend::textWeight).sum(); + weigh(list.textStyle(), weight, weights, byKey); + } + } else if (node instanceof TableNode table) { + for (List row : TableGrid.resolve(table)) { + for (TableGrid.Placement placement : row) { + DocumentTableCell cell = placement.cell(); + if (cell.hasComposedContent()) { + weighTextStyles(cell.content(), weights, byKey); + } else { + long weight = cell.lines().stream().mapToLong(DocxSemanticBackend::textWeight).sum(); + weigh(resolveCellTextStyle(table, placement), weight, weights, byKey); + } + } + } } for (DocumentNode child : node.children()) { weighTextStyles(child, weights, byKey); } } + /** Weighs a list's items, a rich one by its runs and a plain one by its label in the list's style. */ + private static void weighItems(List items, + DocumentTextStyle style, + java.util.Map weights, + java.util.Map byKey) { + for (com.demcha.compose.document.node.ListItem item : items) { + if (item.isRich()) { + weighRuns(item.runs(), style, weights, byKey); + } else if (style != null) { + weigh(style, textWeight(item.label()), weights, byKey); + } + weighItems(item.children(), style, weights, byKey); + } + } + + /** Weighs text runs, each in its own style or, where it has none, in the style it falls back to. */ + private static void weighRuns(List runs, + DocumentTextStyle fallback, + java.util.Map weights, + java.util.Map byKey) { + for (InlineRun run : runs) { + InlineTextRun text = textOf(run); + DocumentTextStyle style = text == null ? null : text.textStyle() != null ? text.textStyle() : fallback; + if (style != null) { + weigh(style, textWeight(text.text()), weights, byKey); + } + } + } + private static void weigh(DocumentTextStyle style, long weight, java.util.Map weights, @@ -9186,7 +9246,7 @@ private boolean resolveCellDirection(TableNode node, TableGrid.Placement placeme *

The same order the layout pipeline merges in: the table's default, then the * column's, then the row's, then the cell's own.

*/ - private DocumentTextStyle resolveCellTextStyle(TableNode node, TableGrid.Placement placement) { + private static DocumentTextStyle resolveCellTextStyle(TableNode node, TableGrid.Placement placement) { DocumentTextStyle authored = resolveCellValue(node, placement, DocumentTableStyle::textStyle); return authored != null ? authored : DEFAULT_CELL_FACE; } @@ -9231,7 +9291,7 @@ private static DocumentTextDecoration documentDecorationOf(TextDecoration decora * then the row's, then the cell's own — applied per field rather than per style object, so * a table-wide border survives a row that only overrides the fill.

*/ - private T resolveCellValue(TableNode node, TableGrid.Placement placement, + private static T resolveCellValue(TableNode node, TableGrid.Placement placement, Function field) { T resolved = null; for (DocumentTableStyle candidate : List.of( diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java index 4af1c3ba6..7e3d1db21 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java @@ -96,6 +96,40 @@ void aRunThatDiffersShouldKeepSayingSo() throws Exception { } } + @Test + void textWrittenInRunsShouldWeighInTheRunsStyleNotTheParagraphs() throws Exception { + // A paragraph made of runs leaves its own style unused: weighed in it, the body text + // of a proposal elected the engine's 14pt default and every body run carried 10.5pt. + try (XWPFDocument document = exported(page -> { + page.addParagraph(p -> p.text("Heading").textStyle(HEADING)); + page.addParagraph(p -> p.inlineText(LONG_BODY, BODY)); + })) { + CTRPr defaults = document.getStyles().getCtStyles().getDocDefaults() + .getRPrDefault().getRPr(); + assertThat(defaults.getSzArray(0).getVal().toString()).as("the runs' 10.5pt").isEqualTo("21"); + } + } + + @Test + void textInTableCellsShouldWeighInTheStyleTheirCellsResolveTo() throws Exception { + // An invoice's text is mostly in its tables: unweighed, Normal was elected from the few + // paragraphs around them. + try (XWPFDocument document = exported(page -> { + page.addParagraph(p -> p.text("Invoice").textStyle(HEADING)); + page.addTable(t -> t + .columns(com.demcha.compose.document.table.DocumentTableColumn.auto(), + com.demcha.compose.document.table.DocumentTableColumn.auto()) + .defaultCellStyle(com.demcha.compose.document.table.DocumentTableStyle.builder() + .textStyle(BODY).build()) + .row("Consulting, discovery and delivery", "1,200.00") + .row("Platform licence for the quarter", "3,400.00")); + })) { + CTRPr defaults = document.getStyles().getCtStyles().getDocDefaults() + .getRPrDefault().getRPr(); + assertThat(defaults.getSzArray(0).getVal().toString()).as("the cells' 10.5pt").isEqualTo("21"); + } + } + @Test void theDefaultShouldBeChosenByCharactersNotByParagraphCount() throws Exception { // Four short headings against one long body paragraph. Counting paragraphs elects diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java index 2294ef8b6..e5ddd6883 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java @@ -82,7 +82,9 @@ void aRichListItemsMarkCarriesItsLastRunsStyleNotTheLists() throws Exception { void aHairlineOfTextInACellIsNotAsTallAsBodyText() throws Exception { // A heading rule drawn as a filled cell holding half-point text: with the mark at the // document's size, the cell came out a line of body text tall. - try (XWPFDocument document = export(page -> page.addTable(t -> t + try (XWPFDocument document = export(page -> page + .addParagraph(p -> p.text("Body text sets the document's size")) + .addTable(t -> t .columns(DocumentTableColumn.fixed(120)) .rowCells(DocumentTableCell.node(new ParagraphBuilder().name("Rule").text(" ").lineSpacing(0) .textStyle(DocumentTextStyle.DEFAULT.withSize(0.5)).build()) @@ -209,7 +211,7 @@ void aMarkIsSizedAsTheTextThatEndsTheLineNotAsTheParagraphsUnusedStyle() throws // A contact block: an icon and runs styled on their own, the paragraph's style left at its // default, on two lines. A mark in that default grew every such line in both editors. try (XWPFDocument document = export(page -> page - .addParagraph(p -> p.text("Body text sets the document's size")) + .addParagraph(p -> p.text("Body text sets the document's size. ".repeat(12))) .addParagraph(p -> p .inlineImage(com.demcha.compose.document.image.DocumentImageData.fromBytes(png()), 14, 14) .inlineText(" billing@example.com".repeat(12), DocumentTextStyle.DEFAULT.withSize(6))))) { From b2c78d81e4fcbda0bedc1b714c1235c2b459ef7e Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 5 Oct 2026 08:00:26 +0100 Subject: [PATCH 2/4] fix(docx): elect Normal by face and size before colour, weigh no empty cell, and style a band's mark --- .../semantic/docx/DocxSemanticBackend.java | 26 ++++++++++-- .../semantic/docx/DocxDocumentStyleTest.java | 40 +++++++++++++++++++ .../semantic/docx/DocxRunStyleTest.java | 2 +- 3 files changed, 64 insertions(+), 4 deletions(-) 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 cd0634bdc..3b3aa734c 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 @@ -1361,6 +1361,9 @@ private void writeBand(XWPFHeaderFooter part, DocumentHeaderFooter band, boolean appendTab(para, style); appendBandSlot(para, band.getRightText(), style, numbers); } + // Its mark in its text's style, as a body paragraph's, so Enter in the band continues it + // rather than Normal. + styleTheMark(para, style); if (separated) { CTPBdr borders = properties.isSetPBdr() ? properties.getPBdr() : properties.addNewPBdr(); CTBorder edge = band.getZone() == DocumentHeaderFooterZone.HEADER @@ -4346,8 +4349,16 @@ private static DocumentTextStyle dominantTextStyle(DocumentGraph graph) { for (DocumentNode root : graph.roots()) { weighTextStyles(root, weights, byKey); } - return weights.entrySet().stream() + // Face and size first, then colour among the styles of that face and size: body text set + // in two colours is one body still, and weighed apart its halves lost to a third style. + // ProposalNorthline's 9pt text, in its body colour and a muted one, lost to its 10pt. + java.util.Map byFaceAndSize = new java.util.HashMap<>(); + weights.forEach((key, weight) -> byFaceAndSize.merge(key.withoutColour(), weight, Long::sum)); + return byFaceAndSize.entrySet().stream() .max(java.util.Map.Entry.comparingByValue()) + .flatMap(face -> weights.entrySet().stream() + .filter(entry -> entry.getKey().withoutColour().equals(face.getKey())) + .max(java.util.Map.Entry.comparingByValue())) .map(entry -> byKey.get(entry.getKey())) .orElse(null); } @@ -4387,8 +4398,12 @@ private static void weighTextStyles(DocumentNode node, if (cell.hasComposedContent()) { weighTextStyles(cell.content(), weights, byKey); } else { - long weight = cell.lines().stream().mapToLong(DocxSemanticBackend::textWeight).sum(); - weigh(resolveCellTextStyle(table, placement), weight, weights, byKey); + // By its letters alone: a grid of empty cells holds no body text, and + // counted at one each would outweigh the document's. + long weight = cell.lines().stream().mapToLong(String::length).sum(); + if (weight > 0) { + weigh(resolveCellTextStyle(table, placement), weight, weights, byKey); + } } } } @@ -4465,6 +4480,11 @@ static StyleKey of(DocumentTextStyle style) { Math.round(style.size() * HALF_POINTS_PER_POINT), style.color() == null ? null : style.color().color().getRGB()); } + + /** The same face and size, any colour. */ + StyleKey withoutColour() { + return new StyleKey(fontName, halfPoints, null); + } } /** At least one, so a style used only by empty text still counts as used. */ diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java index 7e3d1db21..4d51cebb5 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxDocumentStyleTest.java @@ -130,6 +130,46 @@ void textInTableCellsShouldWeighInTheStyleTheirCellsResolveTo() throws Exception } } + @Test + void emptyTableCellsShouldWeighNothing() throws Exception { + // A grid of empty cells in the engine's default face holds no body text; counted at a + // character each, forty of them outweighed the body's paragraph. + try (XWPFDocument document = exported(page -> { + page.addParagraph(p -> p.text("Body text, short.").textStyle(BODY)); + page.addTable(t -> { + t.columns(com.demcha.compose.document.table.DocumentTableColumn.auto(), + com.demcha.compose.document.table.DocumentTableColumn.auto()); + for (int i = 0; i < 20; i++) { + t.row("", ""); + } + }); + })) { + CTRPr defaults = document.getStyles().getCtStyles().getDocDefaults() + .getRPrDefault().getRPr(); + assertThat(defaults.getSzArray(0).getVal().toString()).as("the body's 10.5pt").isEqualTo("21"); + } + } + + @Test + void bodyTextInTwoColoursShouldWeighAsOneFaceAndSize() throws Exception { + // ProposalNorthline sets its 9pt text in its body colour and a muted one: weighed apart, + // each half lost to the 10pt the rest of the text is set in. + DocumentTextStyle muted = DocumentTextStyle.builder() + .fontName(FontName.HELVETICA).size(10.5).color(DocumentColor.rgb(110, 110, 120)).build(); + DocumentTextStyle larger = DocumentTextStyle.builder() + .fontName(FontName.HELVETICA).size(12).color(DocumentColor.rgb(24, 28, 38)).build(); + try (XWPFDocument document = exported(page -> { + page.addParagraph(p -> p.text("x".repeat(60)).textStyle(BODY)); + page.addParagraph(p -> p.text("y".repeat(60)).textStyle(muted)); + page.addParagraph(p -> p.text("z".repeat(80)).textStyle(larger)); + })) { + CTRPr defaults = document.getStyles().getCtStyles().getDocDefaults() + .getRPrDefault().getRPr(); + assertThat(defaults.getSzArray(0).getVal().toString()) + .as("120 characters at 10.5pt over 80 at 12pt").isEqualTo("21"); + } + } + @Test void theDefaultShouldBeChosenByCharactersNotByParagraphCount() throws Exception { // Four short headings against one long body paragraph. Counting paragraphs elects diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java index 4365f9ad1..392dee556 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java @@ -102,7 +102,7 @@ void aCodeChipCarriesItsOwnGlyphStyleRatherThanTheParagraphs() throws Exception // means the chip's monospace face now reaches Word. Its background does not. List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph .textStyle(BASE) - .inlineText("call ") + .inlineText("to render the document, call ") .inlineCode("run()"))); assertThat(runs).hasSize(2); From 65d384eb512ce293e63dc8cfc5146f4deef2f44d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 5 Oct 2026 08:05:07 +0100 Subject: [PATCH 3/4] docs(docx): record Normal reaching the body in all 62 corpus documents --- CHANGELOG.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aca91777..31c3627b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,15 @@ follow semantic versioning; release dates are ISO 8601. proposals, a rota — Normal came from the few paragraphs left, and the body carried a direct size that restyling Normal did not reach: Normal 14pt over 10pt text in `ProposalEditorial`, 12pt over 10.5pt in `CobaltRota`. Each piece of text now counts in the style it is written - in, a table cell's in the style its cascade resolves to. Word's editing protocol finds the - body following Normal in 61 of the 62 corpus documents, against 54; `ProposalNorthline` - still elects 10pt over its 9pt body. LibreOffice sets `VioletGrid` on one page, as the PDF, - instead of two, its 37 lines past 2pt down to none; 65 lines move in all, 50 nearer, and the - 15 further are `CobaltRota`'s by 0.05–0.4pt across. In Word one line moves: - `CobaltRota`'s "quayside bar" heading, 0.36pt further across. + in, a table cell's in the style its cascade resolves to; an empty cell counts for nothing. + The face and size with the most text are chosen before the colour, so body text set in two + colours is not outvoted: `ProposalNorthline`'s 9pt text, in its body colour and a muted one, + lost to its 10pt. A header's or footer's paragraph mark is now in its text's style too. Word's + editing protocol finds the body following Normal in all 62 corpus documents, against 54. + LibreOffice sets `VioletGrid` on one page, as the PDF, instead of two, its 37 lines past 2pt + down to none; 65 lines move in all, 50 nearer, and the 15 further are `CobaltRota`'s by + 0.05–0.4pt across. In Word one line moves: the right-hand slot of `CobaltRota`'s footer, + "quayside bar | 31 aug – 6 sep 2026", 0.36pt further across. - **Enter in a DOCX continues the text it follows.** Word gives a new paragraph its mark's formatting, and the export left the mark of most paragraphs — every one written at an exact line height — in the document's own size and face. Pressing Enter at the end of a paragraph From 5e7404a9f7daf94f6974ed5f94d39db886a0dde8 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 5 Oct 2026 08:45:59 +0100 Subject: [PATCH 4/4] test(docx): take the Linux LibreOffice baseline from CI for Normal elected from runs and cells --- .../docx-fidelity/libreoffice-linux-lines.tsv | 133 +++++++++--------- .../docx-fidelity/libreoffice-linux.tsv | 2 +- 2 files changed, 68 insertions(+), 67 deletions(-) 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 a8ef2fa80..f2f425791 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -1764,45 +1764,46 @@ cv-violet_grid 0:53e5e7c5a884 -0.20 0.10 experience cv-violet_grid 0:5a0774260e0c -0.08 0.10 2022-present cv-violet_grid 0:204f460767ff -0.12 0.02 seniorux/uidesigner|novafin(fint cv-violet_grid 0:318bba211bdc -0.07 -0.28 sanfrancisco,ca -cv-violet_grid 0:318bba211bdc#1 8.85 -0.28 sanfrancisco,ca +cv-violet_grid 0:318bba211bdc#1 -0.15 -0.28 sanfrancisco,ca cv-violet_grid 0:3883e88c63c3 -0.08 0.05 ledend-to-enddesignforapersonalf -cv-violet_grid 0:ccc00663f089 1.70 0.05 conducteduserresearch,builtperso -cv-violet_grid 0:7d0f908c7e5f 3.48 0.05 designedwireframes,prototypes,an -cv-violet_grid 0:9b57c4a85aad 5.27 0.05 establishedandmaintainedascalabl -cv-violet_grid 0:6894eafb99c2 7.05 0.05 collaboratedwithproductmanagers, -cv-violet_grid 0:01e4455cec51 8.84 0.10 2020-2022 -cv-violet_grid 0:24d722f0cfbe 8.80 0.02 uxdesigner|brighthealth(healthte -cv-violet_grid 0:530707fb0897 8.84 0.05 redesignedpatientportalexperienc -cv-violet_grid 0:b2e27a1d9270 10.62 0.05 plannedandfacilitatedusabilityte -cv-violet_grid 0:ab80a52d70fe 12.40 0.05 partneredwithengineerstoensurefe -cv-violet_grid 0:11cbf615189b 14.19 0.05 createdreusablecomponentsandstyl -cv-violet_grid 0:7e5081a3ed48 15.98 0.10 2018-2020 -cv-violet_grid 0:73f7bbf7cd60 15.94 0.02 juniorux/uidesigner|lumendigital -cv-violet_grid 0:c6b06045332f 15.99 -0.17 austin,tx -cv-violet_grid 0:5ea8234925c5 15.98 0.05 supporteddiscoveryworkshops,user -cv-violet_grid 0:46f00c76528c 17.76 0.05 designedresponsivewebsitesandlan -cv-violet_grid 0:f3cea36a9d83 19.54 0.05 collaboratedwithcopywritersandde -cv-violet_grid 0:d94eccbb1fbe 21.31 0.10 selectedprojects -cv-violet_grid 0:45dc8bf81211 21.26 0.08 novafinmobileapp|personalfinance -cv-violet_grid 0:d398b29d3dbb 21.31 0.61 2023 -cv-violet_grid 0:e66c1ce573d3 21.17 0.08 ledux/uidesignforbudgeting,goalt -cv-violet_grid 0:0ed8829e68d0 21.17 0.08 thatinformedasimplifiednavigatio -cv-violet_grid 0:ee9bd17ebccf 21.17 0.08 22%increaseinweeklyactiveusers. -cv-violet_grid 0:28b4603d784a 21.25 0.08 brighthealthpatientportal|health -cv-violet_grid 0:1bea20e1df19 21.30 0.61 2021 -cv-violet_grid 0:999ddb26bed6 21.16 0.08 redesignedkeypatientflowsincludi -cv-violet_grid 0:073d15633c0b 21.16 0.08 systemandcomponentlibraryusedacr -cv-violet_grid 0:68bf0109c6f8 21.41 0.10 education -cv-violet_grid 0:6cb574fa10b6 21.41 0.07 languages -cv-violet_grid 0:0cf7506cf94a 21.48 0.10 bacheloroffineartsingraphicdesig -cv-violet_grid 0:00ab7a6aa5f1 21.49 0.10 californiacollegeofthearts -cv-violet_grid 0:7e2c21bb41db 21.48 0.10 sanfrancisco,ca|2014-2018 -cv-violet_grid 0:b6234d2ea0d6 20.73 0.02 english -cv-violet_grid 0:bef32d2c315a 37.47 -0.03 native -cv-violet_grid 0:bef32d2c315a#1 5.21 -0.03 native -cv-violet_grid 0:9c9b13446cd9 21.96 0.02 spanish -cv-violet_grid 0:e35242c809c8 23.19 0.02 portuguese -cv-violet_grid 0:c5d1de4964d6 23.19 -0.03 conversational +cv-violet_grid 0:ccc00663f089 -0.10 0.05 conducteduserresearch,builtperso +cv-violet_grid 0:7d0f908c7e5f -0.12 0.05 designedwireframes,prototypes,an +cv-violet_grid 0:9b57c4a85aad -0.13 0.05 establishedandmaintainedascalabl +cv-violet_grid 0:6894eafb99c2 -0.15 0.05 collaboratedwithproductmanagers, +cv-violet_grid 0:01e4455cec51 -0.16 0.10 2020-2022 +cv-violet_grid 0:24d722f0cfbe -0.20 0.02 uxdesigner|brighthealth(healthte +cv-violet_grid 0:530707fb0897 -0.16 0.05 redesignedpatientportalexperienc +cv-violet_grid 0:b2e27a1d9270 -0.18 0.05 plannedandfacilitatedusabilityte +cv-violet_grid 0:ab80a52d70fe -0.20 0.05 partneredwithengineerstoensurefe +cv-violet_grid 0:11cbf615189b -0.21 0.05 createdreusablecomponentsandstyl +cv-violet_grid 0:7e5081a3ed48 -0.22 0.10 2018-2020 +cv-violet_grid 0:73f7bbf7cd60 -0.26 0.02 juniorux/uidesigner|lumendigital +cv-violet_grid 0:c6b06045332f -0.21 -0.17 austin,tx +cv-violet_grid 0:5ea8234925c5 -0.22 0.05 supporteddiscoveryworkshops,user +cv-violet_grid 0:46f00c76528c -0.24 0.05 designedresponsivewebsitesandlan +cv-violet_grid 0:f3cea36a9d83 -0.26 0.05 collaboratedwithcopywritersandde +cv-violet_grid 0:d94eccbb1fbe -0.29 0.10 selectedprojects +cv-violet_grid 0:45dc8bf81211 -0.34 0.08 novafinmobileapp|personalfinance +cv-violet_grid 0:d398b29d3dbb -0.29 0.61 2023 +cv-violet_grid 0:e66c1ce573d3 -0.43 0.08 ledux/uidesignforbudgeting,goalt +cv-violet_grid 0:0ed8829e68d0 -0.43 0.08 thatinformedasimplifiednavigatio +cv-violet_grid 0:ee9bd17ebccf -0.43 0.08 22%increaseinweeklyactiveusers. +cv-violet_grid 0:28b4603d784a -0.35 0.08 brighthealthpatientportal|health +cv-violet_grid 0:1bea20e1df19 -0.30 0.61 2021 +cv-violet_grid 0:999ddb26bed6 -0.44 0.08 redesignedkeypatientflowsincludi +cv-violet_grid 0:073d15633c0b -0.44 0.08 systemandcomponentlibraryusedacr +cv-violet_grid 0:68bf0109c6f8 -0.19 0.10 education +cv-violet_grid 0:6cb574fa10b6 -0.19 0.07 languages +cv-violet_grid 0:0cf7506cf94a -0.12 0.10 bacheloroffineartsingraphicdesig +cv-violet_grid 0:00ab7a6aa5f1 -0.11 0.10 californiacollegeofthearts +cv-violet_grid 0:7e2c21bb41db -0.12 0.10 sanfrancisco,ca|2014-2018 +cv-violet_grid 0:b6234d2ea0d6 -1.47 0.02 english +cv-violet_grid 0:bef32d2c315a -1.48 -0.03 native +cv-violet_grid 0:bef32d2c315a#1 -1.44 -0.03 native +cv-violet_grid 0:9c9b13446cd9 -1.44 0.02 spanish +cv-violet_grid 0:e35242c809c8 -1.41 0.02 portuguese +cv-violet_grid 0:c5d1de4964d6 -1.41 -0.03 conversational +cv-violet_grid 0:1de4fede4f63 -1.16 22.41 idesignwithempathy,iteratewithpu invoice-classic 0:c26c22b94338 0.49 0.10 graphcomposestudio invoice-classic 0:c26c22b94338#1 0.25 0.06 graphcomposestudio invoice-classic 0:2a08ce5567fb 0.35 0.10 18layoutstreet @@ -4129,25 +4130,25 @@ receipt-modern 0:403a860bfc00 0.03 0.05 invented. receipt-modern 0:7b14b8d2c5c4 -0.04 0.14 page1of1 rota-cobalt 0:33888e306262 0.18 0.52 quay rota-cobalt 0:f5f481423c57 -0.32 0.52 quaysidebar -rota-cobalt 0:d457d4238a16 0.08 1.23 monday31st -rota-cobalt 0:916a90a10237 0.08 1.29 tuesday1st -rota-cobalt 0:c602d130bfde 0.08 1.27 wednesday2nd -rota-cobalt 0:74cf6babb6f6 0.08 1.23 thursday3rd -rota-cobalt 0:ae6aa379ad0c 0.08 1.18 friday4th -rota-cobalt 0:a6643f169bfd 0.08 1.62 saturday5th -rota-cobalt 0:58752a3c0543 0.08 1.18 sunday6th +rota-cobalt 0:d457d4238a16 0.08 0.73 monday31st +rota-cobalt 0:916a90a10237 0.08 0.74 tuesday1st +rota-cobalt 0:c602d130bfde 0.08 0.77 wednesday2nd +rota-cobalt 0:74cf6babb6f6 0.08 0.73 thursday3rd +rota-cobalt 0:ae6aa379ad0c 0.08 0.68 friday4th +rota-cobalt 0:a6643f169bfd 0.08 0.72 saturday5th +rota-cobalt 0:58752a3c0543 0.08 0.73 sunday6th rota-cobalt 0:c4aee96677b0 -0.23 0.66 /cleancrushedice -rota-cobalt 0:2895e01ceae0 -0.24 0.68 machine&area -rota-cobalt 0:f4e7df025c89 -0.23 0.75 pianist18:30/clean +rota-cobalt 0:2895e01ceae0 -0.24 0.63 machine&area +rota-cobalt 0:f4e7df025c89 -0.23 0.70 pianist18:30/clean rota-cobalt 0:d3acaa767810 -0.24 0.70 crushedicemachine& -rota-cobalt 0:4a91ee5f0106 -0.25 0.61 area -rota-cobalt 0:b913ee5e077a -0.23 0.83 privatedinner44pax/ -rota-cobalt 0:d89987d22df9 -0.24 0.74 soulgf/pianist18:30 -rota-cobalt 0:fb980282ae0a -0.23 0.78 pianist18:30/partner +rota-cobalt 0:4a91ee5f0106 -0.25 0.56 area +rota-cobalt 0:b913ee5e077a -0.23 0.68 privatedinner44pax/ +rota-cobalt 0:d89987d22df9 -0.24 0.69 soulgf/pianist18:30 +rota-cobalt 0:fb980282ae0a -0.23 0.63 pianist18:30/partner rota-cobalt 0:3c1fc038017e -0.24 0.64 meeting3:30pm -rota-cobalt 0:cc96b22615e5 0.07 0.65 pianist19:00 -rota-cobalt 0:bc14a4ce6407 -0.13 2.01 statuslegend -rota-cobalt 0:1f58b9145b24 -0.07 1.03 request +rota-cobalt 0:cc96b22615e5 0.07 0.60 pianist19:00 +rota-cobalt 0:bc14a4ce6407 -0.13 1.26 statuslegend +rota-cobalt 0:1f58b9145b24 -0.07 0.88 request rota-cobalt 0:b4dc66dde806 -0.07 0.73 off rota-cobalt 0:b4dc66dde806#1 -0.06 0.45 off rota-cobalt 0:b4dc66dde806#2 -0.06 0.36 off @@ -4185,11 +4186,11 @@ rota-cobalt 0:cdc2def9aec4#12 0.79 0.40 hol rota-cobalt 0:cdc2def9aec4#13 0.80 0.37 hol rota-cobalt 0:cdc2def9aec4#14 0.80 0.42 hol rota-cobalt 0:cdc2def9aec4#15 0.80 0.37 hol -rota-cobalt 0:08c4242b5f58 -0.07 0.93 stock -rota-cobalt 0:f8f8922da592 -0.07 0.86 standby -rota-cobalt 0:c2fb788c7dee -0.07 1.10 training -rota-cobalt 0:a18603086e5b -0.07 0.86 support -rota-cobalt 0:17ae69cc7e46 -0.09 0.67 covers +rota-cobalt 0:08c4242b5f58 -0.07 0.78 stock +rota-cobalt 0:f8f8922da592 -0.07 0.81 standby +rota-cobalt 0:c2fb788c7dee -0.07 0.85 training +rota-cobalt 0:a18603086e5b -0.07 0.81 support +rota-cobalt 0:17ae69cc7e46 -0.09 0.52 covers rota-cobalt 0:412a5c513322 -0.08 0.64 l104/d50 rota-cobalt 0:304560bf3205 -0.08 0.65 l33/d57 rota-cobalt 0:ca6ce47ada53 -0.08 0.66 l32/d76 @@ -4198,7 +4199,7 @@ rota-cobalt 0:7062fafe1dab -0.08 0.60 l65/d112 rota-cobalt 0:778cb57b62fa -0.08 0.60 l73/d112 rota-cobalt 0:1b4471920f01 -0.08 0.62 l72/d83 rota-cobalt 0:288965a1f2c8 -0.18 0.53 management -rota-cobalt 0:6a61eb7383e8 -0.07 0.91 rosa +rota-cobalt 0:6a61eb7383e8 -0.07 0.76 rosa rota-cobalt 0:3bb0f0bb5552 -0.06 0.26 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#1 0.10 0.22 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#2 0.10 0.23 09:00-18:00 @@ -4226,7 +4227,7 @@ rota-cobalt 0:e669544e9895 -0.85 0.91 08:00-16:00 rota-cobalt 0:e669544e9895#1 -0.07 0.96 08:00-16:00 rota-cobalt 0:19c30663e8ef -0.79 1.71 16:00-21:00 rota-cobalt 0:19c30663e8ef#1 0.00 1.76 16:00-21:00 -rota-cobalt 0:c9affe7f7587 0.04 1.03 tomas +rota-cobalt 0:c9affe7f7587 0.04 0.88 tomas rota-cobalt 0:6d43df86fe4b 0.05 0.26 16:00-00:00 rota-cobalt 0:6d43df86fe4b#1 0.05 0.31 16:00-00:00 rota-cobalt 0:6d43df86fe4b#2 0.10 0.27 16:00-00:00 @@ -4269,12 +4270,12 @@ rota-cobalt 0:953cb9ba8128 0.50 0.90 mirela rota-cobalt 0:c1298a6e85c2 0.51 0.27 12:00-18:00 rota-cobalt 0:c1298a6e85c2#1 0.62 0.27 12:00-18:00 rota-cobalt 0:86846e84f9bf 0.61 0.78 sofia -rota-cobalt 0:effcc54ba75f 0.61 0.87 karol +rota-cobalt 0:effcc54ba75f 0.61 0.82 karol rota-cobalt 0:5c1e134d2d34 0.72 0.89 henrik -rota-cobalt 0:6326e0e8cfda 0.83 0.92 lucia +rota-cobalt 0:6326e0e8cfda 0.83 0.82 lucia rota-cobalt 0:c235e502637c 0.76 0.53 barbacks rota-cobalt 0:bd558de5f794 0.78 0.84 oskar -rota-cobalt 0:4941d6993ee9 0.78 0.99 teodor -rota-cobalt 0:1d3d3d6a07bd 0.79 0.88 vesna +rota-cobalt 0:4941d6993ee9 0.78 0.89 teodor +rota-cobalt 0:1d3d3d6a07bd 0.79 0.83 vesna rota-cobalt 0:59f2ff7c7c70 0.03 0.08 timesshownin24-hourformat.shiftc -rota-cobalt 0:594c41ec0506 0.03 -3.15 quaysidebar|31aug-6sep2026 +rota-cobalt 0:594c41ec0506 0.03 -3.60 quaysidebar|31aug-6sep2026 diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv index 0333713a2..d30ed489d 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv @@ -25,7 +25,7 @@ cv-slate_orange 1 1 87 86 0.42 1.02 0 cv-teal_pulse 1 1 57 57 0.22 0.64 0 cv-terracotta_rail 1 1 102 102 0.44 0.94 0 cv-timeline_minimal 2 2 89 86 0.45 0.50 0 -cv-violet_grid 1 2 80 77 0.36 21.41 37 +cv-violet_grid 1 1 80 78 0.22 0.44 0 invoice-classic 1 1 56 56 0.12 0.30 0 invoice-classic_long 2 2 166 166 0.20 0.27 0 invoice-consulting 1 1 92 92 0.67 0.96 0