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

### Public API

- **A DOCX export ends a rule where the page ends it in Word.**
- A horizontal line, a divider or a thin bar is written as an empty paragraph's bottom border,
its ends the paragraph's indents. Word draws a paragraph's border past both indents, so every
rule stood about 1.4pt longer at its left and 1.5pt at its right. Subscription Invoice's
four-coloured strip had its segments run that far into each other.
- Each indent is now taken in by Word's reach: 1.42pt at the left, 1.51pt at the right. A rule
running past the width it is written in is held at that width's edge as before, and only what
Word draws further than the page ends it is taken in. A rule shorter than both keeps a tenth of
a point of its paragraph: rules of 1, 2 and 3pt stood 3.0, 2.88 and 3.12pt long in Word, each
centred where the page centres it.
- LibreOffice draws the border between the indents, so it now sets each end of a rule that much
further in.
- Measured on Windows:
- Nine rules in the body, half a point to 4pt thick: Word drew them 1.36 to 1.49pt past the left
indent and 1.44 to 1.59pt past the right. Neither a side border written as none nor the size
of the paragraph's mark changed that. Rules of 1 and 3pt in a row's column and in a table
cell fell in the same range, and a dashed and a dotted one reached as far as a solid one.
- On the corpus, in Word, 280 strokes stand nearer the page's and 13 further. The 13 are rules
whose column the export already starts right of the page's, which Word's reach used to cover
in part: Receipt Modern's leaders and Violet Grid's separators, after a row's auto column
given an editor's point, now start 1pt right instead of 0.45pt left.
- In LibreOffice, 281 rules move in: 249 by 1.4pt at the left and 1.5pt at the right, 21 at the
left only, being held at their width's edge, the rest by less at the right.
- No word moves in either editor.
- A header's or footer's separator is its text's own border and keeps its indents: Word still
draws it past the margins.
- 43 of the corpus's 62 documents change. No note changes.
- **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
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.

