Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export anchors a drawing in a paragraph written by any writer of text — a list's item,
the one line two halves of a band share — so a card whose only text is written so moves its
drawings with it when the text above is edited.**
- A paragraph was offered to the drawings standing beside it only while a paragraph node was
being written, so a block whose text another writer writes had no paragraph to offer: a card
whose only text is a list's items, and a CV entry's head band written as one Word line. Their
drawings went to the text nearest them — in the body, the paragraph above the card, whose top
does not move when that paragraph grows a line and the card does.
- Measured in Word 16.0.20527 and LibreOffice 24.2, with a paragraph above lengthened by one
line: the card's own text moved 9.72pt in Word and 19.40pt in LibreOffice, and the drawing
moved 0.00 in both. Carried by the item or by the shared line, it moves with them — 9.70 and
19.35pt — and stands on the card's corner where the page draws it, 0.09pt from it in Word
against 13.76pt before.
- Each item of a list is offered with its own text's box, so a drawing standing beside the third
item is anchored in that item — where the export matched the layout's items to the list's one
by one, which the items' line gap and their own seating need as well; a list with an item split
across a page is laid out as more pieces of text than it has items, and offers none. A band's
shared line is offered as the half that starts it, from its text's own start and reaching
across to the end of the other's box: both halves are written in that one paragraph, and that
half's text begins where the line does. Its text's start is the inked one, not the edge of the
box the half is set in, because a drawing anchored in a cell is placed from the cell's text
column and that column is read from the place's left edge less the indent written on the
paragraph: stated as the box, the column came out the band's width to the left, and a card
carried its drawing 292pt across its cell where the page draws it at 150. The line's reach
across is the whole line's, so that a drawing beside the dates at its right end, or in the gap
between the two texts, stands beside the line. A line the layout starts left of its cell's text
column offers no place at all — a band pulled out by a margin below zero, into the cell's
padding or past it: Word draws no text past a cell's left edge, so the indent written for such
a line is held at nought and its text does not start where the layout puts it.
- A table's own cell text offers no place yet, and a drawing whose block's text is written only
so keeps the nearest-text rule: a table's cells state no width of their own — their table's
grid holds it — and a drawing anchored in a cell is placed from the cell's text column, which
cannot be read without that width; a text cell is set to the middle by default besides, and such a
cell carries no drawing.
- **A DOCX export anchors what a table's composed cell draws in that cell, so it moves with the
cell's row when the text above is edited.**
- Such a drawing has no node of its own — a composed cell's content is laid out under its
Expand Down
11 changes: 10 additions & 1 deletion docs/architecture/backend-capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ nodes are a different case: `ShapeNode`, `LineNode`, `EllipseNode`,
`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 first paragraph the nearest block holding it writes on its page — the paragraph nearest it where none does, the first paragraph of the cell whose box holds it 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
polygons and paths as custom geometry). Three writers offer a paragraph to be
anchored in: a paragraph node's own, an item of a list the layout's items are
matched to, and the one line two halves of a band share. A table's own cell text
offers none — its cell states no width for a shape to be placed from — so a
shape whose block's text is written only so keeps to the text nearest it. A
shared line is offered from its text's own start, which its indent is written
from, and reaches across to the end of the box its other half is set in; one
the layout starts left of its cell's text column offers none, its indent being
held at nought there.
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
table cell out inside the cell. A filled or outlined
Expand Down
30 changes: 26 additions & 4 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -1517,10 +1517,32 @@ The paragraph's top is where Word places from: the space written above its first
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.
lines carries none. Three writers of text offer their paragraphs, and the first
paragraph of each piece of text that holds text takes the place: a paragraph node's own
paragraph, in a column's cell too; each item of a list, so that a card whose only text is a
list's items carries the drawings laid over it, and a shape standing beside the third item takes
that item; and the one line two halves of a band share — a title at the left, its dates at the
right — offered as the half that starts the line, since both are written in that one paragraph.
That line's text starts where the line's own indent puts it, at that half's inked start rather
than at the edge of the box it is laid out in, which is as wide as the band where the half is set
in it by its alignment: a shape anchored in a cell is placed from the cell's text column, and the
column is read from the place's left edge less the indent written on the paragraph, so the two
must be the one edge — stated as the box, the column comes out the band's width to the left and
the cell carries the shape from there. How far the line reaches across is the whole line's, to the
end of the box the other half is laid out in, so that a shape beside the dates at its right end,
or in the gap between the two texts, stands beside the line and not beside some other paragraph.
A line the layout starts left of its cell's text column offers no place — a band pulled out by a
margin below zero, into the cell's padding or past it: Word draws no text past a cell's left edge,
so the indent written for such a line is held at nought and its text does not start where the
layout puts it, and the shapes beside it keep to the text nearest them.
An item offers its place only where the export matched the layout's items to the list's one by
one, which its line gap and its own seating need as well: a list with an item split across a
page is laid out as more pieces of text than it has items, and none of its items offers a place.
A table's own cell text offers none yet either: a table's cells state no width of their own,
their table's grid holding it, and a shape anchored in a cell is placed from the cell's text
column, which cannot be read without that width; a text cell is also set to the middle by
default, and such a cell carries no drawing (above). So a shape whose block's text is written
only as a table's cells goes to the paragraph nearest it.

What a table's composed cell draws has no node of its own: a composed cell's content is laid out
under its table, and its drawings stand among the table's own fragments. The cell whose box the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ List<ItemText> itemLines(DocumentNode list) {
}
boolean sameItem = previous != null && level(previous, fragment);
ItemText text = new ItemText(paragraph.lines(), fragment.x() + paragraph.padding().left(),
fragment.pageIndex(), Math.max(0, paragraph.lineGap()));
fragment.pageIndex(), Math.max(0, paragraph.lineGap()), textBoxOf(fragment, paragraph));
if (!sameItem) {
items.add(text);
} else if (text.x() >= items.get(items.size() - 1).x()) {
Expand All @@ -604,11 +604,15 @@ List<ItemText> itemLines(DocumentNode list) {
* An item's text as a list laid it out.
*
* @param lines its lines
* @param x where its text starts across the page, in points
* @param x where its text starts across the page, in points — the box's left edge as
* well, held apart from it because an item the page laid no line of has no box
* @param page the page it was set on
* @param lineGap the gap the page sets between its lines, in points
* @param box where the page sets that same text, which the item's paragraph is offered to
* the shapes beside with ({@code DocxSemanticBackend.offerASeat}); {@code null}
* for an item the page laid no line of, which has no text to stand beside
*/
record ItemText(List<ParagraphLine> lines, double x, int page, double lineGap) {
record ItemText(List<ParagraphLine> lines, double x, int page, double lineGap, TextBox box) {

/**
* How tall the page sets the item's lines together, as {@link #linesHeight(DocumentNode)}
Expand Down Expand Up @@ -1188,18 +1192,33 @@ OptionalDouble firstLineTop(DocumentNode node) {
*/
java.util.Optional<TextBox> firstTextBox(DocumentNode node) {
for (PlacedFragment fragment : textFragmentsOf(node)) {
if (fragment.payload() instanceof ParagraphFragmentPayload paragraph && !paragraph.lines().isEmpty()) {
double top = ParagraphLineGeometry.contentTop(fragment.y(), fragment.height(), paragraph.padding().top());
ParagraphLine first = paragraph.lines().get(0);
return java.util.Optional.of(new TextBox(fragment.pageIndex(),
fragment.x() + paragraph.padding().left(),
fragment.x() + fragment.width() - paragraph.padding().right(),
top, top - first.lineHeight() + first.baselineOffsetFromBottom()));
if (fragment.payload() instanceof ParagraphFragmentPayload paragraph) {
TextBox box = textBoxOf(fragment, paragraph);
if (box != null) {
return java.util.Optional.of(box);
}
}
}
return java.util.Optional.empty();
}

/**
* Where the page sets the lines of one fragment of paragraph text: its content box, inside the
* paragraph's padding, and its first line's baseline. {@code null} for a fragment holding no
* line.
*/
private static TextBox textBoxOf(PlacedFragment fragment, ParagraphFragmentPayload paragraph) {
if (paragraph.lines().isEmpty()) {
return null;
}
double top = ParagraphLineGeometry.contentTop(fragment.y(), fragment.height(), paragraph.padding().top());
ParagraphLine first = paragraph.lines().get(0);
return new TextBox(fragment.pageIndex(),
fragment.x() + paragraph.padding().left(),
fragment.x() + fragment.width() - paragraph.padding().right(),
top, top - first.lineHeight() + first.baselineOffsetFromBottom());
}

/**
* The content box of a node's first fragment of text, and its first line's baseline.
*
Expand Down
Loading
Loading