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

### Public API

- **A DOCX export holds a shape's row less its borders as wide as Word draws them, and a table's
row between two others less the rules it holds.**
- **A shape's borders come off as wide as Word draws them.** A border is written in eighths of a
point, and a row counts it two and a half twips an eighth, rounded down; a shape container
held whole had the lighter of its borders taken off its row's height, and both off the room
it leaves its label, at the width the page strokes them. A 20.3pt chip outlined at 0.7pt,
which Word draws 0.75pt wide, stood a twip taller in Word and LibreOffice on Windows, and six
of them in a table's rows six twips lower below them; one outlined at 0.9pt, drawn 0.875pt
wide, was held a twip short, the row round it holding it (measured). A panel's border is now
written from two to ninety-six eighths of a point, as a table cell's rule is, where a hairline
thinner than 0.19pt was written at one eighth and a border past 12pt above the range.
- **A row between two others is held less the rules it holds:** the rest of its own rule above
it and half the rule of the cell under it, which is one rule where the two are ruled alike, as
it was held. A row is held less the most any one of its cells takes above and below its
content, its margins and those rules together. Ruled unlike the next, a row ruled at 0.5pt over one ruled at 1.5pt stood half a point
taller in Word and all under it half a point lower; held so, the rows of five, ruled either
way round in the middle, stand where the page sets them in Word (measured). LibreOffice makes
room at the edge between two rows for the heavier of their rules, where Word makes it for the
lower row's: under a row ruled heavier than it, a row and all under it stand the difference
lower there, 1pt for these, on Windows and in LibreOffice 24.2.

No document of the fidelity corpus moves: its DOCX are byte-identical.

