diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f99af38..29a574406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,34 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **A DOCX export's lines holding a picture within their text grow with it.** + - A line holding a picture set in it, an icon or a drawn bullet beside its words, was written at + an exact height, which Word keeps whatever its text holds. + - Word lowers such a picture by its run's position, in a least line too; LibreOffice stands it a + tenth of a point above the baseline, whatever the position; and a picture run's own size sets + none of the line (measured in Word and LibreOffice 24.2: a 9pt icon lowered 1.5pt beside 8pt + Lato left a 9.95pt least line as tall in Word, and grew it to 10.75pt in LibreOffice). + - A line whose pictures stay within its text's own line where either editor stands them — no + higher than its text, its faces' line gap included, no lower than its text's descent — is now + written at a least height where its text alone would be, and stands its text there. A least + line is given no position to seat its text, which Word does not move it by. One whose + picture rises past its text in either editor, as `CharcoalGold`'s contact icons do in + LibreOffice, is written as before, and so are pictures in a paragraph of broken lines or among + spaces alone, a rating's dots. + - The corpus writes 92 more paragraphs at a least height, in seven documents. In Word, two + points more on `Normal` leaves 1005 of the 1832 paragraphs it grows on lines shorter than 1.1 + times their letters, from 1041, and 14 documents pass, from 13. + - Of the seven documents' lines, 16 stand nearer the page in LibreOffice 24.2 and 9 further, of + 880; in Word 9 nearer and 55 further, of 882. The step from a line to the next differs from the + page's by more than 0.2pt for 34 of 489 steps in LibreOffice, from 36, and 29 of 472 in Word, + from 31. + - Where such a line is taller than its text, the gap between lines written into it, and the + space above it does not take the difference, its text stands lower than as an exact line: + `TealPulse`'s duties 0.24pt lower in Word, inside the quarter point a least line is held to + there, and its skills 0.3pt lower in LibreOffice. `ConsultingInvoice`'s line items, whose + description now seats its row's lift differently, stand one step of Word's 0.12pt line grid + lower there and where they stood in LibreOffice. + - **A DOCX export's table text cells grow with their text where they are as tall as their faces' own.** - A table's text cell was written at an exact line, which Word keeps whatever its text holds, and its row's margins, its lines' heights or its text's position moved it onto the page's baseline. @@ -35,7 +63,8 @@ follow semantic versioning; release dates are ISO 8601. the faces' own tables: the tallest run's reach above the baseline and the deepest run's below, the paragraph's mark among them. It is so where the runs are set in faces the document ships, all the same depth below the baseline, left to right, none raised or a script, no picture in - the line, and the paragraph is seated on its baseline, not a Word list's item. + the line past its text (see above), and the paragraph is seated on its baseline, not a Word + list's item. - Word and LibreOffice keep a least line's spare above its text and stand its baseline the runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to 0.15pt). That is where the page sets it, so such a line needs no position to stand there; it diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 4ce61db7f..1b4e1d838 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -245,7 +245,7 @@ it cannot work out for itself: | What | Where it lands | |---|---| -| Line height | `w:spacing w:line` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph's, a list item's or a table text cell's line is written as a least height (`w:lineRule="atLeast"`), which Word grows when the text is made larger, where it is at least as tall as its runs' own line — the faces' line by their own tables, the tallest run's reach above the baseline and the deepest run's below, the paragraph's mark among them — in faces the document ships, every run the same depth below its baseline, left to right, none raised or a script, no picture in the line, the paragraph seated on its baseline and not a Word list's item, and where Word then stands its text within a quarter point of the page's: Word and LibreOffice keep a least line's spare above its text and stand its baseline the runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to 0.15pt), which is where the page sets it. Every other line is exact, which Word keeps whatever its text holds: one the page sets tighter than its faces' own — Poppins' and Gothic A1's, whose own line holds a line gap the page's does not, or one rounded to the twip below its own, which a least line would grow back on every line; one in a face the document does not ship, the default Helvetica's among them, or in one whose `hhea` extents, which the page measures by, are not the metrics its line is set on; a page zone's, a text box's, a stacked line and a line pair. The report names how many paragraphs of text are exact, once for the document (`line height`). 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 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 not owed or hung below its paragraph, as Word sets lines on a grid of its own, 0.12pt apart (measured), but carried on to the next paragraph's and settled with it once the two come to more; what is carried stays on its page, starts again where the space owed is set anew — a band's or a column's layer, below a band — and in each cell, whose end settles what its own lines carry there, however little — what they fall short of the page owed below its last block, what they pass it by hung below the cell as a line hanging there is — and a paragraph that owed for its lines and whose pictures then grow them takes that back and carries on what the lines above it carried (measured: `Panel`'s card lines, each written a fiftieth of a point taller than the page's, stood a median 0.46pt low in Word and 0.39pt in LibreOffice on Windows; carried, 0.35 and 0.34pt). A list's item is settled with the page as a paragraph is, its lines carried with the paragraphs' around it (measured: `Panel`'s skills and projects, items of two lines and more, each line a fiftieth of a point taller than the page's, ran 0.21 and 0.35pt taller in Word and grew their cards by as much; items and cells' ends settled, `Panel` stood a median 0.21pt from the page in Word and in LibreOffice on Windows). 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 opening a column of a row of columns or of a stack's columns takes it from the space above the row, which the row is lifted out of, its other cells starting as much lower inside (one opening with a table below a hairline paragraph holding the lift) and a row held to a height held as much taller — only where the lift raises the first line half a point or more above where sharing the gaps out sets it, a smaller rise leaving a cell's last paragraph high with nothing under it to lower it into; not on a page's first row or in an overlay (measured: `EditorialProposal`'s goal cells, three lines each, 1.46 and 0.56pt off the page in Word, stand within 0.16pt; `OrangeOps`' degree, two lines 0.45pt low and 0.45pt high, within 0.27pt). One with no room above it for the gap otherwise (a cell padded less, a gap too small) 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, and so do a band's later layer and the space below the band — a stack laying one layer over another, a shape container — which measure what follows them from the page too (measured: `ConsultingInvoice`'s contact lines, in a band, half a point low in Word and both LibreOffice builds and its address 0.3pt, within 0.03pt raised; a band's first paragraph or list item, where a band in the flow, a stack of one layer's included, opens its page, is not raised, as a paragraph's there is not). A line pair's shared line moves up into the space written above it, not a cell's padding, for both its halves, by the lesser of what they call for, where neither is moved by its position — moved for one alone, it would move the other too — and not where a half is cut to its own measure or the layout opens a page with the pair (measured: `ConsultingInvoice`'s bank details, a label and its value to a line, 0.7 to 0.8pt low in Word and both LibreOffice builds, stand 0.2 to 0.33pt low raised, the rest a drift all under its line-item table shares; `LumaStudioInvoice`'s, in Carlito, which both editors set higher than the export reckons, rise too far: in its short invoice the bank details stand 0.27pt high in Word and 0.34pt in LibreOffice, where they stood 0.03 and 0.14pt high). Not a line in another overlay — a canvas, a text box — whose writers measure what follows them from the page and pay nothing, though a stack of one layer is the flow, its lines raised and its rows lifted as there (measured: `SlateOrange`'s experience timeline, set in one, its employers' lines and their items 0.12 to 0.36pt nearer the page in Word and 0.2pt in LibreOffice, `SubscriptionInvoice`'s contact rows 0.12 to 0.24pt and 0.25pt); 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, the first of a stack of one layer it moves there whole among them or the first of a band in the flow that opens its 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. A line of its own Word sets higher than the page by a tenth of a point or more, and under half a point, is lowered by the space between it and what follows: its space above grows by what is left of that space once the block under it has taken its own of it — the gap between its lines, its own raise, the room its pictures reach into, a row's lift out of the space above its table — above that block, or below the line where a table or a cell's end follows, and the block under it stands where it did. What nothing gives — a block on a later page, whose space above Word drops, a block in an overlay, a page break, a block the layout moves to the next page, a column's later layer — it is not lowered by, and where the section's end leaves the space below a cell's last line out, the lowering that space gave is taken back with it. Not a list's item, which its own list lowers, nor a line in an overlay (a stack of one layer is the flow), one opening its page or a panel, cut to fit or not on one page (measured, with the rules below a heading hung as below: `LetterSidebarPortrait`'s lines, 0.41pt high by median in Word, stand within 0.07pt, `TimelineMinimal`'s within 0.05pt in Word and LibreOffice). What the space above cannot give of a raise, and a line Word sets half a point or more high, 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 in an exact line, as Poppins' is (a line as tall as its faces' own is a least line instead, which needs no position). 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 set low keeps what the space above does not take of it; set high by half a point or more, it is lowered by the space above it instead, marker and text together, the items under it giving that back from the space above them — one that cannot, and stands less high itself, has it taken back as far — and what the list's last items still owe hung below the list as a line hanging below a block is — taken out of the space above what follows, or at a cell's end out of what the cell owes under the list, the room under it in its row or under its layer stack's column, or a card's padding — and taken back, the items as high as before, where that hang is let go: below a table's composed cell, under a column's later layer, at a block the layout sets on a later page, which takes nothing of the hang left on the page above, or at a section's end, among others (measured: `MidnightNavy`'s duties, 0.70 to 1.06pt high in Word and 0.82 to 1.04pt in LibreOffice, stand within 0.2pt); not where the layout opens a page with the item, sets its list over two pages in a cell, opens a card with it, or sets it in an overlay — a stack laying one layer over another, a band, a shape container — though a stack of one layer is the flow. A list composed in a table cell has its lines among its table's, and is matched to the run of them that holds its items in turn, opening at a line no paragraph or list before it in the table's order took — each a line of the item's text as the page lays it out, the marker and indent the page sets in that text before it, and, in a `hangingIndent` list, the line of the item's visible marker set level before it in a column of its own, a paragraph before it that the page reads as markdown matched first; its items take their height, the list's lineSpacing, a `hangingIndent` list's marker column and their seat from those lines as a list's in the flow do, and an item in a row Word keeps whole on its page rises into the gap above it, as a paragraph composed there does (measured: `SlateOrange`'s certifications, written at no height at all, stood 0.3pt lower with every item, 1.18pt low by the fourth in Word; matched, 0.26pt low at most). Having no place of its own, a Word list composed so whose marker the page sets in its text keeps its stated column. An item the layout's lines are not matched to is not seated — among them every item of a list in a table's composed cell holding an item of runs, whose text is not read there, one the page reads as markdown, its marks dropped, or a blank item of a `hangingIndent` list, which the page draws as a marker alone. A table text cell's exact 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, written exact and raised 4pt, stood 2.4pt high in LibreOffice; at its own height it is now a least line, which no position moves. 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:line` on every paragraph, cells and list items included — the height the engine measured, not a multiple Word would measure again against a substituted font. A paragraph's, a list item's or a table text cell's line is written as a least height (`w:lineRule="atLeast"`), which Word grows when the text is made larger, where it is at least as tall as its runs' own line — the faces' line by their own tables, the tallest run's reach above the baseline and the deepest run's below, the paragraph's mark among them — in faces the document ships, every run the same depth below its baseline, left to right, none raised or a script, no picture in the line that passes its text's own line, its faces' line gap included, where either editor stands it — Word lowers a picture by its run's position, in a least line too, and LibreOffice stands it a tenth of a point above the baseline whatever the position, and a picture run's own size sets none of the line (measured: a 9pt icon lowered 1.5pt beside 8pt Lato left a 9.95pt least line as tall in Word and grew it to 10.75pt in LibreOffice) — nor a picture in a paragraph of broken lines or beside spaces alone, the paragraph seated on its baseline and not a Word list's item, and where Word then stands its text within a quarter point of the page's: Word and LibreOffice keep a least line's spare above its text and stand its baseline the runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to 0.15pt), which is where the page sets it. Every other line is exact, which Word keeps whatever its text holds: one the page sets tighter than its faces' own — Poppins' and Gothic A1's, whose own line holds a line gap the page's does not, or one rounded to the twip below its own, which a least line would grow back on every line; one in a face the document does not ship, the default Helvetica's among them, or in one whose `hhea` extents, which the page measures by, are not the metrics its line is set on; a page zone's, a text box's, a stacked line and a line pair. The report names how many paragraphs of text are exact, once for the document (`line height`). 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 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 not owed or hung below its paragraph, as Word sets lines on a grid of its own, 0.12pt apart (measured), but carried on to the next paragraph's and settled with it once the two come to more; what is carried stays on its page, starts again where the space owed is set anew — a band's or a column's layer, below a band — and in each cell, whose end settles what its own lines carry there, however little — what they fall short of the page owed below its last block, what they pass it by hung below the cell as a line hanging there is — and a paragraph that owed for its lines and whose pictures then grow them takes that back and carries on what the lines above it carried (measured: `Panel`'s card lines, each written a fiftieth of a point taller than the page's, stood a median 0.46pt low in Word and 0.39pt in LibreOffice on Windows; carried, 0.35 and 0.34pt). A list's item is settled with the page as a paragraph is, its lines carried with the paragraphs' around it (measured: `Panel`'s skills and projects, items of two lines and more, each line a fiftieth of a point taller than the page's, ran 0.21 and 0.35pt taller in Word and grew their cards by as much; items and cells' ends settled, `Panel` stood a median 0.21pt from the page in Word and in LibreOffice on Windows). 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 opening a column of a row of columns or of a stack's columns takes it from the space above the row, which the row is lifted out of, its other cells starting as much lower inside (one opening with a table below a hairline paragraph holding the lift) and a row held to a height held as much taller — only where the lift raises the first line half a point or more above where sharing the gaps out sets it, a smaller rise leaving a cell's last paragraph high with nothing under it to lower it into; not on a page's first row or in an overlay (measured: `EditorialProposal`'s goal cells, three lines each, 1.46 and 0.56pt off the page in Word, stand within 0.16pt; `OrangeOps`' degree, two lines 0.45pt low and 0.45pt high, within 0.27pt). One with no room above it for the gap otherwise (a cell padded less, a gap too small) 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, and so do a band's later layer and the space below the band — a stack laying one layer over another, a shape container — which measure what follows them from the page too (measured: `ConsultingInvoice`'s contact lines, in a band, half a point low in Word and both LibreOffice builds and its address 0.3pt, within 0.03pt raised; a band's first paragraph or list item, where a band in the flow, a stack of one layer's included, opens its page, is not raised, as a paragraph's there is not). A line pair's shared line moves up into the space written above it, not a cell's padding, for both its halves, by the lesser of what they call for, where neither is moved by its position — moved for one alone, it would move the other too — and not where a half is cut to its own measure or the layout opens a page with the pair (measured: `ConsultingInvoice`'s bank details, a label and its value to a line, 0.7 to 0.8pt low in Word and both LibreOffice builds, stand 0.2 to 0.33pt low raised, the rest a drift all under its line-item table shares; `LumaStudioInvoice`'s, in Carlito, which both editors set higher than the export reckons, rise too far: in its short invoice the bank details stand 0.27pt high in Word and 0.34pt in LibreOffice, where they stood 0.03 and 0.14pt high). Not a line in another overlay — a canvas, a text box — whose writers measure what follows them from the page and pay nothing, though a stack of one layer is the flow, its lines raised and its rows lifted as there (measured: `SlateOrange`'s experience timeline, set in one, its employers' lines and their items 0.12 to 0.36pt nearer the page in Word and 0.2pt in LibreOffice, `SubscriptionInvoice`'s contact rows 0.12 to 0.24pt and 0.25pt); 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, the first of a stack of one layer it moves there whole among them or the first of a band in the flow that opens its 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. A line of its own Word sets higher than the page by a tenth of a point or more, and under half a point, is lowered by the space between it and what follows: its space above grows by what is left of that space once the block under it has taken its own of it — the gap between its lines, its own raise, the room its pictures reach into, a row's lift out of the space above its table — above that block, or below the line where a table or a cell's end follows, and the block under it stands where it did. What nothing gives — a block on a later page, whose space above Word drops, a block in an overlay, a page break, a block the layout moves to the next page, a column's later layer — it is not lowered by, and where the section's end leaves the space below a cell's last line out, the lowering that space gave is taken back with it. Not a list's item, which its own list lowers, nor a line in an overlay (a stack of one layer is the flow), one opening its page or a panel, cut to fit or not on one page (measured, with the rules below a heading hung as below: `LetterSidebarPortrait`'s lines, 0.41pt high by median in Word, stand within 0.07pt, `TimelineMinimal`'s within 0.05pt in Word and LibreOffice). What the space above cannot give of a raise, and a line Word sets half a point or more high, 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 in an exact line, as Poppins' is (a line as tall as its faces' own is a least line instead, which needs no position). 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 set low keeps what the space above does not take of it; set high by half a point or more, it is lowered by the space above it instead, marker and text together, the items under it giving that back from the space above them — one that cannot, and stands less high itself, has it taken back as far — and what the list's last items still owe hung below the list as a line hanging below a block is — taken out of the space above what follows, or at a cell's end out of what the cell owes under the list, the room under it in its row or under its layer stack's column, or a card's padding — and taken back, the items as high as before, where that hang is let go: below a table's composed cell, under a column's later layer, at a block the layout sets on a later page, which takes nothing of the hang left on the page above, or at a section's end, among others (measured: `MidnightNavy`'s duties, 0.70 to 1.06pt high in Word and 0.82 to 1.04pt in LibreOffice, stand within 0.2pt); not where the layout opens a page with the item, sets its list over two pages in a cell, opens a card with it, or sets it in an overlay — a stack laying one layer over another, a band, a shape container — though a stack of one layer is the flow. A list composed in a table cell has its lines among its table's, and is matched to the run of them that holds its items in turn, opening at a line no paragraph or list before it in the table's order took — each a line of the item's text as the page lays it out, the marker and indent the page sets in that text before it, and, in a `hangingIndent` list, the line of the item's visible marker set level before it in a column of its own, a paragraph before it that the page reads as markdown matched first; its items take their height, the list's lineSpacing, a `hangingIndent` list's marker column and their seat from those lines as a list's in the flow do, and an item in a row Word keeps whole on its page rises into the gap above it, as a paragraph composed there does (measured: `SlateOrange`'s certifications, written at no height at all, stood 0.3pt lower with every item, 1.18pt low by the fourth in Word; matched, 0.26pt low at most). Having no place of its own, a Word list composed so whose marker the page sets in its text keeps its stated column. An item the layout's lines are not matched to is not seated — among them every item of a list in a table's composed cell holding an item of runs, whose text is not read there, one the page reads as markdown, its marks dropped, or a blank item of a `hangingIndent` list, which the page draws as a marker alone. A table text cell's exact 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, written exact and raised 4pt, stood 2.4pt high in LibreOffice; at its own height it is now a least line, which no position moves. 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; read off the table's rows, so a table with an `auto` column and a column edge no row of it shows has none, and its columns are the editor's (measured on one such table: Word set them as the page did, LibreOffice on Windows otherwise), and the report names it (`TableNode`) | | 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, 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. The column already holds the next child's left margin, and a paragraph's own right side gives what the paragraph does not write as an indent; a paragraph set left takes what that does not give from the gap after its column, which stands where the page sets it: widened instead, it moved what stood right of it a point right in Word, `ReceiptModern`'s leaders and `EditorialProposal`'s signature lines. What neither holds comes out of the row's weight columns, so the row keeps its width, and what stands at the edges between them moves up to that much — `ReceiptModern`'s amounts at the row's end 0.84 to 1.1pt left in Word and 0.86 to 1.02pt in LibreOffice — which the report names (`right end`) unless the weight columns giving it hold only spacers or it is under a tenth of a point. A row with no weight column gives what its gaps hold and no more; one with no auto 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/DocxGrownLines.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLines.java index 60c94357f..df85f0287 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLines.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLines.java @@ -27,9 +27,18 @@ * *

A text's position does not move it in a least line: Word, raising every run of a line, keeps * the text on the line's foot until it passes the line's top, and LibreOffice moves it and grows - * the line. So only a paragraph whose runs hold no position, no picture set in the line and no raise - * of their own is read — a shape anchored to it takes no room in the line, and is passed over; the - * rest, and one set in a face the document does not ship, have no reach.

+ * the line. So only a paragraph whose runs of text hold no position and no raise of their own is + * read — a shape anchored to it takes no room in the line, and is passed over; the rest, and one + * set in a face the document does not ship, have no reach.

+ * + *

A picture set in the line counts by where each editor stands it: Word lowers it by its run's + * position, in a least line too, and LibreOffice stands it {@link #PICTURE_RISE} above the + * baseline, whatever the position; a picture run's own face sets none of the line. A least line's + * height is its runs' and pictures' reach, the tallest above the baseline and the deepest below + * (measured: a 9pt picture lowered 1.5pt beside 8pt Lato left a 9.95pt least line as tall in Word, + * and grew it to 10.75pt in LibreOffice). Only a line whose pictures stay within its text's own line + * in both places — no higher than its text, its faces' line gap included, no lower than its text's + * descent — stands its text where it would without them (see {@link Reach#holdsItsPictures}).

* * @author Artem Demchyshyn */ @@ -57,6 +66,18 @@ final class DocxGrownLines { /** Half points in a point, as Word writes sizes. */ private static final double HALF_POINTS_PER_POINT = 2; private static final double POINT_TO_TWIP = 20; + /** EMUs in a point, as a drawing's extent is written. */ + private static final double EMU_PER_POINT = 12700; + /** + * How far above the baseline LibreOffice stands a picture set in a line, its run's position + * aside, in points (measured in LibreOffice 24.2: a tenth of a point beside 8pt and 10pt Lato, + * for pictures 6 to 11pt tall and lowered by 0 to 3pt). Word lowers it by the position. The + * export's own reckoning of a picture's reach stands LibreOffice's on the baseline + * (DocxSemanticBackend.PictureReach); the tenth counts here, in whether a least line holds it. + */ + static final double PICTURE_RISE = 0.1; + /** What a run holding pictures set in the line may hold besides them. */ + private static final Set PICTURE_RUN_PARTS = Set.of("rPr", "drawing"); private final DocxFontTable.ShippedLines faces; private final DocumentTextStyle defaults; @@ -76,8 +97,12 @@ final class DocxGrownLines { * line, in points. * * @param paragraph the written paragraph - * @return the reach, or null where one of its runs is not read: a picture, a raise, a face the - * document does not ship, a run Word sets right to left, or a marker Word draws itself + * @return the reach, its pictures' among it, or null where one of its runs is not read: a raise, + * a face the document does not ship, a run Word sets right to left, a marker Word draws + * itself, a drawing anchored beside pictures or holding none, a picture with no extent + * written or whose run's position is not a number, or pictures in a + * paragraph whose lines are broken, one of which may hold a picture alone, or that holds + * no letter beside them */ Reach reachOf(CTP paragraph) { CTPPr properties = paragraph.getPPr(); @@ -92,6 +117,10 @@ Reach reachOf(CTP paragraph) { return null; } boolean anyRun = false; + boolean broken = false; + boolean lettered = false; + double pictureTop = 0; + double pictureFoot = Double.NEGATIVE_INFINITY; try (XmlCursor cursor = paragraph.newCursor()) { cursor.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' " + "./w:r | ./w:hyperlink/w:r"); @@ -106,9 +135,26 @@ Reach reachOf(CTP paragraph) { // least line as tall, in Word and in LibreOffice). continue; } + if (text.sizeOfDrawingArray() > 0 && holdsOnly(text, PICTURE_RUN_PARTS)) { + // Its face sets none of the line either (measured: a 24pt picture run left an + // 8pt least line as tall, in both editors). + double[] pictures = picturesIn(text); + if (pictures == null) { + return null; + } + pictureTop = Math.max(pictureTop, pictures[0]); + pictureFoot = Math.max(pictureFoot, pictures[1]); + continue; + } if (!holdsOnly(text, RUN_PARTS)) { return null; } + broken |= text.sizeOfBrArray() > 0; + for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText letter : text.getTList()) { + // A no-break space is a space too. + lettered |= letter.getStringValue() != null && letter.getStringValue().codePoints() + .anyMatch(c -> !Character.isWhitespace(c) && !Character.isSpaceChar(c)); + } reach = reachOf(text.isSetRPr() ? text.getRPr() : null, reach); if (reach == null) { return null; @@ -116,7 +162,55 @@ Reach reachOf(CTP paragraph) { anyRun = true; } } - return anyRun ? reach : null; + // Pictures among spaces alone, a rating's dots, LibreOffice sets on a taller line (measured: + // 6.4pt dots between runs of spaces in 8pt Barlow, written at a least 9.7pt, stood the + // languages under them 0.56pt low there, and a point lower for each row of dots more). + boolean pictured = pictureFoot > Double.NEGATIVE_INFINITY; + // A line wrapped where the page wraps it may leave a picture alone on a line too; the + // corpus has none, and a break is where one is known. + if (!anyRun || pictured && (broken || !lettered)) { + return null; + } + return new Reach(reach.above(), reach.below(), reach.shallowest(), pictureTop, pictureFoot); + } + + /** + * How high above the baseline a run's pictures set in the line reach, the higher of Word's and + * LibreOffice's places of them, and how far below it Word lowers them, in points; null where a + * drawing in it is not a picture of a written size set in the line. + */ + private static double[] picturesIn(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run) { + double lowered = 0; + if (run.isSetRPr() && run.getRPr().sizeOfPositionArray() > 0) { + if (!(run.getRPr().getPositionArray(0).getVal() instanceof Number position)) { + return null; + } + lowered = -position.doubleValue() / HALF_POINTS_PER_POINT; + } + double top = 0; + for (org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline picture + : inlinesIn(run)) { + if (picture == null || picture.getExtent() == null) { + return null; + } + double height = picture.getExtent().getCy() / EMU_PER_POINT; + top = Math.max(top, Math.max(height - lowered, height + PICTURE_RISE)); + } + return new double[]{top, lowered}; + } + + /** A run's drawings set in its line, one null for a drawing holding anything else. */ + private static java.util.List inlinesIn( + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run) { + java.util.List inlines = + new java.util.ArrayList<>(); + for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing drawing : run.getDrawingList()) { + if (drawing.sizeOfAnchorArray() > 0 || drawing.sizeOfInlineArray() == 0) { + inlines.add(null); + } + inlines.addAll(drawing.getInlineList()); + } + return inlines; } /** @@ -131,7 +225,8 @@ Reach reachOf(CTP paragraph) { * * @param paragraph the written paragraph * @return whether its runs are read ({@link #reachOf}), reach no taller than its line, to - * {@link #EPSILON}, and share one descent + * {@link #EPSILON}, share one descent, and hold its pictures within them + * ({@link Reach#holdsItsPictures}) */ boolean mayGrow(CTP paragraph) { CTPPr properties = paragraph.getPPr(); @@ -140,8 +235,9 @@ boolean mayGrow(CTP paragraph) { return false; } Reach reach = reachOf(paragraph); - return reach != null && twips.doubleValue() / POINT_TO_TWIP + EPSILON >= reach.height() - && reach.below() - reach.shallowest() <= EPSILON; + double line = twips.doubleValue() / POINT_TO_TWIP; + return reach != null && line + EPSILON >= reach.height() + && reach.below() - reach.shallowest() <= EPSILON && reach.holdsItsPictures(); } /** @@ -150,7 +246,8 @@ boolean mayGrow(CTP paragraph) { * * @param paragraph the written paragraph * @return the line, or null where its height is the editor's own, or a least line's runs are - * not read ({@link #reachOf}) + * not read ({@link #reachOf}) or its pictures reach past its text, where the two editors + * stand them apart ({@link Reach#holdsItsPictures}) */ Line lineOf(CTP paragraph) { CTPPr properties = paragraph.getPPr(); @@ -166,7 +263,11 @@ Line lineOf(CTP paragraph) { return null; } Reach reach = reachOf(paragraph); - if (reach == null) { + // A picture past the text is set elsewhere by each editor; and a line holding pictures + // written shorter than its text's own is one grown to its pictures (see + // DocxSemanticBackend#makeRoomForPictures), not read here. + if (reach == null || reach.holdsPictures() && !(written + EPSILON >= reach.height() + && reach.holdsItsPictures())) { return null; } double height = Math.max(written, reach.height()); @@ -191,7 +292,7 @@ private Reach reachOf(XmlObject runProperties, Reach so) { return null; } return new Reach(Math.max(so.above(), line.ascent() * size), Math.max(so.below(), line.descent() * size), - Math.min(so.shallowest(), line.descent() * size)); + Math.min(so.shallowest(), line.descent() * size), so.pictureTop(), so.pictureFoot()); } /** @@ -234,19 +335,39 @@ private static boolean holdsOnly(XmlObject element, Set parts) { } /** - * How far a line's text reaches above and below its baseline, in points. + * How far a line's text and the pictures set in it reach above and below its baseline, in + * points. * - * @param above how far above, its faces' line gap included - * @param below how far below: its deepest run's descent - * @param shallowest its shallowest run's descent + * @param above how far its text reaches above, its faces' line gap included + * @param below how far its text reaches below: its deepest run's descent + * @param shallowest its shallowest run's descent + * @param pictureTop how far above its pictures reach, the higher of where Word and LibreOffice + * stand them; 0 for none + * @param pictureFoot how far below Word lowers its pictures; negative infinity for none */ - record Reach(double above, double below, double shallowest) { - static final Reach NONE = new Reach(0, 0, Double.POSITIVE_INFINITY); + record Reach(double above, double below, double shallowest, double pictureTop, double pictureFoot) { + static final Reach NONE = new Reach(0, 0, Double.POSITIVE_INFINITY, 0, Double.NEGATIVE_INFINITY); - /** The line's height. */ + /** The text's line's height. */ double height() { return above + below; } + + /** + * Whether its pictures stay within its text's own line in both editors: none reaches above + * its text, its faces' line gap included, nor below its shallowest run's descent, so that + * neither editor grows a line as tall as its text's for them nor stands its text elsewhere. + * Read against the text's own line, not the line written: a line written taller for its + * pictures, {@code makeRoomForPictures}' in DocxSemanticBackend, would hold them too. + */ + boolean holdsItsPictures() { + return pictureTop <= above + EPSILON && pictureFoot <= shallowest + EPSILON; + } + + /** Whether its line holds a picture set in it. */ + boolean holdsPictures() { + return pictureFoot > Double.NEGATIVE_INFINITY; + } } /** 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 ff5d9fce2..9daa98a57 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 @@ -9529,7 +9529,8 @@ private double takeFromTheLineAbove(double points, ParagraphNode pulled) { ? writtenTwips(spacing.getLine()) : null; java.util.Optional laid = layout.firstLine(above); // The letters' reach off their own baseline, before any seat: the position written on - // the runs below carries the seat, as far as it was written. + // the runs below carries the seat, as far as it was written. A line holding a picture has + // none (DocxInk), and is not shortened: the picture would be cut where the letters are not. double[] ink = laid.isEmpty() ? null : DocxInk.of(laid.get(), measuredFonts()); if (line == null || ink == null || spacing.isSetAfter()) { return 0; @@ -13160,8 +13161,10 @@ private double shiftToThePagesBaseline(XWPFParagraph para, } // 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; + // position, not four tenths. Not a least line's: Word does not move its text by a + // position, and LibreOffice moves it and grows the line (DocxGrownLines). + boolean least = para.getCTP().getPPr().getSpacing().getLineRule() == STLineSpacingRule.AT_LEAST; + boolean moved = !least && (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, staysOnItsPage, para.getCTP().getPPr().getSpacing(), shift, !moved); @@ -13608,7 +13611,10 @@ private static void setTextBrokenAtLines(XWPFRun run, String text) { /** * Lets a line hold the pictures in it (see {@link PictureReach}). * - *

A paragraph whose pictures stay within its text keeps its exact height. One holding a + *

A paragraph whose pictures stay within its text keeps its exact height here; it is + * written at a least height where they stay within it in both editors and Word then stands + * its text within a quarter point of the page's ({@link #letTheLinesGrow}, + * {@link DocxGrownLines.Reach#holdsItsPictures}). One holding a * picture that rises above the text is written with its lines at least the * height the picture reaches where Word puts it instead: the editor then grows the line to * the picture rather diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedListTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedListTest.java index e950f84d0..98daf3014 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedListTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxComposedListTest.java @@ -195,7 +195,8 @@ void aListWhoseMarkerIsDrawnInItsOwnColumnInACellIsWrittenOnItsLines() throws Ex XWPFParagraph inTheFlow = flow.getParagraphs().stream() .filter(paragraph -> paragraph.getText().contains("Setup")).findFirst().orElseThrow(); - assertThat(spacing(composed).getLineRule()).hasToString("exact"); + assertThat(spacing(composed).getLineRule()).as("its marker within its text in both editors") + .hasToString("atLeast").hasToString(spacing(inTheFlow).getLineRule().toString()); assertThat(line(composed)).as("as in the flow").isEqualTo(line(inTheFlow)); } } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java index 3876da0e3..cb9eec859 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesExportTest.java @@ -261,6 +261,103 @@ void aRightToLeftTextCellStaysExact() throws Exception { } } + @Test + void aLineHoldingAPictureWithinItsTextIsWrittenAtALeastHeight() throws Exception { + // A 6pt picture on the baseline of 10pt Lato stays under the text's top in both editors, + // LibreOffice's tenth of a point above the baseline included: the line is the text's. + byte[] png = png(); + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.inlineImage(com.demcha.compose.document.image.DocumentImageData.fromBytes(png), + 6, 6, com.demcha.compose.document.node.InlineImageAlignment.BASELINE, 0, null) + .inlineText(" Small", face(FontName.LATO, 10))))) { + XWPFParagraph small = paragraph(document, " Small"); + + assertThat(small.getCTP().getRArray(0).sizeOfDrawingArray()).as("the premise: its picture first").isOne(); + assertThat(spacing(small).getLineRule()).isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(DocxTwips.of(spacing(small).getLine())).as("Lato's own line").isEqualTo(240); + assertThat(positions(small)).containsOnly(0L); + } + } + + @Test + void aLineGrownToAPictureRisingPastItsTextIsGivenNoPosition() throws Exception { + // An 11pt icon set at the middle of 10pt Poppins rises past the text the page measures, and + // the line is written at least the icon's 14pt reach, shorter than Poppins' own 15pt line: + // grown to its picture, it is not read as a line grown with its text. A least line's text + // is not moved by a position in Word, and LibreOffice moves it and grows the line: none is + // written. + byte[] png = png(); + try (XWPFDocument document = DocxExports.withLayout(200, 600, 20, page -> page + .addParagraph(p -> p.inlineImage(com.demcha.compose.document.image.DocumentImageData.fromBytes(png), + 11, 11, com.demcha.compose.document.node.InlineImageAlignment.CENTER) + .inlineText(" Icon beside text long enough to wrap onto a second line", face(FontName.POPPINS, 10))))) { + XWPFParagraph icon = paragraph(document, " Icon"); + + var runs = icon.getCTP().getRList(); + assertThat(spacing(icon).getLineRule()).as("the premise: grown to its icon").isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(DocxTwips.of(spacing(icon).getLine())).as("the premise: shorter than Poppins' own 15pt") + .isLessThan(300); + assertThat(runs.get(0).sizeOfDrawingArray()).as("the premise: the icon first").isOne(); + assertThat(positions(icon).get(0)).as("the premise: lowered to the middle").isNegative(); + assertThat(positions(icon).subList(1, runs.size())).as("its text").containsOnly(0L); + } + } + + @Test + void aLeastLineGrownToAPictureWithinItsFacesOwnLineIsGivenNoPosition() throws Exception { + // An 11pt picture on the baseline of 10pt Poppins rises past the ascent the page measures + // and stays under Poppins' own, its line gap above its letters; the 2pt between the lines + // make the line taller than that own line. Seated, a least line is still given no position. + byte[] png = png(); + try (XWPFDocument document = DocxExports.withLayout(200, 600, 20, page -> page + .addParagraph(p -> p.inlineImage(com.demcha.compose.document.image.DocumentImageData.fromBytes(png), + 11, 11, com.demcha.compose.document.node.InlineImageAlignment.BASELINE, 0, null) + .inlineText(" Gapped text long enough to wrap onto a second line", face(FontName.POPPINS, 10)) + .lineSpacing(2)))) { + XWPFParagraph gapped = paragraph(document, " Gapped"); + + assertThat(spacing(gapped).getLineRule()).as("the premise: grown to its picture") + .isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(DocxTwips.of(spacing(gapped).getLine())).as("the premise: as tall as Poppins' own line") + .isGreaterThanOrEqualTo(300); + assertThat(positions(gapped).subList(1, gapped.getCTP().getRList().size())).as("its text") + .containsOnly(0L); + } + } + + @Test + void aLineGrownToARaisedPictureIsGivenNoPosition() throws Exception { + // A 9pt picture raised 2pt off the baseline of 10pt Lato reaches 11pt up in Word, past the + // text's own line, and opening the page it has no space above to be held in: the line is + // written at least that reach, taller than Lato's own 12pt line. Its text is given no + // position, which Word would not move it by, and its picture keeps its own raise. + byte[] png = png(); + try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page + .addParagraph(p -> p.inlineImage(com.demcha.compose.document.image.DocumentImageData.fromBytes(png), + 9, 9, com.demcha.compose.document.node.InlineImageAlignment.BASELINE, 2, null) + .inlineText(" Raised", face(FontName.LATO, 10))))) { + XWPFParagraph raised = paragraph(document, " Raised"); + List positions = positions(raised); + + assertThat(spacing(raised).getLineRule()).as("the premise: grown to its picture") + .isEqualTo(STLineSpacingRule.AT_LEAST); + assertThat(DocxTwips.of(spacing(raised).getLine())).as("the premise: taller than Lato's own line") + .isGreaterThan(240); + assertThat(positions.get(0)).as("the picture's own raise").isEqualTo(4L); + assertThat(positions.subList(1, positions.size())).as("its text").containsOnly(0L); + } + } + + private static byte[] png() { + try (java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream()) { + javax.imageio.ImageIO.write(new java.awt.image.BufferedImage(8, 8, + java.awt.image.BufferedImage.TYPE_INT_RGB), "png", out); + return out.toByteArray(); + } catch (java.io.IOException failure) { + throw new IllegalStateException(failure); + } + } + private static DocumentTableCell textCell(String text, DocumentTableTextAnchor anchor) { return DocumentTableCell.text(text).withStyle(cellStyle(face(FontName.LATO, 8), anchor, 0)); } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesTest.java index c0d110a4d..bc70fa645 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxGrownLinesTest.java @@ -119,7 +119,7 @@ void aRunHoldingOnlyAShapeAnchoredToTheParagraphIsPassedOver() throws Exception run(inline, "Lato", 7.5); assertThat(lines.reachOf(paragraph.getCTP()).below()).isCloseTo(LATO_DOWN * 7.5, within(0.01)); - assertThat(lines.reachOf(inline.getCTP())).as("a picture set in the line").isNull(); + assertThat(lines.reachOf(inline.getCTP())).as("a picture set in the line, its size not written").isNull(); } @Test @@ -141,7 +141,7 @@ void aParagraphHoldingWhatALineIsNotReadByHasNoReach() throws Exception { run(listed, "Lato", 10); listed.getCTP().addNewPPr().addNewNumPr(); - assertThat(lines.reachOf(picture.getCTP())).as("a picture").isNull(); + assertThat(lines.reachOf(picture.getCTP())).as("a drawing in a run of text").isNull(); assertThat(lines.reachOf(raised.getCTP())).as("a raise, which a least line does not keep").isNull(); assertThat(lines.reachOf(script.getCTP())).as("a script").isNull(); assertThat(lines.reachOf(rightToLeft.getCTP())).as("right to left, in its complex-script face").isNull(); @@ -184,6 +184,99 @@ void aLeastLineStandsItsBaselineItsRunsDescentAboveItsFootAndAnExactOneFourFifth assertThat(lines.lineOf(auto.getCTP())).as("the editor's own measure").isNull(); } + @Test + void aLineMayGrowOnlyWhereItsPicturesStayWithinItsTextInBothEditors() throws Exception { + // A 12pt line of 10pt Lato leaves 9.87pt above the text's 2.13pt descent. Word lowers a + // picture by its run's position; LibreOffice stands it a tenth of a point above the baseline. + DocxGrownLines lines = shipping(LATO, LATO); + XWPFParagraph small = lato(240, 10); + picture(small, 6, 0, 0); + XWPFParagraph lowered = lato(240, 10); + picture(lowered, 9, -3, 0); + XWPFParagraph tall = lato(240, 10); + picture(tall, 9.8, 0, 0); + XWPFParagraph deep = lato(240, 10); + picture(deep, 6, -6, 0); + XWPFParagraph ownSize = lato(240, 10); + picture(ownSize, 6, 0, 48); + XWPFParagraph broken = lato(240, 10); + broken.getRuns().get(0).addBreak(); + picture(broken, 6, 0, 0); + XWPFParagraph spaced = lato(240, 10); + spaced.getRuns().get(0).setText(" ", 0); + picture(spaced, 6, 0, 0); + XWPFParagraph unbroken = lato(240, 10); + unbroken.getRuns().get(0).setText("   ", 0); + picture(unbroken, 6, 0, 0); + XWPFParagraph raised = lato(240, 10); + picture(raised, 6, 8, 0); + + assertThat(lines.mayGrow(small.getCTP())).as("on the baseline, under the text's top").isTrue(); + assertThat(lines.mayGrow(lowered.getCTP())).as("Word's 7.5pt up and 1.5pt down, LibreOffice's 9.1pt up") + .isTrue(); + assertThat(lines.mayGrow(tall.getCTP())).as("9.9pt up in LibreOffice").isFalse(); + assertThat(lines.mayGrow(deep.getCTP())).as("3pt down in Word, past the text's descent").isFalse(); + assertThat(lines.mayGrow(ownSize.getCTP())).as("the picture run's 24pt sets none of the line").isTrue(); + assertThat(lines.reachOf(broken.getCTP())).as("a broken line may hold the picture alone").isNull(); + assertThat(lines.reachOf(spaced.getCTP())).as("pictures among spaces, no letter").isNull(); + assertThat(lines.reachOf(unbroken.getCTP())).as("among no-break spaces").isNull(); + assertThat(lines.mayGrow(raised.getCTP())).as("raised 4pt, 10pt up in Word; 6.1pt in LibreOffice").isFalse(); + assertThat(lines.reachOf(small.getCTP()).height()).as("the text's line").isCloseTo(12, within(0.01)); + + XWPFParagraph grownToItsPicture = lato(200, 10); + picture(grownToItsPicture, 6, 0, 0); + // Written 14pt for a 9pt picture raised 2pt, 11pt up in Word: taller than the text's own + // line, the line holds it, but its text's own line does not. + XWPFParagraph grownToARaise = lato(280, 10); + picture(grownToARaise, 9, 4, 0); + rule(small, STLineSpacingRule.AT_LEAST); + rule(tall, STLineSpacingRule.AT_LEAST); + rule(grownToItsPicture, STLineSpacingRule.AT_LEAST); + rule(grownToARaise, STLineSpacingRule.AT_LEAST); + assertThat(lines.lineOf(small.getCTP()).baseline()).isCloseTo(12 - LATO_DOWN * 10, within(0.01)); + assertThat(lines.lineOf(tall.getCTP())).as("set apart by the two editors").isNull(); + assertThat(lines.lineOf(grownToItsPicture.getCTP())).as("written shorter than its text's own line") + .isNull(); + assertThat(lines.lineOf(grownToARaise.getCTP())).as("grown to a raised picture").isNull(); + } + + @Test + void aPictureRunHoldingAnythingButPicturesOfAWrittenSizeIsNotRead() throws Exception { + DocxGrownLines lines = shipping(LATO, LATO); + XWPFParagraph anchoredBeside = lato(240, 10); + var drawing = anchoredBeside.createRun().getCTR().addNewDrawing(); + drawing.addNewAnchor(); + var extent = drawing.addNewInline().addNewExtent(); + extent.setCx(76200); + extent.setCy(76200); + XWPFParagraph measured = lato(240, 10); + picture(measured, 6, 0, 0); + measured.getCTP().getRList().get(1).addNewRPr().addNewPosition().setVal("2pt"); + + assertThat(lines.reachOf(anchoredBeside.getCTP())).as("an anchor beside the picture").isNull(); + assertThat(lines.reachOf(measured.getCTP())).as("a position in points, not half points").isNull(); + } + + /** + * Adds a run holding a picture {@code points} tall set in the line, its run's position + * {@code position} half points and, where not 0, its size {@code halfPoints}. + */ + private static void picture(XWPFParagraph paragraph, double points, int position, int halfPoints) { + var run = paragraph.createRun().getCTR(); + if (position != 0 || halfPoints != 0) { + var properties = run.addNewRPr(); + if (position != 0) { + properties.addNewPosition().setVal(BigInteger.valueOf(position)); + } + if (halfPoints != 0) { + properties.addNewSz().setVal(BigInteger.valueOf(halfPoints)); + } + } + var extent = run.addNewDrawing().addNewInline().addNewExtent(); + extent.setCx(Math.round(points * 12700)); + extent.setCy(Math.round(points * 12700)); + } + /** * The lines of a document that ships the faces of {@code faces} and of its Normal style * {@code normal}, as an export does: Normal is the style the most of its text is set in.