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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,30 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **A DOCX export stands a table's rules where the page draws them beside a column sized to its
content.**
- An auto column's text is given a point of slack past the page's width, so an editor setting it
in its own face keeps the page's line breaks. The slack widened the column, so every rule and
fill right of it, and the table's right end, stood a point right of the page's.
- It now comes out of the column's text cells' margins, on the side their text does not lean on:
the right where it is set left, the left where it is set right, half from each where it is
centred. The column keeps the page's width, its text the same room. A cell spanning more than
one such column shares its margin among them.
- A composed cell keeps its margins: a paragraph in it may run past its text into them, which
Word cuts off at the cell's edge. What a column's cells do not hold widens it, and the report
names it (`right end`): beside a composed cell, or where a cell's padding on the side it gives
from is thinner than the point (each side thinner than half of it, centred). A side gives only
what it has past the inner half of the cell's rule, so a line an editor's face sets wider does
not run onto it.
- Measured on Windows:
- Word stood Classic and Modern Invoice's and Modern Proposal's rules right of the auto column
0.94 to 1.06pt right of the page's, and now within 0.12pt. LibreOffice stood them 0.66 to
1.06pt right, and now with the tables' other rules.
- Every line breaks where it did in both editors. The text right of the column moves the
point back to where the page sets it.
- Orange Ops's aside headings, composed in a table with no padding, keep their accent rules
1pt longer in Word, named.
- 5 of the corpus's 62 documents change. 4 tables gain the `right end` note.
- **A DOCX export names a panel's left edge its table cell holds in.**
- The page sets a panel in a table cell with its left border centred on the cell's text edge,
half of it outside, or further out by a margin below zero. LibreOffice starts a nested table
Expand Down
Binary file modified assets/readme/examples/word-export-companion.docx
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/recipes/docx-export.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15874,7 +15874,7 @@ private void writeTableRows(XWPFDocument document, TableNode node) throws Except
// cell is one cell carrying a span, not several, so a row's physical count is not
// the column count.
XWPFTable table = newTable(document, rowCount, 1);
applyTableWidth(table, node, columnCount);
double[] slackHeld = applyTableWidth(table, node, columnCount, cover);
boolean translucentFills = false;
boolean translucentRules = false;
java.util.Set<Double> ruleWidths = new java.util.TreeSet<>();
Expand Down Expand Up @@ -15918,8 +15918,8 @@ private void writeTableRows(XWPFDocument document, TableNode node) throws Except
int next = placement.row() + placement.rowSpan();
DocumentStroke underneath = next < rowCount ? resolveCellStroke(node, cover[next][placement.column()]) : null;
DocumentInsets cellPadding = resolveCellPadding(node, placement);
applyCellPadding(cell, clearOfTheRules(cellPadding, stroke,
placement.row() == 0, next >= rowCount, underneath));
applyCellPadding(cell, clearOfTheRules(givingTheSlack(node, placement, cellPadding, slackHeld),
stroke, placement.row() == 0, next >= rowCount, underneath));
if (placement.rowSpan() == 1 && placement.cell() != null && placement.cell().content() == null) {
ruleRoom.count(rowIdx, placement, cellPadding, roundTheContent(cellPadding, stroke,
placement.row() == 0, next >= rowCount, underneath), textHeight(node, placement));
Expand Down Expand Up @@ -19018,18 +19018,41 @@ static boolean fixedBesideAuto(TableNode node) {
* numbers the document already carries. A table with an {@code auto} column and no
* stated width keeps Word's own sizing until resolved layout can supply the measured
* widths.</p>
*
* @param cover the table's cells by row and column
* @return the editor's slack each column's cells' margins hold, in points
* ({@link #slackTheMarginsHold}), or {@code null} where the layout measured no columns
*/
private void applyTableWidth(XWPFTable table, TableNode node, int columnCount) {
private double[] applyTableWidth(XWPFTable table, TableNode node, int columnCount,
TableGrid.Placement[][] cover) {
double[] measured = layout.tableColumns(node, columnCount);
if (measured != null && measured.length > 0) {
// The layout resolved every column, an auto one included, so there is nothing
// left to decide: write the widths it arrived at and stop Word re-fitting them —
// with the editor's margin on the columns sized to their content.
double[] columns = withEditorSlack(node, measured);
// with the editor's margin on the columns sized to their content, in their cells'
// margins where those hold it.
double[] slackened = withEditorSlack(node, measured);
double[] held = slackTheMarginsHold(node, cover, measured, slackened);
double[] columns = new double[measured.length];
double wider = 0;
for (int index = 0; index < columns.length; index++) {
columns[index] = slackened[index] - held[index];
wider += columns[index] - measured[index];
}
writeGrid(table, columns);
setTableWidth(table, sum(columns));
setFixedLayout(table);
return;
// Less than a tenth of a point further right is not seen. Named painted or not: the text
// of the columns right of a widened one moves with it, and an editor shows the table's
// right edge as a gridline.
if (wider >= 0.1) {
report.add(DocxExportReport.Severity.APPROXIMATED, RIGHT_END, layout.pathOf(node),
"its columns sized to their content written " + pointsOf(wider) + "pt wider than the "
+ "page sets them, the room an editor setting their text in its own face may need, which "
+ "their cells' margins do not hold: its right end, and what stands right of them, up to "
+ "that much further right");
}
return held;
}

Double authored = node.width() != null && node.width() > 0 ? node.width() : null;
Expand All @@ -19046,7 +19069,7 @@ private void applyTableWidth(XWPFTable table, TableNode node, int columnCount) {
setTableWidth(table, nestedTableWidth());
}
nameTheColumnsLeftToTheEditor(node);
return;
return null;
}

double natural = fixedColumns.stream().mapToDouble(Double::doubleValue).sum();
Expand All @@ -19062,10 +19085,114 @@ private void applyTableWidth(XWPFTable table, TableNode node, int columnCount) {
// first in a different place than the PDF draws it.
columns[columns.length - 1] += width - natural;
writeGrid(table, columns);
return null;
}

/**
* How much of each column's editor slack its cells' margins hold, in points
* ({@link #withEditorSlack}): the column is written that much narrower, as wide as the page
* sets it where they hold all of it, and its cells' text keeps the room the slack gives it.
*
* <p>A column widened by its slack moves every rule and fill right of it, and the table's right
* end, that much right of the page's: measured in Word, {@code ClassicInvoice}'s line items
* stood their column rules 0.98 to 1.05pt right of the page's from the description column on.
* Taken off a cell's margin, the slack gives its text the same room, and so the same line
* breaks, with the rules where the page draws them. A text cell gives it from the side its text
* does not lean on, so its text stays where the page sets it: the right where it is set left,
* the left where it is set right, half from each where it is centred. A cell spanning more than
* one of the columns given slack shares its margin among them. A composed cell gives none: a
* paragraph in it may run past its text into the margin ({@link #measureAtWordsSize}), and with
* the margin taken, past the cell's edge, where Word cuts it off. A column holds the least any
* of its cells gives; the rest widens it, and {@link #applyTableWidth} names it.</p>
*
* @param measured the columns as the layout measured them, in points
* @param slackened the same with the editor's slack
* @return the slack each column's cells' margins hold, in points
*/
private double[] slackTheMarginsHold(TableNode node, TableGrid.Placement[][] cover,
double[] measured, double[] slackened) {
double[] held = new double[measured.length];
for (int index = 0; index < held.length; index++) {
held[index] = Math.max(0, slackened[index] - measured[index]);
}
java.util.Set<TableGrid.Placement> seen =
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
for (TableGrid.Placement[] row : cover) {
for (TableGrid.Placement placement : row) {
if (placement == null || !seen.add(placement)) {
continue;
}
int end = Math.min(placement.column() + placement.colSpan(), held.length);
int sharing = 0;
for (int column = placement.column(); column < end; column++) {
if (slackened[column] > measured[column]) {
sharing++;
}
}
if (sharing == 0) {
continue;
}
double share = marginGivingTheSlack(node, placement) / sharing;
for (int column = placement.column(); column < end; column++) {
held[column] = Math.min(held[column], share);
}
}
}
return held;
}

/**
* How much of its columns' slack a cell's padding can give, in points
* ({@link #slackTheMarginsHold}): a text cell's on the side its text does not lean on, a
* centred one's twice the thinner side; none of a composed cell's. A side gives only what it
* has past the inner half of the cell's rule, as wide as Word draws it ({@link #ruleTwips}):
* the slack taken further, a line an editor's face sets wider would run onto the rule.
*/
private double marginGivingTheSlack(TableNode node, TableGrid.Placement placement) {
if (placement.cell() == null || placement.cell().content() != null) {
return 0;
}
DocumentInsets padding = resolveCellPadding(node, placement);
double halfRule = ruleTwips(resolveCellStroke(node, placement)) / 2.0 / POINT_TO_TWIP;
double left = Math.max(0, padding.left() - halfRule);
double right = Math.max(0, padding.right() - halfRule);
TextAlign leans = leanOf(node, placement);
return leans == TextAlign.RIGHT ? left : leans == TextAlign.CENTER ? 2 * Math.min(left, right) : right;
}

/**
* Widens the columns sized to their content by a point, where there is room for it.
* A text cell's padding less the slack its columns' margins hold ({@link #slackTheMarginsHold}),
* off the side its text does not lean on; as it is where they hold none.
*
* @param held the slack each column's cells' margins hold, or {@code null} where none is given
*/
private DocumentInsets givingTheSlack(TableNode node, TableGrid.Placement placement, DocumentInsets padding,
double[] held) {
if (held == null) {
return padding;
}
double given = 0;
for (int column = placement.column(); column < Math.min(placement.column() + placement.colSpan(),
held.length); column++) {
given += held[column];
}
if (!(given > 0)) {
return padding;
}
TextAlign leans = leanOf(node, placement);
double left = leans == TextAlign.RIGHT ? given : leans == TextAlign.CENTER ? given / 2 : 0;
return new DocumentInsets(padding.top(), padding.right() - (given - left), padding.bottom(),
padding.left() - left);
}

/** The side the page sets a text cell's lines against: left, right or the middle. */
private TextAlign leanOf(TableNode node, TableGrid.Placement placement) {
boolean rightToLeft = resolveCellDirection(node, placement, placement.cell().lines());
return horizontalOf(resolveCellAnchor(node, placement, rightToLeft));
}

/**
* The columns, those sized to their content a point wider, where there is room for it.
*
* <p>An auto column is exactly as wide as its widest unwrapped cell: the engine gives it
* no slack, because on the page it needs none. An editor does. It sets the text in its
Expand All @@ -19074,11 +19201,14 @@ private void applyTableWidth(XWPFTable table, TableNode node, int columnCount) {
* measured on the probe corpus through LibreOffice, a billing table's widest item
* wrapped and its row doubled the moment its cells were written at their true 14pt.</p>
*
* <p>So each auto column gets {@value #EDITOR_COLUMN_SLACK_POINTS}pt more than the page
* gave it — below anything a reader compares by eye, and what keeps the page's line
* breaks. Only where it costs nothing the document stated: a table with an authored
* width keeps that width exactly, a fixed column keeps its size, and the slack never
* takes the table past the width it sits in.</p>
* <p>So each auto column's text gets {@value #EDITOR_COLUMN_SLACK_POINTS}pt more than the
* page gave it — below anything a reader compares by eye, and what keeps the page's line
* breaks — out of its cells' margins where they hold it ({@link #slackTheMarginsHold}), the
* column as wide as the page sets it. Only where it costs nothing the document stated: a
* table with an authored width keeps that width exactly, a fixed column keeps its size, and
* the slack never takes the table past the width it sits in.</p>
*
* @return the columns with the slack, in points
*/
private double[] withEditorSlack(TableNode node, double[] measured) {
if (node.width() != null) {
Expand Down
Loading
Loading