- **A DOCX export seats the label of a chip composed in a table cell where the page sets it.**
- **A label taller than its chip is seated from the chip's top.** The page starts a line taller
than a shape's outline at the outline's top and draws it past it below; the export seated it
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions docs/recipes/docx-export.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -252,17 +252,46 @@ void aLabelSetFromTheChipsTopIsLeftAsItWas() throws Exception {
void anOutlinedChipHoldsItsRowLessBothBorders() throws Exception {
// Word and LibreOffice draw a cell's top and bottom borders outside the height its row
// holds: CobaltRota's outlined chips, 9.2pt outlines with a 1.125pt border, stood 10.3pt
// tall in Word, and each row holding one 1.1pt taller than the page's.
// tall in Word, and each row holding one 1.1pt taller than the page's. A 1.125pt border is
// written at nine eighths, and a row counts it 22 twips.
XWPFTableCell outlined = chipCell(com.demcha.compose.document.style.DocumentStroke.of(
com.demcha.compose.document.style.DocumentColor.rgb(20, 160, 70), 1.125));
XWPFTableCell filled = chipCell(null);

assertThat(heightOf(outlined.getTableRow().getTable())).as("9.2pt less two 1.125pt borders")
.isEqualTo(139);
assertThat(heightOf(outlined.getTableRow().getTable())).as("184 twips less two borders of 22")
.isEqualTo(140);
assertThat(heightOf(filled.getTableRow().getTable())).as("a chip with no border keeps its outline's")
.isEqualTo(184);
}

@Test
void anOutlinedChipHoldsItsRowLessItsBordersAsWordDrawsThem() throws Exception {
// A border is written in eighths of a point, and a row counts it two and a half twips an
// eighth, rounded down: a 0.7pt outline, written at six eighths, takes 15 twips, and a
// 0.9pt one, at seven, 17. Taken off as the page strokes them, 14 and 18, a 20.3pt chip
// outlined at 0.7pt stood a twip taller in Word and LibreOffice, and six of them in a
// table's rows six twips lower below them.
XWPFTableCell thin = aroundTheChip(stroke(0.7), com.demcha.compose.document.node.LayerAlign.CENTER, 8.2, 20.3)
.getTables().get(0).getRow(0).getCell(0);
XWPFTableCell thick = aroundTheChip(stroke(0.9), com.demcha.compose.document.node.LayerAlign.CENTER, 8.2, 20.3)
.getTables().get(0).getRow(0).getCell(0);

assertThat(heightOf(thin.getTableRow().getTable())).as("406 twips less two borders of 15").isEqualTo(376);
assertThat(heightOf(thick.getTableRow().getTable())).as("406 twips less two borders of 17").isEqualTo(372);
}

@Test
void anOutlinedChipsLabelHasTheRoomItsBordersLeaveAsWordDrawsThem() throws Exception {
// An 11.8pt chip outlined at 0.7pt leaves Lato's line 236 twips less two borders of 15,
// 206, short of the 212 that would seat it. Counted at 14 each, the line and the row it is
// held at were written 208 and 207, and stood the chip that much taller in Word.
XWPFTableCell chip = beside(labelledChip("09:00-16:00", com.demcha.compose.font.FontName.LATO, 8.2, 11.8,
DocumentInsets.zero(), stroke(0.7), null));

assertThat(lineOf(chip)).isEqualTo(206 / 20.0);
assertThat(heightOf(chip.getTableRow().getTable())).isEqualTo(206);
}

@Test
void aChipComposedInACellIsHeldAndItsLabelCutRoundedDownToTheTwip() throws Exception {
// CobaltRota's stacked chips are 183.69 twips: held at 184 and their labels cut to 184,
Expand Down Expand Up @@ -295,18 +324,32 @@ void theCellAnOutlinedChipIsAllOfGivesBackWhatItsLettersKeepPastItsOutline() thr

@Test
void anOddNumberOfTwipsIsGivenBackHalfRoundedDownAboveAndTheRestBelow() throws Exception {
// A 1.05pt outline is written at eight eighths, 20 twips: the chip's row holds 163 less 20,
// 143, and its letters keep its label at 172 — 29 twips, 14 above and 15 below.
// A 1.05pt outline is written at eight eighths, 20 twips: a 9.195pt chip's row holds 183 less
// two of them, 143, and its letters keep its label at 172 — 29 twips, 14 above and 15 below.
com.demcha.compose.document.style.DocumentStroke outline = com.demcha.compose.document.style.DocumentStroke.of(
com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.05);
XWPFTableCell around = aloneInARow(outline, DocumentInsets.of(4));
XWPFTableCell around = aloneInARow(outline, DocumentInsets.of(4), 9.195);
XWPFTableCell chip = around.getTables().get(0).getRow(0).getCell(0);

assertThat(DocxTwips.of(chip.getParagraphs().get(0).getCTP().getPPr().getSpacing().getLine())
- heightOf(chip.getTableRow().getTable())).as("the premise: an odd number of twips").isEqualTo(29);
assertThat(verticalMarginsOf(around)).containsExactly(60L - 14, 60L - 15);
}

@Test
void aCellWhoseMarginsHoldJustWhatItsChipGrowsByGivesItAllBack() throws Exception {
// The chip of the odd case above, its cell's margins 14 twips above and 15 below — its
// padding less the 1pt rule: all Word grows the chip by, given back, the label seated on
// the chip's middle. Its borders counted at the 1.05pt the page strokes rather than the 20
// twips Word draws, the chip would grow by 30, more than the margins above hold.
XWPFTableCell around = aloneInARow(com.demcha.compose.document.style.DocumentStroke.of(
com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.05),
new DocumentInsets(1.7, 4, 1.75, 4), 9.195);

assertThat(verticalMarginsOf(around)).containsExactly(0L, 0L);
assertThat(lineOf(around.getTables().get(0).getRow(0).getCell(0))).isEqualTo(172 / 20.0);
}

@Test
void aCellGivesBackNoMoreThanItsMarginsHold() throws Exception {
// 1.25pt of padding less the 1pt rule leaves 5 twips above and below, less than the 10 each.
Expand Down Expand Up @@ -540,10 +583,21 @@ private static long positionOf(XWPFTableCell chip) {
private static final com.demcha.compose.document.style.DocumentStroke OUTLINE =
com.demcha.compose.document.style.DocumentStroke.of(com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), 1.1);

/** An outline {@code width} wide. */
private static com.demcha.compose.document.style.DocumentStroke stroke(double width) {
return com.demcha.compose.document.style.DocumentStroke.of(com.demcha.compose.document.style.DocumentColor.rgb(26, 86, 148), width);
}

/** The cell of a one-column table a 9.2pt chip is all of. */
private static XWPFTableCell aloneInARow(com.demcha.compose.document.style.DocumentStroke stroke,
DocumentInsets padding) throws Exception {
DocumentNode node = chip(stroke, 9.2);
return aloneInARow(stroke, padding, 9.2);
}

/** The cell of a one-column table a chip {@code height} tall is all of. */
private static XWPFTableCell aloneInARow(com.demcha.compose.document.style.DocumentStroke stroke,
DocumentInsets padding, double height) throws Exception {
DocumentNode node = chip(stroke, height);
return onlyTableCell(page -> page.addTable(t -> t
.columns(DocumentTableColumn.fixed(100))
.defaultCellStyle(com.demcha.compose.document.table.DocumentTableStyle.builder().padding(padding).build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ void aUniformStrokeDrawsAllFourSides() throws Exception {
}
}

@Test
void aPanelsBorderIsWrittenFromWordsThinnestToItsThickest() throws Exception {
// A line border's w:sz runs from two eighths of a point to ninety-six, as a table cell's
// rule is written: a row counts the border at the width written.
for (double[] stroke : new double[][] {{0.1, 2}, {14, 96}}) {
try (XWPFDocument document = export(page -> page.addSection("Card", card -> card
.stroke(DocumentStroke.of(ACCENT, stroke[0]))
.addParagraph(p -> p.text("Outlined"))))) {
var edges = onlyCell(document).getCTTc().getTcPr().getTcBorders();

assertThat(edges.getTop().getSz().intValue()).as("a %spt border", stroke[0]).isEqualTo((int) stroke[1]);
}
}
}

@Test
void anUnpaintedSectionIsNotATable() throws Exception {
try (XWPFDocument document = export(page -> page.addSection("Plain", plain -> plain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,112 @@ void aHeaderRuledHeavierThanItsRowsGivesHalfTheirRuleNotItsOwn() throws Exceptio
}
}

@Test
void aRowRuledUnlikeTheNextIsHeldAtItsContentsHeight() throws Exception {
// A row between two others gives the rest of its own rule above it and half the next row's
// below. Measured, a row ruled at 0.5pt over one ruled at 1.5pt, held less its own rule
// alone, stood half a point taller in Word, and all under it half a point lower; the row
// ruled at 1.5pt was held half a point short, its content holding it. Either way round, in
// the middle of five rows.
for (double[] rules : new double[][] {{0.5, 1.5}, {1.5, 0.5}}) {
try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> {
t.columns(DocumentTableColumn.auto())
.defaultCellStyle(DocumentTableStyle.builder()
.padding(DocumentInsets.of(7))
.stroke(DocumentStroke.of(DocumentColor.BLACK, rules[0]))
.build())
.rowStyle(2, DocumentTableStyle.builder().stroke(DocumentStroke.of(DocumentColor.BLACK, rules[1])).build());
for (int i = 0; i < 5; i++) {
t.row("Row");
}
}))) {
var table = document.getTables().get(0);
long content = line(table.getRow(0));

for (int i = 0; i < 5; i++) {
assertThat((long) table.getRow(i).getHeight()).as("row %s, ruled %s with %s in the middle", i,
rules[0], rules[1]).isEqualTo(content);
}
}
}
}

@Test
void aTableRuledColumnByColumnHoldsEveryRowAsOneRuledAlike() throws Exception {
// Each cell gives half the rule of the cell under it, not of the heaviest in the next row:
// a column ruled at 1.5pt beside one at 0.5pt, alike from row to row, holds its rows where
// a table ruled alike does.
try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> {
t.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(DocumentTableStyle.builder()
.padding(DocumentInsets.of(7))
.stroke(DocumentStroke.of(DocumentColor.BLACK, 0.5))
.build())
.columnStyle(1, DocumentTableStyle.builder().stroke(DocumentStroke.of(DocumentColor.BLACK, 1.5)).build());
for (int i = 0; i < 4; i++) {
t.row("Row", "Row");
}
}))) {
var table = document.getTables().get(0);
long content = line(table.getRow(0));

for (int i = 0; i < 4; i++) {
assertThat((long) table.getRow(i).getHeight()).as("row %s", i).isEqualTo(content);
}
}
}

@Test
void aCellAfterASpanGivesHalfTheRuleOfTheCellUnderIt() throws Exception {
// The second row's last cell starts the third column of the grid, past a cell spanning two:
// the cell under it is the third column's, ruled at 0.5pt, not the second's at 1.5pt. The
// spanning cell takes the rule under its first column, at 0.5pt, as its padding gives up
// half of it (clearOfTheRules).
try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(DocumentTableStyle.builder()
.padding(DocumentInsets.of(7))
.stroke(DocumentStroke.of(DocumentColor.BLACK, 0.5))
.build())
.columnStyle(1, DocumentTableStyle.builder().stroke(DocumentStroke.of(DocumentColor.BLACK, 1.5)).build())
.row("Row", "Row", "Row")
.rowCells(com.demcha.compose.document.table.DocumentTableCell.text("Row").colSpan(2),
com.demcha.compose.document.table.DocumentTableCell.text("Row"))
.row("Row", "Row", "Row")
.row("Row", "Row", "Row")))) {
XWPFTableRow spanned = document.getTables().get(0).getRow(1);

assertThat((long) spanned.getHeight()).isEqualTo(line(spanned));
}
}