21 changes: 21 additions & 0 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,27 @@ Word's thickest border, or a filled bar no taller than that — what `addDivider
draws — is Word's own rule: an empty paragraph
whose bottom border is the stroke, in its colour and thickness, from where the
line starts to where it ends, with the space above and below the stroke kept.
Word draws a paragraph's border past both its indents, and LibreOffice between
them, so each indent is taken in by Word's reach: 1.42pt at the left and 1.51pt at
the right (measured on nine rules in the body, half a point to 4pt thick: 1.36 to
1.49pt past the left, 1.44 to 1.59pt past the right, whatever the paragraph's mark or a
side border written as none; rules of 1 and 3pt in a row's column and in a table cell
fell in the same range, and a dashed and a dotted one reached as far as a solid one).
Word sets the rule where the page ends it; LibreOffice sets each end that much further
in. A rule
running past the width it is written in is held at that width's edge, and only what
Word draws further than the page ends it is taken in. A rule whose column the export
starts right of the page's — the weight column after a row's auto column given an
editor's point — starts that much right in Word too, where Word's reach used to cover
part of it (measured on the corpus: in Word 280 strokes nearer the page's and 13
further, `ReceiptModern`'s leaders and `VioletGrid`'s separators starting 1pt right
instead of 0.45pt left; in LibreOffice 281 rules moved in, 249 by 1.4pt at the left and
1.5pt at the right; no word moved in either). A rule shorter than both keeps a tenth of
a point of its paragraph, which Word still draws its reach past (measured: rules of 1, 2
and 3pt stood 3.0, 2.88 and 3.12pt long in Word, each centred where the page centres
it). A header's or footer's separator is its text's own paragraph
border, so its indents stay its text's, and Word draws it that much past the
margins.
Word makes the paragraph a tenth of a point of line where the stroke has no room
above it, and the border as wide as it draws it, in eighths of a point: what that
stands past the stroke's box comes off the space owed below the rule first, and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14559,7 +14559,9 @@ private void writeBarcode(XWPFDocument document, com.demcha.compose.document.nod
* The paragraph is placed where the rule's box is: the space above the stroke is the
* paragraph's height, held to a tenth of a point when there is none, the space below is
* owed to what follows, what Word's paragraph stands taller than the box hangs below it,
* and the rule's two ends are the paragraph's indents. Word draws
* and the rule's two ends are the paragraph's indents, each taken in by what Word draws a
* border past it ({@link #wordsRuleReach}): LibreOffice, which draws it between them, sets each
* end that much further in. Word draws
* a border in its own dash lengths, so a dashed rule keeps its dash and not the pattern's
* lengths, and the report says so.</p>
*/
Expand All @@ -14584,21 +14586,34 @@ private void writeRule(XWPFDocument document, DocumentNode node, DocxRules.Rule
// known: a cell whose grid this export did not write has none, and measuring against the
// page there put the rule's end past the cell's.
boolean widthKnown = currentCell != null ? Double.isFinite(currentCellWidth) : contentWidth < Double.MAX_VALUE;
// Word draws a paragraph's border past both its indents, and LibreOffice between them: the
// indents are taken in by Word's reach, so the rule ends where the page ends it in Word.
// A rule running past the width it is written in is held at that width's edge, as before,
// and only what Word draws further than the page ends it is taken in.
double reachLeft = wordsRuleReach(drawnAs != null ? drawnAs.end() - drawnAs.start() : to - from,
WORD_RULE_REACH_LEFT_POINTS);
double reachRight = wordsRuleReach(drawnAs != null ? drawnAs.end() - drawnAs.start() : to - from,
WORD_RULE_REACH_RIGHT_POINTS);
if (drawnAs != null) {
// Written in the cell itself, it runs where that drawing does, between the cell's
// margins: a composed cell's content has no place of its own, and the alignment or the
// width the layout gave a line there was lost — a line centred in its cell came out at
// the cell's left.
double textLeft = drawnAs.box().left() + cellMarginOf(currentCell, true);
double textRight = drawnAs.box().right() - cellMarginOf(currentCell, false);
indent.setLeft(BigInteger.valueOf(leftIndentTwips(drawnAs.start() - textLeft)));
indent.setRight(BigInteger.valueOf(toTwips(Math.max(0, textRight - drawnAs.end()))));
indent.setLeft(BigInteger.valueOf(leftIndentTwips(drawnAs.start() - textLeft + reachLeft)));
indent.setRight(BigInteger.valueOf(toTwips(Math.max(0, textRight - drawnAs.end() + reachRight))));
} else {
// A rule in a container hanging left starts out past the margin with its text.
indent.setLeft(BigInteger.valueOf(leftIndentTwips(insetLeft + sideLeft + from)));
indent.setLeft(BigInteger.valueOf(leftIndentTwips(insetLeft + sideLeft + from + reachLeft)));
if (widthKnown) {
double rightGap = availableWidth() - sideLeft - to;
indent.setRight(BigInteger.valueOf(toTwips(insetRight + Math.max(0, rightGap))));
indent.setRight(BigInteger.valueOf(toTwips(insetRight + Math.max(0, rightGap + reachRight))));
} else {
// It runs to the edge Word gives it, held in from it by the containers round it
// (applyInset), and that much less past it.
long held = twipsOf(indent.isSetRight() ? indent.getRight() : null);
indent.setRight(BigInteger.valueOf(held + Math.round(reachRight * POINT_TO_TWIP)));
}
}

Expand Down Expand Up @@ -14660,6 +14675,38 @@ private void writeRule(XWPFDocument document, DocumentNode node, DocxRules.Rule
reportWrittenWithout(node, "written as a paragraph border");
}

/**
* How far one of a rule's indents is taken in for what Word draws its border past it, in
* points: Word's reach, but no further than leaves the rule's paragraph a tenth of a point.
*
* <p>Word draws a paragraph's bottom border past both its indents and LibreOffice between
* them. Measured on nine rules in the body, half a point to 4pt thick, indented from nothing
* to 61.7pt: Word drew them 1.36 to 1.49pt past the left indent and 1.44 to 1.59pt past the
* right, whatever the size of the paragraph's mark or a left and right border written as none;
* rules of 1 and 3pt in a row's column and in a table cell fell in the same range, and a
* dashed and a dotted one reached as far as a solid one. {@code SubscriptionInvoice}'s
* separators stood 1.32pt left of the page's and 1.57pt right, and its four-coloured strip's
* segments ran that far into each other. A rule shorter than Word's reach at both ends keeps a
* tenth of a point of its paragraph, which Word still draws the reach past: measured, rules of
* 1, 2 and 3pt stood 3.0, 2.88 and 3.12pt long in Word, each centred within 0.05pt of where
* the page centres it.</p>
*
* @param length the rule's length on the page, in points
* @param reach how far past that indent Word draws it, in points
*/
private static double wordsRuleReach(double length, double reach) {
return Math.min(reach, Math.max(0, (length - SHORTEST_RULE_PARAGRAPH_POINTS) / 2));
}

/** How far past its left indent Word draws a paragraph's border, in points: measured, 1.36 to 1.49. */
static final double WORD_RULE_REACH_LEFT_POINTS = 1.42;

/** How far past its right indent Word draws a paragraph's border, in points: measured, 1.44 to 1.59. */
static final double WORD_RULE_REACH_RIGHT_POINTS = 1.51;

/** What a rule shorter than Word's reach at both ends keeps of its paragraph's width, in points. */
private static final double SHORTEST_RULE_PARAGRAPH_POINTS = 0.1;

/** Word's border for a rule's dash: dots where the dashes are no longer than the stroke. */
private static STBorder.Enum dashOf(DocxRules.Rule rule) {
if (rule.dashed() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ void aLineCentredInItsCellIsWrittenWhereThePageCentresIt() throws Exception {
// The cell's text starts 4pt in from its edge at 20, as its margin in the file does.
double start = line.x() + ((LineFragmentPayload) line.payload()).startX() - 24;

// Each indent taken in by what Word draws its border past it (wordsRuleReach).
CTInd indent = export.rule().getCTP().getPPr().getInd();
assertThat(DocxTwips.of(indent.getLeft())).isEqualTo(Math.round(start * 20));
assertThat(DocxTwips.of(indent.getRight())).as("as far from the right").isEqualTo(Math.round(start * 20));
assertThat(DocxTwips.of(indent.getLeft()))
.isEqualTo(Math.round((start + DocxSemanticBackend.WORD_RULE_REACH_LEFT_POINTS) * 20));
assertThat(DocxTwips.of(indent.getRight())).as("as far from the right")
.isEqualTo(Math.round((start + DocxSemanticBackend.WORD_RULE_REACH_RIGHT_POINTS) * 20));
assertThat(export.body()).doesNotContain(LINE);
}

Expand All @@ -86,7 +89,8 @@ void aRuleTakesItsOwnDrawingNotAnotherLineBeforeIt() throws Exception {
assertThat(export.body().split(LINE, -1)).as("one line drawn").hasSize(2);
assertThat(export.body()).as("the vertical one").contains("<wp:extent cx=\"0\" cy=\"" + Units.toEMU(20) + "\"");
assertThat(DocxTwips.of(export.rule().getCTP().getPPr().getInd().getRight()))
.as("the rule 100pt long in its 192pt cell").isEqualTo(92 * 20);
.as("the rule 100pt long in its 192pt cell, Word's reach in")
.isEqualTo(92 * 20 + Math.round(DocxSemanticBackend.WORD_RULE_REACH_RIGHT_POINTS * 20));

// A rule running to the end of its width has no length of its own to tell it by.
Export filling = inACell(new SectionBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,15 @@ void aFirstColumnWrittenAsARuleKeepsItsWidth() throws Exception {

assertThat(firstColumn).as("the dash's column, whole").isCloseTo(DASH, offset(0.1));
CTInd dash = nested.getRow(0).getCell(0).getParagraphs().get(0).getCTP().getPPr().getInd();
assertThat(dash == null || !dash.isSetRight() || DocxTwips.of(dash.getRight()) <= 10)
// Its right indent is Word's reach past it (wordsRuleReach), and no more.
long reach = Math.round(DocxSemanticBackend.WORD_RULE_REACH_RIGHT_POINTS * 20);
assertThat(dash == null || !dash.isSetRight() || DocxTwips.of(dash.getRight()) <= reach + 10)
.as("and the dash across all of it: the hang its cells take moves text, not widths")
.isTrue();
// It starts at its cell's text, the hang being the cell's: its left indent is Word's reach.
assertThat(dash == null || !dash.isSetLeft() ? 0 : DocxTwips.of(dash.getLeft()))
.as("from its cell's text, Word's reach in")
.isEqualTo(Math.round(DocxSemanticBackend.WORD_RULE_REACH_LEFT_POINTS * 20));
}
}

Expand Down
Loading
Loading