From 8eddbfde98960f1490d9dfcbc719cbc38a75490b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 12 Oct 2026 10:05:15 +0100 Subject: [PATCH 1/3] fix(docx): seat a list's items where the page sets them, as a paragraph's - A list item's exact lines were never seated: Word stands the baseline four fifths of the way down, the page its face's ascent down, and an item's text stood where Word put it. seatTheItem runs it through the paragraph's seat: a line set low rises into the gap above it as far as the gap goes and owes as much below, the item under it rising into that; under half a point, one opening a cell takes the rest from the cell's padding or the space above its row of columns; what is left of half a point and more is the text's position. - Not raised into the gap: an item the layout opens a page with, the first of a list it starts on a new page or one on a later page than the item before it, and an item of a list in a cell broken over a page. ItemText carries the page it was set on. - An item whose marker Word draws keeps no position: LibreOffice does not move a Word list's marker by the mark's position, and the text would stand off its bullet there. - shiftToThePagesBaseline takes the lines and their gap, and raiseIntoTheSpaceAbove whether the line stays on its page, so a paragraph and an item share one seat. - Baselines: Word and LibreOffice on Windows rewritten from the corpus; the Word export companion re-rendered. - Tests: DocxListItemSeatTest; DocxVerticalSpacingTest's list counts its items' raise. --- CHANGELOG.md | 29 ++ .../examples/word-export-companion.docx | Bin 10225 -> 10230 bytes .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../libreoffice-windows-lines.tsv | 152 +++---- .../docx-fidelity/libreoffice-windows.tsv | 14 +- .../docx-fidelity/word-windows-lines.tsv | 152 +++---- .../resources/docx-fidelity/word-windows.tsv | 16 +- .../semantic/docx/DocxLayoutMetrics.java | 6 +- .../semantic/docx/DocxSemanticBackend.java | 126 +++++- .../backend/semantic/docx/DocxExports.java | 3 +- .../semantic/docx/DocxListItemSeatTest.java | 401 ++++++++++++++++++ .../docx/DocxVerticalSpacingTest.java | 6 +- 13 files changed, 719 insertions(+), 190 deletions(-) create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f49987303..04f8932cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export stands a list's items where the page sets them in their exact lines, as it does a + paragraph's.** Word and LibreOffice stand an exact line's baseline four fifths of the way down it, + the page its face's ascent below the line's top, and a paragraph's line is moved there; a list + item's was not moved at all. By the export's own measure of Word's seat, 68 items of the corpus + stood low by under half a point, a median 0.22pt, and 16 high by half a point and more; measured, + they stood about where it put them: `BoxedSections`' skills, 0.18pt low by it, 0.23 to 0.28pt low + in Word and LibreOffice, `MidnightNavy`'s two-line duties, 0.9pt high by it, 0.76 to 1.06pt high. + An item is now seated as a paragraph is: a line set low rises into the gap above it as far as the + gap goes and owes as much below, which the item under it rises into in turn; under half a point, one + opening a cell takes what the gap cannot give from the cell's padding, or from the space above its + row of columns; and what is left where the whole difference is half a point or more, and a line set + that much high, is its text's position. Not raised into the gap, its position still moved: an item + the layout opens a page with — the first of a list it starts on a new page in the flow, or one it + sets on a later page than the item before it — whose space above Word drops there, nor one of a + list in a cell the layout breaks over a page, in a row not kept whole, where what an item owed + would be the space above the next page's first, inside the cell. An item whose marker + Word draws, a Word list's, is not moved by its position: Word sets that marker from the + paragraph's mark, which LibreOffice does not move by the mark's position — lowered a point by + both, `MidnightNavy`'s duties stood 1.15pt under their bullets there — so its line keeps what the + space above does not take of it, and `MidnightNavy`'s and `NavySidebar`'s duties stand as high as + before. An item the layout's lines are not matched to, as `SlateOrange`'s certifications in a + table's composed cell, is not seated. + + `ClassicInvoice`'s notes moved up 0.24 to 0.36pt in Word and 0.3pt in LibreOffice, to within + 0.05pt of the page, `BoxedSections`' and `ModernProfessional`'s skills and projects 0.12 to 0.24pt + and 0.2pt, and `Executive`'s project descriptions, items of a list Word does not number, down + 0.48pt and 0.55pt, to within 0.06pt. By median, 5 documents moved nearer the page in Word, 3 in + LibreOffice on Windows and 3 in LibreOffice 24.2, none further. + - **A DOCX export stands the first line of a panel, a table's composed cell and a row's column where the page sets it, where nothing was written above the line to raise it into.** Word and LibreOffice stand an exact line's baseline four fifths of the way down it, and a line the page sets higher is diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index fe15fa70aa8d84c0905980d232e9582b127e3b36..2aed4c66ec6c73521cffe90763a193672e1a8aba 100644 GIT binary patch delta 3387 zcmYLMWn2?{7u{e}M9{Gj(jC$$awq@;p~Fa&jg^cW8zpdv9CAPl6XTSDpX z2M`2C2vWYv^FHtU|8mbc_kVx)!~Jp&$>@g>wXr@qh!#LWK>_eUBFm`JK=R*}7!SO0 z0kDN+{eJ{(Fu4Sp{;dEthAeo3PUrzmWJVcA23{sIpgLLAX*dNaYT!-V{&m_J5K_6V z__$NGn5d8X><@IUlq#MPZa+JgsiRdT5Kx)KYTC;@z9YkvmXqL9*z3+F5vXQ-F5)D2 zT=E2g_RF)%%4f3ThJ1cT-!-XQ#o6h(8JOvINr$D4jz0;N>vPi4c`A4zR$~dK z$nfavDqh_|yC1AB3ql}>0MVGB( zuLFt!lF}g48ZMu)zMv$B#4M*s&JY$*oL@F>{nzlcA7-w$RpaiV8Q8?sd8v{qLg(@3 z;n^ab-B|xJg_LvNhlfA_Ac714`1eBKixXi{-1JyYFgumiuKaGAW;E+Sb(A628_bNx z7p=4zCZ<^dbcGq}=0sB<^v4HP?L692EnsK8{KZCs1w>Ds{)^Kv{`C9Mh;`BK)JpcR z{f$#DhlPjg(5N>$T%vzx_E`1ox9n-k@xg@el_&G~Jm@+C=@;xK%DN~a@A~xzEs#!($6K1bw$KfysNLQ4 zrH9kb*yJnW=SPR=L59J(tMFyda4U7=m%<}@=_N0>`^woNgM5(+a;26Z@m#JIfrSpv z$>wL?HlNitJ|!ThJ~_-H?YYUpG=i=hUs@AP3_%N0CN-z&(qAg?hSzF`*XZ%M_)Bo+p`G@qw9V0}?@t27 zEwTB&xgXRQ7S-NO_+L8y>VmQ*@0I^Gdp*yROEgsbsm7e5h zKZ@XR9Ssj2QX^_2`y%opVrf5$`=suAA0+SK5@-#?e^GK~1%$(+ex$i$*V|}am0tN5 zXLPnyW(eryhj`7tMfx`f^qG5P@Gny{Q6QEXSl&O2rInF1PTrzxi;hG{(zsHhV*N5D zD5j&2>!JfO)=C-+=)xmNEJc&$3(U>GAHP@EO@@{@hboXRoxC|*h3zPE4HM$?{XWZ4 z9m+jB?z~cQzSq4s^qNzd1a!cRw!2qo^WYq%H*V^(^RG1NO(1F%>7Xsco-(}-lq(l2@k|^O{w0f@+ZGVEl?ma|@Y5ThwW4huJK-s-Z9(O`Nni-~hK9 z=He-t&^vx38zaG`vVvYZ@g}&fHPgJ!Vw?I(-zeX$7pGOWcC3 zgl)9_6{nnhPxGF*pfkUsTA8K;=KiUooSXdfbVsQT=;(EUZh6elsGU=`oKNJI@_V_J ztHhXqpC`2b)=XK!`r?YZin_Phb~job86w>5)%Q#-U|@`-7z zql2Cb9!H!KueP7owXP3!x+(j}J(3GKmOH4LAJ(Gphn}N_7XnwySPip*hRoC$Q_N6) zyy>rL^q<~qUR5WQKiky8{`ykI`sgUjpHC4Ya*q<*i4~wZ>lRd^)=ZG+pOK-x!dCpn zg28D<>w@b<^TTH~)gE{7-{TysaJPjPxnsK~=C_dFg|uJyfteB#3?dQFLqVuRg?tIO%?-EsFp)rA}~c>hqleN*E1p1XIyZx-6)QE|)EKG^(O=P!fE(DRW3QH94V7U!C zv-rpy6!;`irhoBhvsug-Y2QDHq0uz!GE&$u$Ehsmjs>k-;2tuR4|ZK)aIY`Ui$)ti z9>X6s2eHNGesjuD(Sd&jL6v}Bn`x4|C>}1*&^!0xLh?_8-39Q9)jsq*_q7jS_~&VL z4?eWH_N>#qH(~0z$*)R{U~ucFPi8}kq#fV`^b4V%GK%%_-dmzrlhM}w&gjfL^r3VD z_A?TO+RHdyJpo0!?)>}TY`-te1fO%{t<%LiY8l{Y8V4bpW=95QvgtHQFvd7@)b}6f z03+k~R~hQfO$zx;S{Km{UHsp$)EL*dIzcY1(iW%Y@BetY+yVb7UKpzuqyD5X`YRgu=Ixx8hqsy1d}oOun>QqFfK?KnsZg% z7eeTo)%Mf+)Vi14uX3DVeqv6dS8>bY)0P$ z&0D3xryp%gUC?>%z4yhPO!Pw@4_9AG-y$1eheIlYQ8c7`bjysiAbYW?1G#R+ZKOJh8dh^s_uwJ8gsh zDSWMi#qGp~?ZHy<3_ZVYZjDU%lgjc;?S-%`+?0}*rUse^7ADx;ZuhqJ#?fYalV24B zfnI$xSf{R-z>@iQL8Z{ycIxDs!u8F;RQ(^HF=# z_y=XWtuN|^iL|LEy%EZxRqCSM2!I19g z(q(`S_mRGXOqHVOh*6>*@$w!6F*6D2!{z20Xv#&x<&otI7P%yMn^7!T#xRcepdN|J0$rVXP~wmFOhg@i z8*b87^pfFD*>pt)qtwJ+!zQM?VI{uY_Wb%ES1VWfMSq480O$_+zpK44wL~q*&3}T2 zFAW#uB7vlER{xVgP~0UK0vXT!e*_O+m5WG1;FYx;FG2PL;kl4 z>&Yj1@$D@iGf)}Zz$bLUHu#vy+-R_2Gz?gFeyR&el%E+GhehynUNEoUOb;8)&vC(u Rf3st3H$T@UdtSir_aA~nYmxu} delta 3504 zcmY*c1yGb-8(m=OT3xypkXTw81Oau0B?P1vTdCxu1y?5r$+&Sk^Ye$PxTb%$%10W$G0YDqo%PEm~1Xr~( z7LVZ?U<&aE{ty^H0#T%17L+mq-*0lPwCW-Ivo5x8Lmf5p1opFRazeqShly6rmwS+@ zgM+NCIaY$|fl`?0AYta%K$UmH&7-u2bu~bFg|gScf*Q-~4li9tGkf}NEmt;zS~%!# zBL4=$vU^>CPj#ydArTvf{zy;n+qom@l#B3vUjyxvH|u3HVnlYT@mT!g&5;p6zm5Mb z@;Ct#~p02SN{4J6x+(JQ6Sy+mH@yB~CvU}IaakRye=23tF z;!dWe1c;Cp5-|N{@Y6@jfXX`Xqm-n+;-1RviFoU3L%q&jT;k;-W_y4A{@yKUi*dtu zxptUlXw{&fJQY%y!J#H{gx7(nYvm&aw5Dp>p*7Se*zx3UTw-SJ65`ABbeFh7h!;_? z&v8P-NmF3h(sMRD%KhF^3RAtW!^f2A8NuXAXXB%W>$*u_rrjgxXXZId{Yvq1xQd-8 z=r*wYVMC)#Ke4ttG0BU-8_S+}06-`{0PxQZ5nf*rMwn2NVq#ZzHw6~OGWa1<3*$n_ zj4UYn*`fJ5#Sj1&q;HrNP6W}N9@L)5DC@QpTB|lJv=keFRPWObTTPUnZ|H8VXYWpp zVK4XB*2OI6tu$^%W~y)s+ivv2d-q57U^1Lf1x+jBxm=xFj~UbpcC)0P%B#teM}m}G zS^Vx*f7|a$=1GXrsHe_A{V)n{6`L%rLnc37JOf#U;!&!8rTV1(ldJ1y2fMNaZGr%U zJNDtxs`wak^TR2Kk80lD&YN*G%s;u~oe75rOK+631HxfxEGmEVLWRUxm!(!flo3AK zMVS)l{gOMcY4%3ZBaUdsL8c4eA!oz-1>^B2gT{0-kJ4-*KYyn0wf{KEDtj3`GmZ2+ z$ypqe>|QDO*u@71UhHgLW{PCC(A%|6GL(m9QWzCtBupr0)2wHCGarGxTut9Q={_8? z+tBMAffk+47AA*AdvT=H1avm0v?~*laq10?=R|PirjxYizjHsZhY>OjC9{j=-zVd; z1+1!vWw~i)S2E02M#9uB?{Xj`+L3dmQ+|R5-ce-pTp^bznBIZ{d>u!Ud)B;n1mUF> z(_&2G603o#07>P;6?@Ao7P;$H;~6;=RDq{I%q|09xM6+Vu#k=bgwjqbZ$26@u^h;CN0K9O0o;@{F0h5!2?EY{Xz3PoH zJJM2IBf2+KOz1dzuM(!wADS16rm4B@n!M|B5Wk%eM}ro8VnBsu>G(lr0 zkM<}@>u#@3gR0~QJo}!d;nBa5S~M=D)x#_SitiDTffF$EIay^wXV^NTP-=2pY2ijn`3KWyZZ}%@62*L$XOA~N(9LL`?Akn>N(ouE+zsE z-7k*GJ`Oae7fMWX)Ngrr zl5JoN8dz4{3qoqxp9}F(845)yy|5aJ{VCMej(?{k@;tdquN04+moBx%$hpWjF2u4S z`=cUS@i}(Ydu`?`Gi_~-3Qo;upHKI2T=;wMhYsjbyM@MWL35$M)PAwedkL;1(E68GU`D+*}vp#KCX;*+mDu%$o>%#8N04>M%&txHdlt_a7NefOBggqw5!tD?o_&n>NjrlpmJOj8eAPJbH$!3mh&G zhR1JV6y%}5br!toC>8^4LnDfw9htQi68HtB6^@g68_wFl9ORH(G$57aaw`UF=vzwv z;M97cq+}tc2{X!D>5{F}$Y6A3E7r{aEYy3R`7+T_-0HR@ua0dBO5PqBEsTU%e0LWg zyMOoUspQH;xtZmH)Y6u~uWxs;JON%9)G)vBQTCAcioIPIoQs@*CFMp(fj!qE(a`%qiDnweje_ftCmc#s)L{#Zc(bM3S^s z6);t=28noy@l$Tx5I;tVJ0kb9qzQFQ1VvL_eSx_6aQAjn?a&eH)Jno!VqNt-AH4={ zebU)Sh+I$GO<|1A_{aV3s0?GeKw79p#tqHz#RRx2bc*&9ug$92=6uQB7RYFo$`8ZG zy4D>clE5k2K=2saCTtNFeoJ4kp-Hh7OtaEX=#mm&82P142C`j#ioAOIQ`a7UH&#K@ z#?@r^7<+2f#eD^aaOnen1re7x$T!g@rmfTUy|QSnJRS9+aeZ7YaMnX{Z%XYMggeiM z1w3er{Zjagy~w|-dARpimQ5=55%)MW#=a-ToZ#dprA1k$Jq!=L}`G5tPB@ zhDi8bf=WN)EuNyiFqiMS2p;m;Lwpf$=k@CD`EWVYbjib=QbDQBW1S+y%-3vtYE{T<9JaZwpw69o0SiX0w?K~8IiCvY_&esR_MTL{V7o7htCY#^K+Qc&T`TopUhK`cY)PGv;vd> zT}DQ;QT0+su!EdyVnL%K?BQezHGW$Y!%M?A96#bono!Dmq|W}2r)#VX3`ITydRfVB zw2qd8k+}!uIW3({P{B?5q%vvbS{bgp#)VbtU~;$Q!Hc8^EnbFf{qwVT)(wBX*si4- zTuKAF1PsWY76}vIA+B#yIx?Kxq6(r4I>Xx@`pb*Bi_+ZP++T)43(LP*wp3ONIn>1d z-uN3)sjhe{c`Ergl1|9pi)g~~sDHBs@8Eao1n8Nmt{f)4RmCuC|`$u^xuOLvm&mf2K6QIu=GQi)28}YkY3y za96-HK0_1uTq5v{+zKu1mu#g2+%eko((c^}-)X}+;Lr{Ba7YUy{nA-U;ijC9lUcGh z#6)*W8_fO=*ehCH0FRghzCP^SP%FZ1N$B(xh0`0CPgkWficjt}t=IN6b#9fJT~PnA zxr&WOFDA$UfX0A-ZSJ+ptyBZ?{hdU-Z*YRHBS{*^hrc6(w!+|4(y(8Q|q4`iuJy`5$zjdGP=M diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index b17055745..273464453 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -63,7 +63,7 @@ Payload records live in `core` under | Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) | |---|---|---|---| -| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, matched at a paragraph's middle line: a line of its own Word sets a tenth of a point or more low is moved up into the space written above it and that space owed below it, in the flow and in cells, not in an overlay, a paragraph the layout moves to a new page or breaks over one, nor the space a panel's top border takes above its first line; a line opening a cell, under half a point low, takes what that space cannot give from the cell's top padding — a panel's, its row held as much taller, and a table's composed cell's in a row whose margins even — and a row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, what follows kept in place (not a row the layout moves to a new page, held to a height, or whose column opens with a panel or a table); a paragraph in a composed table cell of a placed row is raised as anywhere, its row kept on one page; what that space cannot give, and a line Word sets high, is moved in the same position where the two are half a point or more apart — Spectral's, not Lato's — which LibreOffice moves further by the face's height over its em (not yet a list item's; a table text cell's by its row's margins, see Table rows; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair, as a badge's initials or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it; an auto-sized paragraph's text that takes its style is written at the size the page fits it to, read off the laid-out lines, a run with a style of its own keeping it, on every path that writes a paragraph — at its style's size where the lines do not tell the fitted size (no lines read, lines in sizes that do not say which is the paragraph's, or one paragraph added at more than one place); a paragraph a session reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, read through the page's own parser, one run a piece in the face, family, colour, tracking and size the page's laid-out lines hold (an auto-sized one's read at the size the page fits it to, a heading at its multiple of it), its marks dropped, wherever the lines hold the pieces' letters so; where they are not read, or hold other letters or none (text the parser reads into nothing, which the page sets as nothing), as authored, its marks as letters; a `bookmark(...)` is Word's `HeadingN`, which Word's outline lists by the text of its Word paragraph — an overlay's pair's whole line, one level for both sides — at no level past the ninth. Outside a header or footer, the paragraph's report note (`ParagraphNode`) names each of these where it moves or renames something: the prefix's letters, and the room a path that writes no prefix leaves out where it moves a line; the size an auto-sized paragraph's text is written at, where the size the page fits it to is not measured; the marks of a paragraph the page read as markdown and the file holds as letters, where its laid-out lines hold fewer of them than its text, not measured where its lines are not read; a markdown heading written taller than the line the page sets it in, which Word cuts on screen; an outline title that is not the text Word lists, a level past the ninth that shares it with another, and the right side's entry where the left holds the line's level | +| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, matched at a paragraph's middle line: a line of its own Word sets a tenth of a point or more low is moved up into the space written above it and that space owed below it, in the flow and in cells, not in an overlay, a paragraph the layout moves to a new page or breaks over one, nor the space a panel's top border takes above its first line; a line opening a cell, under half a point low, takes what that space cannot give from the cell's top padding — a panel's, its row held as much taller, and a table's composed cell's in a row whose margins even — and a row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, what follows kept in place (not a row the layout moves to a new page, held to a height, or whose column opens with a panel or a table); a paragraph in a composed table cell of a placed row is raised as anywhere, its row kept on one page; what that space cannot give, and a line Word sets high, is moved in the same position where the two are half a point or more apart — Spectral's, not Lato's — which LibreOffice moves further by the face's height over its em (a list's item seated as a paragraph is — not raised into the space above where the layout opens a page with it or breaks its list's cell over a page, an item whose marker Word draws not moved by its position, since LibreOffice does not move that marker by its mark's, and one the layout's lines are not matched to not seated; a table text cell's by its row's margins, see Table rows; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair, as a badge's initials or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it; an auto-sized paragraph's text that takes its style is written at the size the page fits it to, read off the laid-out lines, a run with a style of its own keeping it, on every path that writes a paragraph — at its style's size where the lines do not tell the fitted size (no lines read, lines in sizes that do not say which is the paragraph's, or one paragraph added at more than one place); a paragraph a session reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, read through the page's own parser, one run a piece in the face, family, colour, tracking and size the page's laid-out lines hold (an auto-sized one's read at the size the page fits it to, a heading at its multiple of it), its marks dropped, wherever the lines hold the pieces' letters so; where they are not read, or hold other letters or none (text the parser reads into nothing, which the page sets as nothing), as authored, its marks as letters; a `bookmark(...)` is Word's `HeadingN`, which Word's outline lists by the text of its Word paragraph — an overlay's pair's whole line, one level for both sides — at no level past the ninth. Outside a header or footer, the paragraph's report note (`ParagraphNode`) names each of these where it moves or renames something: the prefix's letters, and the room a path that writes no prefix leaves out where it moves a line; the size an auto-sized paragraph's text is written at, where the size the page fits it to is not measured; the marks of a paragraph the page read as markdown and the file holds as letters, where its laid-out lines hold fewer of them than its text, not measured where its lines are not read; a markdown heading written taller than the line the page sets it in, which Word cuts on screen; an outline title that is not the text Word lists, a level past the ninth that shares it with another, and the right side's entry where the left holds the line's level | | List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ⚠️ the top level only. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — or, with rich items or a drawn marker, as paragraphs; content and nesting are unaffected. With the flag, the top level's marker column is the layout's — the marker's width and `markerGap`, the text and its wrapped lines where the page sets them — where the gap covers what Word may set the marker wider: a picture at its written size, its edges included, or text in the page's face (embedded, or a standard one Word sets in the same widths) grown to its half-point size, half a point clear. A Word list's level then indents and hangs by that column; a list of paragraphs writes the marker, a tab to a stop there, and hangs the item there. Word places content at absolute indents and has no relative-advance primitive, so without the layout's measure the gap could not be honoured; a Word list without the flag that the layout placed and that does not nest takes the page's column too, the spaces the page sets its wrapped lines after, its marker followed by a space (`w:suff`) and an item that wraps measured at Word's half-point size; a list that nests items, a list built as a tree of items (laid out flattened), and a marker the gap does not clear keep the stated column (180 twips, plus 120 per nesting level) — except, in a list of paragraphs, a nested rich item with no marker, which stands where the layout set its text, its measure weighed at Word's half-point sizes, where the layout's items are matched to the list's; a list that nests only such items sets its top level at the page's column too. The report counts, on the list, the items that stand at a stated column, a space past their marker or two spaces a level in, and names a centred or right-aligned list written flush left, a lineSpacing not written where the layout's items are not the list's own and one wraps (in a list composed in a table cell, its wrapping not measured), a continuationIndent not written where an item of a markerless list or a tree of items without the flag wraps or its wrapping is not measured, the rows the page draws as a marker alone for blank items of a flagged list, which are not written, and what items the page reads as markdown lose. An item of plain text the page reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, matched to the lines the page laid it out in and read as the page lays it out (a flat item after the marker the page sets before it; a flagged item as its text alone; an item of a list built as a tree of items without the flag after the indent and marker the page reads with it), one run a piece in the face, family, colour, tracking and size those lines hold, Word or the file drawing the marker where it did (Word draws a tree's bullets regular, as the parser sets the marker it reads), the font table shipping the faces the page sets the pieces in; it is written as authored, its marks as letters, where its list's items are not matched one by one to the layout's (no layout, composed in a table cell, an item run onto the next page, a flagged list with a blank item), where its lines hold other letters, where the parser reads a tree's marker as markdown with the item (`*a*`), and where the parser reads it into nothing, and named where the page drops a mark from it or sets none of its text — not where it changes only its face or letters no mark is made of (`1.`); a markdown heading written taller than its item's line is named | | Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding above and below the letters are not in the file, and the export records them. The padding beside the letters is written as the room it takes (`spaceAfterTheLastLetter`): character spacing after the chip's last letter, shaded with it, and after the letter before the chip, unshaded. A chip opening its line or following a picture has no letter before it, so its left padding is not in the file; no space is written after right-to-left letters or after a symbol or emoji. The export records, chip by chip, how each side was written. LibreOffice sets no spacing after a line's last letter, so it does not apply the right padding of a chip that ends a line. A `w:shd` fill is opaque, so a translucent chip is flattened first against what Word paints underneath it — the paragraph's shading, the cell's, or else the colour the page paints under the paragraph, a page background included — so the chip agrees with the file it is in and shows the colour the PDF shows. It stops being translucent, and that is recorded with the rest | | Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; a paragraph of one line of text in a Word paragraph of its own, with room above for its pictures' reach, keeps an exact line at the page's height of it, the pictures set in it where the page puts them in Word and what their ink reaches past it taken from the gaps around it, and in LibreOffice a lowered picture there stands higher and loses what passes the line's top; its description is the text it stands for or empty) | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 2de9c229a..aeae647dd 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -185,7 +185,7 @@ it cannot work out for itself: | What | Where it lands | |---|---| -| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet a list item's, or text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is left either way: Word sets lines on a grid of its own, 0.12pt apart (measured). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One with no room above it for the gap (opening a column, or a cell padded less) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it. Not a line pair's shared line, which moved would move its other half; not a line in an overlay — a band, a layer stack, a shape container, a canvas, a text box — whose writers measure what follows them from the page; not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. What that space cannot give, and a line Word sets higher than the page — moved down, it would take its room out of the space below it, not known when it is written — is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent, as Spectral's is. A list item's lines are not seated yet; a table text cell's are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, raised 4pt, stood 2.4pt high in LibreOffice. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | +| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet a list item's, or text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is left either way: Word sets lines on a grid of its own, 0.12pt apart (measured). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One with no room above it for the gap (opening a column, or a cell padded less) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it. Not a line pair's shared line, which moved would move its other half; not a line in an overlay — a band, a layer stack, a shape container, a canvas, a text box — whose writers measure what follows them from the page; not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. What that space cannot give, and a line Word sets higher than the page — moved down, it would take its room out of the space below it, not known when it is written — is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent, as Spectral's is. A list's item is seated as a paragraph is: raised into the gap above it, the item under it rising into what it owes, under half a point into its cell's padding or out of the space above its row of columns where it opens one, and what is left of half a point and more moved by its position. Not raised into the gap, its position still moved: an item the layout opens a page with, the first of a list it starts on a new page in the flow or one it sets on a later page than the item before it, whose space above Word drops there, nor one of a list in a cell the layout breaks over a page, in a row not kept whole, where what an item owed would be the space above the next page's first, inside the cell. An item whose marker Word draws, a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, which LibreOffice does not move by the mark's position (measured: `MidnightNavy`'s duties, lowered a point by both, stood 1.15pt under their bullets there), so its line keeps what the space above does not take of it. An item the layout's lines are not matched to — `SlateOrange`'s certifications, a list in a table's composed cell — is not seated. A table text cell's lines are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, raised 4pt, stood 2.4pt high in LibreOffice. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | | Table columns | the resolved cell widths as `w:gridCol`, with `w:tblLayout` fixed so Word does not re-fit them | | Row columns | where the layout placed each child, with the row's gap and padding folded into the neighbouring column and taken back out as that cell's margin — in a painted row's panel the padding is the panel cell's margins instead, and the columns are without it. A column sized to its content (`DocumentRowColumn.auto()`) gets a point more, taken from the row's weight columns so the row keeps its width, for the reason a table's does: the editor's substitute font would wrap it — a table of contents' labels broke mid-word ("Intr" / "o") in LibreOffice without it. A row with no auto column, no weight column, or no stated columns (weights, an even split) is written as placed | 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 c1778c7f6..12220fe35 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -58,12 +58,12 @@ cv-boxed_sections 0:80cc87fdc4c4 -0.05 0.10 platformengineerwith10+yearsbuil cv-boxed_sections 0:bddc0bd093ce -0.05 0.10 systems.specialisedinhigh-throug cv-boxed_sections 0:95a43d7b3df0 -0.04 0.10 snapshot-testedlibrariesthatscal cv-boxed_sections 0:5f94c50dc5bf 0.07 1.32 technicalskills -cv-boxed_sections 0:eea537278f83 0.24 0.10 •languages:java21,kotlin,groovy, -cv-boxed_sections 0:9c2cd7231a53 0.25 0.10 •document&print:pdfbox,apachepoi -cv-boxed_sections 0:7bf8639ec5d3 0.25 0.10 •layoutengines:customdsldesign,s -cv-boxed_sections 0:8e66776d28d7 0.26 0.10 •build&infrastructure:maven,grad -cv-boxed_sections 0:1ed4fc7d7768 0.26 0.10 •testing:junit5,assertj,pdfbox-b -cv-boxed_sections 0:26671c309eb9 0.27 0.10 •distribution:mavencentral,sonat +cv-boxed_sections 0:eea537278f83 0.04 0.10 •languages:java21,kotlin,groovy, +cv-boxed_sections 0:9c2cd7231a53 0.05 0.10 •document&print:pdfbox,apachepoi +cv-boxed_sections 0:7bf8639ec5d3 0.05 0.10 •layoutengines:customdsldesign,s +cv-boxed_sections 0:8e66776d28d7 0.06 0.10 •build&infrastructure:maven,grad +cv-boxed_sections 0:1ed4fc7d7768 0.06 0.10 •testing:junit5,assertj,pdfbox-b +cv-boxed_sections 0:26671c309eb9 0.07 0.10 •distribution:mavencentral,sonat cv-boxed_sections 0:68bf0109c6f8 0.08 1.23 education cv-boxed_sections 0:0ad1957d6bab 0.08 0.10 msccomputerscience cv-boxed_sections 0:3fa772117260 0.04 -0.76 2019-2021 @@ -78,16 +78,16 @@ cv-boxed_sections 0:d40b22f00acb 0.11 -0.76 2023-2024 cv-boxed_sections 0:2669e1f6d55c 0.14 0.10 professionaltrack cv-boxed_sections 0:3857373829c1 0.15 0.10 java17platformdeep-dive:records, cv-boxed_sections 0:2577c0f557b2 0.17 1.18 projects -cv-boxed_sections 0:b2b029995203 0.34 0.10 •graphcompose(java21,pdfbox,mave +cv-boxed_sections 0:b2b029995203 0.14 0.10 •graphcompose(java21,pdfbox,mave cv-boxed_sections 0:67593a76ef70 0.04 0.11 declarativejavapdflayoutengine.s cv-boxed_sections 0:b9292cbd1454 0.05 0.11 pipelinesforhiringtoolsandbillin -cv-boxed_sections 0:1b50bb81df5a 0.35 0.10 •templatestudio(kotlin,composede -cv-boxed_sections 0:836e76daecbf 0.36 0.11 internaltoolforevaluatingcv,prop -cv-boxed_sections 0:25cdca2beb8b 0.36 0.10 •layoutlint(java21,javaparser,sp +cv-boxed_sections 0:1b50bb81df5a 0.15 0.10 •templatestudio(kotlin,composede +cv-boxed_sections 0:836e76daecbf 0.16 0.11 internaltoolforevaluatingcv,prop +cv-boxed_sections 0:25cdca2beb8b 0.16 0.10 •layoutlint(java21,javaparser,sp cv-boxed_sections 0:7488006d7864 0.07 0.11 staticanalyserthatflagsfragileau cv-boxed_sections 0:c4d0c415b4f8 0.07 0.11 production. -cv-boxed_sections 0:fb1927f88597 0.38 0.10 •chromeforge(java,graphcompose,p -cv-boxed_sections 0:e7e62230eb1f 0.38 0.11 editorial-magazinedocumenttoolki +cv-boxed_sections 0:fb1927f88597 0.18 0.10 •chromeforge(java,graphcompose,p +cv-boxed_sections 0:e7e62230eb1f 0.18 0.11 editorial-magazinedocumenttoolki cv-boxed_sections 0:c2f296ef30a3 0.20 1.51 professionalexperience cv-boxed_sections 0:a76f47480818 0.19 0.10 seniorplatformengineer cv-boxed_sections 0:060300b18051 0.16 0.01 2024-present @@ -528,14 +528,14 @@ cv-executive 0:2669e1f6d55c -0.26 0.10 professionaltrack cv-executive 0:3857373829c1 -0.26 0.10 java17platformdeep-dive:records, cv-executive 0:2577c0f557b2 -0.02 0.10 projects cv-executive 0:b2b029995203 -0.28 0.10 •graphcompose(java21,pdfbox,mave -cv-executive 0:cf13c8a0c3d0 -0.53 0.12 declarativejavapdflayoutengine.s -cv-executive 0:cfef54184ea6 -0.53 0.12 proposalpipelinesforhiringtoolsa +cv-executive 0:cf13c8a0c3d0 0.02 0.12 declarativejavapdflayoutengine.s +cv-executive 0:cfef54184ea6 0.02 0.12 proposalpipelinesforhiringtoolsa cv-executive 0:1b50bb81df5a -0.28 0.10 •templatestudio(kotlin,composede -cv-executive 0:7127a108c33c -0.53 0.12 internaltoolforevaluatingcv,prop -cv-executive 0:6b844249c695 -0.53 0.12 freezing. +cv-executive 0:7127a108c33c 0.02 0.12 internaltoolforevaluatingcv,prop +cv-executive 0:6b844249c695 0.02 0.12 freezing. cv-executive 0:25cdca2beb8b -0.28 0.10 •layoutlint(java21,javaparser,sp -cv-executive 0:f5fa2add7746 -0.53 0.12 staticanalyserthatflagsfragileau -cv-executive 0:9f5414016bd1 -0.53 0.12 toproduction. +cv-executive 0:f5fa2add7746 0.02 0.12 staticanalyserthatflagsfragileau +cv-executive 0:9f5414016bd1 0.02 0.12 toproduction. cv-executive 0:fb1927f88597 -0.28 0.10 •chromeforge(java,graphcompose,p cv-executive 0:e7e62230eb1f -0.28 0.12 editorial-magazinedocumenttoolki cv-executive 0:c2f296ef30a3 -0.04 0.10 professionalexperience @@ -650,12 +650,12 @@ cv-minimal_underlined 0:80cc87fdc4c4 -0.02 0.10 platformengineerwith10+yearsbuil cv-minimal_underlined 0:bddc0bd093ce -0.02 0.10 systems.specialisedinhigh-throug cv-minimal_underlined 0:95a43d7b3df0 -0.01 0.10 snapshot-testedlibrariesthatscal cv-minimal_underlined 0:5f94c50dc5bf 0.12 0.10 technicalskills -cv-minimal_underlined 0:eea537278f83 0.35 0.10 •languages:java21,kotlin,groovy, -cv-minimal_underlined 0:9c2cd7231a53 0.36 0.10 •document&print:pdfbox,apachepoi -cv-minimal_underlined 0:7bf8639ec5d3 0.36 0.10 •layoutengines:customdsldesign,s -cv-minimal_underlined 0:8e66776d28d7 0.37 0.10 •build&infrastructure:maven,grad -cv-minimal_underlined 0:1ed4fc7d7768 0.37 0.10 •testing:junit5,assertj,pdfbox-b -cv-minimal_underlined 0:26671c309eb9 0.38 0.10 •distribution:mavencentral,sonat +cv-minimal_underlined 0:eea537278f83 0.15 0.10 •languages:java21,kotlin,groovy, +cv-minimal_underlined 0:9c2cd7231a53 0.16 0.10 •document&print:pdfbox,apachepoi +cv-minimal_underlined 0:7bf8639ec5d3 0.16 0.10 •layoutengines:customdsldesign,s +cv-minimal_underlined 0:8e66776d28d7 0.17 0.10 •build&infrastructure:maven,grad +cv-minimal_underlined 0:1ed4fc7d7768 0.17 0.10 •testing:junit5,assertj,pdfbox-b +cv-minimal_underlined 0:26671c309eb9 0.18 0.10 •distribution:mavencentral,sonat cv-minimal_underlined 0:68bf0109c6f8 0.21 0.10 education cv-minimal_underlined 0:0ad1957d6bab 0.27 0.10 msccomputerscience cv-minimal_underlined 0:3fa772117260 0.23 -0.76 2019-2021 @@ -670,17 +670,17 @@ cv-minimal_underlined 0:d40b22f00acb 0.30 -0.76 2023-2024 cv-minimal_underlined 0:2669e1f6d55c 0.33 0.10 professionaltrack cv-minimal_underlined 0:3857373829c1 0.34 0.10 java17platformdeep-dive:records, cv-minimal_underlined 0:2577c0f557b2 0.37 0.10 projects -cv-minimal_underlined 0:b2b029995203 0.61 0.10 •graphcompose(java21,pdfbox,mave +cv-minimal_underlined 0:b2b029995203 0.41 0.10 •graphcompose(java21,pdfbox,mave cv-minimal_underlined 0:67593a76ef70 0.31 0.11 declarativejavapdflayoutengine.s cv-minimal_underlined 0:b9292cbd1454 0.32 0.11 pipelinesforhiringtoolsandbillin -cv-minimal_underlined 0:1b50bb81df5a 0.62 0.10 •templatestudio(kotlin,composede +cv-minimal_underlined 0:1b50bb81df5a 0.42 0.10 •templatestudio(kotlin,composede cv-minimal_underlined 0:7127a108c33c 0.33 0.11 internaltoolforevaluatingcv,prop cv-minimal_underlined 0:6b844249c695 0.33 0.11 freezing. -cv-minimal_underlined 0:25cdca2beb8b 0.64 0.10 •layoutlint(java21,javaparser,sp +cv-minimal_underlined 0:25cdca2beb8b 0.44 0.10 •layoutlint(java21,javaparser,sp cv-minimal_underlined 0:7488006d7864 0.34 0.11 staticanalyserthatflagsfragileau cv-minimal_underlined 0:c4d0c415b4f8 0.35 0.11 production. -cv-minimal_underlined 0:fb1927f88597 0.65 0.10 •chromeforge(java,graphcompose,p -cv-minimal_underlined 0:e7e62230eb1f 0.66 0.11 editorial-magazinedocumenttoolki +cv-minimal_underlined 0:fb1927f88597 0.45 0.10 •chromeforge(java,graphcompose,p +cv-minimal_underlined 0:e7e62230eb1f 0.46 0.11 editorial-magazinedocumenttoolki cv-minimal_underlined 0:c2f296ef30a3 0.49 0.10 professionalexperience cv-minimal_underlined 0:a76f47480818 0.55 0.10 seniorplatformengineer cv-minimal_underlined 0:060300b18051 0.51 0.01 2024-present @@ -767,12 +767,12 @@ cv-modern_professional 0:8817bf9310b6 0.11 0.30 jordan.rivera@example.com|linked cv-modern_professional 0:b944906ed84c 0.01 0.10 professionalsummary cv-modern_professional 0:6fbdc8b79878 -0.02 0.10 platformengineerwith10+yearsbuil cv-modern_professional 0:5f94c50dc5bf 0.01 0.10 technicalskills -cv-modern_professional 0:eea537278f83 0.28 0.10 •languages:java21,kotlin,groovy, -cv-modern_professional 0:9c2cd7231a53 0.28 0.10 •document&print:pdfbox,apachepoi -cv-modern_professional 0:7bf8639ec5d3 0.28 0.10 •layoutengines:customdsldesign,s -cv-modern_professional 0:8e66776d28d7 0.28 0.10 •build&infrastructure:maven,grad -cv-modern_professional 0:1ed4fc7d7768 0.28 0.10 •testing:junit5,assertj,pdfbox-b -cv-modern_professional 0:26671c309eb9 0.28 0.10 •distribution:mavencentral,sonat +cv-modern_professional 0:eea537278f83 0.08 0.10 •languages:java21,kotlin,groovy, +cv-modern_professional 0:9c2cd7231a53 0.08 0.10 •document&print:pdfbox,apachepoi +cv-modern_professional 0:7bf8639ec5d3 0.08 0.10 •layoutengines:customdsldesign,s +cv-modern_professional 0:8e66776d28d7 0.08 0.10 •build&infrastructure:maven,grad +cv-modern_professional 0:1ed4fc7d7768 0.08 0.10 •testing:junit5,assertj,pdfbox-b +cv-modern_professional 0:26671c309eb9 0.08 0.10 •distribution:mavencentral,sonat cv-modern_professional 0:68bf0109c6f8 0.02 0.10 education cv-modern_professional 0:0ad1957d6bab 0.08 0.10 msccomputerscience cv-modern_professional 0:3fa772117260 0.09 0.18 2019-2021 @@ -788,16 +788,16 @@ cv-modern_professional 0:d40b22f00acb 0.09 0.18 2023-2024 cv-modern_professional 0:2669e1f6d55c 0.03 0.10 professionaltrack cv-modern_professional 0:3857373829c1 0.09 0.10 java17platformdeep-dive:records, cv-modern_professional 0:2577c0f557b2 0.02 0.10 projects -cv-modern_professional 0:b2b029995203 0.29 0.10 •graphcompose(java21,pdfbox,mave +cv-modern_professional 0:b2b029995203 0.09 0.10 •graphcompose(java21,pdfbox,mave cv-modern_professional 0:dd39d2ff3520 -0.01 0.10 declarativejavapdflayoutengine.s cv-modern_professional 0:9ecaa6df6d41 -0.01 0.10 invoice/proposalpipelinesforhiri -cv-modern_professional 0:1b50bb81df5a 0.29 0.10 •templatestudio(kotlin,composede +cv-modern_professional 0:1b50bb81df5a 0.09 0.10 •templatestudio(kotlin,composede cv-modern_professional 0:18eec9e76e2d -0.01 0.10 internaltoolforevaluatingcv,prop cv-modern_professional 0:6bc2807453c6 -0.01 0.10 baselinefreezing. -cv-modern_professional 0:25cdca2beb8b 0.29 0.10 •layoutlint(java21,javaparser,sp +cv-modern_professional 0:25cdca2beb8b 0.09 0.10 •layoutlint(java21,javaparser,sp cv-modern_professional 0:cc2850ddd8ba -0.01 0.10 staticanalyserthatflagsfragileau cv-modern_professional 0:7b985a1d2b08 -0.01 0.10 shiptoproduction. -cv-modern_professional 0:fb1927f88597 0.29 0.10 •chromeforge(java,graphcompose,p +cv-modern_professional 0:fb1927f88597 0.09 0.10 •chromeforge(java,graphcompose,p cv-modern_professional 0:cda7db6c6ea6 -0.01 0.10 editorial-magazinedocumenttoolki cv-modern_professional 0:fe63fe18585a -0.01 0.10 callouts. cv-modern_professional 0:c2f296ef30a3 0.03 0.10 professionalexperience @@ -1748,17 +1748,17 @@ invoice-classic 0:d7f4425b8b16 0.02 1.10 gbp1,030 invoice-classic 0:11239872d178 0.03 1.10 total invoice-classic 0:45a5ad5ad430 0.03 1.10 gbp6,180 invoice-classic 0:ab5aa97074c4 0.20 0.10 notes -invoice-classic 0:b78eb53cbd11 0.27 0.10 •pleaseincludetheinvoicenumberon -invoice-classic 0:8c8fd62f15a7 0.27 0.12 yourremittanceadvice. -invoice-classic 0:f73f1b89e354 0.27 0.10 •allworkwasdeliveredasagreedduri -invoice-classic 0:49c1ececed52 0.27 0.12 theaprilimplementationwindow. +invoice-classic 0:b78eb53cbd11 -0.03 0.10 •pleaseincludetheinvoicenumberon +invoice-classic 0:8c8fd62f15a7 -0.03 0.12 yourremittanceadvice. +invoice-classic 0:f73f1b89e354 -0.03 0.10 •allworkwasdeliveredasagreedduri +invoice-classic 0:49c1ececed52 -0.03 0.12 theaprilimplementationwindow. invoice-classic 0:fb854bac39c8 0.20 0.06 paymentterms -invoice-classic 0:24274f39b50b 0.27 0.06 •paymentduewithin14calendardays. -invoice-classic 0:c438440ba377 0.27 0.06 •banktransferpreferred;contact -invoice-classic 0:434cc906ae90 0.27 0.09 billing@graphcompose.devfor -invoice-classic 0:8822ea375315 0.27 0.09 remittancedetails. -invoice-classic 0:1a7b5c840e25 0.27 0.06 •latepaymentsmaydelayadditional -invoice-classic 0:1631a06d245f 0.27 0.09 templatecustomizationwork. +invoice-classic 0:24274f39b50b -0.03 0.06 •paymentduewithin14calendardays. +invoice-classic 0:c438440ba377 -0.03 0.06 •banktransferpreferred;contact +invoice-classic 0:434cc906ae90 -0.03 0.09 billing@graphcompose.devfor +invoice-classic 0:8822ea375315 -0.03 0.09 remittancedetails. +invoice-classic 0:1a7b5c840e25 -0.03 0.06 •latepaymentsmaydelayadditional +invoice-classic 0:1631a06d245f -0.03 0.09 templatecustomizationwork. invoice-classic 0:e3337beaa171 0.05 0.10 thankyouforchoosinggraphcomposef invoice-classic_long 0:c26c22b94338 0.09 0.10 graphcomposestudio invoice-classic_long 0:c26c22b94338#1 0.10 0.06 graphcomposestudio @@ -1916,15 +1916,15 @@ invoice-classic_long 1:e7fda33332e9 -0.03 1.10 gbp8,978 invoice-classic_long 1:11239872d178 -0.02 1.10 total invoice-classic_long 1:90b9dec01ea9 -0.02 1.10 gbp53,868 invoice-classic_long 1:ab5aa97074c4 0.15 0.10 notes -invoice-classic_long 1:970fb4bf9d95 0.22 0.10 •sprints1-3weredeliveredunderthe -invoice-classic_long 1:a781fa6c76d2 0.22 0.12 frameworkagreement. -invoice-classic_long 1:c980fb1ebf98 0.22 0.10 •hardwareandthird-partylicencesa -invoice-classic_long 1:be1dea5817a5 0.22 0.12 billedseparately. +invoice-classic_long 1:970fb4bf9d95 -0.08 0.10 •sprints1-3weredeliveredunderthe +invoice-classic_long 1:a781fa6c76d2 -0.08 0.12 frameworkagreement. +invoice-classic_long 1:c980fb1ebf98 -0.08 0.10 •hardwareandthird-partylicencesa +invoice-classic_long 1:be1dea5817a5 -0.08 0.12 billedseparately. invoice-classic_long 1:fb854bac39c8 0.15 0.06 paymentterms -invoice-classic_long 1:24274f39b50b 0.22 0.06 •paymentduewithin14calendardays. -invoice-classic_long 1:c438440ba377 0.22 0.06 •banktransferpreferred;contact -invoice-classic_long 1:434cc906ae90 0.22 0.09 billing@graphcompose.devfor -invoice-classic_long 1:8822ea375315 0.22 0.09 remittancedetails. +invoice-classic_long 1:24274f39b50b -0.08 0.06 •paymentduewithin14calendardays. +invoice-classic_long 1:c438440ba377 -0.08 0.06 •banktransferpreferred;contact +invoice-classic_long 1:434cc906ae90 -0.08 0.09 billing@graphcompose.devfor +invoice-classic_long 1:8822ea375315 -0.08 0.09 remittancedetails. invoice-classic_long 1:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef invoice-consulting 0:d44ceaea97d1 0.27 0.10 level8,1collinsstreet invoice-consulting 0:78a0bb33347f 0.29 0.10 melbournevic3000australia @@ -2773,18 +2773,18 @@ invoice-modern 0:d7f4425b8b16 -0.08 0.76 gbp1,030 invoice-modern 0:11239872d178 -0.07 0.76 total invoice-modern 0:45a5ad5ad430 -0.07 0.76 gbp6,180 invoice-modern 0:ab5aa97074c4 0.10 0.10 notes -invoice-modern 0:b78eb53cbd11 0.17 0.10 •pleaseincludetheinvoicenumberon -invoice-modern 0:8c8fd62f15a7 0.17 0.12 yourremittanceadvice. -invoice-modern 0:0c98130c526f 0.17 0.10 •allworkwasdeliveredasagreed -invoice-modern 0:2e9787ed79bf 0.17 0.12 duringtheaprilimplementation -invoice-modern 0:e41212011d98 0.17 0.12 window. +invoice-modern 0:b78eb53cbd11 -0.13 0.10 •pleaseincludetheinvoicenumberon +invoice-modern 0:8c8fd62f15a7 -0.13 0.12 yourremittanceadvice. +invoice-modern 0:0c98130c526f -0.13 0.10 •allworkwasdeliveredasagreed +invoice-modern 0:2e9787ed79bf -0.13 0.12 duringtheaprilimplementation +invoice-modern 0:e41212011d98 -0.13 0.12 window. invoice-modern 0:fb854bac39c8 0.10 0.11 paymentterms -invoice-modern 0:24274f39b50b 0.17 0.11 •paymentduewithin14calendardays. -invoice-modern 0:c438440ba377 0.17 0.11 •banktransferpreferred;contact -invoice-modern 0:434cc906ae90 0.17 0.14 billing@graphcompose.devfor -invoice-modern 0:8822ea375315 0.17 0.14 remittancedetails. -invoice-modern 0:1a7b5c840e25 0.17 0.11 •latepaymentsmaydelayadditional -invoice-modern 0:1631a06d245f 0.17 0.14 templatecustomizationwork. +invoice-modern 0:24274f39b50b -0.13 0.11 •paymentduewithin14calendardays. +invoice-modern 0:c438440ba377 -0.13 0.11 •banktransferpreferred;contact +invoice-modern 0:434cc906ae90 -0.13 0.14 billing@graphcompose.devfor +invoice-modern 0:8822ea375315 -0.13 0.14 remittancedetails. +invoice-modern 0:1a7b5c840e25 -0.13 0.11 •latepaymentsmaydelayadditional +invoice-modern 0:1631a06d245f -0.13 0.14 templatecustomizationwork. invoice-modern 0:e3337beaa171 -0.05 0.10 thankyouforchoosinggraphcomposef invoice-modern_long 0:52d6e3de4fa0 0.00 0.10 invoice invoice-modern_long 0:61a964e78950 0.00 0.10 invoicegc-2026-042issued04may202 @@ -2938,15 +2938,15 @@ invoice-modern_long 1:e7fda33332e9 -0.03 0.75 gbp8,978 invoice-modern_long 1:11239872d178 -0.02 0.80 total invoice-modern_long 1:90b9dec01ea9 -0.02 0.75 gbp53,868 invoice-modern_long 1:ab5aa97074c4 0.15 0.10 notes -invoice-modern_long 1:970fb4bf9d95 0.22 0.10 •sprints1-3weredeliveredunderthe -invoice-modern_long 1:a781fa6c76d2 0.22 0.12 frameworkagreement. -invoice-modern_long 1:c980fb1ebf98 0.22 0.10 •hardwareandthird-partylicencesa -invoice-modern_long 1:be1dea5817a5 0.22 0.12 billedseparately. +invoice-modern_long 1:970fb4bf9d95 -0.08 0.10 •sprints1-3weredeliveredunderthe +invoice-modern_long 1:a781fa6c76d2 -0.08 0.12 frameworkagreement. +invoice-modern_long 1:c980fb1ebf98 -0.08 0.10 •hardwareandthird-partylicencesa +invoice-modern_long 1:be1dea5817a5 -0.08 0.12 billedseparately. invoice-modern_long 1:fb854bac39c8 0.15 0.11 paymentterms -invoice-modern_long 1:24274f39b50b 0.22 0.11 •paymentduewithin14calendardays. -invoice-modern_long 1:c438440ba377 0.22 0.11 •banktransferpreferred;contact -invoice-modern_long 1:434cc906ae90 0.22 0.14 billing@graphcompose.devfor -invoice-modern_long 1:8822ea375315 0.22 0.14 remittancedetails. +invoice-modern_long 1:24274f39b50b -0.08 0.11 •paymentduewithin14calendardays. +invoice-modern_long 1:c438440ba377 -0.08 0.11 •banktransferpreferred;contact +invoice-modern_long 1:434cc906ae90 -0.08 0.14 billing@graphcompose.devfor +invoice-modern_long 1:8822ea375315 -0.08 0.14 remittancedetails. invoice-modern_long 1:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef invoice-obsidian 0:07f15cde5b18 0.14 0.09 kestrel invoice-obsidian 0:52d6e3de4fa0 0.12 0.09 invoice diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv index 0660c00ff..b851441fc 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows.tsv @@ -1,18 +1,18 @@ # libreoffice build bf6ef2662401 on windows document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.13 0.45 0 -cv-boxed_sections 2 2 58 58 0.09 0.34 0 +cv-boxed_sections 2 2 58 58 0.08 0.17 0 cv-centered_headline 2 2 58 56 0.13 0.31 0 cv-charcoal_gold 1 1 85 83 0.17 0.24 0 cv-classic_serif 2 2 57 54 0.07 0.15 0 cv-compact_mono 1 1 58 58 0.17 0.29 0 cv-editorial_blue 2 2 82 82 0.14 0.30 0 cv-engineering_resume 1 1 57 57 0.26 0.38 0 -cv-executive 2 2 60 60 0.26 0.50 0 +cv-executive 2 2 60 60 0.26 0.30 0 cv-midnight_navy 1 1 86 84 0.28 0.87 0 -cv-minimal_underlined 2 2 59 59 0.29 0.54 0 +cv-minimal_underlined 2 2 59 59 0.21 0.45 0 cv-mint_editorial 2 2 59 59 0.07 0.30 0 -cv-modern_professional 2 2 61 59 0.04 0.28 0 +cv-modern_professional 2 2 61 59 0.04 0.09 0 cv-monogram_sidebar 1 1 53 50 0.20 0.44 0 cv-navy_sidebar 1 1 71 71 0.20 0.45 0 cv-nordic_clean 1 1 63 58 0.21 0.38 0 @@ -26,8 +26,8 @@ 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 87 0.45 0.49 0 cv-violet_grid 1 1 80 78 0.26 0.54 0 -invoice-classic 1 1 56 56 0.05 0.27 0 -invoice-classic_long 2 2 166 166 0.03 0.10 0 +invoice-classic 1 1 56 56 0.03 0.12 0 +invoice-classic_long 2 2 166 166 0.03 0.08 0 invoice-consulting 1 1 92 92 0.19 1.01 0 invoice-consulting_long 2 2 236 236 0.18 0.51 0 invoice-luma_studio 1 1 97 97 0.14 0.46 2 @@ -35,7 +35,7 @@ invoice-luma_studio_long 3 3 192 192 0.11 0.41 2 invoice-merchant 1 2 94 90 0.07 0.21 0 invoice-metered 1 1 99 99 0.10 0.83 0 invoice-modern 1 1 54 54 0.08 0.17 0 -invoice-modern_long 2 2 162 162 0.07 0.12 0 +invoice-modern_long 2 2 162 162 0.07 0.10 0 invoice-obsidian 1 1 81 81 0.26 0.99 0 invoice-payments 1 1 104 104 0.05 0.21 0 invoice-platform 1 1 111 111 0.32 0.43 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 eea5ab9a4..5d72dc50d 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows-lines.tsv @@ -58,12 +58,12 @@ cv-boxed_sections 0:80cc87fdc4c4 0.05 -0.08 platformengineerwith10+yearsbuil cv-boxed_sections 0:bddc0bd093ce 0.10 -0.08 systems.specialisedinhigh-throug cv-boxed_sections 0:95a43d7b3df0 0.02 -0.08 snapshot-testedlibrariesthatscal cv-boxed_sections 0:5f94c50dc5bf 0.15 0.39 technicalskills -cv-boxed_sections 0:eea537278f83 0.23 -0.08 •languages:java21,kotlin,groovy, -cv-boxed_sections 0:9c2cd7231a53 0.23 -0.08 •document&print:pdfbox,apachepoi -cv-boxed_sections 0:7bf8639ec5d3 0.24 -0.08 •layoutengines:customdsldesign,s -cv-boxed_sections 0:8e66776d28d7 0.27 -0.08 •build&infrastructure:maven,grad -cv-boxed_sections 0:1ed4fc7d7768 0.28 -0.08 •testing:junit5,assertj,pdfbox-b -cv-boxed_sections 0:26671c309eb9 0.28 -0.08 •distribution:mavencentral,sonat +cv-boxed_sections 0:eea537278f83 0.11 -0.08 •languages:java21,kotlin,groovy, +cv-boxed_sections 0:9c2cd7231a53 0.11 -0.08 •document&print:pdfbox,apachepoi +cv-boxed_sections 0:7bf8639ec5d3 0.12 -0.08 •layoutengines:customdsldesign,s +cv-boxed_sections 0:8e66776d28d7 0.15 -0.08 •build&infrastructure:maven,grad +cv-boxed_sections 0:1ed4fc7d7768 0.16 -0.08 •testing:junit5,assertj,pdfbox-b +cv-boxed_sections 0:26671c309eb9 0.16 -0.08 •distribution:mavencentral,sonat cv-boxed_sections 0:68bf0109c6f8 0.17 0.29 education cv-boxed_sections 0:0ad1957d6bab 0.07 0.04 msccomputerscience cv-boxed_sections 0:3fa772117260 0.12 -0.87 2019-2021 @@ -78,15 +78,15 @@ cv-boxed_sections 0:d40b22f00acb 0.19 -0.87 2023-2024 cv-boxed_sections 0:2669e1f6d55c 0.14 -0.08 professionaltrack cv-boxed_sections 0:3857373829c1 0.20 -0.08 java17platformdeep-dive:records, cv-boxed_sections 0:2577c0f557b2 0.33 0.15 projects -cv-boxed_sections 0:b2b029995203 0.40 -0.08 •graphcompose(java21,pdfbox,mave +cv-boxed_sections 0:b2b029995203 0.16 -0.08 •graphcompose(java21,pdfbox,mave cv-boxed_sections 0:67593a76ef70 0.13 -0.06 declarativejavapdflayoutengine.s cv-boxed_sections 0:b9292cbd1454 0.17 -0.06 pipelinesforhiringtoolsandbillin -cv-boxed_sections 0:1b50bb81df5a 0.41 -0.08 •templatestudio(kotlin,composede -cv-boxed_sections 0:836e76daecbf 0.37 -0.06 internaltoolforevaluatingcv,prop -cv-boxed_sections 0:25cdca2beb8b 0.46 -0.08 •layoutlint(java21,javaparser,sp +cv-boxed_sections 0:1b50bb81df5a 0.17 -0.08 •templatestudio(kotlin,composede +cv-boxed_sections 0:836e76daecbf 0.25 -0.06 internaltoolforevaluatingcv,prop +cv-boxed_sections 0:25cdca2beb8b 0.22 -0.08 •layoutlint(java21,javaparser,sp cv-boxed_sections 0:7488006d7864 0.18 -0.06 staticanalyserthatflagsfragileau cv-boxed_sections 0:c4d0c415b4f8 0.23 -0.06 production. -cv-boxed_sections 0:fb1927f88597 0.43 -0.08 •chromeforge(java,graphcompose,p +cv-boxed_sections 0:fb1927f88597 0.19 -0.08 •chromeforge(java,graphcompose,p cv-boxed_sections 0:e7e62230eb1f 0.40 -0.06 editorial-magazinedocumenttoolki cv-boxed_sections 0:c2f296ef30a3 0.28 0.54 professionalexperience cv-boxed_sections 0:a76f47480818 0.30 0.04 seniorplatformengineer @@ -528,14 +528,14 @@ cv-executive 0:2669e1f6d55c -0.17 -0.04 professionaltrack cv-executive 0:3857373829c1 -0.15 -0.04 java17platformdeep-dive:records, cv-executive 0:2577c0f557b2 0.13 -0.04 projects cv-executive 0:b2b029995203 -0.19 -0.04 •graphcompose(java21,pdfbox,mave -cv-executive 0:cf13c8a0c3d0 -0.43 0.00 declarativejavapdflayoutengine.s -cv-executive 0:cfef54184ea6 -0.48 0.00 proposalpipelinesforhiringtoolsa +cv-executive 0:cf13c8a0c3d0 0.05 0.00 declarativejavapdflayoutengine.s +cv-executive 0:cfef54184ea6 0.00 0.00 proposalpipelinesforhiringtoolsa cv-executive 0:1b50bb81df5a -0.20 -0.04 •templatestudio(kotlin,composede -cv-executive 0:7127a108c33c -0.44 0.00 internaltoolforevaluatingcv,prop -cv-executive 0:6b844249c695 -0.46 0.00 freezing. +cv-executive 0:7127a108c33c 0.04 0.00 internaltoolforevaluatingcv,prop +cv-executive 0:6b844249c695 0.02 0.00 freezing. cv-executive 0:25cdca2beb8b -0.18 -0.04 •layoutlint(java21,javaparser,sp -cv-executive 0:f5fa2add7746 -0.42 0.00 staticanalyserthatflagsfragileau -cv-executive 0:9f5414016bd1 -0.47 0.00 toproduction. +cv-executive 0:f5fa2add7746 0.06 0.00 staticanalyserthatflagsfragileau +cv-executive 0:9f5414016bd1 0.01 0.00 toproduction. cv-executive 0:fb1927f88597 -0.19 -0.04 •chromeforge(java,graphcompose,p cv-executive 0:e7e62230eb1f -0.19 0.00 editorial-magazinedocumenttoolki cv-executive 0:c2f296ef30a3 0.09 -0.04 professionalexperience @@ -650,12 +650,12 @@ cv-minimal_underlined 0:80cc87fdc4c4 -0.02 0.00 platformengineerwith10+yearsbuil cv-minimal_underlined 0:bddc0bd093ce 0.03 0.00 systems.specialisedinhigh-throug cv-minimal_underlined 0:95a43d7b3df0 0.07 0.00 snapshot-testedlibrariesthatscal cv-minimal_underlined 0:5f94c50dc5bf 0.06 0.04 technicalskills -cv-minimal_underlined 0:eea537278f83 0.37 0.00 •languages:java21,kotlin,groovy, -cv-minimal_underlined 0:9c2cd7231a53 0.37 0.00 •document&print:pdfbox,apachepoi -cv-minimal_underlined 0:7bf8639ec5d3 0.38 0.00 •layoutengines:customdsldesign,s -cv-minimal_underlined 0:8e66776d28d7 0.41 0.00 •build&infrastructure:maven,grad -cv-minimal_underlined 0:1ed4fc7d7768 0.42 0.00 •testing:junit5,assertj,pdfbox-b -cv-minimal_underlined 0:26671c309eb9 0.42 0.00 •distribution:mavencentral,sonat +cv-minimal_underlined 0:eea537278f83 0.13 0.00 •languages:java21,kotlin,groovy, +cv-minimal_underlined 0:9c2cd7231a53 0.13 0.00 •document&print:pdfbox,apachepoi +cv-minimal_underlined 0:7bf8639ec5d3 0.14 0.00 •layoutengines:customdsldesign,s +cv-minimal_underlined 0:8e66776d28d7 0.17 0.00 •build&infrastructure:maven,grad +cv-minimal_underlined 0:1ed4fc7d7768 0.18 0.00 •testing:junit5,assertj,pdfbox-b +cv-minimal_underlined 0:26671c309eb9 0.18 0.00 •distribution:mavencentral,sonat cv-minimal_underlined 0:68bf0109c6f8 0.17 0.04 education cv-minimal_underlined 0:0ad1957d6bab 0.30 0.00 msccomputerscience cv-minimal_underlined 0:3fa772117260 0.35 -0.95 2019-2021 @@ -670,17 +670,17 @@ cv-minimal_underlined 0:d40b22f00acb 0.42 -0.95 2023-2024 cv-minimal_underlined 0:2669e1f6d55c 0.37 0.00 professionaltrack cv-minimal_underlined 0:3857373829c1 0.43 0.00 java17platformdeep-dive:records, cv-minimal_underlined 0:2577c0f557b2 0.41 0.04 projects -cv-minimal_underlined 0:b2b029995203 0.60 0.00 •graphcompose(java21,pdfbox,mave +cv-minimal_underlined 0:b2b029995203 0.48 0.00 •graphcompose(java21,pdfbox,mave cv-minimal_underlined 0:67593a76ef70 0.45 0.02 declarativejavapdflayoutengine.s cv-minimal_underlined 0:b9292cbd1454 0.49 0.02 pipelinesforhiringtoolsandbillin -cv-minimal_underlined 0:1b50bb81df5a 0.70 0.00 •templatestudio(kotlin,composede +cv-minimal_underlined 0:1b50bb81df5a 0.46 0.00 •templatestudio(kotlin,composede cv-minimal_underlined 0:7127a108c33c 0.45 0.02 internaltoolforevaluatingcv,prop cv-minimal_underlined 0:6b844249c695 0.50 0.02 freezing. -cv-minimal_underlined 0:25cdca2beb8b 0.70 0.00 •layoutlint(java21,javaparser,sp -cv-minimal_underlined 0:7488006d7864 0.43 0.02 staticanalyserthatflagsfragileau +cv-minimal_underlined 0:25cdca2beb8b 0.46 0.00 •layoutlint(java21,javaparser,sp +cv-minimal_underlined 0:7488006d7864 0.55 0.02 staticanalyserthatflagsfragileau cv-minimal_underlined 0:c4d0c415b4f8 0.47 0.02 production. -cv-minimal_underlined 0:fb1927f88597 0.80 0.00 •chromeforge(java,graphcompose,p -cv-minimal_underlined 0:e7e62230eb1f 0.88 0.02 editorial-magazinedocumenttoolki +cv-minimal_underlined 0:fb1927f88597 0.56 0.00 •chromeforge(java,graphcompose,p +cv-minimal_underlined 0:e7e62230eb1f 0.52 0.02 editorial-magazinedocumenttoolki cv-minimal_underlined 0:c2f296ef30a3 0.50 0.04 professionalexperience cv-minimal_underlined 0:a76f47480818 0.63 0.00 seniorplatformengineer cv-minimal_underlined 0:060300b18051 0.69 -0.01 2024-present @@ -767,12 +767,12 @@ cv-modern_professional 0:8817bf9310b6 0.07 0.13 jordan.rivera@example.com|linked cv-modern_professional 0:b944906ed84c 0.02 0.00 professionalsummary cv-modern_professional 0:6fbdc8b79878 -0.01 0.00 platformengineerwith10+yearsbuil cv-modern_professional 0:5f94c50dc5bf -0.01 0.00 technicalskills -cv-modern_professional 0:eea537278f83 0.33 0.00 •languages:java21,kotlin,groovy, -cv-modern_professional 0:9c2cd7231a53 0.36 0.00 •document&print:pdfbox,apachepoi -cv-modern_professional 0:7bf8639ec5d3 0.39 0.00 •layoutengines:customdsldesign,s -cv-modern_professional 0:8e66776d28d7 0.30 0.00 •build&infrastructure:maven,grad -cv-modern_professional 0:1ed4fc7d7768 0.33 0.00 •testing:junit5,assertj,pdfbox-b -cv-modern_professional 0:26671c309eb9 0.36 0.00 •distribution:mavencentral,sonat +cv-modern_professional 0:eea537278f83 0.09 0.00 •languages:java21,kotlin,groovy, +cv-modern_professional 0:9c2cd7231a53 0.12 0.00 •document&print:pdfbox,apachepoi +cv-modern_professional 0:7bf8639ec5d3 0.15 0.00 •layoutengines:customdsldesign,s +cv-modern_professional 0:8e66776d28d7 0.18 0.00 •build&infrastructure:maven,grad +cv-modern_professional 0:1ed4fc7d7768 0.21 0.00 •testing:junit5,assertj,pdfbox-b +cv-modern_professional 0:26671c309eb9 0.12 0.00 •distribution:mavencentral,sonat cv-modern_professional 0:68bf0109c6f8 0.11 0.00 education cv-modern_professional 0:0ad1957d6bab 0.17 0.00 msccomputerscience cv-modern_professional 0:3fa772117260 0.16 0.07 2019-2021 @@ -788,16 +788,16 @@ cv-modern_professional 0:d40b22f00acb 0.22 0.07 2023-2024 cv-modern_professional 0:2669e1f6d55c 0.11 0.00 professionaltrack cv-modern_professional 0:3857373829c1 0.24 0.00 java17platformdeep-dive:records, cv-modern_professional 0:2577c0f557b2 0.10 0.00 projects -cv-modern_professional 0:b2b029995203 0.44 0.00 •graphcompose(java21,pdfbox,mave +cv-modern_professional 0:b2b029995203 0.20 0.00 •graphcompose(java21,pdfbox,mave cv-modern_professional 0:dd39d2ff3520 0.07 0.00 declarativejavapdflayoutengine.s cv-modern_professional 0:9ecaa6df6d41 0.15 0.00 invoice/proposalpipelinesforhiri -cv-modern_professional 0:1b50bb81df5a 0.44 0.00 •templatestudio(kotlin,composede +cv-modern_professional 0:1b50bb81df5a 0.20 0.00 •templatestudio(kotlin,composede cv-modern_professional 0:18eec9e76e2d 0.07 0.00 internaltoolforevaluatingcv,prop cv-modern_professional 0:6bc2807453c6 0.15 0.00 baselinefreezing. -cv-modern_professional 0:25cdca2beb8b 0.44 0.00 •layoutlint(java21,javaparser,sp +cv-modern_professional 0:25cdca2beb8b 0.20 0.00 •layoutlint(java21,javaparser,sp cv-modern_professional 0:cc2850ddd8ba 0.07 0.00 staticanalyserthatflagsfragileau cv-modern_professional 0:7b985a1d2b08 0.15 0.00 shiptoproduction. -cv-modern_professional 0:fb1927f88597 0.44 0.00 •chromeforge(java,graphcompose,p +cv-modern_professional 0:fb1927f88597 0.20 0.00 •chromeforge(java,graphcompose,p cv-modern_professional 0:cda7db6c6ea6 0.07 0.00 editorial-magazinedocumenttoolki cv-modern_professional 0:fe63fe18585a 0.15 0.00 callouts. cv-modern_professional 0:c2f296ef30a3 0.14 0.00 professionalexperience @@ -1846,17 +1846,17 @@ invoice-classic 0:d7f4425b8b16 0.06 1.05 gbp1,030 invoice-classic 0:11239872d178 0.07 0.98 total invoice-classic 0:45a5ad5ad430 0.07 1.01 gbp6,180 invoice-classic 0:ab5aa97074c4 0.33 0.04 notes -invoice-classic 0:b78eb53cbd11 0.36 0.04 •pleaseincludetheinvoicenumberon -invoice-classic 0:8c8fd62f15a7 0.25 0.00 yourremittanceadvice. -invoice-classic 0:f73f1b89e354 0.26 0.04 •allworkwasdeliveredasagreedduri -invoice-classic 0:49c1ececed52 0.27 0.00 theaprilimplementationwindow. +invoice-classic 0:b78eb53cbd11 0.00 0.04 •pleaseincludetheinvoicenumberon +invoice-classic 0:8c8fd62f15a7 0.01 0.00 yourremittanceadvice. +invoice-classic 0:f73f1b89e354 0.02 0.04 •allworkwasdeliveredasagreedduri +invoice-classic 0:49c1ececed52 0.03 0.00 theaprilimplementationwindow. invoice-classic 0:fb854bac39c8 0.33 0.07 paymentterms -invoice-classic 0:24274f39b50b 0.36 0.07 •paymentduewithin14calendardays. -invoice-classic 0:c438440ba377 0.25 0.07 •banktransferpreferred;contact -invoice-classic 0:434cc906ae90 0.26 0.04 billing@graphcompose.devfor -invoice-classic 0:8822ea375315 0.27 0.04 remittancedetails. -invoice-classic 0:1a7b5c840e25 0.28 0.07 •latepaymentsmaydelayadditional -invoice-classic 0:1631a06d245f 0.29 0.04 templatecustomizationwork. +invoice-classic 0:24274f39b50b 0.00 0.07 •paymentduewithin14calendardays. +invoice-classic 0:c438440ba377 0.01 0.07 •banktransferpreferred;contact +invoice-classic 0:434cc906ae90 0.02 0.04 billing@graphcompose.devfor +invoice-classic 0:8822ea375315 0.03 0.04 remittancedetails. +invoice-classic 0:1a7b5c840e25 0.04 0.07 •latepaymentsmaydelayadditional +invoice-classic 0:1631a06d245f 0.05 0.04 templatecustomizationwork. invoice-classic 0:e3337beaa171 0.16 0.00 thankyouforchoosinggraphcomposef invoice-classic_long 0:c26c22b94338 0.15 0.00 graphcomposestudio invoice-classic_long 0:c26c22b94338#1 0.10 0.01 graphcomposestudio @@ -2014,15 +2014,15 @@ invoice-classic_long 1:e7fda33332e9 0.00 1.05 gbp8,978 invoice-classic_long 1:11239872d178 0.13 0.98 total invoice-classic_long 1:90b9dec01ea9 0.13 1.01 gbp53,868 invoice-classic_long 1:ab5aa97074c4 0.27 0.04 notes -invoice-classic_long 1:970fb4bf9d95 0.30 0.04 •sprints1-3weredeliveredunderthe -invoice-classic_long 1:a781fa6c76d2 0.31 0.00 frameworkagreement. -invoice-classic_long 1:c980fb1ebf98 0.32 0.04 •hardwareandthird-partylicencesa -invoice-classic_long 1:be1dea5817a5 0.33 0.00 billedseparately. +invoice-classic_long 1:970fb4bf9d95 -0.06 0.04 •sprints1-3weredeliveredunderthe +invoice-classic_long 1:a781fa6c76d2 -0.05 0.00 frameworkagreement. +invoice-classic_long 1:c980fb1ebf98 -0.04 0.04 •hardwareandthird-partylicencesa +invoice-classic_long 1:be1dea5817a5 -0.03 0.00 billedseparately. invoice-classic_long 1:fb854bac39c8 0.27 0.07 paymentterms -invoice-classic_long 1:24274f39b50b 0.30 0.07 •paymentduewithin14calendardays. -invoice-classic_long 1:c438440ba377 0.31 0.07 •banktransferpreferred;contact -invoice-classic_long 1:434cc906ae90 0.32 0.04 billing@graphcompose.devfor -invoice-classic_long 1:8822ea375315 0.33 0.04 remittancedetails. +invoice-classic_long 1:24274f39b50b -0.06 0.07 •paymentduewithin14calendardays. +invoice-classic_long 1:c438440ba377 -0.05 0.07 •banktransferpreferred;contact +invoice-classic_long 1:434cc906ae90 -0.04 0.04 billing@graphcompose.devfor +invoice-classic_long 1:8822ea375315 -0.03 0.04 remittancedetails. invoice-classic_long 1:e3337beaa171 0.17 0.00 thankyouforchoosinggraphcomposef invoice-consulting 0:d44ceaea97d1 0.26 0.02 level8,1collinsstreet invoice-consulting 0:78a0bb33347f 0.33 0.02 melbournevic3000australia @@ -2875,18 +2875,18 @@ invoice-modern 0:d7f4425b8b16 -0.04 1.03 gbp1,030 invoice-modern 0:11239872d178 -0.03 1.09 total invoice-modern 0:45a5ad5ad430 -0.03 1.11 gbp6,180 invoice-modern 0:ab5aa97074c4 0.21 0.00 notes -invoice-modern 0:b78eb53cbd11 0.24 0.00 •pleaseincludetheinvoicenumberon -invoice-modern 0:8c8fd62f15a7 0.25 -0.04 yourremittanceadvice. -invoice-modern 0:0c98130c526f 0.26 0.00 •allworkwasdeliveredasagreed -invoice-modern 0:2e9787ed79bf 0.15 -0.04 duringtheaprilimplementation -invoice-modern 0:e41212011d98 0.16 -0.04 window. +invoice-modern 0:b78eb53cbd11 -0.12 0.00 •pleaseincludetheinvoicenumberon +invoice-modern 0:8c8fd62f15a7 -0.11 -0.04 yourremittanceadvice. +invoice-modern 0:0c98130c526f -0.10 0.00 •allworkwasdeliveredasagreed +invoice-modern 0:2e9787ed79bf -0.09 -0.04 duringtheaprilimplementation +invoice-modern 0:e41212011d98 -0.08 -0.04 window. invoice-modern 0:fb854bac39c8 0.21 0.07 paymentterms -invoice-modern 0:24274f39b50b 0.24 0.07 •paymentduewithin14calendardays. -invoice-modern 0:c438440ba377 0.25 0.07 •banktransferpreferred;contact -invoice-modern 0:434cc906ae90 0.26 0.04 billing@graphcompose.devfor -invoice-modern 0:8822ea375315 0.15 0.04 remittancedetails. -invoice-modern 0:1a7b5c840e25 0.16 0.07 •latepaymentsmaydelayadditional -invoice-modern 0:1631a06d245f 0.17 0.04 templatecustomizationwork. +invoice-modern 0:24274f39b50b -0.12 0.07 •paymentduewithin14calendardays. +invoice-modern 0:c438440ba377 -0.11 0.07 •banktransferpreferred;contact +invoice-modern 0:434cc906ae90 -0.10 0.04 billing@graphcompose.devfor +invoice-modern 0:8822ea375315 -0.09 0.04 remittancedetails. +invoice-modern 0:1a7b5c840e25 -0.08 0.07 •latepaymentsmaydelayadditional +invoice-modern 0:1631a06d245f -0.07 0.04 templatecustomizationwork. invoice-modern 0:e3337beaa171 0.09 -0.04 thankyouforchoosinggraphcomposef invoice-modern_long 0:52d6e3de4fa0 0.18 0.00 invoice invoice-modern_long 0:61a964e78950 -0.05 0.00 invoicegc-2026-042issued04may202 @@ -3040,15 +3040,15 @@ invoice-modern_long 1:e7fda33332e9 0.00 1.13 gbp8,978 invoice-modern_long 1:11239872d178 0.13 1.09 total invoice-modern_long 1:90b9dec01ea9 0.13 1.09 gbp53,868 invoice-modern_long 1:ab5aa97074c4 0.23 0.00 notes -invoice-modern_long 1:970fb4bf9d95 0.26 0.00 •sprints1-3weredeliveredunderthe -invoice-modern_long 1:a781fa6c76d2 0.27 -0.04 frameworkagreement. -invoice-modern_long 1:c980fb1ebf98 0.28 0.00 •hardwareandthird-partylicencesa -invoice-modern_long 1:be1dea5817a5 0.29 -0.04 billedseparately. +invoice-modern_long 1:970fb4bf9d95 -0.10 0.00 •sprints1-3weredeliveredunderthe +invoice-modern_long 1:a781fa6c76d2 0.03 -0.04 frameworkagreement. +invoice-modern_long 1:c980fb1ebf98 0.04 0.00 •hardwareandthird-partylicencesa +invoice-modern_long 1:be1dea5817a5 0.05 -0.04 billedseparately. invoice-modern_long 1:fb854bac39c8 0.23 0.07 paymentterms -invoice-modern_long 1:24274f39b50b 0.26 0.07 •paymentduewithin14calendardays. -invoice-modern_long 1:c438440ba377 0.27 0.07 •banktransferpreferred;contact -invoice-modern_long 1:434cc906ae90 0.28 0.04 billing@graphcompose.devfor -invoice-modern_long 1:8822ea375315 0.29 0.04 remittancedetails. +invoice-modern_long 1:24274f39b50b -0.10 0.07 •paymentduewithin14calendardays. +invoice-modern_long 1:c438440ba377 0.03 0.07 •banktransferpreferred;contact +invoice-modern_long 1:434cc906ae90 0.04 0.04 billing@graphcompose.devfor +invoice-modern_long 1:8822ea375315 0.05 0.04 remittancedetails. invoice-modern_long 1:e3337beaa171 0.09 -0.04 thankyouforchoosinggraphcomposef invoice-obsidian 0:07f15cde5b18 0.11 -0.04 kestrel invoice-obsidian 0:52d6e3de4fa0 0.15 -0.04 invoice diff --git a/qa/src/test/resources/docx-fidelity/word-windows.tsv b/qa/src/test/resources/docx-fidelity/word-windows.tsv index 845c57397..52c070114 100644 --- a/qa/src/test/resources/docx-fidelity/word-windows.tsv +++ b/qa/src/test/resources/docx-fidelity/word-windows.tsv @@ -1,18 +1,18 @@ # word 16.0 (16.0.20430) on windows document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.11 0.35 0 -cv-boxed_sections 2 2 58 58 0.16 0.37 0 +cv-boxed_sections 2 2 58 58 0.15 0.28 0 cv-centered_headline 2 2 58 56 0.11 0.26 0 cv-charcoal_gold 1 1 85 83 0.18 0.27 0 cv-classic_serif 2 2 57 54 0.16 0.24 0 cv-compact_mono 1 1 58 58 0.15 0.30 0 cv-editorial_blue 2 2 82 82 0.08 0.21 0 cv-engineering_resume 1 1 57 57 0.22 0.34 0 -cv-executive 2 2 60 60 0.19 0.34 0 +cv-executive 2 2 60 60 0.17 0.29 0 cv-midnight_navy 1 1 86 84 0.22 0.82 0 -cv-minimal_underlined 2 2 59 59 0.34 0.63 0 +cv-minimal_underlined 2 2 59 59 0.21 0.50 0 cv-mint_editorial 2 2 59 59 0.08 0.19 0 -cv-modern_professional 2 2 61 59 0.17 0.36 0 +cv-modern_professional 2 2 61 59 0.15 0.21 0 cv-monogram_sidebar 1 1 53 50 0.19 0.39 0 cv-navy_sidebar 1 1 71 69 0.13 0.36 0 cv-nordic_clean 1 1 63 58 0.16 0.35 0 @@ -26,16 +26,16 @@ cv-teal_pulse 1 1 57 57 0.11 0.38 0 cv-terracotta_rail 1 1 102 102 0.36 0.87 0 cv-timeline_minimal 2 2 89 87 0.36 0.47 0 cv-violet_grid 1 1 80 78 0.23 0.42 0 -invoice-classic 1 1 56 56 0.10 0.29 0 -invoice-classic_long 2 2 166 166 0.05 0.16 0 +invoice-classic 1 1 56 56 0.06 0.17 0 +invoice-classic_long 2 2 166 166 0.05 0.13 0 invoice-consulting 1 1 92 92 0.36 1.02 0 invoice-consulting_long 2 2 236 236 0.30 0.65 0 invoice-luma_studio 1 1 97 97 0.09 0.41 0 invoice-luma_studio_long 3 3 192 192 0.16 0.40 0 invoice-merchant 1 1 94 94 0.13 0.22 0 invoice-metered 1 1 99 99 0.09 0.69 0 -invoice-modern 1 1 54 54 0.09 0.25 0 -invoice-modern_long 2 2 162 162 0.05 0.17 0 +invoice-modern 1 1 54 54 0.09 0.18 0 +invoice-modern_long 2 2 162 162 0.05 0.14 0 invoice-obsidian 1 1 81 81 0.21 0.84 0 invoice-payments 1 1 104 104 0.10 0.20 0 invoice-platform 1 1 111 111 0.21 0.41 0 diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java index 9d98bbd02..55a7ea488 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxLayoutMetrics.java @@ -457,7 +457,8 @@ List itemLines(DocumentNode list) { && previous.pageIndex() == fragment.pageIndex() && Math.abs(previous.y() - fragment.y()) < 0.01 && Math.abs(previous.height() - fragment.height()) < 0.01; - ItemText text = new ItemText(paragraph.lines(), fragment.x() + paragraph.padding().left()); + ItemText text = new ItemText(paragraph.lines(), fragment.x() + paragraph.padding().left(), + fragment.pageIndex()); if (!sameItem) { items.add(text); } else if (text.x() >= items.get(items.size() - 1).x()) { @@ -473,8 +474,9 @@ List itemLines(DocumentNode list) { * * @param lines its lines * @param x where its text starts across the page, in points + * @param page the page it was set on */ - record ItemText(List lines, double x) { + record ItemText(List lines, double x, int page) { } /** 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 1623a2081..bc94a6479 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 @@ -532,6 +532,18 @@ public final class DocxSemanticBackend implements SemanticBackend { /** The gap between the wrapped lines of an item of the list being written. */ private double listLineGap; + /** + * Whether the layout starts the list being written in the flow on a page after the block before + * it; never in a cell or an overlay. + */ + private boolean listOpensItsPage; + + /** Whether the layout placed the list being written all on one page. */ + private boolean listOnOnePage; + + /** The page the layout set the last item of the list being written on; -1 before its first. */ + private int itemPage = -1; + /** Where the list being written starts its items on the page, in points; NaN unknown. */ private double listTextLeft = Double.NaN; /** Whether the layout's items are matched to the list's, one by one: each then has its place. */ @@ -4262,6 +4274,13 @@ private void writeList(XWPFDocument document, boolean previousItemWritten = anItemWasWritten; anItemWasWritten = false; double previousLineGap = listLineGap; + boolean previousOpensItsPage = listOpensItsPage; + boolean previousOnOnePage = listOnOnePage; + int previousItemPage = itemPage; + // Where the layout set the list's items, for seating them (seatTheItem). + listOpensItsPage = currentCell == null && overlayDepth == 0 && startsAPageOfItsOwn(list); + listOnOnePage = layout.onOnePage(list); + itemPage = -1; java.util.ArrayDeque previousItemLines = listItemLines; // The list's lineSpacing stands between the lines of an item that wraps, and only @@ -4302,6 +4321,9 @@ private void writeList(XWPFDocument document, pendingItemSpacing = previousItemSpacing; anItemWasWritten = previousItemWritten; listLineGap = previousLineGap; + listOpensItsPage = previousOpensItsPage; + listOnOnePage = previousOnOnePage; + itemPage = previousItemPage; listItemLines = previousItemLines; listTextLeft = previousTextLeft; listItemsMatched = previousMatched; @@ -4809,6 +4831,47 @@ private void writeListLine(XWPFDocument document, DocumentTextStyle style, } writeItemText(para, style, numId != null ? "" : " ".repeat(depth) + marker, label, reading, laidOut); styleTheMark(para, style); + seatTheItem(para, laidOut, numId != null); + } + + /** + * Stands an item's text where the page sets it in its exact lines, as a paragraph's is + * ({@link #shiftToThePagesBaseline}): Word stands an exact line's baseline four fifths of the + * way down it whatever the face, where the page sets it its ascent below the line's top. + * + *

An item's line Word sets low rises into the space above it, the gap between the items, as + * far as that space goes, and owes as much below, which the item under it rises into in turn. + * Under half a point, one opening a cell takes what that space cannot give from the cell's + * padding, or from the space above its row of columns. What is left, where the whole difference + * is half a point or more, and a line set that much high, is the text's position — but not for + * an item whose marker Word draws: Word sets that marker from the paragraph's mark, and + * LibreOffice does not move it by the mark's position — {@code MidnightNavy}'s duties, lowered a + * point by their text's and their mark's, stood 1.15pt under their bullets there (measured). Its + * line keeps what the space above does not take of it.

+ * + *

Not raised into the space above — its position still moved — an item the layout opens a + * page with: the first of a list it starts on a new page in the flow, or one on a later page + * than the item before it. Word drops its space above at the top of a page, and what its line + * owed would stand the rest lower. Nor an item of a list in a cell the layout breaks over a page, + * in a row not kept whole: what an item owed there would be the space above the one opening the + * next page, inside the cell, which Word is not known to drop. An item the layout's lines are not + * matched to is not seated at all.

+ * + * @param laidOut how the layout set the item, or {@code null} where its items are + * not matched + * @param wordDrawsTheMarker whether the item is a Word list's, its marker drawn by Word + */ + private void seatTheItem(XWPFParagraph para, DocxLayoutMetrics.ItemText laidOut, boolean wordDrawsTheMarker) { + if (laidOut == null) { + return; + } + boolean opensItsPage = itemPage < 0 ? listOpensItsPage : laidOut.page() > itemPage; + itemPage = laidOut.page(); + boolean staysOnItsPage = !opensItsPage && (currentCell == null || listOnOnePage || inARowKeptWhole()); + double shift = shiftToThePagesBaseline(para, laidOut.lines(), listLineGap, 0, false, true, staysOnItsPage); + if (!wordDrawsTheMarker) { + raiseRunsFrom(para, 0, Math.round(shift * HALF_POINTS_PER_POINT)); + } } /** @@ -4998,6 +5061,7 @@ private boolean writeRichListLine(XWPFDocument document, DocumentTextStyle style } makeRoomForPictures(para, pictures); styleTheMark(para, markStyle); + seatTheItem(para, laidOut, false); return atItsText || textTab || pictureTab || depth == 0 && drawsNoMarker(marker); } @@ -9829,6 +9893,38 @@ private double seatShift(ParagraphNode node) { */ private double shiftToThePagesBaseline(XWPFParagraph para, ParagraphNode node, double lineTopAbove, boolean heldExact, boolean ownsItsLine) { + // A line starting elsewhere than the page's — a stack's, a line pair's — was cut to fit + // its letters where the page sets them, and is seated however little; so is one cut to + // its pictures' reach, whose ink stands at its edges. + boolean cutToFit = lineTopAbove != 0 || stackedLineHeights.containsKey(node) || heldExact; + // Not a paragraph the layout opens a page with, nor one it breaks over a page, but one in + // a cell of a row Word keeps whole (raiseIntoTheSpaceAbove). + boolean staysOnItsPage = node != paragraphOpeningItsPage && (layout.onOnePage(node) || inARowKeptWhole()); + return shiftToThePagesBaseline(para, layout.lines(node), layout.lineGap(node), lineTopAbove, cutToFit, + ownsItsLine, staysOnItsPage); + } + + /** + * How far Word's baseline in exact lines of text the layout set stands below the page's, in + * points, positive when Word's is lower, after what the space above took of it: the position + * left to move, 0 where the whole difference is under half a point, which the space above, a + * cell's padding or the space above a row of columns takes instead (see + * {@link #shiftToThePagesBaseline(XWPFParagraph, ParagraphNode, double, boolean, boolean)}). + * + * @param lines the lines the layout set the text on + * @param lineGap the gap the layout puts between them, beyond their own height + * @param lineTopAbove how far above the page's first line the Word line starts, in points + * @param cutToFit whether the line was cut to fit its letters or its pictures, and so + * is seated however little + * @param ownsItsLine whether the Word paragraph holds this text alone, and so its line can + * move for it + * @param staysOnItsPage whether the space above the line stays above it on its page, which a + * raise into it needs + */ + private double shiftToThePagesBaseline(XWPFParagraph para, + List lines, + double lineGap, double lineTopAbove, boolean cutToFit, + boolean ownsItsLine, boolean staysOnItsPage) { CTPPr properties = para.getCTP().getPPr(); if (properties == null || !properties.isSetSpacing()) { return 0; @@ -9839,28 +9935,24 @@ private double shiftToThePagesBaseline(XWPFParagraph para, ParagraphNode node, d if (written == null) { return 0; } - for (com.demcha.compose.document.layout.payloads.ParagraphLine line : layout.lines(node)) { + for (com.demcha.compose.document.layout.payloads.ParagraphLine line : lines) { if (line.spans().stream().anyMatch( span -> span instanceof com.demcha.compose.document.layout.payloads.ParagraphTextSpan)) { // Lines whose gaps Word shares out step closer than the page's; the middle one is // matched, so the first and last are off by as little as the steps allow. double wordLine = written / POINT_TO_TWIP; - double middle = Math.max(0, layout.lineCount(node) - 1) / 2.0; - double pageStep = line.lineHeight() + layout.lineGap(node); + double middle = Math.max(0, lines.size() - 1) / 2.0; + double pageStep = line.lineHeight() + lineGap; double pages = lineTopAbove + lineTopsTakenIn.getOrDefault(para.getCTP(), 0.0) + middle * pageStep + line.lineHeight() - line.baselineOffsetFromBottom(); double shift = middle * wordLine + wordLine * DocxTextBands.BASELINE_SHARE - pages; - // A line starting elsewhere than the page's — a stack's, a line pair's — was cut - // to fit its letters where the page sets them, and is seated however little; so - // is one cut to its pictures' reach, whose ink stands at its edges. - boolean cutToFit = lineTopAbove != 0 || stackedLineHeights.containsKey(node) || heldExact; // The position is moved as before where the whole shift called for it, whatever // the space above took off it: a line 0.6pt low with 0.2pt above it is left a tenth // off by its position, not four tenths. boolean moved = Math.abs(shift) >= LEAST_BASELINE_SHIFT_POINTS || cutToFit; if (ownsItsLine && !cutToFit) { // Under half a point the position moves nothing, so the cell's padding may. - shift -= raiseIntoTheSpaceAbove(para, node, spacing, shift, !moved); + shift -= raiseIntoTheSpaceAbove(para, staysOnItsPage, spacing, shift, !moved); if (!moved) { noteAFirstLineLow(para, shift); } @@ -9901,10 +9993,12 @@ private double shiftToThePagesBaseline(XWPFParagraph para, ParagraphNode node, d * that page's top, where the space above moves none of them, and what it owed would stand * what follows them lower. A paragraph in a cell of a row Word keeps whole is on one page * wherever the row lands, though a table's composed cell has no placement of its own to say - * so ({@link #inARowKeptWhole}). Not the space above a panel's first paragraph that the - * panel's top border takes ({@link #takeTheTopBorderInside}): raised into it, the line would - * give it back and the panel grow by what it owed. Less than - * {@link #LEAST_RAISE_INTO_THE_SPACE_ABOVE} is not moved.

+ * so ({@link #inARowKeptWhole}). A list's item is raised as a paragraph is, but for the first + * of a list the layout starts on a new page, one it sets on a later page than the item before + * it, and one of a list in a cell it breaks over a page ({@link #seatTheItem}). Not the space + * above a panel's first paragraph that the panel's top border takes + * ({@link #takeTheTopBorderInside}): raised into it, the line would give it back and the panel + * grow by what it owed. Less than {@link #LEAST_RAISE_INTO_THE_SPACE_ABOVE} is not moved.

* *

A paragraph opening a cell has no space above it where the cell's padding is its own * margin, and its line was left as low as Word set it under half a point. What its space above @@ -9917,17 +10011,17 @@ private double shiftToThePagesBaseline(XWPFParagraph para, ParagraphNode node, d * (measured).

* * @param para the Word paragraph - * @param node the paragraph it is written for + * @param staysOnItsPage whether the layout sets the line on the page the space above it is + * on: neither opening a page nor broken over one * @param spacing the paragraph's spacing, its space above already written * @param raise how far Word sets the line below the page's, in points * @param intoThePadding whether the cell's padding may give what the space above cannot * @return how far the line was raised, in points */ - private double raiseIntoTheSpaceAbove(XWPFParagraph para, ParagraphNode node, CTSpacing spacing, + private double raiseIntoTheSpaceAbove(XWPFParagraph para, boolean staysOnItsPage, CTSpacing spacing, double raise, boolean intoThePadding) { // A band and the text laid over the flow are overlays too (writeOverlayBand, writeOverTheFlow). - if (!(raise >= LEAST_RAISE_INTO_THE_SPACE_ABOVE) || overlayDepth > 0 - || node == paragraphOpeningItsPage || !layout.onOnePage(node) && !inARowKeptWhole()) { + if (!(raise >= LEAST_RAISE_INTO_THE_SPACE_ABOVE) || overlayDepth > 0 || !staysOnItsPage) { return 0; } long room = twipsOf(spacing.isSetBefore() ? spacing.getBefore() : null) diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxExports.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxExports.java index 31d19296c..6b4249070 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxExports.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxExports.java @@ -37,7 +37,8 @@ final class DocxExports { * text, and a row of such cells is moved up as much through its margins * ({@code DocxCellSeatTest}); a panel's or a composed cell's first line rises as much into its * top padding, and a row of columns' out of the space above its table - * ({@code DocxFirstLineSeatTest}). + * ({@code DocxFirstLineSeatTest}); a list's item rises as much into the gap above it, the item + * under it into what it owes ({@code DocxListItemSeatTest}). */ static final long DEFAULT_LINE_RAISE = 6; /** {@link #DEFAULT_LINE_RAISE} for the default face at 7pt, the size tests set small text in. */ diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java new file mode 100644 index 000000000..4c22da6b7 --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java @@ -0,0 +1,401 @@ +package com.demcha.compose.document.backend.semantic.docx; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.dsl.ListBuilder; +import com.demcha.compose.document.dsl.ShapeBuilder; +import com.demcha.compose.document.layout.PlacedFragment; +import com.demcha.compose.document.node.LayerAlign; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextStyle; +import com.demcha.compose.document.table.DocumentTableCell; +import com.demcha.compose.document.table.DocumentTableColumn; +import com.demcha.compose.font.FontName; +import org.apache.poi.xwpf.usermodel.IBodyElement; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; +import org.apache.poi.xwpf.usermodel.XWPFTable; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; +import org.junit.jupiter.api.Test; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSpacing; + +import java.io.ByteArrayInputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * A list's item stands on the page's baseline in Word's exact lines, as a paragraph does. + * + *

Word stands an exact line's baseline four fifths of the way down it, whatever the face; + * the page sets it the face's ascent below the line's top. An item set low rises into the space + * above it, the gap between the items, and owes as much below; one set high, or low by more than + * that space gives, is moved by its position.

+ */ +class DocxListItemSeatTest { + + /** Spectral's ascent, in ems: 1059 of 1000 units. */ + private static final double SPECTRAL_ASCENT = 1.059; + /** Spectral's own line, in ems: its ascent and 463 units of descent. */ + private static final double SPECTRAL_LINE = 1.522; + /** Lato's ascent, in ems: 1974 of 2000 units. */ + private static final double LATO_ASCENT = 0.987; + private static final long RAISE = DocxExports.DEFAULT_LINE_RAISE; + + @Test + void anItemWordSetsLowRisesIntoTheSpaceAboveItAndTheNextIntoWhatItOwes() throws Exception { + // Spectral's 30pt line stands nearly 5pt low in Word. The first item moves up into the + // list's 20pt above it; the second into the gap and what the first owes, the gap left as + // it was; the paragraph under the list stands where it did, the last item's raise above it. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Lead").textStyle(lato(10))) + .addList(list -> list.textStyle(spectral(30)).itemSpacing(10).margin(DocumentInsets.top(20)) + .items("One", "Two")) + .addParagraph(p -> p.text("Body").textStyle(lato(10))))) { + XWPFParagraph one = paragraph(document, "One"); + XWPFParagraph two = paragraph(document, "Two"); + double raise = 0.8 * line(one) - SPECTRAL_ASCENT * 30; + + assertThat(raise).as("the premise: Word's baseline is the lower, by less than the gap").isBetween(1.0, 10.0); + assertThat(before(one)).isCloseTo(Math.round((20 - raise) * 20), within(1L)); + assertThat(before(two)).as("the gap, what the first owed taken back").isCloseTo(200L, within(1L)); + assertThat(position(one)).isZero(); + assertThat(position(two)).isZero(); + assertThat(before(paragraph(document, "Body"))).as("what the last item owes below it") + .isCloseTo(Math.round(raise * 20), within(1L)); + } + } + + @Test + void anItemWhoseMarkerIsWrittenAsTextIsLoweredWithItByItsPosition() throws Exception { + // Lato's 24pt line stands 0.65pt high in Word: moved down, the space above as written, and + // the marker, a run of the item's own, with it. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Lead").textStyle(lato(10))) + .addList(list -> list.textStyle(lato(24)).marker("•").hangingIndent(true).markerGap(6) + .margin(DocumentInsets.top(10)).addItem(text -> text.style("Heading", lato(24)))))) { + XWPFParagraph item = document.getParagraphs().stream() + .filter(paragraph -> paragraph.getText().contains("Heading")).findFirst().orElseThrow(); + double raise = 0.8 * line(item) - LATO_ASCENT * 24; + + assertThat(item.getText()).as("the premise: its marker written as its text").startsWith("•"); + assertThat(item.getCTP().getPPr().getSpacing().getLineRule()).as("the premise: an exact line") + .isEqualTo(org.openxmlformats.schemas.wordprocessingml.x2006.main.STLineSpacingRule.EXACT); + assertThat(raise).as("the premise: Word's baseline is the higher, by more than half a point") + .isLessThan(-0.5); + assertThat(before(item)).isEqualTo(200); + for (var run : item.getCTP().getRList()) { + assertThat(positionOf(run)).as("every run, the marker's too").isEqualTo(Math.round(raise * 2)); + } + } + } + + @Test + void anItemWhoseMarkerWordDrawsIsNotMovedByItsPosition() throws Exception { + // Word sets a Word list's marker from the paragraph's mark, and LibreOffice does not move + // it by the mark's position: lowered by its position, the text would stand off its bullet. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Lead").textStyle(lato(10))) + .addList(list -> list.textStyle(lato(24)).items("Heading")))) { + XWPFParagraph item = paragraph(document, "Heading"); + double raise = 0.8 * line(item) - LATO_ASCENT * 24; + + assertThat(item.getCTP().getPPr().isSetNumPr()).as("the premise: a Word list's item").isTrue(); + assertThat(raise).as("the premise: Word's baseline is the higher, by more than half a point") + .isLessThan(-0.5); + assertThat(position(item)).isZero(); + assertThat(markPosition(item)).isZero(); + } + } + + @Test + void anItemWhoseMarkerWordDrawsRisesAsFarAsTheSpaceAboveAndNoFurther() throws Exception { + // Spectral's 30pt line stands nearly 5pt low in Word, under 2pt of space: the line and its + // marker rise those 2pt, and what is left stays, rather than the text alone moving on. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Lead").textStyle(lato(10))) + .addList(list -> list.textStyle(spectral(30)).margin(DocumentInsets.top(2)).items("Title")) + .addParagraph(p -> p.text("Body").textStyle(lato(10))))) { + XWPFParagraph item = paragraph(document, "Title"); + double raise = 0.8 * line(item) - SPECTRAL_ASCENT * 30; + + assertThat(item.getCTP().getPPr().isSetNumPr()).as("the premise: a Word list's item").isTrue(); + assertThat(raise).as("the premise: lower than the space above holds").isGreaterThan(2.0); + assertThat(before(item)).as("all of the 2pt").isZero(); + assertThat(position(item)).isZero(); + assertThat(markPosition(item)).isZero(); + assertThat(before(paragraph(document, "Body"))).as("the 2pt the line took").isEqualTo(40); + } + } + + @Test + void anItemInALayerStackKeepsTheSpaceAboveIt() throws Exception { + // A stack measures what follows it from the page: a raise owed below an item inside it + // would not reach what follows. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Above").textStyle(lato(10))) + .addLayerStack(stack -> stack.name("Stack") + .back(new ShapeBuilder().name("Backdrop").size(200, 80).build()) + .layer(new ListBuilder().noMarker().textStyle(spectral(30)).margin(DocumentInsets.top(20)) + .items("Title").build(), LayerAlign.TOP_LEFT)))) { + XWPFParagraph item = anywhere(document, "Title"); + double raise = 0.8 * line(item) - SPECTRAL_ASCENT * 30; + + assertThat(raise).as("the premise: Word's baseline is the lower").isGreaterThan(1); + assertThat(after(item)).as("nothing owed below it").isZero(); + assertThat(position(item)).as("raised by its position").isCloseTo(Math.round(raise * 2), within(1L)); + } + } + + @Test + void anItemTheLayoutsLinesAreNotMatchedToIsLeftAsWordSetsIt() throws Exception { + // A list in a table's composed cell lays out no lines of its own to seat it by. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t + .columns(DocumentTableColumn.fixed(200)) + .rowCells(DocumentTableCell.node(new ListBuilder().noMarker().textStyle(spectral(30)) + .items("Inside").build()))))) { + XWPFParagraph inside = inACell(document, "Inside"); + + assertThat(position(inside)).isZero(); + assertThat(before(inside)).isZero(); + } + } + + @Test + void anItemsWrappedLinesAreSeatedAtTheirMiddleOne() throws Exception { + // Two lines 10pt apart and no space above to give: Word's lines are each 5pt taller than + // the face, stepping 5pt closer than the page's, and the error is shared by the two. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addList(list -> list.noMarker().textStyle(spectral(20)).lineSpacing(10).items("First\nSecond")))) { + XWPFParagraph item = document.getParagraphs().stream() + .filter(paragraph -> paragraph.getText().contains("First")).findFirst().orElseThrow(); + double face = SPECTRAL_LINE * 20; + double line = line(item); + double middle = 0.5; + + assertThat(line).as("the premise: the one gap shared by two lines").isCloseTo(face + 5, within(0.05)); + assertThat(position(item)).isCloseTo(Math.round( + (middle * line + 0.8 * line - middle * (face + 10) - SPECTRAL_ASCENT * 20) * 2), within(1L)); + } + } + + @Test + void anItemTheLayoutSetsOnANewPageKeepsTheSpaceAboveIt() throws Exception { + // Word drops a paragraph's space above at the top of a page: raised into it, the second + // item would stand as low as before and what it owed would stand the third lower. + try (DocumentSession session = GraphCompose.document().pageSize(400, 600).margin(DocumentInsets.of(20)).create()) { + session.pageFlow(page -> page.spacer(1, 480) + .addList(list -> list.name("Duties").noMarker().textStyle(spectral(30)).itemSpacing(10) + .margin(DocumentInsets.top(10)).items("One", "Two", "Three"))); + List pages = session.layoutGraph().fragments().stream() + .filter(fragment -> fragment.path().contains("Duties")) + .map(PlacedFragment::pageIndex).toList(); + try (XWPFDocument document = new XWPFDocument( + new ByteArrayInputStream(session.export(new DocxSemanticBackend())))) { + XWPFParagraph one = paragraph(document, "One"); + XWPFParagraph two = paragraph(document, "Two"); + XWPFParagraph three = paragraph(document, "Three"); + double raise = 0.8 * line(one) - SPECTRAL_ASCENT * 30; + + assertThat(pages).as("the premise: the second item opens the next page").containsExactly(0, 1, 1); + assertThat(before(one)).as("the spacer's height past its hairline and the list's 10pt, less the raise") + .isCloseTo(Math.round((480 - 0.1 + 10 - raise) * 20), within(1L)); + assertThat(before(two)).as("the gap and what the first owed, whole") + .isCloseTo(Math.round((10 + raise) * 20), within(1L)); + assertThat(position(two)).as("raised by its position").isCloseTo(Math.round(raise * 2), within(1L)); + assertThat(before(three)).as("the gap less its own raise, nothing owed above it") + .isCloseTo(Math.round((10 - raise) * 20), within(1L)); + } + } + } + + @Test + void theFirstItemOfAListTheLayoutStartsOnANewPageKeepsTheSpaceAboveIt() throws Exception { + try (DocumentSession session = GraphCompose.document().pageSize(400, 600).margin(DocumentInsets.of(20)).create()) { + session.pageFlow(page -> page.spacer(1, 520) + .addList(list -> list.name("Duties").noMarker().textStyle(spectral(30)).margin(DocumentInsets.top(10)) + .items("One")) + .addParagraph(p -> p.text("Body").textStyle(lato(10)))); + List pages = session.layoutGraph().fragments().stream() + .filter(fragment -> fragment.path().contains("Duties")) + .map(PlacedFragment::pageIndex).toList(); + try (XWPFDocument document = new XWPFDocument( + new ByteArrayInputStream(session.export(new DocxSemanticBackend())))) { + XWPFParagraph one = paragraph(document, "One"); + double raise = 0.8 * line(one) - SPECTRAL_ASCENT * 30; + + assertThat(pages).as("the premise: the list opens the next page").containsExactly(1); + assertThat(position(one)).as("raised by its position").isCloseTo(Math.round(raise * 2), within(1L)); + assertThat(before(paragraph(document, "Body"))).as("nothing owed below it").isZero(); + } + } + } + + @Test + void anItemOpeningAPanelRisesIntoItsPadding() throws Exception { + // The default text stands RAISE twips low in Word, under half a point: the item opening a + // card has no space above it, and takes the raise from the card's padding. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph("Before") + .addSection("Card", card -> card.fillColor(DocumentColor.rgb(238, 243, 249)) + .padding(DocumentInsets.of(8)).addList(list -> list.items("Inside"))))) { + XWPFTableCell panel = document.getTables().get(0).getRow(0).getCell(0); + XWPFParagraph inside = panel.getParagraphs().get(0); + + assertThat(inside.getText()).as("the premise: the card opens with the item").isEqualTo("Inside"); + assertThat(topMargin(panel)).as("its padding, less what the item rose into").isEqualTo(8 * 20 - RAISE); + assertThat(after(inside)).as("given back below the item").isEqualTo(RAISE); + assertThat(position(inside)).isZero(); + } + } + + @Test + void anItemOpeningARowsColumnRisesOutOfTheSpaceAboveItsTable() throws Exception { + // A row of columns has its space above written above its table: the row is lifted out of + // it, and each item gives what it rose back below itself. + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.spacing(10) + .addParagraph("Above") + .addRow(row -> row.addSection(column -> column.add(new ListBuilder().items("Left").build())) + .addSection(column -> column.add(new ListBuilder().items("Right").build()))) + .addParagraph("After"))) { + XWPFTable table = document.getTables().get(0); + List body = document.getBodyElements(); + XWPFParagraph above = (XWPFParagraph) body.get(body.indexOf(table) - 1); + + assertThat(after(above)).as("the gap, less the lift").isEqualTo(10 * 20 - RAISE); + for (XWPFTableCell cell : table.getRow(0).getTableCells()) { + XWPFParagraph first = cell.getParagraphs().get(0); + assertThat(first.getCTP().getPPr().isSetNumPr()).as("the premise: the column opens with an item").isTrue(); + assertThat(before(first)).isZero(); + assertThat(after(first)).as("given back below the item").isEqualTo(RAISE); + } + } + } + + @Test + void anItemOfAListInACellTheLayoutBreaksOverAPageKeepsTheSpaceAboveIt() throws Exception { + // A panel the page breaks is one cell going on over the next page: what the first item + // owed would be the space above the one opening that page, inside the cell. + try (DocumentSession session = GraphCompose.document().pageSize(400, 600).margin(DocumentInsets.of(20)).create()) { + session.pageFlow(page -> page.spacer(1, 460) + .addSection("Card", card -> card.fillColor(DocumentColor.rgb(238, 243, 249)) + .addList(list -> list.name("Duties").noMarker().textStyle(spectral(30)).itemSpacing(10) + .margin(DocumentInsets.top(10)).items("One", "Two", "Three")))); + List pages = session.layoutGraph().fragments().stream() + .filter(fragment -> fragment.path().contains("Duties")) + .map(PlacedFragment::pageIndex).toList(); + try (XWPFDocument document = new XWPFDocument( + new ByteArrayInputStream(session.export(new DocxSemanticBackend())))) { + XWPFParagraph one = inACell(document, "One"); + XWPFParagraph two = inACell(document, "Two"); + XWPFParagraph three = inACell(document, "Three"); + double raise = 0.8 * line(three) - SPECTRAL_ASCENT * 30; + + assertThat(pages).as("the premise: the second item opens the next page").containsExactly(0, 1, 1); + assertThat(document.getTables()).as("the premise: one cell over both pages").hasSize(1); + for (XWPFParagraph item : List.of(one, two, three)) { + assertThat(before(item)).as("the space above %s, as written", item.getText()).isEqualTo(200); + assertThat(position(item)).as("raised by its position").isCloseTo(Math.round(raise * 2), within(1L)); + } + } + } + } + + @Test + void anItemOfRunsIsSeatedAsAPlainOneIs() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.text("Lead").textStyle(lato(10))) + .addList(list -> list.textStyle(spectral(30)).marker("•").hangingIndent(true).markerGap(6) + .margin(DocumentInsets.top(20)) + .addItem(text -> text.style("Rich ", spectral(30)).style("item", spectral(30)))))) { + XWPFParagraph item = document.getParagraphs().stream() + .filter(paragraph -> paragraph.getText().contains("item")).findFirst().orElseThrow(); + double raise = 0.8 * line(item) - SPECTRAL_ASCENT * 30; + + assertThat(item.getCTP().getPPr().isSetNumPr()).as("the premise: its marker written as text").isFalse(); + assertThat(before(item)).isCloseTo(Math.round((20 - raise) * 20), within(1L)); + assertThat(position(item)).isZero(); + } + } + + private static DocumentTextStyle spectral(double size) { + return DocumentTextStyle.builder().fontName(FontName.SPECTRAL).size(size).build(); + } + + private static DocumentTextStyle lato(double size) { + return DocumentTextStyle.builder().fontName(FontName.LATO).size(size).build(); + } + + private static XWPFParagraph paragraph(XWPFDocument document, String text) { + return document.getParagraphs().stream() + .filter(paragraph -> paragraph.getText().equals(text)).findFirst().orElseThrow(); + } + + /** The paragraph holding exactly {@code text}, in the body or a table's cell. */ + private static XWPFParagraph anywhere(XWPFDocument document, String text) { + return document.getParagraphs().stream().filter(paragraph -> paragraph.getText().equals(text)).findFirst() + .orElseGet(() -> inACell(document, text)); + } + + /** The paragraph of a table's cell, at any depth, holding exactly {@code text}. */ + private static XWPFParagraph inACell(XWPFDocument document, String text) { + java.util.ArrayDeque tables = new java.util.ArrayDeque<>(document.getTables()); + while (!tables.isEmpty()) { + for (var row : tables.pop().getRows()) { + for (XWPFTableCell cell : row.getTableCells()) { + for (XWPFParagraph paragraph : cell.getParagraphs()) { + if (paragraph.getText().equals(text)) { + return paragraph; + } + } + tables.addAll(cell.getTables()); + } + } + } + throw new AssertionError("no cell holds " + text); + } + + private static double line(XWPFParagraph paragraph) { + return DocxTwips.of(paragraph.getCTP().getPPr().getSpacing().getLine()) / 20.0; + } + + private static CTSpacing spacing(XWPFParagraph paragraph) { + return paragraph.getCTP().getPPr() == null ? null : paragraph.getCTP().getPPr().getSpacing(); + } + + private static long before(XWPFParagraph paragraph) { + CTSpacing spacing = spacing(paragraph); + return spacing == null || !spacing.isSetBefore() ? 0 : DocxTwips.of(spacing.getBefore()); + } + + private static long after(XWPFParagraph paragraph) { + CTSpacing spacing = spacing(paragraph); + return spacing == null || !spacing.isSetAfter() ? 0 : DocxTwips.of(spacing.getAfter()); + } + + private static long topMargin(XWPFTableCell cell) { + var properties = cell.getCTTc().getTcPr(); + return properties == null || !properties.isSetTcMar() || !properties.getTcMar().isSetTop() ? 0 + : DocxTwips.of(properties.getTcMar().getTop().getW()); + } + + /** The {@code w:position} of a paragraph's first run, in half points; 0 for none. */ + private static long position(XWPFParagraph paragraph) { + return positionOf(paragraph.getCTP().getRArray(0)); + } + + private static long positionOf(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run) { + var properties = run.getRPr(); + return properties == null || properties.sizeOfPositionArray() == 0 ? 0 + : ((Number) properties.getPositionArray(0).getVal()).longValue(); + } + + /** The {@code w:position} of a paragraph's mark, in half points; 0 for none. */ + private static long markPosition(XWPFParagraph paragraph) { + var properties = paragraph.getCTP().getPPr(); + return properties == null || !properties.isSetRPr() || properties.getRPr().sizeOfPositionArray() == 0 ? 0 + : ((Number) properties.getRPr().getPositionArray(0).getVal()).longValue(); + } +} diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxVerticalSpacingTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxVerticalSpacingTest.java index 58badef58..67ccb65e1 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxVerticalSpacingTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxVerticalSpacingTest.java @@ -510,16 +510,18 @@ void aListKeepsItsOwnBoxAndTheSpaceBetweenItsItems() throws Exception { .items("First", "Second", "Third")) .addParagraph(p -> p.text("After"))); + // Each item's line rises into the space above it as a paragraph's does, and owes as much + // to the line under it, which rises into it in turn. assertThat(before(paragraphs.get(1))) .as("the list's top edge, above its first item, and no item gap yet") - .isEqualTo(Math.round(4 * TWIPS_PER_POINT)); + .isEqualTo(Math.round(4 * TWIPS_PER_POINT) - RAISE); assertThat(before(paragraphs.get(2))) .as("the gap between two items") .isEqualTo(Math.round(3 * TWIPS_PER_POINT)); assertThat(before(paragraphs.get(3))).isEqualTo(Math.round(3 * TWIPS_PER_POINT)); assertThat(before(paragraphs.get(4))) .as("the list's bottom edge, on the paragraph after it") - .isEqualTo(Math.round(10 * TWIPS_PER_POINT) - RAISE); + .isEqualTo(Math.round(10 * TWIPS_PER_POINT)); } @Test From d69887fef228639779b55f5aaaf080a58e13e714 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 12 Oct 2026 10:31:19 +0100 Subject: [PATCH 2/3] test(qa): rewrite the Linux LibreOffice fidelity baseline from this change's CI run - The Linux baseline was last written for the composed chip's label seat. The text cells beside composed ones, the first lines of cells and now the list items have moved lines since, each inside the gate's half point, so it held them to their old places. - Taken from the DOCX Fidelity artifact of this change's CI run. Against the run before it, 3 documents moved nearer by median, none further; 76 lines nearer, none further; 52 of 62 still pass. --- .../docx-fidelity/libreoffice-linux-lines.tsv | 768 +++++++++--------- .../docx-fidelity/libreoffice-linux.tsv | 36 +- 2 files changed, 402 insertions(+), 402 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 03db20e13..90b89ad0d 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -52,59 +52,59 @@ cv-blue_banner 0:88b226d51fe1 0.01 0.10 pngdiffing,side-by-sidelayout,ba cv-blue_banner 0:339245d63103 -0.28 0.10 layoutlint(java21,javaparser,spo cv-blue_banner 0:8c87b84946f3 0.02 0.10 beforetheyshiptoproduction. cv-boxed_sections 0:8f1010120ed6 0.46 2.20 jordanrivera -cv-boxed_sections 0:8d7a2cca3060 0.23 0.12 +442055551000|jordan.rivera@exam -cv-boxed_sections 0:b944906ed84c 0.28 6.44 professionalsummary +cv-boxed_sections 0:8d7a2cca3060 0.08 0.12 +442055551000|jordan.rivera@exam +cv-boxed_sections 0:b944906ed84c 0.08 6.44 professionalsummary cv-boxed_sections 0:80cc87fdc4c4 -0.05 0.10 platformengineerwith10+yearsbuil cv-boxed_sections 0:bddc0bd093ce -0.05 0.10 systems.specialisedinhigh-throug cv-boxed_sections 0:95a43d7b3df0 -0.04 0.10 snapshot-testedlibrariesthatscal -cv-boxed_sections 0:5f94c50dc5bf 0.27 6.32 technicalskills -cv-boxed_sections 0:eea537278f83 0.24 0.10 •languages:java21,kotlin,groovy, -cv-boxed_sections 0:9c2cd7231a53 0.25 0.10 •document&print:pdfbox,apachepoi -cv-boxed_sections 0:7bf8639ec5d3 0.25 0.10 •layoutengines:customdsldesign,s -cv-boxed_sections 0:8e66776d28d7 0.26 0.10 •build&infrastructure:maven,grad -cv-boxed_sections 0:1ed4fc7d7768 0.26 0.10 •testing:junit5,assertj,pdfbox-b -cv-boxed_sections 0:26671c309eb9 0.27 0.10 •distribution:mavencentral,sonat -cv-boxed_sections 0:68bf0109c6f8 0.28 6.18 education -cv-boxed_sections 0:0ad1957d6bab 0.28 0.10 msccomputerscience -cv-boxed_sections 0:3fa772117260 0.24 -0.76 2019-2021 +cv-boxed_sections 0:5f94c50dc5bf 0.07 6.32 technicalskills +cv-boxed_sections 0:eea537278f83 0.04 0.10 •languages:java21,kotlin,groovy, +cv-boxed_sections 0:9c2cd7231a53 0.05 0.10 •document&print:pdfbox,apachepoi +cv-boxed_sections 0:7bf8639ec5d3 0.05 0.10 •layoutengines:customdsldesign,s +cv-boxed_sections 0:8e66776d28d7 0.06 0.10 •build&infrastructure:maven,grad +cv-boxed_sections 0:1ed4fc7d7768 0.06 0.10 •testing:junit5,assertj,pdfbox-b +cv-boxed_sections 0:26671c309eb9 0.07 0.10 •distribution:mavencentral,sonat +cv-boxed_sections 0:68bf0109c6f8 0.08 6.18 education +cv-boxed_sections 0:0ad1957d6bab 0.08 0.10 msccomputerscience +cv-boxed_sections 0:3fa772117260 0.04 -0.76 2019-2021 cv-boxed_sections 0:47ce0218bbea 0.07 0.10 universityofmanchester cv-boxed_sections 0:2ffe115b06e5 0.08 0.10 distinction.thesis:composablelay -cv-boxed_sections 0:b6749d0b616b 0.31 0.10 bscsoftwareengineering -cv-boxed_sections 0:30f9262640a0 0.28 -0.76 2015-2019 +cv-boxed_sections 0:b6749d0b616b 0.11 0.10 bscsoftwareengineering +cv-boxed_sections 0:30f9262640a0 0.08 -0.76 2015-2019 cv-boxed_sections 0:89ad9656f393 0.10 0.10 imperialcollegelondon cv-boxed_sections 0:baa173b4a82e 0.12 0.10 first-classhonours.specialisatio -cv-boxed_sections 0:6d3d5673c39f 0.35 0.10 oraclejavacertification -cv-boxed_sections 0:d40b22f00acb 0.31 -0.76 2023-2024 +cv-boxed_sections 0:6d3d5673c39f 0.15 0.10 oraclejavacertification +cv-boxed_sections 0:d40b22f00acb 0.11 -0.76 2023-2024 cv-boxed_sections 0:2669e1f6d55c 0.14 0.10 professionaltrack cv-boxed_sections 0:3857373829c1 0.15 0.10 java17platformdeep-dive:records, -cv-boxed_sections 0:2577c0f557b2 0.37 6.18 projects -cv-boxed_sections 0:b2b029995203 0.34 0.10 •graphcompose(java21,pdfbox,mave +cv-boxed_sections 0:2577c0f557b2 0.17 6.18 projects +cv-boxed_sections 0:b2b029995203 0.14 0.10 •graphcompose(java21,pdfbox,mave cv-boxed_sections 0:67593a76ef70 0.04 0.11 declarativejavapdflayoutengine.s cv-boxed_sections 0:b9292cbd1454 0.05 0.11 pipelinesforhiringtoolsandbillin -cv-boxed_sections 0:1b50bb81df5a 0.35 0.10 •templatestudio(kotlin,composede -cv-boxed_sections 0:836e76daecbf 0.36 0.11 internaltoolforevaluatingcv,prop -cv-boxed_sections 0:25cdca2beb8b 0.36 0.10 •layoutlint(java21,javaparser,sp +cv-boxed_sections 0:1b50bb81df5a 0.15 0.10 •templatestudio(kotlin,composede +cv-boxed_sections 0:836e76daecbf 0.16 0.11 internaltoolforevaluatingcv,prop +cv-boxed_sections 0:25cdca2beb8b 0.16 0.10 •layoutlint(java21,javaparser,sp cv-boxed_sections 0:7488006d7864 0.07 0.11 staticanalyserthatflagsfragileau cv-boxed_sections 0:c4d0c415b4f8 0.07 0.11 production. -cv-boxed_sections 0:fb1927f88597 0.38 0.10 •chromeforge(java,graphcompose,p -cv-boxed_sections 0:e7e62230eb1f 0.38 0.11 editorial-magazinedocumenttoolki -cv-boxed_sections 0:c2f296ef30a3 0.40 6.46 professionalexperience -cv-boxed_sections 0:a76f47480818 0.39 0.10 seniorplatformengineer -cv-boxed_sections 0:060300b18051 0.36 0.01 2024-present +cv-boxed_sections 0:fb1927f88597 0.18 0.10 •chromeforge(java,graphcompose,p +cv-boxed_sections 0:e7e62230eb1f 0.18 0.11 editorial-magazinedocumenttoolki +cv-boxed_sections 0:c2f296ef30a3 0.20 6.46 professionalexperience +cv-boxed_sections 0:a76f47480818 0.19 0.10 seniorplatformengineer +cv-boxed_sections 0:060300b18051 0.16 0.01 2024-present cv-boxed_sections 0:f890faef907f 0.18 0.10 northwindsystems cv-boxed_sections 1:49faf14f1f56 -0.13 0.10 ledthereusabledocument-generatio cv-boxed_sections 1:6bd3f168e89e -0.13 0.10 maintenancetimeby70%byretiringpe -cv-boxed_sections 1:daf65306b3b6 0.20 0.10 softwareengineer -cv-boxed_sections 1:e7cc1822c6dc 0.17 -0.76 2021-2024 +cv-boxed_sections 1:daf65306b3b6 0.00 0.10 softwareengineer +cv-boxed_sections 1:e7cc1822c6dc -0.03 -0.76 2021-2024 cv-boxed_sections 1:998c1a985022 -0.01 0.10 brightleaflabs cv-boxed_sections 1:e55664011cda -0.09 0.10 builtbackendservicesandproductio cv-boxed_sections 1:e80236b6d2b1 -0.09 0.10 itexttoacustomlayoutengine,elimi -cv-boxed_sections 1:41bb54fbae8d 0.24 0.10 backendengineer -cv-boxed_sections 1:3fa772117260 0.21 -0.76 2019-2021 +cv-boxed_sections 1:41bb54fbae8d 0.04 0.10 backendengineer +cv-boxed_sections 1:3fa772117260 0.01 -0.76 2019-2021 cv-boxed_sections 1:ef148f221e31 0.03 0.10 helixprintco cv-boxed_sections 1:9d31a6bfbc69 -0.05 0.10 maintainedahigh-volumeinvoice-pr cv-boxed_sections 1:03af8a628309 -0.05 0.10 change. -cv-boxed_sections 1:676dfdd56ad2 0.27 6.49 additionalinformation +cv-boxed_sections 1:676dfdd56ad2 0.07 6.49 additionalinformation cv-boxed_sections 1:f20997652453 0.04 0.10 languages:english(fluent),german cv-boxed_sections 1:ec9d3622395e 0.04 0.10 workeligibility:eligibletoworkin cv-boxed_sections 1:dd20ae01c3cf 0.05 0.10 opensource:maintainerofgraphcomp @@ -250,7 +250,7 @@ cv-charcoal_gold 0:3def82282260 0.10 -0.71 technicaltools cv-charcoal_gold 0:eb04bd42c7fb 0.16 -1.19 microsoft365|jira|confluence|asa cv-classic_serif 0:8f1010120ed6 -0.15 2.60 jordanrivera cv-classic_serif 0:8d7a2cca3060 -0.01 -0.52 +442055551000|jordan.rivera@exam -cv-classic_serif 0:3c69d6bb4892 0.24 20.27 professionalprofile +cv-classic_serif 0:3c69d6bb4892 0.04 20.27 professionalprofile cv-classic_serif 0:32c1cf49a2fe 0.19 0.10 skills cv-classic_serif 0:873a24d153c8 0.21 0.10 languages:java21,kotlin,groovy,p cv-classic_serif 0:05e907ef2b18 0.19 0.10 document&print:pdfbox,apachepoi( @@ -259,19 +259,19 @@ cv-classic_serif 0:1cd141fef347 0.14 0.10 build&infrastructure:maven,gradl cv-classic_serif 0:181ddda7f959 0.11 0.10 testing:junit5,assertj,pdfbox-ba cv-classic_serif 0:c40c656d9558 0.09 0.10 distribution:mavencentral,sonaty cv-classic_serif 0:53e5e7c5a884 0.05 0.10 experience -cv-classic_serif 0:a76f47480818 0.26 0.10 seniorplatformengineer -cv-classic_serif 0:060300b18051 0.23 0.26 2024-present +cv-classic_serif 0:a76f47480818 0.06 0.10 seniorplatformengineer +cv-classic_serif 0:060300b18051 0.03 0.26 2024-present cv-classic_serif 0:f890faef907f 0.04 0.10 northwindsystems cv-classic_serif 0:49faf14f1f56 0.01 0.10 ledthereusabledocument-generatio cv-classic_serif 0:6bd3f168e89e 0.00 0.10 maintenancetimeby70%byretiringpe -cv-classic_serif 0:daf65306b3b6 0.28 0.10 softwareengineer -cv-classic_serif 0:e7cc1822c6dc 0.25 1.12 2021-2024 +cv-classic_serif 0:daf65306b3b6 0.08 0.10 softwareengineer +cv-classic_serif 0:e7cc1822c6dc 0.05 1.12 2021-2024 cv-classic_serif 0:998c1a985022 0.06 0.10 brightleaflabs cv-classic_serif 0:e55664011cda 0.03 0.10 builtbackendservicesandproductio cv-classic_serif 0:e80236b6d2b1 0.02 0.10 itexttoacustomlayoutengine,elimi -cv-classic_serif 0:41bb54fbae8d 0.29 0.10 backendengineer -cv-classic_serif 0:3fa772117260 0.26 1.12 2019-2021 -cv-classic_serif 0:3fa772117260#1 0.28 1.12 2019-2021 +cv-classic_serif 0:41bb54fbae8d 0.09 0.10 backendengineer +cv-classic_serif 0:3fa772117260 0.06 1.12 2019-2021 +cv-classic_serif 0:3fa772117260#1 0.08 1.12 2019-2021 cv-classic_serif 0:ef148f221e31 0.07 0.10 helixprintco cv-classic_serif 0:9d31a6bfbc69 0.04 0.10 maintainedahigh-volumeinvoice-pr cv-classic_serif 0:03af8a628309 0.03 0.10 change. @@ -286,15 +286,15 @@ cv-classic_serif 0:dc31b6a18325 0.08 0.10 staticanalyserthatflagsfragileau cv-classic_serif 0:ee180ea1e0a5 0.10 0.10 chromeforge(java,graphcompose,pa cv-classic_serif 0:e7e62230eb1f 0.08 0.10 editorial-magazinedocumenttoolki cv-classic_serif 0:68bf0109c6f8 0.10 0.10 education -cv-classic_serif 0:0ad1957d6bab 0.31 0.10 msccomputerscience +cv-classic_serif 0:0ad1957d6bab 0.11 0.10 msccomputerscience cv-classic_serif 0:47ce0218bbea 0.09 0.10 universityofmanchester cv-classic_serif 0:2ffe115b06e5 0.11 0.10 distinction.thesis:composablelay -cv-classic_serif 0:b6749d0b616b 0.33 0.10 bscsoftwareengineering -cv-classic_serif 0:30f9262640a0 0.30 1.12 2015-2019 +cv-classic_serif 0:b6749d0b616b 0.13 0.10 bscsoftwareengineering +cv-classic_serif 0:30f9262640a0 0.10 1.12 2015-2019 cv-classic_serif 1:89ad9656f393 0.16 0.10 imperialcollegelondon cv-classic_serif 1:baa173b4a82e -0.02 0.10 first-classhonours.specialisatio -cv-classic_serif 1:6d3d5673c39f 0.20 0.10 oraclejavacertification -cv-classic_serif 1:d40b22f00acb 0.17 1.12 2023-2024 +cv-classic_serif 1:6d3d5673c39f 0.00 0.10 oraclejavacertification +cv-classic_serif 1:d40b22f00acb -0.03 1.12 2023-2024 cv-classic_serif 1:2669e1f6d55c -0.02 0.10 professionaltrack cv-classic_serif 1:3857373829c1 0.00 0.10 java17platformdeep-dive:records, cv-classic_serif 1:7b476727faf3 0.03 0.10 additional @@ -302,7 +302,7 @@ cv-classic_serif 1:f20997652453 0.04 0.10 languages:english(fluent),german cv-classic_serif 1:ec9d3622395e 0.02 0.10 workeligibility:eligibletoworkin cv-classic_serif 1:dd20ae01c3cf -0.01 0.10 opensource:maintainerofgraphcomp cv-classic_serif 1:33c35e9d236d -0.03 0.10 speaking:jvmsummit2024,devoxxuk2 -cv-compact_mono 0:8f1010120ed6 0.31 0.10 jordanrivera +cv-compact_mono 0:8f1010120ed6 -0.04 0.10 jordanrivera cv-compact_mono 0:f9e4560bd641 -0.24 0.10 london,uk/+442055551000/jordan.r cv-compact_mono 0:32c1cf49a2fe 0.14 0.10 skills cv-compact_mono 0:551282493299 -0.11 0.10 languagesjava21,kotlin,groovy,py @@ -528,14 +528,14 @@ cv-executive 0:2669e1f6d55c -0.26 0.10 professionaltrack cv-executive 0:3857373829c1 -0.26 0.10 java17platformdeep-dive:records, cv-executive 0:2577c0f557b2 -0.02 0.10 projects cv-executive 0:b2b029995203 -0.28 0.10 •graphcompose(java21,pdfbox,mave -cv-executive 0:cf13c8a0c3d0 -0.53 0.12 declarativejavapdflayoutengine.s -cv-executive 0:cfef54184ea6 -0.53 0.12 proposalpipelinesforhiringtoolsa +cv-executive 0:cf13c8a0c3d0 0.02 0.12 declarativejavapdflayoutengine.s +cv-executive 0:cfef54184ea6 0.02 0.12 proposalpipelinesforhiringtoolsa cv-executive 0:1b50bb81df5a -0.28 0.10 •templatestudio(kotlin,composede -cv-executive 0:7127a108c33c -0.53 0.12 internaltoolforevaluatingcv,prop -cv-executive 0:6b844249c695 -0.53 0.12 freezing. +cv-executive 0:7127a108c33c 0.02 0.12 internaltoolforevaluatingcv,prop +cv-executive 0:6b844249c695 0.02 0.12 freezing. cv-executive 0:25cdca2beb8b -0.28 0.10 •layoutlint(java21,javaparser,sp -cv-executive 0:f5fa2add7746 -0.53 0.12 staticanalyserthatflagsfragileau -cv-executive 0:9f5414016bd1 -0.53 0.12 toproduction. +cv-executive 0:f5fa2add7746 0.02 0.12 staticanalyserthatflagsfragileau +cv-executive 0:9f5414016bd1 0.02 0.12 toproduction. cv-executive 0:fb1927f88597 -0.28 0.10 •chromeforge(java,graphcompose,p cv-executive 0:e7e62230eb1f -0.28 0.12 editorial-magazinedocumenttoolki cv-executive 0:c2f296ef30a3 -0.04 0.10 professionalexperience @@ -644,60 +644,60 @@ cv-midnight_navy 0:86e529fac99d -0.07 0.65 hubspotcontentmarketingcertified cv-midnight_navy 0:200b36afcfde -0.09 0.09 hubspotacademy cv-midnight_navy 0:1bea20e1df19 -0.11 0.09 2021 cv-minimal_underlined 0:8f1010120ed6 0.46 2.20 jordanrivera -cv-minimal_underlined 0:8d7a2cca3060 0.18 0.12 +442055551000|jordan.rivera@exam -cv-minimal_underlined 0:b944906ed84c 0.24 0.10 professionalsummary +cv-minimal_underlined 0:8d7a2cca3060 0.03 0.12 +442055551000|jordan.rivera@exam +cv-minimal_underlined 0:b944906ed84c 0.04 0.10 professionalsummary cv-minimal_underlined 0:80cc87fdc4c4 -0.02 0.10 platformengineerwith10+yearsbuil cv-minimal_underlined 0:bddc0bd093ce -0.02 0.10 systems.specialisedinhigh-throug cv-minimal_underlined 0:95a43d7b3df0 -0.01 0.10 snapshot-testedlibrariesthatscal -cv-minimal_underlined 0:5f94c50dc5bf 0.32 0.10 technicalskills -cv-minimal_underlined 0:eea537278f83 0.35 0.10 •languages:java21,kotlin,groovy, -cv-minimal_underlined 0:9c2cd7231a53 0.36 0.10 •document&print:pdfbox,apachepoi -cv-minimal_underlined 0:7bf8639ec5d3 0.36 0.10 •layoutengines:customdsldesign,s -cv-minimal_underlined 0:8e66776d28d7 0.37 0.10 •build&infrastructure:maven,grad -cv-minimal_underlined 0:1ed4fc7d7768 0.37 0.10 •testing:junit5,assertj,pdfbox-b -cv-minimal_underlined 0:26671c309eb9 0.38 0.10 •distribution:mavencentral,sonat -cv-minimal_underlined 0:68bf0109c6f8 0.41 0.10 education -cv-minimal_underlined 0:0ad1957d6bab 0.47 0.10 msccomputerscience -cv-minimal_underlined 0:3fa772117260 0.43 -0.76 2019-2021 +cv-minimal_underlined 0:5f94c50dc5bf 0.12 0.10 technicalskills +cv-minimal_underlined 0:eea537278f83 0.15 0.10 •languages:java21,kotlin,groovy, +cv-minimal_underlined 0:9c2cd7231a53 0.16 0.10 •document&print:pdfbox,apachepoi +cv-minimal_underlined 0:7bf8639ec5d3 0.16 0.10 •layoutengines:customdsldesign,s +cv-minimal_underlined 0:8e66776d28d7 0.17 0.10 •build&infrastructure:maven,grad +cv-minimal_underlined 0:1ed4fc7d7768 0.17 0.10 •testing:junit5,assertj,pdfbox-b +cv-minimal_underlined 0:26671c309eb9 0.18 0.10 •distribution:mavencentral,sonat +cv-minimal_underlined 0:68bf0109c6f8 0.21 0.10 education +cv-minimal_underlined 0:0ad1957d6bab 0.27 0.10 msccomputerscience +cv-minimal_underlined 0:3fa772117260 0.23 -0.76 2019-2021 cv-minimal_underlined 0:47ce0218bbea 0.26 0.10 universityofmanchester cv-minimal_underlined 0:2ffe115b06e5 0.27 0.10 distinction.thesis:composablelay -cv-minimal_underlined 0:b6749d0b616b 0.50 0.10 bscsoftwareengineering -cv-minimal_underlined 0:30f9262640a0 0.47 -0.76 2015-2019 +cv-minimal_underlined 0:b6749d0b616b 0.30 0.10 bscsoftwareengineering +cv-minimal_underlined 0:30f9262640a0 0.27 -0.76 2015-2019 cv-minimal_underlined 0:89ad9656f393 0.29 0.10 imperialcollegelondon cv-minimal_underlined 0:baa173b4a82e 0.31 0.10 first-classhonours.specialisatio -cv-minimal_underlined 0:6d3d5673c39f 0.54 0.10 oraclejavacertification -cv-minimal_underlined 0:d40b22f00acb 0.50 -0.76 2023-2024 +cv-minimal_underlined 0:6d3d5673c39f 0.34 0.10 oraclejavacertification +cv-minimal_underlined 0:d40b22f00acb 0.30 -0.76 2023-2024 cv-minimal_underlined 0:2669e1f6d55c 0.33 0.10 professionaltrack cv-minimal_underlined 0:3857373829c1 0.34 0.10 java17platformdeep-dive:records, -cv-minimal_underlined 0:2577c0f557b2 0.57 0.10 projects -cv-minimal_underlined 0:b2b029995203 0.61 0.10 •graphcompose(java21,pdfbox,mave +cv-minimal_underlined 0:2577c0f557b2 0.37 0.10 projects +cv-minimal_underlined 0:b2b029995203 0.41 0.10 •graphcompose(java21,pdfbox,mave cv-minimal_underlined 0:67593a76ef70 0.31 0.11 declarativejavapdflayoutengine.s cv-minimal_underlined 0:b9292cbd1454 0.32 0.11 pipelinesforhiringtoolsandbillin -cv-minimal_underlined 0:1b50bb81df5a 0.62 0.10 •templatestudio(kotlin,composede +cv-minimal_underlined 0:1b50bb81df5a 0.42 0.10 •templatestudio(kotlin,composede cv-minimal_underlined 0:7127a108c33c 0.33 0.11 internaltoolforevaluatingcv,prop cv-minimal_underlined 0:6b844249c695 0.33 0.11 freezing. -cv-minimal_underlined 0:25cdca2beb8b 0.64 0.10 •layoutlint(java21,javaparser,sp +cv-minimal_underlined 0:25cdca2beb8b 0.44 0.10 •layoutlint(java21,javaparser,sp cv-minimal_underlined 0:7488006d7864 0.34 0.11 staticanalyserthatflagsfragileau cv-minimal_underlined 0:c4d0c415b4f8 0.35 0.11 production. -cv-minimal_underlined 0:fb1927f88597 0.65 0.10 •chromeforge(java,graphcompose,p -cv-minimal_underlined 0:e7e62230eb1f 0.66 0.11 editorial-magazinedocumenttoolki -cv-minimal_underlined 0:c2f296ef30a3 0.69 0.10 professionalexperience -cv-minimal_underlined 0:a76f47480818 0.75 0.10 seniorplatformengineer -cv-minimal_underlined 0:060300b18051 0.71 0.01 2024-present +cv-minimal_underlined 0:fb1927f88597 0.45 0.10 •chromeforge(java,graphcompose,p +cv-minimal_underlined 0:e7e62230eb1f 0.46 0.11 editorial-magazinedocumenttoolki +cv-minimal_underlined 0:c2f296ef30a3 0.49 0.10 professionalexperience +cv-minimal_underlined 0:a76f47480818 0.55 0.10 seniorplatformengineer +cv-minimal_underlined 0:060300b18051 0.51 0.01 2024-present cv-minimal_underlined 0:f890faef907f 0.54 0.10 northwindsystems cv-minimal_underlined 1:49faf14f1f56 -0.13 0.10 ledthereusabledocument-generatio cv-minimal_underlined 1:6bd3f168e89e -0.13 0.10 maintenancetimeby70%byretiringpe -cv-minimal_underlined 1:daf65306b3b6 0.20 0.10 softwareengineer -cv-minimal_underlined 1:e7cc1822c6dc 0.17 -0.76 2021-2024 +cv-minimal_underlined 1:daf65306b3b6 0.00 0.10 softwareengineer +cv-minimal_underlined 1:e7cc1822c6dc -0.03 -0.76 2021-2024 cv-minimal_underlined 1:998c1a985022 -0.01 0.10 brightleaflabs cv-minimal_underlined 1:e55664011cda -0.09 0.10 builtbackendservicesandproductio cv-minimal_underlined 1:e80236b6d2b1 -0.09 0.10 itexttoacustomlayoutengine,elimi -cv-minimal_underlined 1:41bb54fbae8d 0.24 0.10 backendengineer -cv-minimal_underlined 1:3fa772117260 0.21 -0.76 2019-2021 +cv-minimal_underlined 1:41bb54fbae8d 0.04 0.10 backendengineer +cv-minimal_underlined 1:3fa772117260 0.01 -0.76 2019-2021 cv-minimal_underlined 1:ef148f221e31 0.03 0.10 helixprintco cv-minimal_underlined 1:9d31a6bfbc69 -0.05 0.10 maintainedahigh-volumeinvoice-pr cv-minimal_underlined 1:03af8a628309 -0.05 0.10 change. -cv-minimal_underlined 1:676dfdd56ad2 0.28 0.10 additionalinformation +cv-minimal_underlined 1:676dfdd56ad2 0.08 0.10 additionalinformation cv-minimal_underlined 1:f20997652453 0.12 0.10 languages:english(fluent),german cv-minimal_underlined 1:ec9d3622395e 0.12 0.10 workeligibility:eligibletoworkin cv-minimal_underlined 1:dd20ae01c3cf 0.13 0.10 opensource:maintainerofgraphcomp @@ -767,51 +767,51 @@ cv-modern_professional 0:8817bf9310b6 0.11 0.10 jordan.rivera@example.com|linked cv-modern_professional 0:b944906ed84c -0.04 0.10 professionalsummary cv-modern_professional 0:6fbdc8b79878 -0.07 0.10 platformengineerwith10+yearsbuil cv-modern_professional 0:5f94c50dc5bf -0.04 0.10 technicalskills -cv-modern_professional 0:eea537278f83 0.23 0.10 •languages:java21,kotlin,groovy, -cv-modern_professional 0:9c2cd7231a53 0.23 0.10 •document&print:pdfbox,apachepoi -cv-modern_professional 0:7bf8639ec5d3 0.23 0.10 •layoutengines:customdsldesign,s -cv-modern_professional 0:8e66776d28d7 0.23 0.10 •build&infrastructure:maven,grad -cv-modern_professional 0:1ed4fc7d7768 0.23 0.10 •testing:junit5,assertj,pdfbox-b -cv-modern_professional 0:26671c309eb9 0.23 0.10 •distribution:mavencentral,sonat +cv-modern_professional 0:eea537278f83 0.03 0.10 •languages:java21,kotlin,groovy, +cv-modern_professional 0:9c2cd7231a53 0.03 0.10 •document&print:pdfbox,apachepoi +cv-modern_professional 0:7bf8639ec5d3 0.03 0.10 •layoutengines:customdsldesign,s +cv-modern_professional 0:8e66776d28d7 0.03 0.10 •build&infrastructure:maven,grad +cv-modern_professional 0:1ed4fc7d7768 0.03 0.10 •testing:junit5,assertj,pdfbox-b +cv-modern_professional 0:26671c309eb9 0.03 0.10 •distribution:mavencentral,sonat cv-modern_professional 0:68bf0109c6f8 -0.03 0.10 education -cv-modern_professional 0:0ad1957d6bab 0.23 0.10 msccomputerscience -cv-modern_professional 0:3fa772117260 0.24 0.18 2019-2021 -cv-modern_professional 0:3fa772117260#1 0.25 0.18 2019-2021 +cv-modern_professional 0:0ad1957d6bab 0.03 0.10 msccomputerscience +cv-modern_professional 0:3fa772117260 0.04 0.18 2019-2021 +cv-modern_professional 0:3fa772117260#1 0.05 0.18 2019-2021 cv-modern_professional 0:47ce0218bbea -0.02 0.10 universityofmanchester cv-modern_professional 0:2ffe115b06e5 0.04 0.10 distinction.thesis:composablelay -cv-modern_professional 0:b6749d0b616b 0.23 0.10 bscsoftwareengineering -cv-modern_professional 0:30f9262640a0 0.24 0.18 2015-2019 +cv-modern_professional 0:b6749d0b616b 0.03 0.10 bscsoftwareengineering +cv-modern_professional 0:30f9262640a0 0.04 0.18 2015-2019 cv-modern_professional 0:89ad9656f393 -0.02 0.10 imperialcollegelondon cv-modern_professional 0:baa173b4a82e 0.04 0.10 first-classhonours.specialisatio -cv-modern_professional 0:6d3d5673c39f 0.23 0.10 oraclejavacertification -cv-modern_professional 0:d40b22f00acb 0.24 0.18 2023-2024 +cv-modern_professional 0:6d3d5673c39f 0.03 0.10 oraclejavacertification +cv-modern_professional 0:d40b22f00acb 0.04 0.18 2023-2024 cv-modern_professional 0:2669e1f6d55c -0.02 0.10 professionaltrack cv-modern_professional 0:3857373829c1 0.04 0.10 java17platformdeep-dive:records, cv-modern_professional 0:2577c0f557b2 -0.03 0.10 projects -cv-modern_professional 0:b2b029995203 0.24 0.10 •graphcompose(java21,pdfbox,mave +cv-modern_professional 0:b2b029995203 0.04 0.10 •graphcompose(java21,pdfbox,mave cv-modern_professional 0:dd39d2ff3520 -0.06 0.10 declarativejavapdflayoutengine.s cv-modern_professional 0:9ecaa6df6d41 -0.06 0.10 invoice/proposalpipelinesforhiri -cv-modern_professional 0:1b50bb81df5a 0.24 0.10 •templatestudio(kotlin,composede +cv-modern_professional 0:1b50bb81df5a 0.04 0.10 •templatestudio(kotlin,composede cv-modern_professional 0:18eec9e76e2d -0.06 0.10 internaltoolforevaluatingcv,prop cv-modern_professional 0:6bc2807453c6 -0.06 0.10 baselinefreezing. -cv-modern_professional 0:25cdca2beb8b 0.24 0.10 •layoutlint(java21,javaparser,sp +cv-modern_professional 0:25cdca2beb8b 0.04 0.10 •layoutlint(java21,javaparser,sp cv-modern_professional 0:cc2850ddd8ba -0.06 0.10 staticanalyserthatflagsfragileau cv-modern_professional 0:7b985a1d2b08 -0.06 0.10 shiptoproduction. -cv-modern_professional 0:fb1927f88597 0.24 0.10 •chromeforge(java,graphcompose,p +cv-modern_professional 0:fb1927f88597 0.04 0.10 •chromeforge(java,graphcompose,p cv-modern_professional 0:cda7db6c6ea6 -0.06 0.10 editorial-magazinedocumenttoolki cv-modern_professional 0:fe63fe18585a -0.06 0.10 callouts. cv-modern_professional 0:c2f296ef30a3 -0.02 0.10 professionalexperience -cv-modern_professional 0:a76f47480818 0.24 0.10 seniorplatformengineer -cv-modern_professional 0:060300b18051 0.25 0.15 2024-present +cv-modern_professional 0:a76f47480818 0.04 0.10 seniorplatformengineer +cv-modern_professional 0:060300b18051 0.05 0.15 2024-present cv-modern_professional 0:f890faef907f -0.01 0.10 northwindsystems cv-modern_professional 0:a6525dacbac7 -0.05 0.10 ledthereusabledocument-generatio cv-modern_professional 0:15e3905f4890 -0.05 0.10 reducedtemplatemaintenancetimeby -cv-modern_professional 0:daf65306b3b6 0.24 0.10 softwareengineer -cv-modern_professional 0:e7cc1822c6dc 0.25 0.18 2021-2024 +cv-modern_professional 0:daf65306b3b6 0.04 0.10 softwareengineer +cv-modern_professional 0:e7cc1822c6dc 0.05 0.18 2021-2024 cv-modern_professional 0:998c1a985022 -0.01 0.10 brightleaflabs cv-modern_professional 0:e86691b0219f -0.05 0.10 builtbackendservicesandproductio cv-modern_professional 0:4ec57434564a -0.05 0.10 migrationfromitexttoacustomlayou -cv-modern_professional 0:41bb54fbae8d 0.24 0.10 backendengineer +cv-modern_professional 0:41bb54fbae8d 0.04 0.10 backendengineer cv-modern_professional 0:ef148f221e31 -0.01 0.10 helixprintco cv-modern_professional 0:a32c7f2401d9 -0.05 0.10 maintainedahigh-volumeinvoice-pr cv-modern_professional 0:7068c1d7034b -0.05 0.10 everytemplatechange. @@ -1805,23 +1805,23 @@ cv-violet_grid 0:9c9b13446cd9 -1.54 0.02 spanish cv-violet_grid 0:e35242c809c8 -1.51 0.02 portuguese cv-violet_grid 0:c5d1de4964d6 -1.51 -0.03 conversational cv-violet_grid 0:1de4fede4f63 -1.26 22.41 idesignwithempathy,iteratewithpu -invoice-classic 0:c26c22b94338 0.49 0.10 graphcomposestudio +invoice-classic 0:c26c22b94338 0.09 0.10 graphcomposestudio invoice-classic 0:c26c22b94338#1 0.05 0.06 graphcomposestudio invoice-classic 0:2a08ce5567fb 0.15 0.10 18layoutstreet invoice-classic 0:2a08ce5567fb#1 -0.02 0.06 18layoutstreet invoice-classic 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic 0:38e9951985e4 0.12 0.06 gc-2026-041 invoice-classic 0:20fe884effee 0.12 0.06 issued02apr2026 -invoice-classic 0:57a66d21eb1b 0.30 16.10 totaldue +invoice-classic 0:57a66d21eb1b 0.10 16.10 totaldue invoice-classic 0:30d85a94548c 0.07 16.10 invoicegc-2026-041issued02apr202 -invoice-classic 0:aeba3a7b3598 0.28 0.10 billto +invoice-classic 0:aeba3a7b3598 0.08 0.10 billto invoice-classic 0:f890faef907f 0.05 0.10 northwindsystems invoice-classic 0:1b405eb431ca -0.02 0.10 attn:financeteam invoice-classic 0:91eefd794f79 -0.05 0.10 410marketavenue invoice-classic 0:00a97da4e243 -0.07 0.10 manchester,uk invoice-classic 0:62fcfa8745fc -0.10 0.10 ap@northwind.example invoice-classic 0:2081a41a7764 -0.12 0.10 +441615552200 -invoice-classic 0:75857a458999 0.28 0.06 from +invoice-classic 0:75857a458999 0.08 0.06 from invoice-classic 0:fbeea547d99e -0.05 0.06 london,uk invoice-classic 0:faf1569a98e9 -0.07 0.06 ec1a4gc invoice-classic 0:befc6e9b5c24 -0.10 0.06 billing@graphcompose.dev @@ -1849,35 +1849,35 @@ invoice-classic 0:d7f4425b8b16 -0.03 8.40 gbp1,030 invoice-classic 0:11239872d178 -0.02 8.40 total invoice-classic 0:45a5ad5ad430 -0.02 8.40 gbp6,180 invoice-classic 0:ab5aa97074c4 0.15 0.10 notes -invoice-classic 0:b78eb53cbd11 0.22 0.10 •pleaseincludetheinvoicenumberon -invoice-classic 0:8c8fd62f15a7 0.22 0.12 yourremittanceadvice. -invoice-classic 0:f73f1b89e354 0.22 0.10 •allworkwasdeliveredasagreedduri -invoice-classic 0:49c1ececed52 0.22 0.12 theaprilimplementationwindow. +invoice-classic 0:b78eb53cbd11 -0.08 0.10 •pleaseincludetheinvoicenumberon +invoice-classic 0:8c8fd62f15a7 -0.08 0.12 yourremittanceadvice. +invoice-classic 0:f73f1b89e354 -0.08 0.10 •allworkwasdeliveredasagreedduri +invoice-classic 0:49c1ececed52 -0.08 0.12 theaprilimplementationwindow. invoice-classic 0:fb854bac39c8 0.15 0.06 paymentterms -invoice-classic 0:24274f39b50b 0.22 0.06 •paymentduewithin14calendardays. -invoice-classic 0:c438440ba377 0.22 0.06 •banktransferpreferred;contact -invoice-classic 0:434cc906ae90 0.22 0.09 billing@graphcompose.devfor -invoice-classic 0:8822ea375315 0.22 0.09 remittancedetails. -invoice-classic 0:1a7b5c840e25 0.22 0.06 •latepaymentsmaydelayadditional -invoice-classic 0:1631a06d245f 0.22 0.09 templatecustomizationwork. +invoice-classic 0:24274f39b50b -0.08 0.06 •paymentduewithin14calendardays. +invoice-classic 0:c438440ba377 -0.08 0.06 •banktransferpreferred;contact +invoice-classic 0:434cc906ae90 -0.08 0.09 billing@graphcompose.devfor +invoice-classic 0:8822ea375315 -0.08 0.09 remittancedetails. +invoice-classic 0:1a7b5c840e25 -0.08 0.06 •latepaymentsmaydelayadditional +invoice-classic 0:1631a06d245f -0.08 0.09 templatecustomizationwork. invoice-classic 0:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef -invoice-classic_long 0:c26c22b94338 0.49 0.10 graphcomposestudio +invoice-classic_long 0:c26c22b94338 0.09 0.10 graphcomposestudio invoice-classic_long 0:c26c22b94338#1 0.05 0.06 graphcomposestudio invoice-classic_long 0:2a08ce5567fb 0.15 0.10 18layoutstreet invoice-classic_long 0:2a08ce5567fb#1 -0.02 0.06 18layoutstreet invoice-classic_long 0:52d6e3de4fa0 0.10 0.06 invoice invoice-classic_long 0:3594d2085950 0.12 0.06 gc-2026-042 invoice-classic_long 0:25851a973edd 0.12 0.06 issued04may2026 -invoice-classic_long 0:57a66d21eb1b 0.30 16.10 totaldue +invoice-classic_long 0:57a66d21eb1b 0.10 16.10 totaldue invoice-classic_long 0:9eea1e05ff47 0.07 16.10 invoicegc-2026-042issued04may202 -invoice-classic_long 0:aeba3a7b3598 0.28 0.10 billto +invoice-classic_long 0:aeba3a7b3598 0.08 0.10 billto invoice-classic_long 0:f890faef907f 0.05 0.10 northwindsystems invoice-classic_long 0:1b405eb431ca -0.02 0.10 attn:financeteam invoice-classic_long 0:91eefd794f79 -0.05 0.10 410marketavenue invoice-classic_long 0:00a97da4e243 -0.07 0.10 manchester,uk invoice-classic_long 0:62fcfa8745fc -0.10 0.10 ap@northwind.example invoice-classic_long 0:2081a41a7764 -0.12 0.10 +441615552200 -invoice-classic_long 0:75857a458999 0.28 0.06 from +invoice-classic_long 0:75857a458999 0.08 0.06 from invoice-classic_long 0:fbeea547d99e -0.05 0.06 london,uk invoice-classic_long 0:faf1569a98e9 -0.07 0.06 ec1a4gc invoice-classic_long 0:befc6e9b5c24 -0.10 0.06 billing@graphcompose.dev @@ -2017,15 +2017,15 @@ invoice-classic_long 1:e7fda33332e9 -0.03 8.40 gbp8,978 invoice-classic_long 1:11239872d178 -0.02 8.40 total invoice-classic_long 1:90b9dec01ea9 -0.02 8.40 gbp53,868 invoice-classic_long 1:ab5aa97074c4 0.15 0.10 notes -invoice-classic_long 1:970fb4bf9d95 0.22 0.10 •sprints1-3weredeliveredunderthe -invoice-classic_long 1:a781fa6c76d2 0.22 0.12 frameworkagreement. -invoice-classic_long 1:c980fb1ebf98 0.22 0.10 •hardwareandthird-partylicencesa -invoice-classic_long 1:be1dea5817a5 0.22 0.12 billedseparately. +invoice-classic_long 1:970fb4bf9d95 -0.08 0.10 •sprints1-3weredeliveredunderthe +invoice-classic_long 1:a781fa6c76d2 -0.08 0.12 frameworkagreement. +invoice-classic_long 1:c980fb1ebf98 -0.08 0.10 •hardwareandthird-partylicencesa +invoice-classic_long 1:be1dea5817a5 -0.08 0.12 billedseparately. invoice-classic_long 1:fb854bac39c8 0.15 0.06 paymentterms -invoice-classic_long 1:24274f39b50b 0.22 0.06 •paymentduewithin14calendardays. -invoice-classic_long 1:c438440ba377 0.22 0.06 •banktransferpreferred;contact -invoice-classic_long 1:434cc906ae90 0.22 0.09 billing@graphcompose.devfor -invoice-classic_long 1:8822ea375315 0.22 0.09 remittancedetails. +invoice-classic_long 1:24274f39b50b -0.08 0.06 •paymentduewithin14calendardays. +invoice-classic_long 1:c438440ba377 -0.08 0.06 •banktransferpreferred;contact +invoice-classic_long 1:434cc906ae90 -0.08 0.09 billing@graphcompose.devfor +invoice-classic_long 1:8822ea375315 -0.08 0.09 remittancedetails. invoice-classic_long 1:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef invoice-consulting 0:d44ceaea97d1 0.27 0.10 level8,1collinsstreet invoice-consulting 0:78a0bb33347f 0.29 0.10 melbournevic3000australia @@ -2068,32 +2068,32 @@ invoice-consulting 0:4e9682fa850e 0.12 0.47 hrs invoice-consulting 0:4e9682fa850e#1 0.18 0.47 hrs invoice-consulting 0:aa92f0e23a97 0.11 0.73 250.00 invoice-consulting 0:e48d9b7a2993 0.11 0.91 2,500.00 -invoice-consulting 0:1bcfde966d94 0.59 0.39 strategicconsultation -invoice-consulting 0:fd3ca268f319 0.62 0.39 leadershipalignmentworkshops +invoice-consulting 0:1bcfde966d94 0.24 0.39 strategicconsultation +invoice-consulting 0:fd3ca268f319 0.27 0.39 leadershipalignmentworkshops invoice-consulting 0:8d93e0c47a8c 0.13 0.76 1–15may2025 invoice-consulting 0:cf9dcf6da8a8 0.13 0.48 1.00 invoice-consulting 0:cf9dcf6da8a8#1 0.15 0.48 1.00 invoice-consulting 0:cf9dcf6da8a8#2 0.19 0.48 1.00 invoice-consulting 0:97e47b250054 0.13 0.82 3,500.00 invoice-consulting 0:97e47b250054#1 0.13 0.87 3,500.00 -invoice-consulting 0:5581aef375ff 0.61 0.39 market&competitiveanalysis -invoice-consulting 0:957080ea09d9 0.64 0.39 researchandanalysisreport +invoice-consulting 0:5581aef375ff 0.26 0.39 market&competitiveanalysis +invoice-consulting 0:957080ea09d9 0.29 0.39 researchandanalysisreport invoice-consulting 0:47102c31f226 0.15 0.79 5–20may2025 invoice-consulting 0:ec6305055be9 0.15 0.85 4,800.00 invoice-consulting 0:ec6305055be9#1 0.15 0.89 4,800.00 -invoice-consulting 0:53dea97cf116 0.63 0.39 digitalroadmapdevelopment -invoice-consulting 0:acf357e68297 0.66 0.39 strategyandroadmapcreation +invoice-consulting 0:53dea97cf116 0.28 0.39 digitalroadmapdevelopment +invoice-consulting 0:acf357e68297 0.31 0.39 strategyandroadmapcreation invoice-consulting 0:c947fb16eab2 0.17 0.81 15–22may2025 invoice-consulting 0:017144e2271f 0.17 0.52 6.00 invoice-consulting 0:be6012a47af0 0.17 0.73 220.00 invoice-consulting 0:172c0a785bc7 0.17 0.87 1,320.00 -invoice-consulting 0:fe04037aa850 0.65 0.39 stakeholderreviewsessions -invoice-consulting 0:4658a1bff292 0.68 0.39 facilitationanddocumentation +invoice-consulting 0:fe04037aa850 0.30 0.39 stakeholderreviewsessions +invoice-consulting 0:4658a1bff292 0.33 0.39 facilitationanddocumentation invoice-consulting 0:86f73207c794 0.19 0.83 20–25may2025 invoice-consulting 0:55907ef405f6 0.19 0.82 1,880.00 invoice-consulting 0:55907ef405f6#1 0.19 0.87 1,880.00 -invoice-consulting 0:f2e12ad6cc9f 0.67 0.39 presentation&finalreport -invoice-consulting 0:50de6dc61b80 0.70 0.39 executivepresentationanddelivery +invoice-consulting 0:f2e12ad6cc9f 0.32 0.39 presentation&finalreport +invoice-consulting 0:50de6dc61b80 0.35 0.39 executivepresentationanddelivery invoice-consulting 0:d6577e95efba -0.11 0.12 subtotal invoice-consulting 0:6311b1798398 0.23 0.63 14,000.00 invoice-consulting 0:e370855bde0c -0.11 0.12 gst(10%) @@ -2171,12 +2171,12 @@ invoice-consulting_long 0:aa92f0e23a97#2 0.31 0.73 250.00 invoice-consulting_long 0:e48d9b7a2993 0.11 0.91 2,500.00 invoice-consulting_long 0:e48d9b7a2993#1 0.21 0.91 2,500.00 invoice-consulting_long 0:e48d9b7a2993#2 0.31 0.91 2,500.00 -invoice-consulting_long 0:1bcfde966d94 0.59 0.39 strategicconsultation -invoice-consulting_long 0:1bcfde966d94#1 0.69 0.39 strategicconsultation -invoice-consulting_long 0:1bcfde966d94#2 0.79 0.39 strategicconsultation -invoice-consulting_long 0:fd3ca268f319 0.62 0.39 leadershipalignmentworkshops -invoice-consulting_long 0:fd3ca268f319#1 0.72 0.39 leadershipalignmentworkshops -invoice-consulting_long 0:fd3ca268f319#2 0.82 0.39 leadershipalignmentworkshops +invoice-consulting_long 0:1bcfde966d94 0.24 0.39 strategicconsultation +invoice-consulting_long 0:1bcfde966d94#1 0.34 0.39 strategicconsultation +invoice-consulting_long 0:1bcfde966d94#2 0.44 0.39 strategicconsultation +invoice-consulting_long 0:fd3ca268f319 0.27 0.39 leadershipalignmentworkshops +invoice-consulting_long 0:fd3ca268f319#1 0.37 0.39 leadershipalignmentworkshops +invoice-consulting_long 0:fd3ca268f319#2 0.47 0.39 leadershipalignmentworkshops invoice-consulting_long 0:8d93e0c47a8c 0.13 0.76 1–15may2025 invoice-consulting_long 0:8d93e0c47a8c#1 0.23 0.76 1–15may2025 invoice-consulting_long 0:8d93e0c47a8c#2 0.33 0.76 1–15may2025 @@ -2195,12 +2195,12 @@ invoice-consulting_long 0:97e47b250054#2 0.23 0.82 3,500.00 invoice-consulting_long 0:97e47b250054#3 0.23 0.87 3,500.00 invoice-consulting_long 0:97e47b250054#4 0.33 0.82 3,500.00 invoice-consulting_long 0:97e47b250054#5 0.33 0.87 3,500.00 -invoice-consulting_long 0:5581aef375ff 0.61 0.39 market&competitiveanalysis -invoice-consulting_long 0:5581aef375ff#1 0.71 0.39 market&competitiveanalysis -invoice-consulting_long 0:5581aef375ff#2 0.81 0.39 market&competitiveanalysis -invoice-consulting_long 0:957080ea09d9 0.64 0.39 researchandanalysisreport -invoice-consulting_long 0:957080ea09d9#1 0.74 0.39 researchandanalysisreport -invoice-consulting_long 0:957080ea09d9#2 0.84 0.39 researchandanalysisreport +invoice-consulting_long 0:5581aef375ff 0.26 0.39 market&competitiveanalysis +invoice-consulting_long 0:5581aef375ff#1 0.36 0.39 market&competitiveanalysis +invoice-consulting_long 0:5581aef375ff#2 0.46 0.39 market&competitiveanalysis +invoice-consulting_long 0:957080ea09d9 0.29 0.39 researchandanalysisreport +invoice-consulting_long 0:957080ea09d9#1 0.39 0.39 researchandanalysisreport +invoice-consulting_long 0:957080ea09d9#2 0.49 0.39 researchandanalysisreport invoice-consulting_long 0:47102c31f226 0.15 0.79 5–20may2025 invoice-consulting_long 0:47102c31f226#1 0.25 0.79 5–20may2025 invoice-consulting_long 0:47102c31f226#2 0.35 0.79 5–20may2025 @@ -2210,12 +2210,12 @@ invoice-consulting_long 0:ec6305055be9#2 0.25 0.85 4,800.00 invoice-consulting_long 0:ec6305055be9#3 0.25 0.89 4,800.00 invoice-consulting_long 0:ec6305055be9#4 0.35 0.85 4,800.00 invoice-consulting_long 0:ec6305055be9#5 0.35 0.89 4,800.00 -invoice-consulting_long 0:53dea97cf116 0.63 0.39 digitalroadmapdevelopment -invoice-consulting_long 0:53dea97cf116#1 0.73 0.39 digitalroadmapdevelopment -invoice-consulting_long 0:53dea97cf116#2 0.83 0.39 digitalroadmapdevelopment -invoice-consulting_long 0:acf357e68297 0.66 0.39 strategyandroadmapcreation -invoice-consulting_long 0:acf357e68297#1 0.76 0.39 strategyandroadmapcreation -invoice-consulting_long 0:acf357e68297#2 0.86 0.39 strategyandroadmapcreation +invoice-consulting_long 0:53dea97cf116 0.28 0.39 digitalroadmapdevelopment +invoice-consulting_long 0:53dea97cf116#1 0.38 0.39 digitalroadmapdevelopment +invoice-consulting_long 0:53dea97cf116#2 0.48 0.39 digitalroadmapdevelopment +invoice-consulting_long 0:acf357e68297 0.31 0.39 strategyandroadmapcreation +invoice-consulting_long 0:acf357e68297#1 0.41 0.39 strategyandroadmapcreation +invoice-consulting_long 0:acf357e68297#2 0.51 0.39 strategyandroadmapcreation invoice-consulting_long 0:c947fb16eab2 0.17 0.81 15–22may2025 invoice-consulting_long 0:c947fb16eab2#1 0.27 0.81 15–22may2025 invoice-consulting_long 0:c947fb16eab2#2 0.37 0.81 15–22may2025 @@ -2228,12 +2228,12 @@ invoice-consulting_long 0:be6012a47af0#2 0.37 0.73 220.00 invoice-consulting_long 0:172c0a785bc7 0.17 0.87 1,320.00 invoice-consulting_long 0:172c0a785bc7#1 0.27 0.87 1,320.00 invoice-consulting_long 0:172c0a785bc7#2 0.37 0.87 1,320.00 -invoice-consulting_long 0:fe04037aa850 0.65 0.39 stakeholderreviewsessions -invoice-consulting_long 0:fe04037aa850#1 0.75 0.39 stakeholderreviewsessions -invoice-consulting_long 0:fe04037aa850#2 0.85 0.39 stakeholderreviewsessions -invoice-consulting_long 0:4658a1bff292 0.68 0.39 facilitationanddocumentation -invoice-consulting_long 0:4658a1bff292#1 0.78 0.39 facilitationanddocumentation -invoice-consulting_long 0:4658a1bff292#2 0.88 0.39 facilitationanddocumentation +invoice-consulting_long 0:fe04037aa850 0.30 0.39 stakeholderreviewsessions +invoice-consulting_long 0:fe04037aa850#1 0.40 0.39 stakeholderreviewsessions +invoice-consulting_long 0:fe04037aa850#2 0.50 0.39 stakeholderreviewsessions +invoice-consulting_long 0:4658a1bff292 0.33 0.39 facilitationanddocumentation +invoice-consulting_long 0:4658a1bff292#1 0.43 0.39 facilitationanddocumentation +invoice-consulting_long 0:4658a1bff292#2 0.53 0.39 facilitationanddocumentation invoice-consulting_long 0:86f73207c794 0.19 0.83 20–25may2025 invoice-consulting_long 0:86f73207c794#1 0.29 0.83 20–25may2025 invoice-consulting_long 0:86f73207c794#2 0.39 0.83 20–25may2025 @@ -2243,12 +2243,12 @@ invoice-consulting_long 0:55907ef405f6#2 0.29 0.82 1,880.00 invoice-consulting_long 0:55907ef405f6#3 0.29 0.87 1,880.00 invoice-consulting_long 0:55907ef405f6#4 0.39 0.82 1,880.00 invoice-consulting_long 0:55907ef405f6#5 0.39 0.87 1,880.00 -invoice-consulting_long 0:f2e12ad6cc9f 0.67 0.39 presentation&finalreport -invoice-consulting_long 0:f2e12ad6cc9f#1 0.77 0.39 presentation&finalreport -invoice-consulting_long 0:f2e12ad6cc9f#2 0.87 0.39 presentation&finalreport -invoice-consulting_long 0:50de6dc61b80 0.70 0.39 executivepresentationanddelivery -invoice-consulting_long 0:50de6dc61b80#1 0.80 0.39 executivepresentationanddelivery -invoice-consulting_long 0:50de6dc61b80#2 0.90 0.39 executivepresentationanddelivery +invoice-consulting_long 0:f2e12ad6cc9f 0.32 0.39 presentation&finalreport +invoice-consulting_long 0:f2e12ad6cc9f#1 0.42 0.39 presentation&finalreport +invoice-consulting_long 0:f2e12ad6cc9f#2 0.52 0.39 presentation&finalreport +invoice-consulting_long 0:50de6dc61b80 0.35 0.39 executivepresentationanddelivery +invoice-consulting_long 0:50de6dc61b80#1 0.45 0.39 executivepresentationanddelivery +invoice-consulting_long 0:50de6dc61b80#2 0.55 0.39 executivepresentationanddelivery invoice-consulting_long 0:55ee5fe62c33 0.01 0.13 northpointconsultingptyltd|abn12 invoice-consulting_long 0:c843aa25b80f 0.01 0.09 page1/2 invoice-consulting_long 1:c9046f7a37ad -0.02 -0.15 description @@ -2275,12 +2275,12 @@ invoice-consulting_long 1:aa92f0e23a97#2 0.02 0.73 250.00 invoice-consulting_long 1:e48d9b7a2993 -0.18 0.91 2,500.00 invoice-consulting_long 1:e48d9b7a2993#1 -0.08 0.91 2,500.00 invoice-consulting_long 1:e48d9b7a2993#2 0.02 0.91 2,500.00 -invoice-consulting_long 1:1bcfde966d94 0.30 0.39 strategicconsultation -invoice-consulting_long 1:1bcfde966d94#1 0.40 0.39 strategicconsultation -invoice-consulting_long 1:1bcfde966d94#2 0.50 0.39 strategicconsultation -invoice-consulting_long 1:fd3ca268f319 0.33 0.39 leadershipalignmentworkshops -invoice-consulting_long 1:fd3ca268f319#1 0.43 0.39 leadershipalignmentworkshops -invoice-consulting_long 1:fd3ca268f319#2 0.53 0.39 leadershipalignmentworkshops +invoice-consulting_long 1:1bcfde966d94 -0.05 0.39 strategicconsultation +invoice-consulting_long 1:1bcfde966d94#1 0.05 0.39 strategicconsultation +invoice-consulting_long 1:1bcfde966d94#2 0.15 0.39 strategicconsultation +invoice-consulting_long 1:fd3ca268f319 -0.02 0.39 leadershipalignmentworkshops +invoice-consulting_long 1:fd3ca268f319#1 0.08 0.39 leadershipalignmentworkshops +invoice-consulting_long 1:fd3ca268f319#2 0.18 0.39 leadershipalignmentworkshops invoice-consulting_long 1:8d93e0c47a8c -0.16 0.76 1–15may2025 invoice-consulting_long 1:8d93e0c47a8c#1 -0.06 0.76 1–15may2025 invoice-consulting_long 1:cf9dcf6da8a8 -0.16 0.48 1.00 @@ -2293,20 +2293,20 @@ invoice-consulting_long 1:97e47b250054 -0.16 0.82 3,500.00 invoice-consulting_long 1:97e47b250054#1 -0.16 0.87 3,500.00 invoice-consulting_long 1:97e47b250054#2 -0.06 0.82 3,500.00 invoice-consulting_long 1:97e47b250054#3 -0.06 0.87 3,500.00 -invoice-consulting_long 1:5581aef375ff 0.32 0.39 market&competitiveanalysis -invoice-consulting_long 1:5581aef375ff#1 0.42 0.39 market&competitiveanalysis -invoice-consulting_long 1:957080ea09d9 0.35 0.39 researchandanalysisreport -invoice-consulting_long 1:957080ea09d9#1 0.45 0.39 researchandanalysisreport +invoice-consulting_long 1:5581aef375ff -0.03 0.39 market&competitiveanalysis +invoice-consulting_long 1:5581aef375ff#1 0.07 0.39 market&competitiveanalysis +invoice-consulting_long 1:957080ea09d9 0.00 0.39 researchandanalysisreport +invoice-consulting_long 1:957080ea09d9#1 0.10 0.39 researchandanalysisreport invoice-consulting_long 1:47102c31f226 -0.14 0.79 5–20may2025 invoice-consulting_long 1:47102c31f226#1 -0.04 0.79 5–20may2025 invoice-consulting_long 1:ec6305055be9 -0.14 0.85 4,800.00 invoice-consulting_long 1:ec6305055be9#1 -0.14 0.89 4,800.00 invoice-consulting_long 1:ec6305055be9#2 -0.04 0.85 4,800.00 invoice-consulting_long 1:ec6305055be9#3 -0.04 0.89 4,800.00 -invoice-consulting_long 1:53dea97cf116 0.34 0.39 digitalroadmapdevelopment -invoice-consulting_long 1:53dea97cf116#1 0.44 0.39 digitalroadmapdevelopment -invoice-consulting_long 1:acf357e68297 0.37 0.39 strategyandroadmapcreation -invoice-consulting_long 1:acf357e68297#1 0.47 0.39 strategyandroadmapcreation +invoice-consulting_long 1:53dea97cf116 -0.01 0.39 digitalroadmapdevelopment +invoice-consulting_long 1:53dea97cf116#1 0.09 0.39 digitalroadmapdevelopment +invoice-consulting_long 1:acf357e68297 0.02 0.39 strategyandroadmapcreation +invoice-consulting_long 1:acf357e68297#1 0.12 0.39 strategyandroadmapcreation invoice-consulting_long 1:c947fb16eab2 -0.12 0.81 15–22may2025 invoice-consulting_long 1:c947fb16eab2#1 -0.02 0.81 15–22may2025 invoice-consulting_long 1:017144e2271f -0.12 0.52 6.00 @@ -2315,20 +2315,20 @@ invoice-consulting_long 1:be6012a47af0 -0.12 0.73 220.00 invoice-consulting_long 1:be6012a47af0#1 -0.02 0.73 220.00 invoice-consulting_long 1:172c0a785bc7 -0.12 0.87 1,320.00 invoice-consulting_long 1:172c0a785bc7#1 -0.02 0.87 1,320.00 -invoice-consulting_long 1:fe04037aa850 0.36 0.39 stakeholderreviewsessions -invoice-consulting_long 1:fe04037aa850#1 0.46 0.39 stakeholderreviewsessions -invoice-consulting_long 1:4658a1bff292 0.39 0.39 facilitationanddocumentation -invoice-consulting_long 1:4658a1bff292#1 0.49 0.39 facilitationanddocumentation +invoice-consulting_long 1:fe04037aa850 0.01 0.39 stakeholderreviewsessions +invoice-consulting_long 1:fe04037aa850#1 0.11 0.39 stakeholderreviewsessions +invoice-consulting_long 1:4658a1bff292 0.04 0.39 facilitationanddocumentation +invoice-consulting_long 1:4658a1bff292#1 0.14 0.39 facilitationanddocumentation invoice-consulting_long 1:86f73207c794 -0.10 0.83 20–25may2025 invoice-consulting_long 1:86f73207c794#1 0.00 0.83 20–25may2025 invoice-consulting_long 1:55907ef405f6 -0.10 0.82 1,880.00 invoice-consulting_long 1:55907ef405f6#1 -0.10 0.87 1,880.00 invoice-consulting_long 1:55907ef405f6#2 0.00 0.82 1,880.00 invoice-consulting_long 1:55907ef405f6#3 0.00 0.87 1,880.00 -invoice-consulting_long 1:f2e12ad6cc9f 0.38 0.39 presentation&finalreport -invoice-consulting_long 1:f2e12ad6cc9f#1 0.48 0.39 presentation&finalreport -invoice-consulting_long 1:50de6dc61b80 0.41 0.39 executivepresentationanddelivery -invoice-consulting_long 1:50de6dc61b80#1 0.51 0.39 executivepresentationanddelivery +invoice-consulting_long 1:f2e12ad6cc9f 0.03 0.39 presentation&finalreport +invoice-consulting_long 1:f2e12ad6cc9f#1 0.13 0.39 presentation&finalreport +invoice-consulting_long 1:50de6dc61b80 0.06 0.39 executivepresentationanddelivery +invoice-consulting_long 1:50de6dc61b80#1 0.16 0.39 executivepresentationanddelivery invoice-consulting_long 1:d6577e95efba -0.28 0.12 subtotal invoice-consulting_long 1:6311b1798398 0.06 0.63 14,000.00 invoice-consulting_long 1:e370855bde0c -0.28 0.12 gst(10%) @@ -2390,37 +2390,37 @@ invoice-luma_studio 0:725a8f5062e7 0.04 -0.01 shipto invoice-luma_studio 0:254289ce0191 -0.10 -0.01 thefoundry,2ndfloor invoice-luma_studio 0:84db1f48facb -0.14 -0.01 17-19greatsuffolkstreet invoice-luma_studio 0:e7a5f0717d99 -0.18 -0.01 londonse10ns -invoice-luma_studio 0:c9046f7a37ad 0.04 0.10 description -invoice-luma_studio 0:32e8369a9c4f 0.04 0.80 qty -invoice-luma_studio 0:d1062d072acd 0.04 0.73 unitprice -invoice-luma_studio 0:7c512b48ba8a 0.04 0.84 vat -invoice-luma_studio 0:cf38d95c9c6b 0.04 1.54 amount -invoice-luma_studio 0:7b420577e50f 0.07 -0.10 £1,200.00 -invoice-luma_studio 0:7b420577e50f#1 0.07 -0.30 £1,200.00 -invoice-luma_studio 0:dc934fe30a94 0.07 0.00 20% -invoice-luma_studio 0:dc934fe30a94#1 0.08 0.00 20% -invoice-luma_studio 0:dc934fe30a94#2 0.08 0.00 20% -invoice-luma_studio 0:dc934fe30a94#3 0.09 0.00 20% -invoice-luma_studio 0:dc934fe30a94#4 0.10 0.00 20% -invoice-luma_studio 0:d8f4e3a46d2d 0.01 0.03 brandstrategyworkshop +invoice-luma_studio 0:c9046f7a37ad -0.16 0.10 description +invoice-luma_studio 0:32e8369a9c4f -0.16 0.80 qty +invoice-luma_studio 0:d1062d072acd -0.16 0.73 unitprice +invoice-luma_studio 0:7c512b48ba8a -0.16 0.84 vat +invoice-luma_studio 0:cf38d95c9c6b -0.16 1.54 amount +invoice-luma_studio 0:7b420577e50f -0.13 -0.10 £1,200.00 +invoice-luma_studio 0:7b420577e50f#1 -0.13 -0.30 £1,200.00 +invoice-luma_studio 0:dc934fe30a94 -0.13 0.00 20% +invoice-luma_studio 0:dc934fe30a94#1 -0.12 0.00 20% +invoice-luma_studio 0:dc934fe30a94#2 -0.12 0.00 20% +invoice-luma_studio 0:dc934fe30a94#3 -0.11 0.00 20% +invoice-luma_studio 0:dc934fe30a94#4 -0.10 0.00 20% +invoice-luma_studio 0:d8f4e3a46d2d -0.19 0.03 brandstrategyworkshop invoice-luma_studio 0:be624d692600 -0.56 0.03 discoverysession,research&brand invoice-luma_studio 0:40ca2aad940f -0.55 0.03 positioning. -invoice-luma_studio 0:07bcffc86e95 0.08 -0.10 £2,650.00 -invoice-luma_studio 0:07bcffc86e95#1 0.08 -0.30 £2,650.00 -invoice-luma_studio 0:e302dd04d528 0.02 0.03 visualidentitydesign +invoice-luma_studio 0:07bcffc86e95 -0.12 -0.10 £2,650.00 +invoice-luma_studio 0:07bcffc86e95#1 -0.12 -0.30 £2,650.00 +invoice-luma_studio 0:e302dd04d528 -0.18 0.03 visualidentitydesign invoice-luma_studio 0:40e412f627fb -0.55 0.03 logosuite,colourpalette,typograp invoice-luma_studio 0:9c17d116fe08 -0.54 0.03 &guidelines. -invoice-luma_studio 0:a20d66947acc 0.08 -0.10 £3,200.00 -invoice-luma_studio 0:a20d66947acc#1 0.08 -0.30 £3,200.00 -invoice-luma_studio 0:b25ac9cd66bd 0.03 0.03 websitedesign(upto8pages) +invoice-luma_studio 0:a20d66947acc -0.12 -0.10 £3,200.00 +invoice-luma_studio 0:a20d66947acc#1 -0.12 -0.30 £3,200.00 +invoice-luma_studio 0:b25ac9cd66bd -0.17 0.03 websitedesign(upto8pages) invoice-luma_studio 0:dab758788e6f -0.19 0.03 ux/uidesignfordesktopandmobile. -invoice-luma_studio 0:f916278cb74c 0.09 -0.10 £850.00 -invoice-luma_studio 0:f916278cb74c#1 0.09 -0.24 £850.00 -invoice-luma_studio 0:4a1359f78476 0.04 0.03 copywriting +invoice-luma_studio 0:f916278cb74c -0.11 -0.10 £850.00 +invoice-luma_studio 0:f916278cb74c#1 -0.11 -0.24 £850.00 +invoice-luma_studio 0:4a1359f78476 -0.16 0.03 copywriting invoice-luma_studio 0:69afea703455 -0.18 0.03 websitecopy&keymessaging. -invoice-luma_studio 0:07b417914317 0.10 -0.10 £600.00 -invoice-luma_studio 0:07b417914317#1 0.10 -0.24 £600.00 -invoice-luma_studio 0:340455fbac25 0.05 0.03 projectmanagement +invoice-luma_studio 0:07b417914317 -0.10 -0.10 £600.00 +invoice-luma_studio 0:07b417914317#1 -0.10 -0.24 £600.00 +invoice-luma_studio 0:340455fbac25 -0.15 0.03 projectmanagement invoice-luma_studio 0:72534f9863ad -0.52 0.03 planning,coordination&client invoice-luma_studio 0:4df00f9a2b20 -0.52 0.03 liaison. invoice-luma_studio 0:d6577e95efba 0.11 0.10 subtotal @@ -2487,125 +2487,125 @@ invoice-luma_studio_long 0:725a8f5062e7 0.04 -0.01 shipto invoice-luma_studio_long 0:254289ce0191 -0.10 -0.01 thefoundry,2ndfloor invoice-luma_studio_long 0:84db1f48facb -0.14 -0.01 17-19greatsuffolkstreet invoice-luma_studio_long 0:e7a5f0717d99 -0.18 -0.01 londonse10ns -invoice-luma_studio_long 0:c9046f7a37ad 0.04 0.10 description -invoice-luma_studio_long 0:32e8369a9c4f 0.04 0.80 qty -invoice-luma_studio_long 0:d1062d072acd 0.04 0.73 unitprice -invoice-luma_studio_long 0:7c512b48ba8a 0.04 0.84 vat -invoice-luma_studio_long 0:cf38d95c9c6b 0.04 1.54 amount -invoice-luma_studio_long 0:7b420577e50f 0.07 -0.10 £1,200.00 -invoice-luma_studio_long 0:7b420577e50f#1 0.07 -0.30 £1,200.00 -invoice-luma_studio_long 0:7b420577e50f#2 0.12 -0.10 £1,200.00 -invoice-luma_studio_long 0:7b420577e50f#3 0.12 -0.30 £1,200.00 -invoice-luma_studio_long 0:dc934fe30a94 0.07 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#1 0.08 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#2 0.09 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#3 0.10 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#4 0.11 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#5 0.12 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#6 0.13 0.00 20% -invoice-luma_studio_long 0:dc934fe30a94#7 0.14 0.00 20% -invoice-luma_studio_long 0:d8f4e3a46d2d 0.01 0.03 brandstrategyworkshop -invoice-luma_studio_long 0:d8f4e3a46d2d#1 0.06 0.03 brandstrategyworkshop +invoice-luma_studio_long 0:c9046f7a37ad -0.16 0.10 description +invoice-luma_studio_long 0:32e8369a9c4f -0.16 0.80 qty +invoice-luma_studio_long 0:d1062d072acd -0.16 0.73 unitprice +invoice-luma_studio_long 0:7c512b48ba8a -0.16 0.84 vat +invoice-luma_studio_long 0:cf38d95c9c6b -0.16 1.54 amount +invoice-luma_studio_long 0:7b420577e50f -0.13 -0.10 £1,200.00 +invoice-luma_studio_long 0:7b420577e50f#1 -0.13 -0.30 £1,200.00 +invoice-luma_studio_long 0:7b420577e50f#2 -0.08 -0.10 £1,200.00 +invoice-luma_studio_long 0:7b420577e50f#3 -0.08 -0.30 £1,200.00 +invoice-luma_studio_long 0:dc934fe30a94 -0.13 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#1 -0.12 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#2 -0.11 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#3 -0.10 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#4 -0.09 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#5 -0.08 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#6 -0.07 0.00 20% +invoice-luma_studio_long 0:dc934fe30a94#7 -0.06 0.00 20% +invoice-luma_studio_long 0:d8f4e3a46d2d -0.19 0.03 brandstrategyworkshop +invoice-luma_studio_long 0:d8f4e3a46d2d#1 -0.14 0.03 brandstrategyworkshop invoice-luma_studio_long 0:be624d692600 -0.56 0.03 discoverysession,research&brand invoice-luma_studio_long 0:be624d692600#1 -0.51 0.03 discoverysession,research&brand invoice-luma_studio_long 0:d6def76ba3d1 -0.55 0.03 positioning.phase1. -invoice-luma_studio_long 0:07bcffc86e95 0.08 -0.10 £2,650.00 -invoice-luma_studio_long 0:07bcffc86e95#1 0.08 -0.30 £2,650.00 -invoice-luma_studio_long 0:07bcffc86e95#2 0.13 -0.10 £2,650.00 -invoice-luma_studio_long 0:07bcffc86e95#3 0.13 -0.30 £2,650.00 -invoice-luma_studio_long 0:e302dd04d528 0.02 0.03 visualidentitydesign -invoice-luma_studio_long 0:e302dd04d528#1 0.07 0.03 visualidentitydesign +invoice-luma_studio_long 0:07bcffc86e95 -0.12 -0.10 £2,650.00 +invoice-luma_studio_long 0:07bcffc86e95#1 -0.12 -0.30 £2,650.00 +invoice-luma_studio_long 0:07bcffc86e95#2 -0.07 -0.10 £2,650.00 +invoice-luma_studio_long 0:07bcffc86e95#3 -0.07 -0.30 £2,650.00 +invoice-luma_studio_long 0:e302dd04d528 -0.18 0.03 visualidentitydesign +invoice-luma_studio_long 0:e302dd04d528#1 -0.13 0.03 visualidentitydesign invoice-luma_studio_long 0:40e412f627fb -0.55 0.03 logosuite,colourpalette,typograp invoice-luma_studio_long 0:40e412f627fb#1 -0.50 0.03 logosuite,colourpalette,typograp invoice-luma_studio_long 0:270a9e26d2e0 -0.54 0.03 &guidelines.phase1. -invoice-luma_studio_long 0:a20d66947acc 0.09 -0.10 £3,200.00 -invoice-luma_studio_long 0:a20d66947acc#1 0.09 -0.30 £3,200.00 -invoice-luma_studio_long 0:a20d66947acc#2 0.14 -0.10 £3,200.00 -invoice-luma_studio_long 0:a20d66947acc#3 0.14 -0.30 £3,200.00 -invoice-luma_studio_long 0:b25ac9cd66bd 0.03 0.03 websitedesign(upto8pages) -invoice-luma_studio_long 0:b25ac9cd66bd#1 0.08 0.03 websitedesign(upto8pages) +invoice-luma_studio_long 0:a20d66947acc -0.11 -0.10 £3,200.00 +invoice-luma_studio_long 0:a20d66947acc#1 -0.11 -0.30 £3,200.00 +invoice-luma_studio_long 0:a20d66947acc#2 -0.06 -0.10 £3,200.00 +invoice-luma_studio_long 0:a20d66947acc#3 -0.06 -0.30 £3,200.00 +invoice-luma_studio_long 0:b25ac9cd66bd -0.17 0.03 websitedesign(upto8pages) +invoice-luma_studio_long 0:b25ac9cd66bd#1 -0.12 0.03 websitedesign(upto8pages) invoice-luma_studio_long 0:dab758788e6f -0.54 0.03 ux/uidesignfordesktopandmobile. invoice-luma_studio_long 0:dab758788e6f#1 -0.49 0.03 ux/uidesignfordesktopandmobile. invoice-luma_studio_long 0:bb1a79cda1ae -0.53 0.03 phase1. -invoice-luma_studio_long 0:f916278cb74c 0.10 -0.10 £850.00 -invoice-luma_studio_long 0:f916278cb74c#1 0.10 -0.24 £850.00 -invoice-luma_studio_long 0:4a1359f78476 0.04 0.03 copywriting +invoice-luma_studio_long 0:f916278cb74c -0.10 -0.10 £850.00 +invoice-luma_studio_long 0:f916278cb74c#1 -0.10 -0.24 £850.00 +invoice-luma_studio_long 0:4a1359f78476 -0.16 0.03 copywriting invoice-luma_studio_long 0:e6a181326fc9 -0.53 0.03 websitecopy&keymessaging.phase -invoice-luma_studio_long 0:07b417914317 0.11 -0.10 £600.00 -invoice-luma_studio_long 0:07b417914317#1 0.11 -0.24 £600.00 -invoice-luma_studio_long 0:340455fbac25 0.05 0.03 projectmanagement +invoice-luma_studio_long 0:07b417914317 -0.09 -0.10 £600.00 +invoice-luma_studio_long 0:07b417914317#1 -0.09 -0.24 £600.00 +invoice-luma_studio_long 0:340455fbac25 -0.15 0.03 projectmanagement invoice-luma_studio_long 0:72534f9863ad -0.52 0.03 planning,coordination&client invoice-luma_studio_long 0:8f09113ca513 -0.51 0.03 liaison.phase1. invoice-luma_studio_long 0:532160121f5f -0.50 0.03 positioning.phase2. invoice-luma_studio_long 0:82ed33a90b4e -0.49 0.03 &guidelines.phase2. invoice-luma_studio_long 0:81deaef85fed -0.48 0.03 phase2. invoice-luma_studio_long 0:cc6f3d44f8c5 0.01 1.04 page1of3 -invoice-luma_studio_long 1:c9046f7a37ad 0.18 0.10 description -invoice-luma_studio_long 1:32e8369a9c4f 0.18 0.80 qty -invoice-luma_studio_long 1:d1062d072acd 0.18 0.73 unitprice -invoice-luma_studio_long 1:7c512b48ba8a 0.18 0.84 vat -invoice-luma_studio_long 1:cf38d95c9c6b 0.18 1.54 amount -invoice-luma_studio_long 1:f916278cb74c 0.21 -0.10 £850.00 -invoice-luma_studio_long 1:f916278cb74c#1 0.21 -0.24 £850.00 -invoice-luma_studio_long 1:f916278cb74c#2 0.26 -0.10 £850.00 -invoice-luma_studio_long 1:f916278cb74c#3 0.26 -0.24 £850.00 -invoice-luma_studio_long 1:f916278cb74c#4 0.31 -0.10 £850.00 -invoice-luma_studio_long 1:f916278cb74c#5 0.31 -0.24 £850.00 -invoice-luma_studio_long 1:dc934fe30a94 0.21 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#1 0.22 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#2 0.23 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#3 0.24 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#4 0.25 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#5 0.26 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#6 0.27 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#7 0.28 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#8 0.29 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#9 0.30 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#10 0.31 0.00 20% -invoice-luma_studio_long 1:dc934fe30a94#11 0.32 0.00 20% -invoice-luma_studio_long 1:4a1359f78476 0.15 0.03 copywriting -invoice-luma_studio_long 1:4a1359f78476#1 0.20 0.03 copywriting -invoice-luma_studio_long 1:4a1359f78476#2 0.25 0.03 copywriting +invoice-luma_studio_long 1:c9046f7a37ad -0.02 0.10 description +invoice-luma_studio_long 1:32e8369a9c4f -0.02 0.80 qty +invoice-luma_studio_long 1:d1062d072acd -0.02 0.73 unitprice +invoice-luma_studio_long 1:7c512b48ba8a -0.02 0.84 vat +invoice-luma_studio_long 1:cf38d95c9c6b -0.02 1.54 amount +invoice-luma_studio_long 1:f916278cb74c 0.01 -0.10 £850.00 +invoice-luma_studio_long 1:f916278cb74c#1 0.01 -0.24 £850.00 +invoice-luma_studio_long 1:f916278cb74c#2 0.06 -0.10 £850.00 +invoice-luma_studio_long 1:f916278cb74c#3 0.06 -0.24 £850.00 +invoice-luma_studio_long 1:f916278cb74c#4 0.11 -0.10 £850.00 +invoice-luma_studio_long 1:f916278cb74c#5 0.11 -0.24 £850.00 +invoice-luma_studio_long 1:dc934fe30a94 0.01 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#1 0.02 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#2 0.03 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#3 0.04 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#4 0.05 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#5 0.06 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#6 0.07 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#7 0.08 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#8 0.09 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#9 0.10 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#10 0.11 0.00 20% +invoice-luma_studio_long 1:dc934fe30a94#11 0.12 0.00 20% +invoice-luma_studio_long 1:4a1359f78476 -0.05 0.03 copywriting +invoice-luma_studio_long 1:4a1359f78476#1 0.00 0.03 copywriting +invoice-luma_studio_long 1:4a1359f78476#2 0.05 0.03 copywriting invoice-luma_studio_long 1:e6a181326fc9 -0.41 0.03 websitecopy&keymessaging.phase invoice-luma_studio_long 1:e6a181326fc9#1 -0.36 0.03 websitecopy&keymessaging.phase invoice-luma_studio_long 1:e6a181326fc9#2 -0.31 0.03 websitecopy&keymessaging.phase -invoice-luma_studio_long 1:07b417914317 0.22 -0.10 £600.00 -invoice-luma_studio_long 1:07b417914317#1 0.22 -0.24 £600.00 -invoice-luma_studio_long 1:07b417914317#2 0.27 -0.10 £600.00 -invoice-luma_studio_long 1:07b417914317#3 0.27 -0.24 £600.00 -invoice-luma_studio_long 1:07b417914317#4 0.32 -0.10 £600.00 -invoice-luma_studio_long 1:07b417914317#5 0.32 -0.24 £600.00 -invoice-luma_studio_long 1:340455fbac25 0.16 0.03 projectmanagement -invoice-luma_studio_long 1:340455fbac25#1 0.21 0.03 projectmanagement -invoice-luma_studio_long 1:340455fbac25#2 0.26 0.03 projectmanagement +invoice-luma_studio_long 1:07b417914317 0.02 -0.10 £600.00 +invoice-luma_studio_long 1:07b417914317#1 0.02 -0.24 £600.00 +invoice-luma_studio_long 1:07b417914317#2 0.07 -0.10 £600.00 +invoice-luma_studio_long 1:07b417914317#3 0.07 -0.24 £600.00 +invoice-luma_studio_long 1:07b417914317#4 0.12 -0.10 £600.00 +invoice-luma_studio_long 1:07b417914317#5 0.12 -0.24 £600.00 +invoice-luma_studio_long 1:340455fbac25 -0.04 0.03 projectmanagement +invoice-luma_studio_long 1:340455fbac25#1 0.01 0.03 projectmanagement +invoice-luma_studio_long 1:340455fbac25#2 0.07 0.03 projectmanagement invoice-luma_studio_long 1:72534f9863ad -0.40 0.03 planning,coordination&client invoice-luma_studio_long 1:72534f9863ad#1 -0.35 0.03 planning,coordination&client invoice-luma_studio_long 1:72534f9863ad#2 -0.30 0.03 planning,coordination&client invoice-luma_studio_long 1:ad05c4b80bfe -0.40 0.03 liaison.phase2. -invoice-luma_studio_long 1:7b420577e50f 0.23 -0.10 £1,200.00 -invoice-luma_studio_long 1:7b420577e50f#1 0.23 -0.30 £1,200.00 -invoice-luma_studio_long 1:7b420577e50f#2 0.28 -0.10 £1,200.00 -invoice-luma_studio_long 1:7b420577e50f#3 0.28 -0.30 £1,200.00 -invoice-luma_studio_long 1:d8f4e3a46d2d 0.17 0.03 brandstrategyworkshop -invoice-luma_studio_long 1:d8f4e3a46d2d#1 0.22 0.03 brandstrategyworkshop +invoice-luma_studio_long 1:7b420577e50f 0.03 -0.10 £1,200.00 +invoice-luma_studio_long 1:7b420577e50f#1 0.03 -0.30 £1,200.00 +invoice-luma_studio_long 1:7b420577e50f#2 0.08 -0.10 £1,200.00 +invoice-luma_studio_long 1:7b420577e50f#3 0.08 -0.30 £1,200.00 +invoice-luma_studio_long 1:d8f4e3a46d2d -0.03 0.03 brandstrategyworkshop +invoice-luma_studio_long 1:d8f4e3a46d2d#1 0.02 0.03 brandstrategyworkshop invoice-luma_studio_long 1:be624d692600 -0.39 0.03 discoverysession,research&brand invoice-luma_studio_long 1:be624d692600#1 -0.34 0.03 discoverysession,research&brand invoice-luma_studio_long 1:b95a82d5055c -0.39 0.03 positioning.phase3. -invoice-luma_studio_long 1:07bcffc86e95 0.24 -0.10 £2,650.00 -invoice-luma_studio_long 1:07bcffc86e95#1 0.24 -0.30 £2,650.00 -invoice-luma_studio_long 1:07bcffc86e95#2 0.29 -0.10 £2,650.00 -invoice-luma_studio_long 1:07bcffc86e95#3 0.29 -0.30 £2,650.00 -invoice-luma_studio_long 1:e302dd04d528 0.18 0.03 visualidentitydesign -invoice-luma_studio_long 1:e302dd04d528#1 0.23 0.03 visualidentitydesign +invoice-luma_studio_long 1:07bcffc86e95 0.04 -0.10 £2,650.00 +invoice-luma_studio_long 1:07bcffc86e95#1 0.04 -0.30 £2,650.00 +invoice-luma_studio_long 1:07bcffc86e95#2 0.09 -0.10 £2,650.00 +invoice-luma_studio_long 1:07bcffc86e95#3 0.09 -0.30 £2,650.00 +invoice-luma_studio_long 1:e302dd04d528 -0.02 0.03 visualidentitydesign +invoice-luma_studio_long 1:e302dd04d528#1 0.03 0.03 visualidentitydesign invoice-luma_studio_long 1:40e412f627fb -0.38 0.03 logosuite,colourpalette,typograp invoice-luma_studio_long 1:40e412f627fb#1 -0.33 0.03 logosuite,colourpalette,typograp invoice-luma_studio_long 1:708bc0c2e87c -0.38 0.03 &guidelines.phase3. -invoice-luma_studio_long 1:a20d66947acc 0.25 -0.10 £3,200.00 -invoice-luma_studio_long 1:a20d66947acc#1 0.25 -0.30 £3,200.00 -invoice-luma_studio_long 1:a20d66947acc#2 0.30 -0.10 £3,200.00 -invoice-luma_studio_long 1:a20d66947acc#3 0.30 -0.30 £3,200.00 -invoice-luma_studio_long 1:b25ac9cd66bd 0.19 0.03 websitedesign(upto8pages) -invoice-luma_studio_long 1:b25ac9cd66bd#1 0.24 0.03 websitedesign(upto8pages) +invoice-luma_studio_long 1:a20d66947acc 0.05 -0.10 £3,200.00 +invoice-luma_studio_long 1:a20d66947acc#1 0.05 -0.30 £3,200.00 +invoice-luma_studio_long 1:a20d66947acc#2 0.10 -0.10 £3,200.00 +invoice-luma_studio_long 1:a20d66947acc#3 0.10 -0.30 £3,200.00 +invoice-luma_studio_long 1:b25ac9cd66bd -0.01 0.03 websitedesign(upto8pages) +invoice-luma_studio_long 1:b25ac9cd66bd#1 0.04 0.03 websitedesign(upto8pages) invoice-luma_studio_long 1:dab758788e6f -0.37 0.03 ux/uidesignfordesktopandmobile. invoice-luma_studio_long 1:dab758788e6f#1 -0.32 0.03 ux/uidesignfordesktopandmobile. invoice-luma_studio_long 1:155500f32e83 -0.37 0.03 phase3. @@ -2839,7 +2839,7 @@ invoice-metered 0:985e43c2de34 -0.04 0.12 120riversideway,bristol,bs16qt,u invoice-metered 0:7b14b8d2c5c4 -0.04 0.16 page1of1 invoice-modern 0:52d6e3de4fa0 0.00 16.10 invoice invoice-modern 0:30d85a94548c 0.00 16.10 invoicegc-2026-041issued02apr202 -invoice-modern 0:75857a458999 0.20 0.10 from +invoice-modern 0:75857a458999 0.00 0.10 from invoice-modern 0:c26c22b94338 -0.02 0.10 graphcomposestudio invoice-modern 0:2a08ce5567fb -0.10 0.10 18layoutstreet invoice-modern 0:fbeea547d99e -0.12 0.10 london,uk @@ -2847,7 +2847,7 @@ invoice-modern 0:faf1569a98e9 -0.15 0.10 ec1a4gc invoice-modern 0:befc6e9b5c24 -0.17 0.10 billing@graphcompose.dev invoice-modern 0:89674e6f130e -0.20 0.10 +442055551000 invoice-modern 0:a2b2e7d2906f -0.22 0.10 taxidgb-99887766 -invoice-modern 0:aeba3a7b3598 0.20 0.11 billto +invoice-modern 0:aeba3a7b3598 0.00 0.11 billto invoice-modern 0:f890faef907f -0.02 0.11 northwindsystems invoice-modern 0:1b405eb431ca -0.10 0.11 attn:financeteam invoice-modern 0:91eefd794f79 -0.12 0.11 410marketavenue @@ -2878,30 +2878,30 @@ invoice-modern 0:d7f4425b8b16 -0.08 1.36 gbp1,030 invoice-modern 0:11239872d178 -0.07 1.36 total invoice-modern 0:45a5ad5ad430 -0.07 1.36 gbp6,180 invoice-modern 0:ab5aa97074c4 0.10 0.10 notes -invoice-modern 0:b78eb53cbd11 0.17 0.10 •pleaseincludetheinvoicenumberon -invoice-modern 0:8c8fd62f15a7 0.17 0.12 yourremittanceadvice. -invoice-modern 0:0c98130c526f 0.17 0.10 •allworkwasdeliveredasagreed -invoice-modern 0:2e9787ed79bf 0.17 0.12 duringtheaprilimplementation -invoice-modern 0:e41212011d98 0.17 0.12 window. +invoice-modern 0:b78eb53cbd11 -0.13 0.10 •pleaseincludetheinvoicenumberon +invoice-modern 0:8c8fd62f15a7 -0.13 0.12 yourremittanceadvice. +invoice-modern 0:0c98130c526f -0.13 0.10 •allworkwasdeliveredasagreed +invoice-modern 0:2e9787ed79bf -0.13 0.12 duringtheaprilimplementation +invoice-modern 0:e41212011d98 -0.13 0.12 window. invoice-modern 0:fb854bac39c8 0.10 0.11 paymentterms -invoice-modern 0:24274f39b50b 0.17 0.11 •paymentduewithin14calendardays. -invoice-modern 0:c438440ba377 0.17 0.11 •banktransferpreferred;contact -invoice-modern 0:434cc906ae90 0.17 0.14 billing@graphcompose.devfor -invoice-modern 0:8822ea375315 0.17 0.14 remittancedetails. -invoice-modern 0:1a7b5c840e25 0.17 0.11 •latepaymentsmaydelayadditional -invoice-modern 0:1631a06d245f 0.17 0.14 templatecustomizationwork. +invoice-modern 0:24274f39b50b -0.13 0.11 •paymentduewithin14calendardays. +invoice-modern 0:c438440ba377 -0.13 0.11 •banktransferpreferred;contact +invoice-modern 0:434cc906ae90 -0.13 0.14 billing@graphcompose.devfor +invoice-modern 0:8822ea375315 -0.13 0.14 remittancedetails. +invoice-modern 0:1a7b5c840e25 -0.13 0.11 •latepaymentsmaydelayadditional +invoice-modern 0:1631a06d245f -0.13 0.14 templatecustomizationwork. invoice-modern 0:e3337beaa171 -0.05 0.10 thankyouforchoosinggraphcomposef invoice-modern_long 0:52d6e3de4fa0 0.00 16.10 invoice invoice-modern_long 0:61a964e78950 0.00 16.10 invoicegc-2026-042issued04may202 invoice-modern_long 0:62a2fed3d6e0 0.00 16.10 pending -invoice-modern_long 0:75857a458999 0.20 0.10 from +invoice-modern_long 0:75857a458999 0.00 0.10 from invoice-modern_long 0:c26c22b94338 -0.02 0.10 graphcomposestudio invoice-modern_long 0:2a08ce5567fb -0.10 0.10 18layoutstreet invoice-modern_long 0:fbeea547d99e -0.12 0.10 london,uk invoice-modern_long 0:faf1569a98e9 -0.15 0.10 ec1a4gc invoice-modern_long 0:befc6e9b5c24 -0.17 0.10 billing@graphcompose.dev invoice-modern_long 0:89674e6f130e -0.20 0.10 +442055551000 -invoice-modern_long 0:aeba3a7b3598 0.20 0.11 billto +invoice-modern_long 0:aeba3a7b3598 0.00 0.11 billto invoice-modern_long 0:f890faef907f -0.02 0.11 northwindsystems invoice-modern_long 0:1b405eb431ca -0.10 0.11 attn:financeteam invoice-modern_long 0:91eefd794f79 -0.12 0.11 410marketavenue @@ -3043,15 +3043,15 @@ invoice-modern_long 1:e7fda33332e9 -0.03 1.35 gbp8,978 invoice-modern_long 1:11239872d178 -0.02 1.40 total invoice-modern_long 1:90b9dec01ea9 -0.02 1.35 gbp53,868 invoice-modern_long 1:ab5aa97074c4 0.15 0.10 notes -invoice-modern_long 1:970fb4bf9d95 0.22 0.10 •sprints1-3weredeliveredunderthe -invoice-modern_long 1:a781fa6c76d2 0.22 0.12 frameworkagreement. -invoice-modern_long 1:c980fb1ebf98 0.22 0.10 •hardwareandthird-partylicencesa -invoice-modern_long 1:be1dea5817a5 0.22 0.12 billedseparately. +invoice-modern_long 1:970fb4bf9d95 -0.08 0.10 •sprints1-3weredeliveredunderthe +invoice-modern_long 1:a781fa6c76d2 -0.08 0.12 frameworkagreement. +invoice-modern_long 1:c980fb1ebf98 -0.08 0.10 •hardwareandthird-partylicencesa +invoice-modern_long 1:be1dea5817a5 -0.08 0.12 billedseparately. invoice-modern_long 1:fb854bac39c8 0.15 0.11 paymentterms -invoice-modern_long 1:24274f39b50b 0.22 0.11 •paymentduewithin14calendardays. -invoice-modern_long 1:c438440ba377 0.22 0.11 •banktransferpreferred;contact -invoice-modern_long 1:434cc906ae90 0.22 0.14 billing@graphcompose.devfor -invoice-modern_long 1:8822ea375315 0.22 0.14 remittancedetails. +invoice-modern_long 1:24274f39b50b -0.08 0.11 •paymentduewithin14calendardays. +invoice-modern_long 1:c438440ba377 -0.08 0.11 •banktransferpreferred;contact +invoice-modern_long 1:434cc906ae90 -0.08 0.14 billing@graphcompose.devfor +invoice-modern_long 1:8822ea375315 -0.08 0.14 remittancedetails. invoice-modern_long 1:e3337beaa171 0.00 0.10 thankyouforchoosinggraphcomposef invoice-obsidian 0:07f15cde5b18 0.14 0.09 kestrel invoice-obsidian 0:52d6e3de4fa0 0.12 0.09 invoice @@ -3532,7 +3532,7 @@ letter-blue_banner 0:a71e98216baa -0.08 0.10 iwouldwelcometheopportunitytobri letter-blue_banner 0:80a6a628ad52 -0.05 0.10 sincerely, letter-boxed_sections 0:8f1010120ed6 0.46 2.20 jordanrivera letter-boxed_sections 0:8f1010120ed6#1 0.13 0.10 jordanrivera -letter-boxed_sections 0:8d7a2cca3060 0.23 0.12 +442055551000|jordan.rivera@exam +letter-boxed_sections 0:8d7a2cca3060 0.08 0.12 +442055551000|jordan.rivera@exam letter-boxed_sections 0:bef81b541365 0.09 0.10 dearhiringteamatnorthwindsystems letter-boxed_sections 0:ab54410ead82 -0.01 0.10 iamexcitedtosharemyinterestinthe letter-boxed_sections 0:9185168d2cf8 0.00 0.10 document-generationsystemsthatba @@ -3561,7 +3561,7 @@ letter-classic_serif 0:a463bced3db2 -0.13 0.10 ienjoytranslatingfuzzyworkflowre letter-classic_serif 0:dd474e249222 -0.15 0.10 makeadoptioneasierfortherestofth letter-classic_serif 0:a71e98216baa -0.08 0.10 iwouldwelcometheopportunitytobri letter-classic_serif 0:80a6a628ad52 -0.10 0.10 sincerely, -letter-compact_mono 0:8f1010120ed6 0.31 16.10 jordanrivera +letter-compact_mono 0:8f1010120ed6 -0.04 16.10 jordanrivera letter-compact_mono 0:8f1010120ed6#1 -0.13 0.10 jordanrivera letter-compact_mono 0:f9e4560bd641 -0.24 16.10 london,uk/+442055551000/jordan.r letter-compact_mono 0:bef81b541365 -0.08 0.10 dearhiringteamatnorthwindsystems @@ -3759,8 +3759,8 @@ proposal-editorial 1:aaa2a6073d69 -45.38 0.60 refinedlogosuite&brandguidelines proposal-editorial 1:d626f7532564 -45.38 0.60 color,typography&designsystem proposal-editorial 1:018ec557a513 -45.38 0.60 websiteuxwireframes proposal-editorial 1:5ed4970fa285 -45.38 0.60 high-fidelitywebsitedesigns -proposal-editorial 1:a579d6d1c12e -44.88 0.58 responsivedesignfordesktop,table -proposal-editorial 1:d524c1a0811d -45.28 0.10 mobile +proposal-editorial 1:a579d6d1c12e -45.03 0.58 responsivedesignfordesktop,table +proposal-editorial 1:d524c1a0811d -45.43 0.10 mobile proposal-editorial 1:8b712e7e93bf -45.40 0.58 contentstrategy&pagetemplates proposal-editorial 1:e9bf06d9b130 -45.40 0.58 developer-readyassets&specificat proposal-editorial 1:e93e7ba95ceb -45.40 0.58 launchchecklist&trainingsession @@ -3899,9 +3899,9 @@ proposal-indigo 0:29208032c874 0.72 0.05 kestrel.example/business|busines proposal-modern 0:ecd1378bc9dc 0.00 16.10 proposal proposal-modern 0:f592a683c61d -0.03 16.10 documentplatformconsolidation proposal-modern 0:bbe07492ee1f 0.02 16.10 proposalgc-p-2026-014prepared02a -proposal-modern 0:566835ed7fba 0.23 12.35 executivesummary +proposal-modern 0:566835ed7fba 0.03 12.35 executivesummary proposal-modern 0:7ccf0e6c59fa 0.00 12.35 aphasedengagementtoretireper-tea -proposal-modern 0:75857a458999 0.18 0.10 from +proposal-modern 0:75857a458999 -0.02 0.10 from proposal-modern 0:c26c22b94338 -0.05 0.10 graphcomposestudio proposal-modern 0:2a08ce5567fb -0.12 0.10 18layoutstreet proposal-modern 0:fbeea547d99e -0.15 0.10 london,uk @@ -4078,8 +4078,8 @@ receipt-modern 0:e885a92f5bb7 -0.02 -0.02 generatedon09august2026 receipt-modern 0:c288dd6df716 -0.01 -0.20 referencenwp-4821-0067 receipt-modern 0:9de2a2d7cd7a 0.18 17.60 amountcollected receipt-modern 0:465691680b99 0.03 17.60 £66.62 -receipt-modern 0:465691680b99#1 0.29 -0.90 £66.62 -receipt-modern 0:465691680b99#2 0.29 -0.90 £66.62 +receipt-modern 0:465691680b99#1 0.09 -0.90 £66.62 +receipt-modern 0:465691680b99#2 0.09 -0.90 £66.62 receipt-modern 0:4260e5ae365d 0.00 17.60 directdebitcollectedbyharbourfin receipt-modern 0:4ddb3e96801a 0.18 26.66 completed receipt-modern 0:a22e6dacae05 0.04 17.69 valuedate07jul2026 @@ -4099,31 +4099,31 @@ receipt-modern 0:f05e0b2f4fe3 -0.01 16.06 manchesterm00aa receipt-modern 0:597533badf94 0.09 16.06 account••••5604 receipt-modern 0:a5938b687682 0.09 16.06 sortcode00-00-11 receipt-modern 0:5434155c0fda 0.03 0.10 transferdetails -receipt-modern 0:a4d7978db2aa 0.29 0.10 mandatereference -receipt-modern 0:0123b9cf310f 0.29 -0.95 mnd-0110-8054-5652 -receipt-modern 0:3840804e6235 0.29 0.10 transactionid -receipt-modern 0:60e01e42bb33 0.29 -0.92 b71f0c2e-9a44-4f18-bd30-51c7a2e9 -receipt-modern 0:2cbaa23e0a56 0.29 0.10 paymentscheme -receipt-modern 0:1477cb720a28 0.29 -0.94 bacsdirectdebit -receipt-modern 0:1fa78044a7da 0.29 0.10 statementdescription -receipt-modern 0:b77918146dd3 0.29 -0.92 harbourfin4821 +receipt-modern 0:a4d7978db2aa 0.09 0.10 mandatereference +receipt-modern 0:0123b9cf310f 0.09 -0.95 mnd-0110-8054-5652 +receipt-modern 0:3840804e6235 0.09 0.10 transactionid +receipt-modern 0:60e01e42bb33 0.09 -0.92 b71f0c2e-9a44-4f18-bd30-51c7a2e9 +receipt-modern 0:2cbaa23e0a56 0.09 0.10 paymentscheme +receipt-modern 0:1477cb720a28 0.09 -0.94 bacsdirectdebit +receipt-modern 0:1fa78044a7da 0.09 0.10 statementdescription +receipt-modern 0:b77918146dd3 0.09 -0.92 harbourfin4821 receipt-modern 0:c72294b34f1a 0.03 0.10 amountbreakdown -receipt-modern 0:cf38d95c9c6b 0.29 0.10 amount -receipt-modern 0:dc40efafaeb3 0.29 0.10 transferfee -receipt-modern 0:3a82ba6903c6 0.29 -0.86 £0.00 -receipt-modern 0:1cdd81427c35 0.29 0.10 exchangerate -receipt-modern 0:0311534a9954 0.29 -0.91 1.00gbp/gbp -receipt-modern 0:c1dc9f641ca3 0.29 0.10 totaldebited +receipt-modern 0:cf38d95c9c6b 0.09 0.10 amount +receipt-modern 0:dc40efafaeb3 0.09 0.10 transferfee +receipt-modern 0:3a82ba6903c6 0.09 -0.86 £0.00 +receipt-modern 0:1cdd81427c35 0.09 0.10 exchangerate +receipt-modern 0:0311534a9954 0.09 -0.91 1.00gbp/gbp +receipt-modern 0:c1dc9f641ca3 0.09 0.10 totaldebited receipt-modern 0:de149c2431a2 0.03 0.10 paymentprogress -receipt-modern 0:4568e72e6bfd 0.29 0.06 instructed +receipt-modern 0:4568e72e6bfd 0.09 0.06 instructed receipt-modern 0:c5f62da028cc 0.05 0.06 07jul2026,08:12bst·collectionreq -receipt-modern 0:788d0109e065 0.30 0.06 inclearing +receipt-modern 0:788d0109e065 0.10 0.06 inclearing receipt-modern 0:7cb44e46733a 0.06 0.06 08jul2026,09:00bst·three-working -receipt-modern 0:d31e014808ec 0.31 0.06 settled +receipt-modern 0:d31e014808ec 0.11 0.06 settled receipt-modern 0:06cb75439693 0.07 0.06 09jul2026,06:30bst·fundsdebiteda receipt-modern 0:ab5aa97074c4 0.06 0.10 notes receipt-modern 0:a8da22f90eac 0.06 0.10 keepthisconfirmationforyourrecor -receipt-modern 0:59941ac03c12 0.29 0.05 scantocheckthisconfirmationagain +receipt-modern 0:59941ac03c12 0.09 0.05 scantocheckthisconfirmationagain receipt-modern 0:868e2517782a 0.03 0.05 support+442000000000 receipt-modern 0:94d25770d489 0.03 0.05 help@northwind-pay.example receipt-modern 0:d613d9566d09 0.03 0.05 northwindpayisafictionalinstitut @@ -4131,13 +4131,13 @@ receipt-modern 0:403a860bfc00 0.03 0.05 invented. receipt-modern 0:7b14b8d2c5c4 -0.04 0.14 page1of1 rota-cobalt 0:33888e306262 0.28 0.52 quay rota-cobalt 0:f5f481423c57 -0.22 0.52 quaysidebar -rota-cobalt 0:d457d4238a16 0.18 0.73 monday31st -rota-cobalt 0:916a90a10237 0.18 0.74 tuesday1st -rota-cobalt 0:c602d130bfde 0.18 0.77 wednesday2nd -rota-cobalt 0:74cf6babb6f6 0.18 0.73 thursday3rd -rota-cobalt 0:ae6aa379ad0c 0.18 0.68 friday4th -rota-cobalt 0:a6643f169bfd 0.18 0.72 saturday5th -rota-cobalt 0:58752a3c0543 0.18 0.73 sunday6th +rota-cobalt 0:d457d4238a16 -0.02 0.73 monday31st +rota-cobalt 0:916a90a10237 -0.02 0.74 tuesday1st +rota-cobalt 0:c602d130bfde -0.02 0.77 wednesday2nd +rota-cobalt 0:74cf6babb6f6 -0.02 0.73 thursday3rd +rota-cobalt 0:ae6aa379ad0c -0.02 0.68 friday4th +rota-cobalt 0:a6643f169bfd -0.02 0.72 saturday5th +rota-cobalt 0:58752a3c0543 -0.02 0.73 sunday6th rota-cobalt 0:c4aee96677b0 -0.08 0.66 /cleancrushedice rota-cobalt 0:2895e01ceae0 -0.09 0.63 machine&area rota-cobalt 0:f4e7df025c89 -0.08 0.70 pianist18:30/clean @@ -4147,8 +4147,8 @@ rota-cobalt 0:b913ee5e077a -0.08 0.68 privatedinner44pax/ rota-cobalt 0:d89987d22df9 -0.09 0.69 soulgf/pianist18:30 rota-cobalt 0:fb980282ae0a -0.08 0.63 pianist18:30/partner rota-cobalt 0:3c1fc038017e -0.09 0.64 meeting3:30pm -rota-cobalt 0:cc96b22615e5 0.22 0.60 pianist19:00 -rota-cobalt 0:bc14a4ce6407 0.12 1.26 statuslegend +rota-cobalt 0:cc96b22615e5 0.02 0.60 pianist19:00 +rota-cobalt 0:bc14a4ce6407 -0.08 1.26 statuslegend rota-cobalt 0:1f58b9145b24 -0.12 0.88 request rota-cobalt 0:b4dc66dde806 -0.12 0.73 off rota-cobalt 0:b4dc66dde806#1 -0.11 0.45 off @@ -4191,16 +4191,16 @@ rota-cobalt 0:08c4242b5f58 -0.12 0.78 stock rota-cobalt 0:f8f8922da592 0.13 0.81 standby rota-cobalt 0:c2fb788c7dee 0.13 0.85 training rota-cobalt 0:a18603086e5b -0.12 0.81 support -rota-cobalt 0:17ae69cc7e46 0.16 0.52 covers -rota-cobalt 0:412a5c513322 0.17 0.64 l104/d50 -rota-cobalt 0:304560bf3205 0.17 0.65 l33/d57 -rota-cobalt 0:ca6ce47ada53 0.17 0.66 l32/d76 -rota-cobalt 0:e594a1e64b7d 0.17 0.61 l22/d48 -rota-cobalt 0:7062fafe1dab 0.17 0.60 l65/d112 -rota-cobalt 0:778cb57b62fa 0.17 0.60 l73/d112 -rota-cobalt 0:1b4471920f01 0.17 0.62 l72/d83 +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 +rota-cobalt 0:e594a1e64b7d -0.08 0.61 l22/d48 +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.12 0.53 management -rota-cobalt 0:6a61eb7383e8 0.13 0.76 rosa +rota-cobalt 0:6a61eb7383e8 -0.12 0.76 rosa rota-cobalt 0:3bb0f0bb5552 -0.11 0.26 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#1 -0.20 0.22 09:00-18:00 rota-cobalt 0:3bb0f0bb5552#2 -0.20 0.23 09:00-18:00 @@ -4228,7 +4228,7 @@ rota-cobalt 0:e669544e9895 0.00 0.91 08:00-16:00 rota-cobalt 0:e669544e9895#1 -0.32 0.96 08:00-16:00 rota-cobalt 0:19c30663e8ef 0.36 1.71 16:00-21:00 rota-cobalt 0:19c30663e8ef#1 0.05 1.76 16:00-21:00 -rota-cobalt 0:c9affe7f7587 0.09 0.88 tomas +rota-cobalt 0:c9affe7f7587 -0.16 0.88 tomas rota-cobalt 0:6d43df86fe4b -0.15 0.26 16:00-00:00 rota-cobalt 0:6d43df86fe4b#1 -0.15 0.31 16:00-00:00 rota-cobalt 0:6d43df86fe4b#2 -0.20 0.27 16:00-00:00 @@ -4245,7 +4245,7 @@ rota-cobalt 0:6d43df86fe4b#12 -0.60 0.27 16:00-00:00 rota-cobalt 0:6d43df86fe4b#13 -0.53 0.95 16:00-00:00 rota-cobalt 0:5a5f719aae11 -0.05 0.90 11:00-16:00 rota-cobalt 0:df9ba122242c 0.02 0.53 bartenders -rota-cobalt 0:dbaf86dd80e3 0.04 0.82 nadia +rota-cobalt 0:dbaf86dd80e3 -0.21 0.82 nadia rota-cobalt 0:0cdd9d9e75dc -0.10 0.94 09:00-16:00 rota-cobalt 0:0cdd9d9e75dc#1 -0.14 0.96 09:00-16:00 rota-cobalt 0:0cdd9d9e75dc#2 -0.19 0.99 09:00-16:00 @@ -4255,7 +4255,7 @@ rota-cobalt 0:f1e2ce9aa575#1 0.22 1.70 16:00-22:00 rota-cobalt 0:f1e2ce9aa575#2 0.22 1.76 16:00-22:00 rota-cobalt 0:f1e2ce9aa575#3 0.18 1.79 16:00-22:00 rota-cobalt 0:f1e2ce9aa575#4 0.18 1.75 16:00-22:00 -rota-cobalt 0:de7ee76e7373 0.00 0.78 elias +rota-cobalt 0:de7ee76e7373 -0.25 0.78 elias rota-cobalt 0:61587c4c0d6d -0.25 0.31 09:00-17:00 rota-cobalt 0:7195a8ed72b9 -0.14 0.90 12:00-16:00 rota-cobalt 0:7195a8ed72b9#1 -0.23 0.94 12:00-16:00 @@ -4267,16 +4267,16 @@ rota-cobalt 0:7195a8ed72b9#6 -0.45 0.96 12:00-16:00 rota-cobalt 0:7195a8ed72b9#7 -0.45 0.91 12:00-16:00 rota-cobalt 0:7195a8ed72b9#8 -0.50 0.94 12:00-16:00 rota-cobalt 0:7195a8ed72b9#9 -0.50 0.95 12:00-16:00 -rota-cobalt 0:953cb9ba8128 -0.05 0.90 mirela +rota-cobalt 0:953cb9ba8128 -0.30 0.90 mirela rota-cobalt 0:c1298a6e85c2 -0.29 0.27 12:00-18:00 rota-cobalt 0:c1298a6e85c2#1 -0.38 0.27 12:00-18:00 -rota-cobalt 0:86846e84f9bf -0.09 0.78 sofia -rota-cobalt 0:effcc54ba75f -0.14 0.82 karol -rota-cobalt 0:5c1e134d2d34 -0.18 0.89 henrik -rota-cobalt 0:6326e0e8cfda -0.22 0.82 lucia +rota-cobalt 0:86846e84f9bf -0.34 0.78 sofia +rota-cobalt 0:effcc54ba75f -0.39 0.82 karol +rota-cobalt 0:5c1e134d2d34 -0.43 0.89 henrik +rota-cobalt 0:6326e0e8cfda -0.47 0.82 lucia rota-cobalt 0:c235e502637c -0.29 0.53 barbacks -rota-cobalt 0:bd558de5f794 -0.27 0.84 oskar -rota-cobalt 0:4941d6993ee9 -0.32 0.89 teodor -rota-cobalt 0:1d3d3d6a07bd -0.36 0.83 vesna +rota-cobalt 0:bd558de5f794 -0.52 0.84 oskar +rota-cobalt 0:4941d6993ee9 -0.57 0.89 teodor +rota-cobalt 0:1d3d3d6a07bd -0.61 0.83 vesna rota-cobalt 0:59f2ff7c7c70 0.03 0.08 timesshownin24-hourformat.shiftc 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 3256b6d17..1414e02d3 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv @@ -1,18 +1,18 @@ # libreoffice build 420(Build:2) on linux document engine_pages editor_pages lines matched median p90 over2 cv-blue_banner 2 2 54 52 0.13 0.45 0 -cv-boxed_sections 2 2 58 58 0.23 0.36 0 +cv-boxed_sections 2 2 58 58 0.08 0.17 0 cv-centered_headline 2 2 58 56 0.13 0.31 0 cv-charcoal_gold 1 1 85 83 0.17 0.24 0 -cv-classic_serif 2 2 57 54 0.10 0.28 0 -cv-compact_mono 1 1 58 58 0.17 0.30 0 +cv-classic_serif 2 2 57 54 0.07 0.15 0 +cv-compact_mono 1 1 58 58 0.17 0.29 0 cv-editorial_blue 2 2 82 82 0.14 0.30 0 cv-engineering_resume 1 1 57 57 0.26 0.38 0 -cv-executive 2 2 60 60 0.26 0.50 0 +cv-executive 2 2 60 60 0.26 0.30 0 cv-midnight_navy 1 1 86 84 0.28 0.87 0 -cv-minimal_underlined 2 2 59 59 0.33 0.62 0 +cv-minimal_underlined 2 2 59 59 0.21 0.45 0 cv-mint_editorial 2 2 59 59 0.07 0.30 0 -cv-modern_professional 2 2 61 59 0.06 0.24 0 +cv-modern_professional 2 2 61 59 0.04 0.06 0 cv-monogram_sidebar 1 1 53 50 0.20 0.44 0 cv-navy_sidebar 1 1 71 71 0.20 0.45 0 cv-nordic_clean 1 1 63 58 0.25 0.43 0 @@ -26,26 +26,26 @@ cv-teal_pulse 1 1 57 57 0.22 0.63 0 cv-terracotta_rail 1 1 102 102 0.44 0.94 0 cv-timeline_minimal 2 2 89 87 0.45 0.49 0 cv-violet_grid 1 1 80 78 0.26 0.54 0 -invoice-classic 1 1 56 56 0.05 0.22 0 -invoice-classic_long 2 2 166 166 0.03 0.12 0 +invoice-classic 1 1 56 56 0.05 0.12 0 +invoice-classic_long 2 2 166 166 0.03 0.08 0 invoice-consulting 1 1 92 92 0.19 1.01 0 -invoice-consulting_long 2 2 236 236 0.23 0.78 0 -invoice-luma_studio 1 1 97 97 0.12 0.46 2 -invoice-luma_studio_long 3 3 192 192 0.20 0.41 2 +invoice-consulting_long 2 2 236 236 0.18 0.51 0 +invoice-luma_studio 1 1 97 97 0.14 0.46 2 +invoice-luma_studio_long 3 3 192 192 0.11 0.41 2 invoice-merchant 1 1 94 94 0.78 1.96 6 invoice-metered 1 1 99 99 1.01 10.38 48 -invoice-modern 1 1 54 54 0.10 0.20 0 -invoice-modern_long 2 2 162 162 0.10 0.15 0 +invoice-modern 1 1 54 54 0.08 0.17 0 +invoice-modern_long 2 2 162 162 0.07 0.10 0 invoice-obsidian 1 1 81 81 0.27 2.80 15 invoice-payments 1 1 104 104 0.32 5.23 40 invoice-platform 1 1 111 111 0.32 0.43 0 invoice-subscription 1 1 81 81 0.19 0.51 0 invoice-workspace 1 1 90 90 0.19 0.42 0 letter-blue_banner 1 1 10 10 0.21 0.46 0 -letter-boxed_sections 1 1 10 10 0.11 0.23 0 +letter-boxed_sections 1 1 10 10 0.09 0.13 0 letter-centered_headline 1 1 11 11 0.10 0.23 0 letter-classic_serif 1 1 10 10 0.10 0.15 0 -letter-compact_mono 1 1 10 8 0.24 0.35 0 +letter-compact_mono 1 1 10 8 0.13 0.35 0 letter-editorial_blue 1 1 12 12 0.07 0.16 0 letter-engineering_resume 1 1 15 15 0.19 0.41 0 letter-executive 1 1 12 12 0.18 0.30 0 @@ -58,7 +58,7 @@ letter-sidebar_portrait 1 1 12 12 0.41 0.49 0 letter-timeline_minimal 1 1 15 15 0.33 0.53 0 proposal-editorial 2 2 132 129 44.75 45.38 72 proposal-indigo 1 1 72 70 0.26 0.65 0 -proposal-modern 1 1 44 44 0.10 0.20 0 +proposal-modern 1 1 44 44 0.10 0.17 0 proposal-northline 2 2 132 132 0.17 0.45 0 -receipt-modern 1 1 57 57 0.09 0.29 0 -rota-cobalt 1 1 154 151 0.22 0.52 0 +receipt-modern 1 1 57 57 0.09 0.11 0 +rota-cobalt 1 1 154 151 0.25 0.52 0 From c7d6a95ad79e96ce4b06abff722dbbdd62fd44cf Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 12 Oct 2026 10:50:32 +0100 Subject: [PATCH 3/3] fix(docx): raise a list's items in a cell by their list's page alone, and say so - A list's item in a cell is raised into the space above it where its list is on one page; whether its row is kept whole adds nothing, as a row of columns never crosses a page and a list in a table's composed cell lays out no lines to seat it by. - Javadoc and docs: the items not raised into the space above still take a position where they take one; the list, not its cell, is what the layout sets over two pages; what is left goes to the position where the whole difference is half a point or more. - Tests: a list in a composed cell is held to having no exact line, an item of runs to being tabbed to its text's column. --- CHANGELOG.md | 22 +++++++++--------- .../architecture/backend-capability-matrix.md | 2 +- docs/recipes/docx-export.md | 2 +- .../semantic/docx/DocxSemanticBackend.java | 23 ++++++++++--------- .../semantic/docx/DocxListItemSeatTest.java | 13 +++++++---- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f8932cf..69f3f2964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,17 +19,17 @@ follow semantic versioning; release dates are ISO 8601. gap goes and owes as much below, which the item under it rises into in turn; under half a point, one opening a cell takes what the gap cannot give from the cell's padding, or from the space above its row of columns; and what is left where the whole difference is half a point or more, and a line set - that much high, is its text's position. Not raised into the gap, its position still moved: an item - the layout opens a page with — the first of a list it starts on a new page in the flow, or one it - sets on a later page than the item before it — whose space above Word drops there, nor one of a - list in a cell the layout breaks over a page, in a row not kept whole, where what an item owed - would be the space above the next page's first, inside the cell. An item whose marker - Word draws, a Word list's, is not moved by its position: Word sets that marker from the - paragraph's mark, which LibreOffice does not move by the mark's position — lowered a point by - both, `MidnightNavy`'s duties stood 1.15pt under their bullets there — so its line keeps what the - space above does not take of it, and `MidnightNavy`'s and `NavySidebar`'s duties stand as high as - before. An item the layout's lines are not matched to, as `SlateOrange`'s certifications in a - table's composed cell, is not seated. + that much high, is its text's position. Not raised into the gap, though still moved by its position + where it takes one: an item the layout opens a page with — the first of a list it starts on a new + page in the flow, or one it sets on a later page than the item before it — whose space above Word + drops there, nor one of a list the layout sets over two pages in a cell, where what an item owed + would be the space above the next page's first, inside the cell. An item whose marker Word draws, + a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, + which LibreOffice does not move by the mark's position — lowered a point by both, `MidnightNavy`'s + duties stood 1.15pt under their bullets there — so its line keeps what the space above does not + take of it, and `MidnightNavy`'s and `NavySidebar`'s duties stand as high as before. An item the + layout's lines are not matched to, as `SlateOrange`'s certifications in a table's composed cell, is + not seated. `ClassicInvoice`'s notes moved up 0.24 to 0.36pt in Word and 0.3pt in LibreOffice, to within 0.05pt of the page, `BoxedSections`' and `ModernProfessional`'s skills and projects 0.12 to 0.24pt diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 273464453..7a72b8731 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -63,7 +63,7 @@ Payload records live in `core` under | Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) | |---|---|---|---| -| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, matched at a paragraph's middle line: a line of its own Word sets a tenth of a point or more low is moved up into the space written above it and that space owed below it, in the flow and in cells, not in an overlay, a paragraph the layout moves to a new page or breaks over one, nor the space a panel's top border takes above its first line; a line opening a cell, under half a point low, takes what that space cannot give from the cell's top padding — a panel's, its row held as much taller, and a table's composed cell's in a row whose margins even — and a row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, what follows kept in place (not a row the layout moves to a new page, held to a height, or whose column opens with a panel or a table); a paragraph in a composed table cell of a placed row is raised as anywhere, its row kept on one page; what that space cannot give, and a line Word sets high, is moved in the same position where the two are half a point or more apart — Spectral's, not Lato's — which LibreOffice moves further by the face's height over its em (a list's item seated as a paragraph is — not raised into the space above where the layout opens a page with it or breaks its list's cell over a page, an item whose marker Word draws not moved by its position, since LibreOffice does not move that marker by its mark's, and one the layout's lines are not matched to not seated; a table text cell's by its row's margins, see Table rows; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair, as a badge's initials or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it; an auto-sized paragraph's text that takes its style is written at the size the page fits it to, read off the laid-out lines, a run with a style of its own keeping it, on every path that writes a paragraph — at its style's size where the lines do not tell the fitted size (no lines read, lines in sizes that do not say which is the paragraph's, or one paragraph added at more than one place); a paragraph a session reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, read through the page's own parser, one run a piece in the face, family, colour, tracking and size the page's laid-out lines hold (an auto-sized one's read at the size the page fits it to, a heading at its multiple of it), its marks dropped, wherever the lines hold the pieces' letters so; where they are not read, or hold other letters or none (text the parser reads into nothing, which the page sets as nothing), as authored, its marks as letters; a `bookmark(...)` is Word's `HeadingN`, which Word's outline lists by the text of its Word paragraph — an overlay's pair's whole line, one level for both sides — at no level past the ninth. Outside a header or footer, the paragraph's report note (`ParagraphNode`) names each of these where it moves or renames something: the prefix's letters, and the room a path that writes no prefix leaves out where it moves a line; the size an auto-sized paragraph's text is written at, where the size the page fits it to is not measured; the marks of a paragraph the page read as markdown and the file holds as letters, where its laid-out lines hold fewer of them than its text, not measured where its lines are not read; a markdown heading written taller than the line the page sets it in, which Word cuts on screen; an outline title that is not the text Word lists, a level past the ninth that shares it with another, and the right side's entry where the left holds the line's level | +| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; a centred or right-aligned left-to-right line of its own, of text alone and untracked, that Word sets a point or more wider or narrower at its half-point size has its letters spaced by the difference (`w:spacing`) and its room reckoned from the page's width; a `linkTarget` becomes a `w:hyperlink`, with a relationship for an address or `w:anchor` for one of the document's own anchors, and a run's own link wins over the paragraph's; a paragraph seated off its baseline (`TextVerticalAlign`) has its runs raised or lowered in the line (`w:position`) by the PDF backend's own correction (`ParagraphSeating`), one shift for the paragraph where the page seats each line by its own; Word and LibreOffice stand an exact line's baseline four fifths of the way down it whatever the face, where the page sets it the face's ascent down, matched at a paragraph's middle line: a line of its own Word sets a tenth of a point or more low is moved up into the space written above it and that space owed below it, in the flow and in cells, not in an overlay, a paragraph the layout moves to a new page or breaks over one, nor the space a panel's top border takes above its first line; a line opening a cell, under half a point low, takes what that space cannot give from the cell's top padding — a panel's, its row held as much taller, and a table's composed cell's in a row whose margins even — and a row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, what follows kept in place (not a row the layout moves to a new page, held to a height, or whose column opens with a panel or a table); a paragraph in a composed table cell of a placed row is raised as anywhere, its row kept on one page; what that space cannot give, and a line Word sets high, is moved in the same position where the two are half a point or more apart — Spectral's, not Lato's — which LibreOffice moves further by the face's height over its em (a list's item seated as a paragraph is — not raised into the space above where the layout opens a page with it or sets its list over two pages in a cell, an item whose marker Word draws not moved by its position, since LibreOffice does not move that marker by its mark's, and one the layout's lines are not matched to not seated; a table text cell's by its row's margins, see Table rows; a picture among it moves with it in Word and stays on its own baseline in LibreOffice); lines a container stacks over one another tighter than their face each end halfway between their letters and the next line's (Word draws an exact line's text on screen only inside the line; its PDF export does not cut it), and the last layer of a shape container on one page, where its line runs past the foot, ends at the foot or below its letters; letters two lines share are split halfway so the page does not move, and a stack that holds a picture keeps its lines' own heights; a `bulletOffset` of spaces becomes the paragraph's indent (`w:ind` left, hanging or first line, by `indentStrategy`) in the flow and in cells, not yet over the flow, in an overlay's left-and-right pair, as a badge's initials or in a header or footer; one with letters in it is not written, its wrapped lines still set after the spaces that cover it; an auto-sized paragraph's text that takes its style is written at the size the page fits it to, read off the laid-out lines, a run with a style of its own keeping it, on every path that writes a paragraph — at its style's size where the lines do not tell the fitted size (no lines read, lines in sizes that do not say which is the paragraph's, or one paragraph added at more than one place); a paragraph a session reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, read through the page's own parser, one run a piece in the face, family, colour, tracking and size the page's laid-out lines hold (an auto-sized one's read at the size the page fits it to, a heading at its multiple of it), its marks dropped, wherever the lines hold the pieces' letters so; where they are not read, or hold other letters or none (text the parser reads into nothing, which the page sets as nothing), as authored, its marks as letters; a `bookmark(...)` is Word's `HeadingN`, which Word's outline lists by the text of its Word paragraph — an overlay's pair's whole line, one level for both sides — at no level past the ninth. Outside a header or footer, the paragraph's report note (`ParagraphNode`) names each of these where it moves or renames something: the prefix's letters, and the room a path that writes no prefix leaves out where it moves a line; the size an auto-sized paragraph's text is written at, where the size the page fits it to is not measured; the marks of a paragraph the page read as markdown and the file holds as letters, where its laid-out lines hold fewer of them than its text, not measured where its lines are not read; a markdown heading written taller than the line the page sets it in, which Word cuts on screen; an outline title that is not the text Word lists, a level past the ninth that shares it with another, and the right side's entry where the left holds the line's level | | List hanging indent — a marker column and a content column (`ListBuilder.hangingIndent(true)`, `markerGap(...)`) | ✅ marker and content emitted as separate `ParagraphFragmentPayload` fragments at the resolved `markerX` / `contentX` | ✅ the same fragments — the fixed-layout pipeline resolves the geometry before either backend sees it | ⚠️ the top level only. `DocxSemanticBackend` exports a list as a real Word list — `numbering.xml`, `w:numPr` per item, the level carrying the marker — or, with rich items or a drawn marker, as paragraphs; content and nesting are unaffected. With the flag, the top level's marker column is the layout's — the marker's width and `markerGap`, the text and its wrapped lines where the page sets them — where the gap covers what Word may set the marker wider: a picture at its written size, its edges included, or text in the page's face (embedded, or a standard one Word sets in the same widths) grown to its half-point size, half a point clear. A Word list's level then indents and hangs by that column; a list of paragraphs writes the marker, a tab to a stop there, and hangs the item there. Word places content at absolute indents and has no relative-advance primitive, so without the layout's measure the gap could not be honoured; a Word list without the flag that the layout placed and that does not nest takes the page's column too, the spaces the page sets its wrapped lines after, its marker followed by a space (`w:suff`) and an item that wraps measured at Word's half-point size; a list that nests items, a list built as a tree of items (laid out flattened), and a marker the gap does not clear keep the stated column (180 twips, plus 120 per nesting level) — except, in a list of paragraphs, a nested rich item with no marker, which stands where the layout set its text, its measure weighed at Word's half-point sizes, where the layout's items are matched to the list's; a list that nests only such items sets its top level at the page's column too. The report counts, on the list, the items that stand at a stated column, a space past their marker or two spaces a level in, and names a centred or right-aligned list written flush left, a lineSpacing not written where the layout's items are not the list's own and one wraps (in a list composed in a table cell, its wrapping not measured), a continuationIndent not written where an item of a markerless list or a tree of items without the flag wraps or its wrapping is not measured, the rows the page draws as a marker alone for blank items of a flagged list, which are not written, and what items the page reads as markdown lose. An item of plain text the page reads as markdown — the default, unless `markdown(false)` — is written as the page sets it, matched to the lines the page laid it out in and read as the page lays it out (a flat item after the marker the page sets before it; a flagged item as its text alone; an item of a list built as a tree of items without the flag after the indent and marker the page reads with it), one run a piece in the face, family, colour, tracking and size those lines hold, Word or the file drawing the marker where it did (Word draws a tree's bullets regular, as the parser sets the marker it reads), the font table shipping the faces the page sets the pieces in; it is written as authored, its marks as letters, where its list's items are not matched one by one to the layout's (no layout, composed in a table cell, an item run onto the next page, a flagged list with a blank item), where its lines hold other letters, where the parser reads a tree's marker as markdown with the item (`*a*`), and where the parser reads it into nothing, and named where the page drops a mark from it or sets none of its text — not where it changes only its face or letters no mark is made of (`1.`); a markdown heading written taller than its item's line is named | | Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ⚠️ `DocxSemanticBackend` — the fill becomes the run's own `w:shd`, in a paragraph and in a list item alike, so a badge still reads as a badge. What Word has no way to say is the shape: shading covers the glyph box, so the corner radius and the padding above and below the letters are not in the file, and the export records them. The padding beside the letters is written as the room it takes (`spaceAfterTheLastLetter`): character spacing after the chip's last letter, shaded with it, and after the letter before the chip, unshaded. A chip opening its line or following a picture has no letter before it, so its left padding is not in the file; no space is written after right-to-left letters or after a symbol or emoji. The export records, chip by chip, how each side was written. LibreOffice sets no spacing after a line's last letter, so it does not apply the right padding of a chip that ends a line. A `w:shd` fill is opaque, so a translucent chip is flattened first against what Word paints underneath it — the paragraph's shading, the cell's, or else the colour the page paints under the paragraph, a page background included — so the chip agrees with the file it is in and shows the colour the PDF shows. It stops being translucent, and that is recorded with the rest | | Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ✅ `DocxSemanticBackend.writeInlinePicture` (a picture in its own run where it sits among the words, at its size, inside the run's or the paragraph's link; raised or lowered by `w:position` to where the page's alignment and `baselineOffset` put it, from the layout's measure of the paragraph's first line — in a list, the list's text on a line as tall as the item's own tallest picture; LibreOffice ignores `w:position` on a picture and stands it on the baseline, so a picture the export draws itself (icon, emoji, shape) that the page raises carries the rise as transparent rows and needs no `w:position`, while one the page lowers stands in LibreOffice higher than on the page by as much as the page lowers it — up to the text's descent for a centred icon as tall as its line; the editor clips a picture to an exact line height, so a paragraph holding a picture that leaves its text — past the ascent or the descent, in Word's placement or on the baseline — has its lines written at least the height the picture reaches, grown by the editor rather than clipped, every line of the paragraph since Word has one line height for it, and each as tall as the editor's font makes it — for 14pt text about 2.5pt taller than the page's in LibreOffice; a picture inside its text in both editors keeps the exact height; a paragraph of one line of text in a Word paragraph of its own, with room above for its pictures' reach, keeps an exact line at the page's height of it, the pictures set in it where the page puts them in Word and what their ink reaches past it taken from the gaps around it, and in LibreOffice a lowered picture there stands higher and loses what passes the line's top; its description is the text it stands for or empty) | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index aeae647dd..b10c0210e 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -185,7 +185,7 @@ it cannot work out for itself: | What | Where it lands | |---|---| -| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet a list item's, or text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is left either way: Word sets lines on a grid of its own, 0.12pt apart (measured). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One with no room above it for the gap (opening a column, or a cell padded less) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it. Not a line pair's shared line, which moved would move its other half; not a line in an overlay — a band, a layer stack, a shape container, a canvas, a text box — whose writers measure what follows them from the page; not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. What that space cannot give, and a line Word sets higher than the page — moved down, it would take its room out of the space below it, not known when it is written — is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent, as Spectral's is. A list's item is seated as a paragraph is: raised into the gap above it, the item under it rising into what it owes, under half a point into its cell's padding or out of the space above its row of columns where it opens one, and what is left of half a point and more moved by its position. Not raised into the gap, its position still moved: an item the layout opens a page with, the first of a list it starts on a new page in the flow or one it sets on a later page than the item before it, whose space above Word drops there, nor one of a list in a cell the layout breaks over a page, in a row not kept whole, where what an item owed would be the space above the next page's first, inside the cell. An item whose marker Word draws, a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, which LibreOffice does not move by the mark's position (measured: `MidnightNavy`'s duties, lowered a point by both, stood 1.15pt under their bullets there), so its line keeps what the space above does not take of it. An item the layout's lines are not matched to — `SlateOrange`'s certifications, a list in a table's composed cell — is not seated. A table text cell's lines are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, raised 4pt, stood 2.4pt high in LibreOffice. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | +| Line height | `w:spacing w:lineRule="exact"` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. Word has one height for a paragraph's lines: it is the tallest line's text, or, where the page sets lines of different heights further apart than that, the page's mean distance between their baselines, with what a paragraph on one page then falls short of the page owed below it, and what it passes the page by — written at its tallest line, a wrapped line after a large first one, which the page sets at its own height — hung below it, as a line held to its icon is: the space below takes it, and what it cannot take is named (`space above`) (not yet a list item's, or text set over the flow; measured: `CompactMono`'s sidebar, whose skills are two lines a paragraph and each written 0.12pt taller than the page, ended a point low in Word). Lines rounded up to the twip, over enough of them, hang the same way; what rounding takes from a paragraph written at its tallest line is owed below it as well (measured: `ModernInvoice`'s address blocks, six lines of 11.475pt written at 11.45, stood the table under them 0.15pt high in Word and LibreOffice). A tenth of a point or less is left either way: Word sets lines on a grid of its own, 0.12pt apart (measured). In a paragraph of more than one line on one page, the spaces a `bulletOffset` sets before a line, written as an indent, do not count towards that line's text. A paragraph of lines as tall as their text, on one page, opening a table cell takes the gap above its first line from the cell's top padding, no more than steps its lines the page's distance apart — not in a row beside a merged cell or a cell that may open with a table — anything but text, a paragraph, a list, a picture or a spacer: a table, a row of columns, a painted container, a shape — whose margin Word would set the whole row at, nor in a panel. One with no room above it for the gap (opening a column, or a cell padded less) shares the gaps out over all its lines, so its lines stand a little closer than on the page. A paragraph the layout did not measure — one an export without a layout writes — is left to the editor, and a line holding a picture above its text is written "at least" that height — except a paragraph of one line of text with room above for its pictures' reach, held exact at the page's height (see "Inline pictures"); in both the paragraph mark is set in the text's size and face, since the mark counts towards the last line's height. A line holding pictures and no text, where they fill the page's line, has its mark and its pictures' runs set at a point instead, so Word makes the line as tall as the pictures and no taller: in the paragraph's own size the mark's depth went under the picture (measured: `SlateOrange`'s skills, a 12.4pt icon beside each label, stood 0.2pt taller each in Word). A smaller picture in a line the page sets at its font's height keeps its mark, and so does a line holding a letter, a break, a tab, a field or a link's text. A paragraph's own line of drawn shapes and no text written at least their reach, which Word grows to the pictures and the transparent frame each keeps around its ink, has that frame taken from the space above the line and above what follows, where it passes the height written: the ink stands where the page draws it, and what follows where the page sets it. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice on the fifteen faces the templates use and JetBrains Mono, 8 to 36pt), and the page sets it the face's ascent below the line's top, matched at a paragraph's middle line. A line Word sets lower than the page by a tenth of a point or more is moved up into the space written above it, written to the twip, and that space is owed below it, so what follows stays where it was; both editors keep a paragraph's space above as written (by its position alone, Helvetica's lines, which the page seats 0.78 of the way down, stood 0.12 to 0.38pt low in Word). A column's later layer, which measures the space to itself from the page, takes back the raise of the line above it. Not a line pair's shared line, which moved would move its other half; not a line in an overlay — a band, a layer stack, a shape container, a canvas, a text box — whose writers measure what follows them from the page; not a line cut to fit, a container's stacked lines or one held to its pictures; not a paragraph the layout moves to a new page, whose space above Word drops there, or breaks over one; and not into the space above a panel's first line that its top border takes. A line opening a cell has no space above it of its own: a panel's padding and a table cell's are the cell's margin, and a row's columns have their space above written above the row's table. One Word sets less than half a point low takes what its space above cannot give from the top padding of the cell it opens: a table's composed cell's, in a row whose margins all even, as the gap above a first line does, the cells beside it taking the margin back above their first lines; and a panel's, which the gap does not take, less what its top border takes, the panel's row then held as much taller, as Word adds the margin left to the height a row is held at. A row of columns' first row is lifted out of the space above its table by the most its cells' first lines call for, each line rising by its own and the space under it taking that back, so what follows stays (measured: `ModernProfessional`'s entries, a title beside its dates in a row of their own, moved up 0.24pt in Word and 0.2pt in LibreOffice, `ConsultingInvoice`'s line items, composed cells, 0.24 to 0.36pt and 0.35pt, everything under them where it was). Not a row the layout moves to a new page, whose space above stays on the page above, a row held to a height, or one whose column opens with a panel or a table — a column with a fill or a border is a panel in its cell — which would rise whole; LibreOffice 24.2 counts a row's margins inside the height it is held at, so a panel or a table row held taller than its text — a drawing set where the page puts it — comes out taller there by the raise. A paragraph in a table's composed cell, in a row the layout placed, is raised into the space above it as anywhere else: the cell has no placement of its own, but its row is kept on one page; one in a table nested in that cell is not yet. What that space cannot give, and a line Word sets higher than the page — moved down, it would take its room out of the space below it, not known when it is written — is raised or lowered to the page's baseline by `w:position` where the whole difference is half a point or more, a face with a deep descent, as Spectral's is. A list's item is seated as a paragraph is: raised into the gap above it, the item under it rising into what it owes, under half a point into its cell's padding or out of the space above its row of columns where it opens one, and what is left moved by its position where the whole difference is half a point or more. Not raised into the gap, though still moved by its position where it takes one: an item the layout opens a page with, the first of a list it starts on a new page in the flow or one it sets on a later page than the item before it, whose space above Word drops there, nor one of a list the layout sets over two pages in a cell, where what an item owed would be the space above the next page's first, inside the cell. An item whose marker Word draws, a Word list's, is not moved by its position: Word sets that marker from the paragraph's mark, which LibreOffice does not move by the mark's position (measured: `MidnightNavy`'s duties, lowered a point by both, stood 1.15pt under their bullets there), so its line keeps what the space above does not take of it. An item the layout's lines are not matched to — `SlateOrange`'s certifications, a list in a table's composed cell — is not seated. A table text cell's lines are seated by its row's margins where its row allows (see "Tables"). LibreOffice moves text by `w:position` further than it is raised, by the face's height over its em — Spectral's 1.53 times, Poppins' 1.49, Volkhov's 1.31 (measured) — so a line raised by its position stands where the page sets it in Word only: Spectral's 24pt line, raised 4pt, stood 2.4pt high in LibreOffice. A picture among such text moves with it in Word; LibreOffice keeps a picture on its own baseline, where it stood before. Lines a container stacks tighter than their face — a title's lines a pitch apart — each end halfway between their letters and the next line's, since Word draws an exact line's text on screen only inside the line, and the last layer of a shape container, where its line runs past the foot, ends at the foot or below its letters | | Table columns | the resolved cell widths as `w:gridCol`, with `w:tblLayout` fixed so Word does not re-fit them | | Row columns | where the layout placed each child, with the row's gap and padding folded into the neighbouring column and taken back out as that cell's margin — in a painted row's panel the padding is the panel cell's margins instead, and the columns are without it. A column sized to its content (`DocumentRowColumn.auto()`) gets a point more, taken from the row's weight columns so the row keeps its width, for the reason a table's does: the editor's substitute font would wrap it — a table of contents' labels broke mid-word ("Intr" / "o") in LibreOffice without it. A row with no auto column, no weight column, or no stated columns (weights, an even split) is written as placed | 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 bc94a6479..da44d6a46 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 @@ -4849,13 +4849,13 @@ private void writeListLine(XWPFDocument document, DocumentTextStyle style, * point by their text's and their mark's, stood 1.15pt under their bullets there (measured). Its * line keeps what the space above does not take of it.

* - *

Not raised into the space above — its position still moved — an item the layout opens a - * page with: the first of a list it starts on a new page in the flow, or one on a later page - * than the item before it. Word drops its space above at the top of a page, and what its line - * owed would stand the rest lower. Nor an item of a list in a cell the layout breaks over a page, - * in a row not kept whole: what an item owed there would be the space above the one opening the - * next page, inside the cell, which Word is not known to drop. An item the layout's lines are not - * matched to is not seated at all.

+ *

Not raised into the space above, though still moved by its position where it takes one: an + * item the layout opens a page with — the first of a list it starts on a new page in the flow, or + * one on a later page than the item before it. Word drops its space above at the top of a page, + * and what its line owed would stand the rest lower. Nor an item of a list the layout sets over + * two pages in a cell, the cell going on on the next: what an item owed there would be the space + * above the one opening that page, inside the cell, which Word is not known to drop. An item the + * layout's lines are not matched to is not seated at all.

* * @param laidOut how the layout set the item, or {@code null} where its items are * not matched @@ -4867,7 +4867,7 @@ private void seatTheItem(XWPFParagraph para, DocxLayoutMetrics.ItemText laidOut, } boolean opensItsPage = itemPage < 0 ? listOpensItsPage : laidOut.page() > itemPage; itemPage = laidOut.page(); - boolean staysOnItsPage = !opensItsPage && (currentCell == null || listOnOnePage || inARowKeptWhole()); + boolean staysOnItsPage = !opensItsPage && (currentCell == null || listOnOnePage); double shift = shiftToThePagesBaseline(para, laidOut.lines(), listLineGap, 0, false, true, staysOnItsPage); if (!wordDrawsTheMarker) { raiseRunsFrom(para, 0, Math.round(shift * HALF_POINTS_PER_POINT)); @@ -9995,7 +9995,7 @@ private double shiftToThePagesBaseline(XWPFParagraph para, * wherever the row lands, though a table's composed cell has no placement of its own to say * so ({@link #inARowKeptWhole}). A list's item is raised as a paragraph is, but for the first * of a list the layout starts on a new page, one it sets on a later page than the item before - * it, and one of a list in a cell it breaks over a page ({@link #seatTheItem}). Not the space + * it, and one of a list it sets over two pages in a cell ({@link #seatTheItem}). Not the space * above a panel's first paragraph that the panel's top border takes * ({@link #takeTheTopBorderInside}): raised into it, the line would give it back and the panel * grow by what it owed. Less than {@link #LEAST_RAISE_INTO_THE_SPACE_ABOVE} is not moved.

@@ -10011,8 +10011,9 @@ private double shiftToThePagesBaseline(XWPFParagraph para, * (measured).

* * @param para the Word paragraph - * @param staysOnItsPage whether the layout sets the line on the page the space above it is - * on: neither opening a page nor broken over one + * @param staysOnItsPage whether what the line takes from the space above stays above it on its + * page: not where the layout opens a page with the line or breaks it over + * one, nor sets the list it is an item of over two pages in a cell * @param spacing the paragraph's spacing, its space above already written * @param raise how far Word sets the line below the page's, in points * @param intoThePadding whether the cell's padding may give what the space above cannot diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java index 4c22da6b7..38c4a9ca1 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxListItemSeatTest.java @@ -150,21 +150,24 @@ void anItemInALayerStackKeepsTheSpaceAboveIt() throws Exception { } @Test - void anItemTheLayoutsLinesAreNotMatchedToIsLeftAsWordSetsIt() throws Exception { - // A list in a table's composed cell lays out no lines of its own to seat it by. + void anItemOfAListInATablesComposedCellIsLeftAsWordSetsIt() throws Exception { + // A list in a table's composed cell lays out no lines of its own: its items are matched to + // none, and are written at no exact height of the page's to seat them in. try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t .columns(DocumentTableColumn.fixed(200)) .rowCells(DocumentTableCell.node(new ListBuilder().noMarker().textStyle(spectral(30)) .items("Inside").build()))))) { XWPFParagraph inside = inACell(document, "Inside"); + CTSpacing spacing = spacing(inside); + assertThat(spacing == null || !spacing.isSetLineRule()).as("the premise: no line the page measured").isTrue(); assertThat(position(inside)).isZero(); assertThat(before(inside)).isZero(); } } @Test - void anItemsWrappedLinesAreSeatedAtTheirMiddleOne() throws Exception { + void anItemsLinesAreSeatedAtTheirMiddleOne() throws Exception { // Two lines 10pt apart and no space above to give: Word's lines are each 5pt taller than // the face, stepping 5pt closer than the page's, and the error is shared by the two. try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page @@ -275,7 +278,7 @@ void anItemOpeningARowsColumnRisesOutOfTheSpaceAboveItsTable() throws Exception } @Test - void anItemOfAListInACellTheLayoutBreaksOverAPageKeepsTheSpaceAboveIt() throws Exception { + void theItemsOfAListTheLayoutSetsOverTwoPagesInACellKeepTheSpaceAboveThem() throws Exception { // A panel the page breaks is one cell going on over the next page: what the first item // owed would be the space above the one opening that page, inside the cell. try (DocumentSession session = GraphCompose.document().pageSize(400, 600).margin(DocumentInsets.of(20)).create()) { @@ -315,6 +318,8 @@ void anItemOfRunsIsSeatedAsAPlainOneIs() throws Exception { double raise = 0.8 * line(item) - SPECTRAL_ASCENT * 30; assertThat(item.getCTP().getPPr().isSetNumPr()).as("the premise: its marker written as text").isFalse(); + assertThat(item.getCTP().getRList()).as("the premise: tabbed to its text's column") + .anySatisfy(run -> assertThat(run.sizeOfTabArray()).isPositive()); assertThat(before(item)).isCloseTo(Math.round((20 - raise) * 20), within(1L)); assertThat(position(item)).isZero(); }