@Test
void aRowRuledCellByCellIsHeldLessWhatItsMostDemandingCellTakes() throws Exception {
// Cells ruled 1.5pt and 0.5pt over cells ruled the other way round: each gives 20 twips of
// rules and keeps 250 of margins, its other 10 of padding moved into its paragraph. The
// row is held less 270, the most any one cell takes, not the 280 the heavier cell's rule
// counts and the 10 more the lighter one holds beyond its own, taken from different cells.
try (XWPFDocument document = DocxExports.withLayout(400, 600, 20, page -> page.addTable(t -> t
.columns(DocumentTableColumn.auto(), DocumentTableColumn.auto())
.defaultCellStyle(DocumentTableStyle.builder()
.padding(DocumentInsets.of(7))
.stroke(DocumentStroke.of(DocumentColor.BLACK, 0.5))
.build())
.row("Row", "Row")
.rowCells(ruled("Row", 1.5), ruled("Row", 0.5))
.rowCells(ruled("Row", 0.5), ruled("Row", 1.5))
.row("Row", "Row")))) {
XWPFTableRow row = document.getTables().get(0).getRow(1);

assertThat((long) row.getHeight()).as("its line and the padding in its paragraphs").isEqualTo(line(row) + 10);
}
}

private static com.demcha.compose.document.table.DocumentTableCell ruled(String text, double rule) {
return com.demcha.compose.document.table.DocumentTableCell.text(text)
.withStyle(DocumentTableStyle.builder().stroke(DocumentStroke.of(DocumentColor.BLACK, rule)).build());
}

@Test
void aRowItsRulesFillIsStillHeldAtATwip() throws Exception {
// A one-row table no taller than its two 1pt rules: the height left for its content is
Expand Down
Loading