diff --git a/CHANGELOG.md b/CHANGELOG.md index 5746c8590..6c575803e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,38 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export anchors a drawing in a paragraph of the block it belongs to, so it moves with + that block when the text above is edited.** + - A shape stood in the paragraph whose text was nearest it, within 48pt, rising above that + paragraph's top by 24pt at most — in a table cell by a stroke's width —, and otherwise was + placed from the page's edges, where it stayed while the text moved away from it: an icon laid + over a card's corner, a sidebar's portrait, a skill's meter beside its label in a row, a + timeline's dots in a column of their own. + - A drawing a node paints is now anchored in the first paragraph able to carry it that the + nearest block holding it writes on its page — the node itself, a layer stack, a panel, a row, + but not the page's flow —, placed from that paragraph's top however far from its text it + stands. In a table cell, the cell of that paragraph's row holding the drawing across takes it, + its own where it stands in a column of its own; otherwise the paragraph's own cell keeps it, + placed out of its column, unless it holds text — a text box, a badge's letters —, which + LibreOffice 26.8 pulls back into the column. A cell set to the middle or the bottom, or + sitting in one, carries none: the block's next + paragraph able to place it takes the drawing, or else the next block up. A drawing no node + paints — a timeline's rail — and one whose blocks write no such paragraph on its page go to + the paragraph nearest it there. What a table's composed cell draws keeps the nearest-text + rule. The report's note on each drawing says how it is anchored. + - Measured in Word 16.0.20527 and LibreOffice 24.2: a shape anchored in a table cell's + paragraph stands where both say — rising 36pt above its row, hanging below its table, out of + its cell, taller than its row, in an exact row or a nested table, in front of the text or + behind it, and across the fidelity corpus from 155pt above its paragraph to 620pt below —; + neither clips it nor moves a line for it, and both move it with its row when the text above + is edited. LibreOffice 24.2 sets a cell carrying a drawing from its top whatever its vertical + alignment says, 16pt high in a cell set to the middle and 32pt in one set to the bottom. + - Of the 465 drawings the fidelity corpus placed on the page, 270 were drawn by a node, 5 were + timelines' rails and 190 were composed in table cells. All but 14 of the first 275 are now + anchored in a paragraph: the text boxes of `LumaStudio`'s sidebar and closing note, which no + paragraph of their column holds across, and two drawings on pages whose only text is a pair + of lines. + - **A DOCX export writes a painted box holding text in the body as a panel, its text inside its outline.** - A shape container shaped as a rectangle or a rounded rectangle, filled or outlined, holding diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index 808a3ef6f..f804b8074 100644 Binary files a/assets/readme/examples/word-export-companion.docx and b/assets/readme/examples/word-export-companion.docx differ diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 670bb8cc6..c72562112 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -46,7 +46,7 @@ nodes are a different case: `ShapeNode`, `LineNode`, `EllipseNode`, `DocxSemanticBackend.writeNode`. A horizontal `LineNode` or a thin filled `ShapeNode` is written as a rule; any other rectangle, rounded rectangle, ellipse, line, polygon or path is drawn from the fragments the layout placed -it as, a floating DrawingML shape behind the text, anchored in the paragraph whose text it stands beside or else to the page (`DocxDrawings`, `DocxDrawingAnchors`, +it as, a floating DrawingML shape behind the text, anchored in the first paragraph the nearest block holding it writes on its page — the paragraph nearest it where none does, the paragraph whose text is nearest within reach for what a composed cell draws — or else to the page (`DocxDrawings`, `DocxDrawingAnchors`, polygons and paths as custom geometry). Inside a filled panel a shape stands in front of the text unless it frames text; anchored in a paragraph of the panel's cell, it is placed from the cell's text column, as Word lays a shape anchored in a @@ -73,7 +73,7 @@ Payload records live in `core` under | Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ⚠️ `DocxSemanticBackend.writeInlinePicture` + `DocxShapePictures` (a transparent PNG drawn by the shared `InlineSvgRasters` from the outline, fill and stroke — every outline kind, each layer centred in the run's box — placed as an inline picture is; the picture takes as far as the stroked ink reaches past the outline — half the stroke on an edge, more at a sharp corner's miter — and a pixel on each side, measured side by side, and is lowered by what it takes below, so no edge is cut and a shape takes that much more room in the line, which the text beside it gives back in whole tenths of a point a letter where it has letters on one line (`giveBackWidth`) — where none does, the shape's note says, from half a point, up to how far that moves it and what stands beside it in its line, as Word sets the line from the side its alignment names, the paragraph taken as one line; what the tenths miss, up to a twentieth of a point a letter and all of it in a run too long for a tenth a letter, is not named, a quarter point at most on the corpus; a list marker that draws a disc is its picture; at the top level of a `hangingIndent(true)` list that does not nest it is followed by a tab to where the layout starts the item's text, the item's lines hanging there, when the picture clears that stop, else by a space; Word could hold a shape in the line, `wp:inline` round a `wps:wsp`, which the export does not write, so each shape among the words is reported `APPROXIMATED` as a picture) | | Inline SVG (`ParagraphSvgSpan`) | ✅ `PdfParagraphFragmentRenderHandler` + `PdfPathPainter` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` + `PptxInlineSvgRasterizer` (simple layers stay native; arbitrary clips, exact dash/cap/join styles, and off-viewBox art use a transparent PNG fallback — drawn by the shared `InlineSvgRasters`; gradient paints use their primary colour) | ⚠️ `DocxSemanticBackend.writeInlinePicture` (always the transparent PNG: the same layers the layout resolves, through `InlineSvgLayers`, drawn by the raster the PPTX fallback uses — `InlineSvgRasters`, four pixels a point — and placed as an inline picture is; emoji included, so an emoji is a picture rather than a character; every icon is reported `APPROXIMATED` as a picture, an icon's text as its description, and a gradient, which the raster draws flat in its first colour) | | Text an inline icon stands for — copy, search, extraction (`ParagraphSvgSpan.text`, set by `SvgIcon.withText` and on every `EmojiLibrary` emoji) | ✅ `PdfTextLayer` via `PdfRenderEnvironment.writeTextLayer`: one invisible glyph over the icon on the line's baseline, from a Type 3 font of empty glyphs whose `ToUnicode` states each text, a whole ZWJ sequence included; rendering mode 3, so nothing is painted. One font per document, a new one after 255 distinct texts; a text over 256 UTF-16 units is not written. A block icon (`addSvgIcon`, `SvgIcon.node`) writes no text. `ActualText` around the paths was measured to reach none of PDFBox, poppler, pdf.js and MuPDF — it replaces glyphs, and a drawing has none. In a right-to-left line the glyph sits between the words it was written between and states its whole text; reading such a line back, PDFBox reverses the emoji one UTF-16 unit at a time and poppler reverses the code points of a ZWJ sequence or a U+FE0F pair, while pdf.js and MuPDF keep it whole (measured; a reader-side reversal of the glyph's text) | ❌ the icon is drawn and its text is not written | ⚠️ the icon is a picture whose description (`docPr/@descr`) is its text — read by a screen reader, but not a character a reader copies or searches | -| Rectangle shape — fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | ✅ `PdfShapeFragmentRenderHandler` | ⚠️ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners — single-adjust `roundRect` preset — until custom geometry lands; uniform radii and side borders exact) | ⚠️ `DocxSemanticBackend.writeContainerChildren` — a `SectionNode` or `ContainerNode` carrying a fill, per-side borders or a uniform stroke is written as a one-cell table, and so is a `RowNode` in the flow that paints its box (`writePaintedRow`, through the same `writePanelPiece`), its columns a table nested in the cell without its padding — not in a band of layers or a stack's measured column, with a padding or margin it cannot hold, composed in a table cell or in a page zone, where it is its columns alone and its paint is reported: the cell's shading is the fill behind everything inside, its borders are the container's at its full height, and its margins are the padding on all four sides, less half of each border, with the table half a border wider on each side, so the text and the border land where the page draws them — in a cell, a panel set in from the cell's text by half its left border or more keeps its whole left padding as its left margin, and one set in less what it is set in by of that half, as both editors set a nested table's text its left border's centre and its margin in — in the body, the left margin is the whole padding, the table no wider on that side, and its indent places its text, as Word 16 and LibreOffice on Windows centre a body table's left border on its edge (an older LibreOffice, the Linux CI's, reads the indent as the table's edge and sets the text about a padding right). Word sets a cell's content its whole top border and its margin inside the row, and ends the row its margin and its whole bottom border past the content, so half of each border — and as much of its inner half as the padding falls short of it — reaches past the panel: the space round it takes that first, then its padding, the border then drawn that much inside the panel's edge, then, above, the room over its first line; what none of them takes is reported (`space above` on the block below, the panel's kind for its top border). What its content leaves below itself past where the page ends it — a last line held to an icon, a card's border ending it, which that card's padding takes first — comes out of its padding below first, the same way; a card's border ending a row's cell reaches past the row only by what the row's room under that cell does not hold, and one ending a table's composed cell is reported on the card, its row up to that much taller. A negative bottom margin pulls what follows up out of the space between them, and what that space cannot give is reported, as is the tenth of a point of the paragraph between any two tables where the space between them does not take it. A shape container's borders stay inside its outline. A panel the layout measures holds the page's height less its padding as its row's height, which Word and LibreOffice on Windows read as its content's, its margins and borders round it; a shape container holds its outline's, less its margins and its borders as wide as Word draws them, two and a half twips an eighth of a point rounded down. A panel's borders are written from two to ninety-six eighths of a point, as a table cell's rules and a rule are, and a width that, rounded to the eighth, is thinner or wider is named (`border width`). The table takes the width the layout placed the container at. The point of slack an editor's face is given past it comes out of the panel's right margin, so its fill and borders end where the page ends them; where the margin cannot hold it, or a line Word sets at its size to the half point runs further into the margin than the rest holds, the table keeps that much of the slack and the report names it (`right end`). In a cell, a panel whose left border hangs half its width left of the cell's text — or which a margin below zero sets further out — stands up to that much right of the page's, as LibreOffice and Word start a nested table at the cell's text, which the report names (`left edge`); it gives up what its left side had given its text, the half border the table is wider by and what its left margin gave back of the other half, as both editors start it half that border in, so its right end stands where the page's does, and a nested table running past its cell, which widens the cell, fixed layout or not, runs past no further — what more it gives up held in at the cell's edge than at its text coming off its right margin, what holding its right border in has already taken counting towards it, and what of it the cell does not take going back to a line set past its text further than its margin holds, reported (`right end`); one whose right border reaches the cell's edge — the cell's own right margin holds what passes its text, where the cell draws no right rule of its own that may cover it — ends that border and half a point short of it, the points taken from its right margin and no more than it holds, since Word draws a nested table's right border outside the table and, on screen, cuts off what passes the cell's edge, and the report names how much narrower than the page draws the panel its table is written, and about how far what the margin cannot take in still passes that edge (`right border`). A `keepTogether()` panel the layout held on one page is a row that may not split. Panels, rows and tables inside it are tables in its cell; a table cell no style fills is written white, as the page draws it. One deviation: the corner radius is dropped (a cell is rectangular) with one warning per export. A translucent fill is flattened against what Word paints under the panel: the panel or cell around it as written, or on the page the colour the layout paints there (`DocxLayoutMetrics.colourUnder`: the fills drawn before it, at its centre, a page background included and a row's own fill left out, what stands on it being written in the row's panel, on its shading). A translucent border is flattened against the panel's fill where it has one, since a cell's shading and borders are opaque; each is named in the report as `translucency`. An unpainted container is not a table: its paragraphs are indented by every enclosing margin and padding, and its rows and tables get `w:tblInd`. A standalone `ShapeNode` that is only a fill, with no stroke, radius, gradient or transform, and no taller than Word's thickest border (12pt) — an `addDivider` — is a rule, written as a line's is; any other `ShapeNode`, standalone or laid over others, is drawn by `DocxDrawings` as a floating DrawingML shape behind the text, where the layout placed it — `rect`, or `roundRect` with the largest corner radius, in its fill and outline colours with their alpha, the outline mitred at its corners as the page strokes it, each side border a line along its edge; a gradient paint and a transform are not carried, nor are a link, an outline entry or an anchor's bookmark — each named in the shape's report note, as unequal corners are —, and a shape with neither a fill colour nor an outline is dropped and reported. It is anchored in the paragraph whose text it stands beside, placed down from its top, so it moves with that text when the text above is edited; beside no text, in a body paragraph on its page (a table cell's only on a page with no other, reported), where it stays — except a drawing that is all a table cell holds, an icon beside its label (in a row of the flow, a layer stack or shape container that only draws, with no margins, on one page and painting nothing outside its box; in a composed cell, a layer stack of shapes with no margin or padding, the only drawing the cell holds, matched to the table's fragments inside the cell by kind and size), which is anchored in that cell's paragraph, held at its height and placed from its top and the cell's text column, so it moves with its row, and down with the paragraph's line where space is added above that line once it is placed, as a lifted row's cell starts lower; a drawing holding a line stays on the page —, and is drawn in front of the text inside a filled panel unless it frames text or a picture there; reported `APPROXIMATED` | +| Rectangle shape — fill, stroke, per-corner radii, side borders (`ShapeFragmentPayload`) | ✅ `PdfShapeFragmentRenderHandler` | ⚠️ `PptxShapeFragmentRenderHandler` (distinct per-corner radii render with the top-left radius on all corners — single-adjust `roundRect` preset — until custom geometry lands; uniform radii and side borders exact) | ⚠️ `DocxSemanticBackend.writeContainerChildren` — a `SectionNode` or `ContainerNode` carrying a fill, per-side borders or a uniform stroke is written as a one-cell table, and so is a `RowNode` in the flow that paints its box (`writePaintedRow`, through the same `writePanelPiece`), its columns a table nested in the cell without its padding — not in a band of layers or a stack's measured column, with a padding or margin it cannot hold, composed in a table cell or in a page zone, where it is its columns alone and its paint is reported: the cell's shading is the fill behind everything inside, its borders are the container's at its full height, and its margins are the padding on all four sides, less half of each border, with the table half a border wider on each side, so the text and the border land where the page draws them — in a cell, a panel set in from the cell's text by half its left border or more keeps its whole left padding as its left margin, and one set in less what it is set in by of that half, as both editors set a nested table's text its left border's centre and its margin in — in the body, the left margin is the whole padding, the table no wider on that side, and its indent places its text, as Word 16 and LibreOffice on Windows centre a body table's left border on its edge (an older LibreOffice, the Linux CI's, reads the indent as the table's edge and sets the text about a padding right). Word sets a cell's content its whole top border and its margin inside the row, and ends the row its margin and its whole bottom border past the content, so half of each border — and as much of its inner half as the padding falls short of it — reaches past the panel: the space round it takes that first, then its padding, the border then drawn that much inside the panel's edge, then, above, the room over its first line; what none of them takes is reported (`space above` on the block below, the panel's kind for its top border). What its content leaves below itself past where the page ends it — a last line held to an icon, a card's border ending it, which that card's padding takes first — comes out of its padding below first, the same way; a card's border ending a row's cell reaches past the row only by what the row's room under that cell does not hold, and one ending a table's composed cell is reported on the card, its row up to that much taller. A negative bottom margin pulls what follows up out of the space between them, and what that space cannot give is reported, as is the tenth of a point of the paragraph between any two tables where the space between them does not take it. A shape container's borders stay inside its outline. A panel the layout measures holds the page's height less its padding as its row's height, which Word and LibreOffice on Windows read as its content's, its margins and borders round it; a shape container holds its outline's, less its margins and its borders as wide as Word draws them, two and a half twips an eighth of a point rounded down. A panel's borders are written from two to ninety-six eighths of a point, as a table cell's rules and a rule are, and a width that, rounded to the eighth, is thinner or wider is named (`border width`). The table takes the width the layout placed the container at. The point of slack an editor's face is given past it comes out of the panel's right margin, so its fill and borders end where the page ends them; where the margin cannot hold it, or a line Word sets at its size to the half point runs further into the margin than the rest holds, the table keeps that much of the slack and the report names it (`right end`). In a cell, a panel whose left border hangs half its width left of the cell's text — or which a margin below zero sets further out — stands up to that much right of the page's, as LibreOffice and Word start a nested table at the cell's text, which the report names (`left edge`); it gives up what its left side had given its text, the half border the table is wider by and what its left margin gave back of the other half, as both editors start it half that border in, so its right end stands where the page's does, and a nested table running past its cell, which widens the cell, fixed layout or not, runs past no further — what more it gives up held in at the cell's edge than at its text coming off its right margin, what holding its right border in has already taken counting towards it, and what of it the cell does not take going back to a line set past its text further than its margin holds, reported (`right end`); one whose right border reaches the cell's edge — the cell's own right margin holds what passes its text, where the cell draws no right rule of its own that may cover it — ends that border and half a point short of it, the points taken from its right margin and no more than it holds, since Word draws a nested table's right border outside the table and, on screen, cuts off what passes the cell's edge, and the report names how much narrower than the page draws the panel its table is written, and about how far what the margin cannot take in still passes that edge (`right border`). A `keepTogether()` panel the layout held on one page is a row that may not split. Panels, rows and tables inside it are tables in its cell; a table cell no style fills is written white, as the page draws it. One deviation: the corner radius is dropped (a cell is rectangular) with one warning per export. A translucent fill is flattened against what Word paints under the panel: the panel or cell around it as written, or on the page the colour the layout paints there (`DocxLayoutMetrics.colourUnder`: the fills drawn before it, at its centre, a page background included and a row's own fill left out, what stands on it being written in the row's panel, on its shading). A translucent border is flattened against the panel's fill where it has one, since a cell's shading and borders are opaque; each is named in the report as `translucency`. An unpainted container is not a table: its paragraphs are indented by every enclosing margin and padding, and its rows and tables get `w:tblInd`. A standalone `ShapeNode` that is only a fill, with no stroke, radius, gradient or transform, and no taller than Word's thickest border (12pt) — an `addDivider` — is a rule, written as a line's is; any other `ShapeNode`, standalone or laid over others, is drawn by `DocxDrawings` as a floating DrawingML shape behind the text, where the layout placed it — `rect`, or `roundRect` with the largest corner radius, in its fill and outline colours with their alpha, the outline mitred at its corners as the page strokes it, each side border a line along its edge; a gradient paint and a transform are not carried, nor are a link, an outline entry or an anchor's bookmark — each named in the shape's report note, as unequal corners are —, and a shape with neither a fill colour nor an outline is dropped and reported. It is anchored in the first paragraph the nearest block holding it writes on its page — in a table cell its own cell of that paragraph's row where one holds it across, else the paragraph's cell placing it out of its column (one holding text, a text box or a badge's letters, only where it holds it across), none in a cell set to the middle or the bottom or sitting in one, whose text LibreOffice 24.2 then sets from the top — placed down from its top however far off, so it moves with that block when the text above is edited; where no such block writes one, in the paragraph nearest it on its page; what a composed cell draws, in the paragraph whose text is nearest it within reach; with none of these, in a body paragraph on its page (a table cell's only on a page with no other, reported), where it stays — except a drawing that is all a table cell holds, an icon beside its label (in a row of the flow, a layer stack or shape container that only draws, with no margins, on one page and painting nothing outside its box; in a composed cell, a layer stack of shapes with no margin or padding, the only drawing the cell holds, matched to the table's fragments inside the cell by kind and size), which is anchored in that cell's paragraph, held at its height and placed from its top and the cell's text column, so it moves with its row, and down with the paragraph's line where space is added above that line once it is placed, as a lifted row's cell starts lower; a drawing holding a line is not held at its height, and goes by its block —, and is drawn in front of the text inside a filled panel unless it frames text or a picture there; reported `APPROXIMATED` | | Ellipse (`EllipseFragmentPayload`) | ✅ `PdfEllipseFragmentRenderHandler` | ✅ `PptxEllipseFragmentRenderHandler` | ⚠️ `DocxDrawings` — an `ellipse` shape anchored as a rectangle is; a shape container's elliptical outline is drawn the same way, and a picture that fills the container it clips takes the ellipse as its geometry; a transform is not carried, nor a link, an outline entry or an anchor's bookmark (each named in the ellipse's report note), and a shape in a filled panel is drawn in front of the text, over the cell's shading, unless it frames text or a picture; a shape is anchored as a rectangle is — except, as for a rectangle, a drawing that is all a table cell holds — in a row of the flow, a badge alone beside its text — which is anchored in that cell and moves with its row | | Line — dash pattern, line cap (`LineFragmentPayload`) | ✅ `PdfLineFragmentRenderHandler` | ⚠️ `PptxLineFragmentRenderHandler` (numeric dash arrays map to the generic dashed preset; solid lines and caps exact) | ⚠️ `DocxSemanticBackend.writeRule` — a horizontal line with no transform, stroked no thicker than Word's 12pt border, is Word's own rule: an empty paragraph whose bottom border is the stroke (colour, thickness in eighths of a point, a hairline thinner than Word's thinnest drawn at it and reported, `border width`), its ends as the paragraph's indents, each taken in by what Word draws a paragraph's border past it — 1.42pt at the left and 1.51pt at the right, measured 1.36 to 1.59; a rule running past the width it is written in held at that width's edge, less only what Word draws further than the page ends it — so LibreOffice, which draws it between them, sets each end that much further in, and a rule shorter than both keeps a tenth of a point of its paragraph, and the space above and below the stroke in its box as the paragraph's height and the space owed below it, and what Word's paragraph stands taller than the box — the tenth of a point of line a stroke with no room above it is held to, and the border counted in whole twips of its eighths — taken out of the space owed below and, past that, hung below the rule for the space above what follows to take, as a line hanging below a block is; a dash pattern becomes Word's dashed or dotted border, reported `APPROXIMATED`; a translucent stroke is flattened against the colour the page paints under it, a page background included, reported as `translucency`; the line cap and a link are not carried (both reported, a link as `rule link`; an outline entry on it is reported too); composed in a table's cell, it is written once, where the page draws it, and its table does not draw it again — unless it stands in a panel or a row inside the cell, or another drawing of its kind and size there cannot be told from it, where both stay and the report says it stands twice. A thicker line (holding its room in the flow and among the text of a stack of one layer, none in a table's cell, reported), a vertical or slanted line, and a line laid over others in a layer stack, canvas or shape container — a line among the text of a layer stack of one layer excepted, which is a rule —, is drawn by `DocxDrawings` as a `line` shape anchored as a rectangle is, ending in the page's cap — the dash pattern and a transform are not carried, nor a link, an outline entry or an anchor's bookmark (each named in the line's report note); a line in a page zone is dropped and reported | | Polygon (`PolygonFragmentPayload`) | ✅ `PdfPolygonFragmentRenderHandler` | ✅ `PptxPolygonFragmentRenderHandler` + `PptxInlineGeometry` | ⚠️ `DocxDrawings` + `DocxCustomGeometry` — `a:custGeom`, the vertex ring closed, its corners mitred as the page strokes them (LibreOffice bevels one sharper than 30 degrees), anchored as a rectangle is | diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 32515cfa7..5df776c49 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -73,9 +73,9 @@ creation date is real metadata. | Composed cells (`DocumentTableCell.node(...)`) | Written by the same writers that write that node anywhere else, so a cell built from an image, a list or a table carries it. A nested table is a real `w:tbl` followed by the paragraph Word requires a cell to end with — a hairline, which the paragraph written next in the cell takes over, so no empty line opens under the table, and whose mark is hidden where it is left at the cell's end holding nothing and no space, since LibreOffice lays it out — and takes the width of the column it sits in, less its own margins and padding — the column's, not the one the page gives it, because the layout reports a composed cell's content under the owner's path. Its fixed columns are written where every column is fixed; where one is `auto`, the editor divides the width among them (measured: Word and LibreOffice on Windows alike, half the cell each for two), and the report names it (`TableNode`) | | Inline chips (`inlineCode(...)`, `inlineChip(...)`, `highlight(...)`) | The chip's fill becomes the run's own `w:shd`, in a paragraph and in a list item alike. Its shape does not travel — see "What a chip keeps and loses" below | | Images | Embedded pictures at the node's declared size. One contained in its box (`fitToBounds`, `CONTAIN`) is embedded at its fitted size, half the room it leaves above and below written as the space round its paragraph and half at its side as its indent; in a band the room below is its paragraph's own, as the band measures what it owes below from the box. In the flow, in a row's cell and held in by an alignment it stands in the box's middle, as the page draws it. In the flow, the margin and padding above and below are the space round the picture's paragraph; on the left they are not written, and the report names them, as it does a barcode's and a page reference's sides. A picture in the flow, a stack of one layer's included, or first in a band of layers there, that the layout moves to a new page keeps the space the layout leaves above it there — its margin, its padding, the room above it in its box, the edges of the containers opening with it and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, as a body paragraph does. A picture drawn beside its text is fitted to its box with its padding in it, which the report names too | -| Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in the paragraph beside them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | +| Links and anchors | A `linkTarget` becomes a `w:hyperlink` — a relationship for an address, `w:anchor` for one of the document's own anchors — and a run's own link wins over the paragraph's — in a list item as much as in a paragraph. An `anchor(...)` becomes a bookmark wrapping that paragraph's text, named as Word requires; on a section, container, table or image it wraps everything the block wrote, from the start of its first paragraph to the end of its last, so a link to a block lands on its first line; a section or a container written as a layer stack's column wraps what it writes the same way, from a block of it an earlier layer's stand-in holds, written there first, to the last it writes. A block that writes no paragraph — a section, a container or a layer stack's column of drawings alone, whose drawings stand in a paragraph of a block holding it, in the one nearest them or in one written on their page — is bookmarked at the start of the paragraph its first drawing is anchored in, the drawing on the earliest page and highest on it: Word keeps a drawing on its paragraph's page and moves it with that paragraph, so a page reference reads the page the block starts on and goes on reading it once the text above is edited, and a link goes to that page (measured in Word 16, fields updated, and LibreOffice 24.2: a block on page 2 read 2, and 3 with a page put in ahead of it, where the file had held no bookmark and the reference a fixed 2). A block that draws nothing, one whose first drawing stands on a page nothing is written on, and one composed in a table's cell, whose drawings its table draws, have no bookmark, and the report names the anchor: a link to it points at none, and a page reference to it is written as the number it reads. Two anchors that clean to one name, or to a name a suffix gave another, stay two bookmarks; one anchor two nodes declare is one bookmark, the last the file writes one for — the node the page sends a link to and reads a page reference's page from, where that node writes a paragraph to mark — and the report names the anchor, the nodes counted off the document, two cells of one table two of them, and a node of a kind of the caller's own counted where the layout registers it. An anchor in a table's header row the page repeats is drawn again on each page it repeats on, and the page sends a link to the last of them; the file holds the row once, where the table starts, which a link goes to and a page reference reads once Word updates it (measured: the bookmark on page 1, a reference written as 5 reading 1), and the report names it. A `bookmark(...)` outline level becomes Word's own `HeadingN` style, which is what puts the paragraph in the Navigation Pane, the outline view and a generated table of contents. The style states the outline level and nothing else, so the paragraph keeps its own formatting. The role comes from what the document declared, never from how big the text is | | Rows | A one-row table spanning the content width — or only its columns, where fixed columns leave part of the row empty — so editors keep the side-by-side layout. The row's slots become the column grid when they are weights, an even split or fixed columns; fixed columns that add up to less than the row leave the rest of it empty, the last one at its own width and the table no wider than its columns, so its text wraps where the page wraps it; the gap and the row's padding ride in the neighbouring column and come back out as that cell's margin; a cell holds whatever its child is, written as it is anywhere else. The row's `verticalAlign` is every cell's `w:vAlign`, so a child shorter than the row sits at its middle or bottom as on the page — a table of contents' leader on its entry's baseline. A row composed in a table cell (`DocumentTableCell.node(...)`), which the layout places nowhere, that the page divides by what its children measure — an arrangement other than `START`, a spacer that grows, an auto column — takes its columns from where its paragraphs' lines start, the table having laid them out, a spacer where the page spreads the room between them; one holding a child those lines do not place — a picture, a shape, or a spacer in a row of stated columns, which gives it a slot of its own — or written inside a panel or another table in its cell has its columns sized by Word to their text, and the report names it. The row is kept whole across a page break, as the layout keeps it. A row is held at least as tall as the page makes it inside a painted panel, and anywhere its tallest child is a drawing Word holds nothing of in its cell — a badge beside a heading. A section or other container in any cell — a row's, a table's, a panel's — that pulls its first line up with a negative top edge writes that one-line paragraph as much shorter, its text seated where the page sets it — no more than the room above its letters, as Word draws an exact line's text only inside the line. A row that paints its box — `fillColor`, `stroke`, `borders` — is a panel holding its columns (see "What a panel keeps and loses" below): a table of one cell carrying the fill and borders, its margins the row's padding as a panel's are, and in it the columns as a table of their own, without the padding; a corner radius squares, and is named (`corner radius`), and what of its borders above and below Word draws past its box is taken, or named, as a panel's is. A row composed in a table cell, in a page zone's line, in a band of overlapping layers or where a stack's column measures the space round it to its text, with a padding below zero or a column hanging into it, or with a margin below zero above or below it or at a side in a cell, is written as its columns alone, its paint in the report (`row paint`); in the body a row bleeding past the margin is its panel | -| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | +| Sections / containers | Children written in order. A timeline with its markers on the rail (`markerOnRail()`) lays each entry's body out in the header row's content column, below the row; the body is written in the flow, indented to that column where the page puts it. A container with a fill, per-side borders or a uniform stroke is a one-cell table carrying them, its padding as the cell's margins, so a card keeps its panel — see "What a panel keeps and loses" below. A `keepTogether()` or `keepWithNext()` block the layout placed on one page stays on one page in Word too (`w:keepLines` + `w:keepNext`, and a row that may not split for a panel). In a table cell — a row's column, a panel's cell — such a block keeps its lines together and nothing with the next: Word reads keep-with-next on the paragraph a cell opens with as keeping the whole row with what follows its table, and holds no keep across a break inside the row, so there those keeps did nothing else; a row of columns, which may not split, keeps the block whole (measured: `ObsidianInvoice`'s info cards, kept together in a row's two cells, went with the footer divider under them to the next page in Word, 17 lines below the page's; without keep-with-next in the cards, they stood on their page). The paragraph between two tables in a cell still keeps with the next table: Word does not read it as the paragraph the cell opens with, even after the table the cell opens with (measured: a row at the foot of a page stayed there either way). A table of one row in the body kept with what follows — a row of columns — whose cells hold paragraphs alone is written with a row that may break: LibreOffice reads a one-row table whose row may not break as a table that may not split, and keeps such a table with nothing (measured: `EditorialProposal`'s page-two masthead stood at the foot of page one in LibreOffice, and all of page two 45 to 47pt high; freed, it opens page two in LibreOffice, Word set it there either way, and moved down to straddle the foot of page one it went to page two whole in both). A row holding a table keeps `w:cantSplit`: the keep chains a cell's own paragraphs, not the rows of a table in it. A box with no paint is only its contents, so a `fixedWidth` narrower than the column is not written: its paragraphs and lists run the column's width, as a panel's in a table cell do, and the report names it. Under an alignment wrapper, or as a layer of a band, the box is held in to where the page placed it. A painted section's bleed is not written: its fill and borders stop at its box, and where the page bleeds it — in the flow it lays out page by page, the body and the panels in it — the report names it. A line drawn as a shape holds no keep: its drawing is anchored in a paragraph of a block holding it, or near it, a page can end between it and the next block, and in that same flow the report names the keep it loses. A layer stack or a shape container in the flow is kept on its page as a block kept together is (`w:keepLines` on each paragraph, `w:keepNext` on all but the last), since the layout moves it to a new page whole: told nothing, Word set the first lines of a stack of one layer and of a shape container the page moved down at the foot of the page above, and the first layer of a band of two, in Word and LibreOffice alike. In a table's or a panel's cell they are the row's, which keeps or breaks on its own terms | | Spacers | An empty paragraph a tenth of a point tall, which Word keeps; the rest of the spacer's height is the space above the next block, or below this paragraph when a table follows. A spacer shorter than that tenth — a width anchor of no height, as `Panel`'s and `PanelLetter`'s cards open with — has its line reach past it by the rest, which the space below takes, as it takes a line hanging below a block, and what it cannot take is named (`space above`): taken nowhere, the line stood every card a tenth of a point taller in Word. A spacer in the body the layout moves to a new page with the gap before it, because the gap did not fit at the foot of the page above, holds the space the layout leaves above it there — that gap, and the edges of any containers opening with it — in its line, which Word keeps at the top of a page where it drops the space above. So does a spacer opening a stack of one layer, or a band of layers, which the layout moves to a new page whole: the space the layout leaves above it there is held in its line, less, in a stack of one layer, its layer's offset, which is named. It is kept with what follows it in the stack, which Word would otherwise set at the new page's top with the spacer left at the foot of the page above. A spacer's own margin and padding are not written, and the report names them — except below the lowest block of a band, or of a layer another resumes after in its column, where the space is measured from the page, the block's own margin and padding included | | Page breaks | Explicit Word page breaks | @@ -1097,8 +1097,9 @@ recolour what is under it in Word and it keeps the colour it was flattened to. among the layers: a panel opening the band writes its top padding again inside its cell, and one closing it its bottom padding, so the band stands that much too tall there. A card with 14pt padding under an icon laid over its corner stood 14pt low in - Word and LibreOffice, and the paragraph under it 28pt (`icon_over_card` in the - fidelity corpus holds its lines as they stand, not where the icon stands). What a + Word and LibreOffice, and the paragraph under it 28pt; the icon, anchored in the + card's text, stands on the card's corner as each editor sets the card + (`icon_over_card` in the fidelity corpus holds the lines, not where the icon stands). What a later layer's first block holds above its content past the page's distance — a margin, a padding, a contained picture's room reaching above where the layers before it end — comes out of its own @@ -1271,8 +1272,9 @@ recolour what is under it in Word and it keeps the colour it was flattened to. widened by that much, as Word cuts a cell's content at its edge. What a table cell composes (`DocumentTableCell.node(...)`) has no place of its own in the layout: its drawing belongs to the table, and the table draws it — an icon, a - tile, a disc under a number — where the page draws it, anchored as any - other shape is. A layer stack of shapes that is all its cell holds — an icon alone in + tile, a disc under a number — where the page draws it, anchored in the + paragraph whose text is nearest it, within reach: it has no node of its own for + a block to hold (see "drawn as shapes" below). A layer stack of shapes that is all its cell holds — an icon alone in the first column of a band — is anchored in that cell instead: the cell's paragraph is held at the drawing's height and carries it, placed from its top and the cell's text column, which is taken to start where the drawing @@ -1293,7 +1295,8 @@ recolour what is under it in Word and it keeps the colour it was flattened to. 1.35pt above their row in both editors, and Editorial's goal ticks up to 3.1pt. A drawing holding a line, a lone shape, a composed tile and a drawing sharing its table cell with another - stay on the page. A box framing text is left to the panel it is written as; + are not held in their cell so: what a node draws goes by the block holding it, + a composed tile to the text nearest it within reach, as "drawn as shapes" says. A box framing text is left to the panel it is written as; the layout does not say which of a cell's boxes is which node, so in a cell such a box reaches into a box node is reported dropped, and in any other the table draws it and does not report it dropped. @@ -1482,31 +1485,49 @@ Vertical and slanted lines, ellipses, rectangles and rounded bars — standalone layer stack, as a shape container's outline, or drawn by the layout itself, such as a timeline's rail — are **drawn as shapes**: each is a floating DrawingML shape, behind the text, at the place, size, fill and outline the layout gives it, and the report lists -it as approximated. A shape standing beside a paragraph's text — a timeline's dot by its -entry, an icon by its heading, a skill's bar by its label — is anchored in that paragraph -and placed down from its top, so it moves with the paragraph when a reader edits the text -above it. The paragraph is the one whose text is nearest the shape, within 48pt across -and down together, when the shape rises above its top by its own height at most, 24pt at -most, as a dot centred on its first line does — in a table cell by a stroke's width at most: -a rule placed 1pt above its paragraph in a cell moved its row's text in LibreOffice on Linux. -In a table cell, the cell that holds the shape across takes -it, from its text column: the paragraph's own cell, or another of its row from that cell's -first paragraph — a timeline's dot in a column of its own beside its entry's text. Both -editors start each cell's first paragraph at the row's top where the cells set their content -from the top, and give every cell of a row the row's largest top margin, so the other cell's -first paragraph starts where the text's does; one with a border above it is not used; a row whose top is not known that way, a line not set -from the left and a repeated header row carry none. When the nearest paragraph cannot carry -the shape, it stays on the page rather than going to a paragraph further off, which is not -the text it stands by: an icon laid over a panel's top corner whose nearest paragraph is -the panel's first, more than a stroke's width below the icon's top, stays where the page -draws it when the text above the panel moves. The paragraph's top is where Word places from: -the space written above its first line included, and the line where Word stands it — an -exact line's baseline four fifths of the way down it, raised by the run's position — -measured in Word 16.0.20430 and LibreOffice to a tenth of a point. Only the first paragraph -holding text that a paragraph node is written as offers its place, in a column's cell too; a table's own cell -text and a list's items do not yet. - -A shape beside no paragraph stays where the page puts it whatever the text does. It is +it as approximated. A shape belongs to the node that draws it and to the blocks holding that +node — an icon laid over a card's corner to the card's stack, a skill's bar to its row, a +portrait to its sidebar — and is anchored in the first paragraph the nearest of them writes on +its page, placed down from that paragraph's top, so it moves with that block when a reader +edits the text above it. However far from the paragraph's text it stands, where it stands +gives only its offset: measured in Word 16.0.20527 and LibreOffice 24.2, a shape anchored in a +table cell's paragraph stands where both say when it rises above its row or its table, hangs +below them, stands out of its cell, is taller than its row, or sits in a row of an exact height +or in a nested table, in front of the text or behind it — from 155pt above its paragraph to +620pt below across the fidelity corpus; neither editor clips it or moves a line for it, and both move it with the row when the text above is edited. The page's flow, +which holds every block on its pages, is not such a block. In a table cell, the cell of the +paragraph's row that holds the shape across takes it, from its text column: the paragraph's +own cell, or the shape's own of its row from that cell's first paragraph — a timeline's dot in +a column of its own beside its entry's text. Both editors start each cell's first paragraph at +the row's top where the cells set their content from the top, and give every cell of a row the +row's largest top margin, so that cell's first paragraph starts where the text's does; one with +a border above it is not used. A cell that holds the shape across in no other way keeps it in +the paragraph, placed out of its column — unless the shape holds text, a text box or a badge's +letters: LibreOffice 26.8 pulled a text box placed 120pt left of its column back to the column's +start, where Word and LibreOffice 24.2 draw it where it is placed, so such a shape goes on to a +paragraph that holds it across, or to the page. A cell that sets its content from the middle or the +bottom, or sits in one, carries none: LibreOffice 24.2 sets the text of a cell carrying a +drawing from the cell's top, whatever the cell says — 16pt high in a cell set to the middle, +32pt in one set to the bottom, its row three lines tall — so the shape goes to that block's +next paragraph able to place it, or else to the next block up that writes one. A line not set from the cell's left and a repeated +header row carry none either. A shape no node draws — a timeline's rail — and one none of +whose blocks writes such a paragraph on its page — a badge beside a table holding its stack's +text — is anchored in the paragraph nearest it on its page that can place it, however far off. +The paragraph's top is where Word places from: the space written above its first line +included, and the line where Word stands it — an exact line's baseline four fifths of the way +down it, raised by the run's position — measured in Word 16.0.20430 and LibreOffice to a tenth +of a point; a paragraph with a border above its line, contextual spacing or space reckoned in +lines carries none. Only the first paragraph holding text that a paragraph node is written as +offers its place, in a column's cell too; a table's own cell text, a list's items and a pair +of lines do not yet, so a shape whose block's text is written only so goes to the paragraph +nearest it. + +What a table's composed cell draws has no node of its own, and is anchored in the paragraph +whose text is nearest it, within 48pt across and down together, when it rises above that +paragraph's top by its own height at most, 24pt at most, as a dot centred on a first line +does — in a table cell by a stroke's width at most — or else stays where the page puts it. + +A shape no paragraph on its page can carry stays where the page puts it whatever the text does. It is anchored at the start of the first body paragraph written on its page, whether that was written before the shape or after — not in a table cell's, which Word measures it from. A page with no body paragraph — one laid out entirely in a 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 2714dd32e..fd32e9832 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -2,6 +2,7 @@ # rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build 420(Build:2) on linux # rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build 420(Build:2) on linux # rows of synthetic-icon_over_card: libreoffice build 420(Build:2) on linux +# rows of cv-teal_pulse: libreoffice build 420(Build:2) on linux, from CI run 40503780286. Its email line, set in a cell set to the middle, stood +0.08pt while the contact separator was anchored in that cell, LibreOffice 24.2 then setting the cell from its top; the separator now goes elsewhere, as a cell set to the middle carries no drawing, and LibreOffice centres the cell as its neighbours: +0.93pt, Word +1.14pt. Ten other lines stand 0.1 to 0.3pt further, as the same run sets lines of documents the export does not change cv-blue_banner 0:8f1010120ed6 0.02 1.90 jordanrivera cv-blue_banner 0:8d7a2cca3060 0.00 0.14 +442055551000|jordan.rivera@exam cv-blue_banner 0:b944906ed84c -0.20 -0.38 professionalsummary @@ -1487,60 +1488,60 @@ cv-slate_orange 0:07ef0ade6bb2 0.05 0.07 •servsafefoodhandlercertificatio cv-teal_pulse 0:a5800de7a5df -1.87 0.06 isabellamoore cv-teal_pulse 0:aea71faf1482 -0.38 0.06 registerednurse cv-teal_pulse 0:613c7b77c405 1.08 0.17 manchester,unitedkingdom -cv-teal_pulse 0:4ddba42ce115 0.08 0.14 isabella.moore@email.com +cv-teal_pulse 0:4ddba42ce115 0.93 0.14 isabella.moore@email.com cv-teal_pulse 0:a8acc3a90a7b 0.68 0.17 +447700900123 cv-teal_pulse 0:84308bea4540 0.83 0.16 linkedin cv-teal_pulse 0:32c1cf49a2fe 0.15 0.10 skills -cv-teal_pulse 0:9836c1fde1ab -0.09 0.08 patientassessment -cv-teal_pulse 0:7344802265f6 -0.11 0.08 careplanning -cv-teal_pulse 0:32b1d161b612 -0.13 0.08 medicationadministration -cv-teal_pulse 0:b9fff349f0ab -0.15 0.08 clinicaldocumentation -cv-teal_pulse 0:cc981996b30d -0.16 0.08 infectionprevention -cv-teal_pulse 0:48d90339937f -0.18 0.08 ivtherapy -cv-teal_pulse 0:a392ecf06df5 -0.20 0.08 woundcare -cv-teal_pulse 0:7016cd40c2ef -0.21 0.08 patienteducation -cv-teal_pulse 0:ff0a6cdd9508 -0.23 0.08 safeguarding -cv-teal_pulse 0:7b6511893d61 -0.25 0.08 teamcollaboration -cv-teal_pulse 0:d9ff32cb0736 -0.26 0.08 dischargecoordination -cv-teal_pulse 0:ceb75137b9fb -0.28 0.08 electronichealthrecords -cv-teal_pulse 0:b944906ed84c -0.30 0.08 professionalsummary -cv-teal_pulse 0:f8343b9099ef 0.65 0.06 compassionateregisterednursewith -cv-teal_pulse 0:50347a4f84fc 0.64 0.06 high-qualitypatientcareacrossacu -cv-teal_pulse 0:d24f5be95f6a 0.63 0.06 settings.skilledinpatientassessm -cv-teal_pulse 0:824bf3935437 0.62 0.06 multidisciplinarycollaboration,d -cv-teal_pulse 0:8d526cd52034 0.60 0.06 accurateclinicaldocumentation.kn -cv-teal_pulse 0:4d61455b351b 0.59 0.06 communication,andpatient-centred -cv-teal_pulse 0:c2f296ef30a3 -0.35 0.08 professionalexperience +cv-teal_pulse 0:9836c1fde1ab 0.21 0.08 patientassessment +cv-teal_pulse 0:7344802265f6 0.19 0.08 careplanning +cv-teal_pulse 0:32b1d161b612 0.17 0.08 medicationadministration +cv-teal_pulse 0:b9fff349f0ab 0.15 0.08 clinicaldocumentation +cv-teal_pulse 0:cc981996b30d 0.14 0.08 infectionprevention +cv-teal_pulse 0:48d90339937f 0.12 0.08 ivtherapy +cv-teal_pulse 0:a392ecf06df5 0.10 0.08 woundcare +cv-teal_pulse 0:7016cd40c2ef 0.09 0.08 patienteducation +cv-teal_pulse 0:ff0a6cdd9508 0.07 0.08 safeguarding +cv-teal_pulse 0:7b6511893d61 0.05 0.08 teamcollaboration +cv-teal_pulse 0:d9ff32cb0736 0.04 0.08 dischargecoordination +cv-teal_pulse 0:ceb75137b9fb 0.02 0.08 electronichealthrecords +cv-teal_pulse 0:b944906ed84c 0.25 0.08 professionalsummary +cv-teal_pulse 0:f8343b9099ef 0.15 0.06 compassionateregisterednursewith +cv-teal_pulse 0:50347a4f84fc 0.14 0.06 high-qualitypatientcareacrossacu +cv-teal_pulse 0:d24f5be95f6a 0.13 0.06 settings.skilledinpatientassessm +cv-teal_pulse 0:824bf3935437 0.12 0.06 multidisciplinarycollaboration,d +cv-teal_pulse 0:8d526cd52034 0.10 0.06 accurateclinicaldocumentation.kn +cv-teal_pulse 0:4d61455b351b 0.09 0.06 communication,andpatient-centred +cv-teal_pulse 0:c2f296ef30a3 0.20 0.08 professionalexperience cv-teal_pulse 0:5cad9261fd60 0.14 0.06 seniorstaffnurse—manchesterroyal -cv-teal_pulse 0:5149e8e5e6b6 -0.08 0.05 deliverdirectnursingcareforadult -cv-teal_pulse 0:8dcc0d173718 -0.08 0.05 coordinatecareplanswithdoctors,t -cv-teal_pulse 0:325626fb2f7a -0.09 0.05 supervisejuniornursesandsupports -cv-teal_pulse 0:c2864de08768 -0.09 0.05 improveddischargecoordinationand -cv-teal_pulse 0:3ac4bbda9488 -0.10 0.05 maintainprecisedocumentationande +cv-teal_pulse 0:5149e8e5e6b6 0.22 0.05 deliverdirectnursingcareforadult +cv-teal_pulse 0:8dcc0d173718 0.22 0.05 coordinatecareplanswithdoctors,t +cv-teal_pulse 0:325626fb2f7a 0.21 0.05 supervisejuniornursesandsupports +cv-teal_pulse 0:c2864de08768 0.21 0.05 improveddischargecoordinationand +cv-teal_pulse 0:3ac4bbda9488 0.20 0.05 maintainprecisedocumentationande cv-teal_pulse 0:59a7aa760eea 0.13 0.06 staffnurse—salfordgeneralhospita -cv-teal_pulse 0:99bff63b0b23 -0.09 0.05 providednursingcareacrossmedical -cv-teal_pulse 0:dee1c05c380e -0.09 0.05 administeredmedications,monitore -cv-teal_pulse 0:a0cc1403feda -0.10 0.05 supportedinfectioncontrolprocedu -cv-teal_pulse 0:25239222c011 -0.20 0.05 builtstrongrapportwithpatientsan +cv-teal_pulse 0:99bff63b0b23 0.21 0.05 providednursingcareacrossmedical +cv-teal_pulse 0:dee1c05c380e 0.21 0.05 administeredmedications,monitore +cv-teal_pulse 0:a0cc1403feda 0.20 0.05 supportedinfectioncontrolprocedu +cv-teal_pulse 0:25239222c011 0.10 0.05 builtstrongrapportwithpatientsan cv-teal_pulse 0:f25d4331b19b 0.02 0.06 communitynurse—northcarecommunit -cv-teal_pulse 0:19eefb6b6368 -0.19 0.05 deliveredhome-basednursingcarean -cv-teal_pulse 0:16412b3fd5d8 -0.20 0.05 managedwoundcare,medicationsuppo -cv-teal_pulse 0:fb42708d13d3 -0.20 0.05 workedcloselywithgps,socialservi -cv-teal_pulse 0:68bf0109c6f8 -0.31 0.11 education +cv-teal_pulse 0:19eefb6b6368 0.11 0.05 deliveredhome-basednursingcarean +cv-teal_pulse 0:16412b3fd5d8 0.10 0.05 managedwoundcare,medicationsuppo +cv-teal_pulse 0:fb42708d13d3 0.10 0.05 workedcloselywithgps,socialservi +cv-teal_pulse 0:68bf0109c6f8 0.09 0.11 education cv-teal_pulse 0:c1596da36d69 0.04 0.10 bsc(hons)adultnursing -cv-teal_pulse 0:47ce0218bbea -0.02 0.10 universityofmanchester -cv-teal_pulse 0:1da060005bfc -0.25 0.10 2013–2016 -cv-teal_pulse 0:e831beb04c00 -0.31 0.06 certifications -cv-teal_pulse 0:ec8bd1e014bb -0.24 0.04 nmcregisterednurse -cv-teal_pulse 0:669f27afcccf -0.27 0.04 immediatelifesupport(ils) -cv-teal_pulse 0:34b32be15830 -0.30 0.04 venepunctureandcannulationcertif -cv-teal_pulse 0:32215e12a886 -0.33 0.04 safeguardingadultslevel3 -cv-teal_pulse 0:676dfdd56ad2 -0.31 0.08 additionalinformation +cv-teal_pulse 0:47ce0218bbea 0.03 0.10 universityofmanchester +cv-teal_pulse 0:1da060005bfc 0.00 0.10 2013–2016 +cv-teal_pulse 0:e831beb04c00 0.09 0.06 certifications +cv-teal_pulse 0:ec8bd1e014bb 0.06 0.04 nmcregisterednurse +cv-teal_pulse 0:669f27afcccf 0.03 0.04 immediatelifesupport(ils) +cv-teal_pulse 0:34b32be15830 0.00 0.04 venepunctureandcannulationcertif +cv-teal_pulse 0:32215e12a886 -0.03 0.04 safeguardingadultslevel3 +cv-teal_pulse 0:676dfdd56ad2 0.09 0.08 additionalinformation cv-teal_pulse 0:bcc890a69aa7 0.06 0.07 languages:english(native), cv-teal_pulse 0:de15ca9e3d9f 0.03 0.07 polish(conversational) -cv-teal_pulse 0:b035d3696f4e -0.02 0.07 righttowork:unitedkingdom -cv-teal_pulse 0:cfa1d6dd779c -0.17 0.07 availability:1monthnotice -cv-teal_pulse 0:b9b8965717b3 -0.28 1.62 compassionatecare.clinicalexcell +cv-teal_pulse 0:b035d3696f4e 0.03 0.07 righttowork:unitedkingdom +cv-teal_pulse 0:cfa1d6dd779c 0.03 0.07 availability:1monthnotice +cv-teal_pulse 0:b9b8965717b3 0.07 1.62 compassionatecare.clinicalexcell cv-terracotta_rail 0:920a88ef5818 0.10 0.12 oliver.bennett@email.com cv-terracotta_rail 0:a8acc3a90a7b 0.10 0.12 +447700900123 cv-terracotta_rail 0:e8c463a6c54d 0.10 0.12 bristol,unitedkingdom diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv index cf14305c8..cc8d69ca8 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux.tsv @@ -3,6 +3,7 @@ document engine_pages editor_pages lines matched median p90 over2 # rows of invoice-metered_long, invoice-obsidian_long, synthetic-break_in_column, synthetic-break_in_panel, synthetic-card_over_pages: libreoffice build 420(Build:2) on linux # rows of synthetic-clipped_container, synthetic-narrow_container, synthetic-rounded_card: libreoffice build 420(Build:2) on linux # rows of synthetic-icon_over_card: libreoffice build 420(Build:2) on linux +# rows of cv-teal_pulse: libreoffice build 420(Build:2) on linux, from CI run 40503780286. Its email line, set in a cell set to the middle, stood +0.08pt while the contact separator was anchored in that cell, LibreOffice 24.2 then setting the cell from its top; the separator now goes elsewhere, as a cell set to the middle carries no drawing, and LibreOffice centres the cell as its neighbours: +0.93pt, Word +1.14pt. Ten other lines stand 0.1 to 0.3pt further, as the same run sets lines of documents the export does not change cv-blue_banner 2 2 54 52 0.16 0.25 0 cv-boxed_sections 2 2 58 58 0.06 0.11 0 cv-centered_headline 2 2 58 56 0.12 0.29 0 @@ -25,7 +26,7 @@ cv-professional_sidebar 1 1 68 64 0.20 0.28 0 cv-serif_headline 1 1 103 103 0.19 0.59 0 cv-sidebar_portrait 1 1 49 49 0.07 0.18 0 cv-slate_orange 1 1 87 86 0.23 0.99 0 -cv-teal_pulse 1 1 57 57 0.20 0.63 0 +cv-teal_pulse 1 1 57 57 0.12 0.25 0 cv-terracotta_rail 1 1 102 102 0.17 0.52 0 cv-timeline_minimal 2 2 89 87 0.05 0.11 0 cv-violet_grid 1 1 80 78 0.27 0.59 0 diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingAnchors.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingAnchors.java index 558a1561b..fb79e4916 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingAnchors.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxDrawingAnchors.java @@ -25,26 +25,30 @@ import java.util.function.Supplier; /** - * Where each shape {@link DocxDrawings} draws is anchored: in the paragraph whose text it stands - * beside, or else in a paragraph written on its page. + * Where each shape {@link DocxDrawings} draws is anchored: in a paragraph of the block it belongs + * to, or else in a paragraph written on its page. * - *
A shape standing beside a paragraph's text — a timeline's dot by its entry, an icon by its - * heading, a skill's bar by its label — is anchored in that paragraph and placed down from its - * top ({@link #seat}): Word moves it with the paragraph, wherever an edit above takes it. Placed - * from the page's edges, it stayed where the page had put it while the text moved away from it. - * The paragraph is the one whose text is nearest, within {@link #REACH}, when the shape rises - * above its top by the shape's own height at most, as a dot centred on its first line does — in a - * table cell by a stroke's width at most. In a - * table cell, the cell that holds the shape across takes it — the paragraph's own, or another of - * its row, as a timeline's dot in a column of its own beside its entry's text — placed from that - * cell's first paragraph (see {@link Seat#place}). When the nearest paragraph cannot place the - * shape, it is left to the page rather than given to one further off. The shapes wait for the - * section's end, since the paragraph a shape stands beside may be written after it.
+ *A shape a node draws belongs to that node and to the blocks holding it: an icon laid over a + * card's corner to the card's stack, a skill's bar to its row, a portrait to its sidebar. It is + * anchored in the first paragraph the nearest of them writes on its page that can place it, and + * placed from that paragraph's top ({@link #seat}), so Word moves it with that block wherever an + * edit above takes it; where it stands gives only its offset, however far from the paragraph's + * text that is. Placed from the page's edges, it stayed where the page had put it while the text + * moved away from it. In a table cell, the cell of the paragraph's row that holds the shape + * across takes it — a timeline's dot stands in a column of its own beside its entry's text — + * placed from that cell's first paragraph (see {@link Seat#placeHeld}). A shape no node draws — + * a timeline's rail — or whose holders write no paragraph on its page goes to the paragraph + * nearest it there. What a table's composed cell draws has no node of its own, and goes to the + * paragraph whose text is nearest, within {@link #REACH}, when the shape rises above its top by + * its own height at most — in a table cell by a stroke's width at most — or else to the page. The + * shapes wait for the section's end, since the paragraph carrying a shape may be written after + * it.
* - *A shape beside no paragraph is positioned from the page's edges, so any paragraph on its - * page carries it to the right place — but only one on that page: an anchor takes its shape to - * the page the anchoring paragraph lands on. And a paragraph of the body rather than of a table - * cell: Word measures a shape anchored in a cell from the cell, not the page. The first body + *
A shape no paragraph on its page can carry is positioned from the page's edges, so any + * paragraph on its page carries it to the right place — but only one on that page: an anchor + * takes its shape to the page the anchoring paragraph lands on. And a paragraph of the body + * rather than of a table cell: Word measures a shape anchored in a cell from the cell, not the + * page. The first body * paragraph written on each page carries such shapes. The run is put at the front of the * paragraph, so a paragraph that runs on to the next page does not take the shape with it.
* @@ -84,10 +88,15 @@ final class DocxDrawingAnchors { private final MapIts holders — the node drawing it, then each block holding that node — are what it + * belongs to: an icon laid over a card's corner is the card's stack's, a skill's bar its + * row's, a portrait its sidebar's. Anchored in the first paragraph one of them writes on its + * page, it moves with that block wherever an edit above takes it, however far from the + * paragraph's text it stands: where it stands gives only its offset. A holder that writes no + * paragraph there, or only ones that cannot place it, passes it on to the next one up. A shape + * no holder carries — a timeline's rail, which no node draws, or one whose holders' text is a + * list or a table — goes to the paragraph nearest it.
+ * + * @param holders the paths of the node drawing it and of the blocks holding it, nearest first + */ + private Place placeInItsHolder(DocxDrawings.Shape shape, ListA paragraph of the body places it across from the page's edge. One in a table cell
* places it from the cell's text column; a cell that does not hold the shape across hands
@@ -652,6 +745,68 @@ Place place(DocxDrawings.Shape shape, Function Measured in Word 16.0.20527 and LibreOffice 24.2: a shape anchored in a table cell's
+ * paragraph, placed from the cell's text column and the paragraph's top, stands where both
+ * say when it rises above its row or its table, hangs below them, stands out of its cell,
+ * is taller than its row, sits in a row of an exact height or in a nested table, in front of
+ * the text or behind it — from 36pt above its paragraph to 70pt below in probes, from 155pt
+ * above to 620pt below in the fidelity corpus. Neither editor clips it or moves a line for
+ * it, and both move it with the row when the text above it is edited. A cell set to the
+ * middle or the bottom, or held in one, carries none: LibreOffice 24.2 sets the text of a
+ * cell carrying a drawing from the cell's top, whatever drawing it is — 16pt and 32pt high
+ * in a row three lines taller than it. A paragraph of the body places a shape across from
+ * the page's edge. A cell holding the shape across takes it in this paragraph; one that does not hands it
+ * to the cell of its row that does, the shape's own ({@link #inTheRow}), or else keeps it,
+ * placed out of its column — unless it holds text, a text box or a badge's letters, which
+ * LibreOffice 26.8 pulls back into the column. None takes it in a repeated header row,
+ * which Word repeats on every page with what is anchored in it, nor for a line not set from
+ * the cell's left, whose text column its text does not tell. Only what a table's composed cell draws is held to this
+ * ({@link DocxDrawingAnchors#placeBeside}). That contact row's cells are set to the middle,
+ * and LibreOffice 24.2 sets such a cell's text from its top once it carries a drawing,
+ * wherever the drawing stands; a cell set from its top moved no line for a drawing 36pt
+ * above its paragraph ({@link #placeHeld}). A container the tree starts at is taken to be the page's flow, holding every block on its
+ * pages and so none in particular, and is left out: a page flow is one, and every document of
+ * the fidelity corpus starts at one. A painted container added at the root is left out too,
+ * its drawings going to the paragraph nearest them. Any other node the tree starts at — a
+ * section or a stack added at the root — is a block and stays, as does a container further in.
+ * A path the tree does not name — a pass of the layout's own, a timeline's rail — has no
+ * holders. A shape anchored to the page stays where the page put it when a reader edits the text
- * above it; the text moves and the shape does not. A shape standing beside a paragraph's text is
- * anchored in that paragraph and placed down from its top, so Word moves it with the paragraph.
- * The paragraph's top is where Word places the shape from: the space written above its first
- * line included, and its line where Word stands it, not where the page draws it.
Chosen by where it stood, an icon laid over a card's corner was left to the page — the card's + * text starts its padding below the icon's top, more than a shape was let rise above a paragraph + * in a cell — and Word's editing protocol moved the card 36pt and the icon not at all. So was a + * skill's bar beside its label in a row: in a cell, a shape was let rise above its paragraph by a + * stroke's width only.
+ */ +class DocxDrawingHolderTest { + + private static final SvgIcon ICON = SvgIcon.parse(""); + + @Test + void anIconLaidOverACardsCornerIsAnchoredInTheCardsText() throws Exception { + try (XWPFDocument document = DocxExports.withLayout(400, 500, 20, page -> page + .addParagraph(p -> p.text("Before the card.")) + .addLayerStack(stack -> stack + .layer(new SectionBuilder().softPanel(DocumentColor.rgb(232, 240, 250), 0, 14) + .addParagraph(p -> p.text("The card's text.")).build()) + .layer(ICON.node(18), LayerAlign.TOP_RIGHT)) + .addParagraph(p -> p.text("After the card.")))) { + XWPFParagraph card = cellParagraphHolding(document, "The card's text."); + ListChosen by where it stood, an icon laid over a card's corner stood 14pt above the card's + * text and was left to the page, as was every skill bar of a sidebar's rows and its portrait: + * edited above, the text moved and they did not. Anchored by what holds them, they move with + * it. Measured in Word 16.0.20527 and LibreOffice 24.2, a shape in a table cell's paragraph stands + * where both say at any offset — above its row, below its table, out of its cell — unless the + * cell sets its content from the middle or the bottom, whose text LibreOffice moves for it.
+ */ +class DocxDrawingOwnerAnchorsTest { + + private static final String STACK = "Flow[0]/Stack[1]"; + private static final String ICON = STACK + "/Icon[1]"; + + @Test + void aShapeIsAnchoredInTheFirstParagraphItsHolderWritesNotTheOneNearestIt() { + // The paragraph above the stack stands 6pt from the icon; the card's text, the stack's + // first paragraph, 14pt below it. + XWPFDocument document = new XWPFDocument(); + XWPFParagraph before = text(document, "Before"); + XWPFParagraph card = text(document, "Card"); + XWPFParagraph more = text(document, "More"); + DocxDrawingAnchors anchors = anchors(); + anchors.paragraphOn(0, before, true); + anchors.seat(0, before, "Flow[0]/Before[0]", before, 40, 300, 94, 104); + anchors.seat(0, card, STACK + "/Card[0]/Text[0]", card, 54, 300, 114, 124); + anchors.seat(0, more, STACK + "/Card[0]/More[1]", more, 54, 300, 130, 140); + + queue(anchors, ICON, List.of(dot(290, 100))); + anchors.endSection(0, null, document::createParagraph); + + assertThat(drawingsIn(before)).isEmpty(); + assertThat(drawingsIn(more)).isEmpty(); + String drawing = drawingsIn(card).get(0); + assertThat(drawing).contains("