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

### Public API

- **`SlateOrange`'s sidebar stands where the page sets it in Word.** Each skill is a row with a
12.4pt icon on a line of its own, no text in it, and Word sizes that line itself. The
paragraph's mark, set in the icon cell's style at 10.5pt, reached below the icon, so each row
came out 0.2pt taller and the tenth 2.5pt low, with the sidebar under them. Where its
pictures fill the page's line, a line of pictures alone now has its mark and its pictures'
runs set at a point; a smaller picture, in a line the page sets at its font's height, keeps
the mark as it was, and so does a line holding a letter, a break, a tab, a field or a link's
text. In Word, lines more than 2pt off fall from 36 to 9, and the p90 drift from 4.99pt to
3.46. LibreOffice is unchanged.

- **`CobaltRota` stands where the page sets it in Word and LibreOffice.** Its header opens and
closes with a row that is only a rule, its thickness an empty cell's line. The page draws the
rule's borders across that line; Word and LibreOffice keep them outside it and grew each row
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ it cannot work out for itself:

| What | Where it lands |
|---|---|
| Line height | `w:spacing w:lineRule="exact"` 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 the layout did not measure — one in a composed table cell — 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. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice), and the page sets it the face's ascent below the line's top: where the two are half a point or more apart — a face with a deep descent, as Spectral's is — a paragraph's text is raised or lowered to the page's baseline by `w:position`, matched at its middle line; a list item's and a table text cell's are not yet. 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:lineRule="exact"` 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 the layout did not measure — one in a composed table cell — 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. Both editors stand the baseline of an exact line four fifths of the way down it whatever the face (measured in Word and LibreOffice), and the page sets it the face's ascent below the line's top: where the two are half a point or more apart — a face with a deep descent, as Spectral's is — a paragraph's text is raised or lowered to the page's baseline by `w:position`, matched at its middle line; a list item's and a table text cell's are not yet. 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 |
| 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. A column sized to its content (`DocumentRowColumn.auto()`) gets a point more, taken from the row's weight columns so the row keeps its width, 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. A row with no auto column, no weight column, or no stated columns (weights, an even split) is written as placed |

Expand Down
64 changes: 32 additions & 32 deletions qa/src/test/resources/docx-fidelity/word-windows-lines.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -1382,38 +1382,38 @@ cv-slate_orange 0:422684be6cf9 0.32 daniel.harper@email.com
cv-slate_orange 0:84308bea4540 0.36 linkedin
cv-slate_orange 0:a64d6af50109 0.26 austin,texas•opentorelocate
cv-slate_orange 0:32c1cf49a2fe 0.04 skills
cv-slate_orange 0:ee41e992b046 3.82 customerserviceexcellence
cv-slate_orange 0:39433b0c8c42 3.95 operations&processsupport
cv-slate_orange 0:68ac2246858d 4.20 project&calendarmanagement
cv-slate_orange 0:d2b0c62842b8 4.48 datamanagement&reporting
cv-slate_orange 0:713ccc3fc2fc 4.61 communication&stakeholders
cv-slate_orange 0:010f2ad55bed 4.86 problemsolving&adaptability
cv-slate_orange 0:02020d943fbc 4.99 sales&relationshipbuilding
cv-slate_orange 0:88cc29c2fda2 5.24 microsoftoffice&googlesuite
cv-slate_orange 0:e37d86244e99 5.37 crmsystems(salesforce,hubspot)
cv-slate_orange 0:d70e5edb4685 5.62 timemanagement&prioritization
cv-slate_orange 0:8bab53aa1bee 2.10 selectedachievements
cv-slate_orange 0:b08c44dac044 6.78 processimprovement
cv-slate_orange 0:c7966365f508 3.45 designedanewtrackingsystemthat
cv-slate_orange 0:3a5ad5d0dc8d 2.95 reducedreportingtimeby30%and
cv-slate_orange 0:74821a85e7b9 2.57 improvedaccuracy.
cv-slate_orange 0:dd9532e202a3 6.92 customersatisfaction
cv-slate_orange 0:dfc9a4fd0657 3.59 maintaineda96%customer
cv-slate_orange 0:5e04b5e4dbf9 3.21 satisfactionratingthroughrespons
cv-slate_orange 0:c119f318eaae 2.71 supportandissueresolution.
cv-slate_orange 0:365853229c97 7.18 revenuecontribution
cv-slate_orange 0:292a456730c2 3.85 consistentlyexceededmonthlysales
cv-slate_orange 0:8eb387deb3e1 3.35 goalsby15–20%throughrelationship
cv-slate_orange 0:77706bc9ed09 2.97 buildingandconsultativeservice.
cv-slate_orange 0:6cb574fa10b6 2.78 languages
cv-slate_orange 0:890744623577 3.51 spanishprofessionalworking
cv-slate_orange 0:21bb36ad0638 3.79 french
cv-slate_orange 0:fbb7b5bf4ca3 3.82 basic
cv-slate_orange 0:676dfdd56ad2 3.56 additionalinformation
cv-slate_orange 0:ef68dac1790b 6.91 availability:full-time
cv-slate_orange 0:6795aec0e50a 7.13 willingtorelocate:yes
cv-slate_orange 0:b8eeb230ca32 4.48 remotework:opento
cv-slate_orange 0:b73078ca247c 4.42 hybrid/remote
cv-slate_orange 0:ee41e992b046 0.58 customerserviceexcellence
cv-slate_orange 0:39433b0c8c42 0.59 operations&processsupport
cv-slate_orange 0:68ac2246858d 0.60 project&calendarmanagement
cv-slate_orange 0:d2b0c62842b8 0.64 datamanagement&reporting
cv-slate_orange 0:713ccc3fc2fc 0.53 communication&stakeholders
cv-slate_orange 0:010f2ad55bed 0.54 problemsolving&adaptability
cv-slate_orange 0:02020d943fbc 0.55 sales&relationshipbuilding
cv-slate_orange 0:88cc29c2fda2 0.56 microsoftoffice&googlesuite
cv-slate_orange 0:e37d86244e99 0.57 crmsystems(salesforce,hubspot)
cv-slate_orange 0:d70e5edb4685 0.58 timemanagement&prioritization
cv-slate_orange 0:8bab53aa1bee 0.04 selectedachievements
cv-slate_orange 0:b08c44dac044 4.50 processimprovement
cv-slate_orange 0:c7966365f508 1.29 designedanewtrackingsystemthat
cv-slate_orange 0:3a5ad5d0dc8d 0.79 reducedreportingtimeby30%and
cv-slate_orange 0:74821a85e7b9 0.29 improvedaccuracy.
cv-slate_orange 0:dd9532e202a3 4.52 customersatisfaction
cv-slate_orange 0:dfc9a4fd0657 1.19 maintaineda96%customer
cv-slate_orange 0:5e04b5e4dbf9 0.81 satisfactionratingthroughrespons
cv-slate_orange 0:c119f318eaae 0.31 supportandissueresolution.
cv-slate_orange 0:365853229c97 4.54 revenuecontribution
cv-slate_orange 0:292a456730c2 1.21 consistentlyexceededmonthlysales
cv-slate_orange 0:8eb387deb3e1 0.83 goalsby15–20%throughrelationship
cv-slate_orange 0:77706bc9ed09 0.33 buildingandconsultativeservice.
cv-slate_orange 0:6cb574fa10b6 0.14 languages
cv-slate_orange 0:890744623577 0.99 spanishprofessionalworking
cv-slate_orange 0:21bb36ad0638 1.15 french
cv-slate_orange 0:fbb7b5bf4ca3 1.18 basic
cv-slate_orange 0:676dfdd56ad2 0.92 additionalinformation
cv-slate_orange 0:ef68dac1790b 4.15 availability:full-time
cv-slate_orange 0:6795aec0e50a 4.13 willingtorelocate:yes
cv-slate_orange 0:b8eeb230ca32 1.48 remotework:opento
cv-slate_orange 0:b73078ca247c 1.42 hybrid/remote
cv-slate_orange 0:3c69d6bb4892 0.04 professionalprofile
cv-slate_orange 0:36eb8f626c42 0.03 adaptableandresourcefulprofessio
cv-slate_orange 0:f4e999c08086 0.00 customerservice,sales,andproject
Expand Down
2 changes: 1 addition & 1 deletion qa/src/test/resources/docx-fidelity/word-windows.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ cv-panel 1 1 79 73 0.43 0.94 0
cv-professional_sidebar 1 1 68 64 0.11 0.27 0
cv-serif_headline 1 1 103 103 0.51 1.05 0
cv-sidebar_portrait 1 1 49 49 0.08 0.27 0
cv-slate_orange 1 1 87 85 0.62 4.99 36
cv-slate_orange 1 1 87 85 0.54 3.46 9
cv-teal_pulse 1 1 57 57 0.11 0.38 0
cv-terracotta_rail 1 1 102 102 0.42 0.87 0
cv-timeline_minimal 2 2 89 86 1.44 1.57 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
private static final double TWIPS_PER_POINT = 20.0;
/** {@code w:sz} and {@code w:szCs} count half-points. */
private static final double HALF_POINTS_PER_POINT = 2.0;

/**
* How far short of the top of the page's line a picture may reach and still fill it, in
* points (see {@link #letThePicturesSetTheLine}).
*/
private static final double PICTURE_FILLS_ITS_LINE = 0.5;

/** A one-point font size, as Word writes sizes: in half points. */
private static final long ONE_POINT_IN_HALF_POINTS = 2;
private static final double POINT_TO_TWIP = 20.0;
/** The least difference between Word's baseline and the page's that is moved: one half point. */
private static final double LEAST_BASELINE_SHIFT_POINTS = 0.5;
Expand Down Expand Up @@ -396,6 +405,8 @@ public final class DocxSemanticBackend implements SemanticBackend<byte[]> {
// letters' reach (see inkOf).
private List<FontFamilyDefinition> measuredFamilies = List.of();
private FontLibrary seatFonts;
// Those fonts' line heights, for a line the layout placed none of (see styleLineHeight).
private com.demcha.compose.document.chart.ChartTextMetrics styleMetrics;
// What this export could not carry as authored. Collected whether or not anyone asked
// for it: building it costs a list, and deciding later that nobody wanted it is not
// something the writers can do halfway through.
Expand Down Expand Up @@ -660,6 +671,7 @@ private byte[] write(List<SemanticSection> sections, Path outputFile) throws Exc
wordFamilies = DocxFontTable.familiesByName(fonts);
measuredFamilies = List.copyOf(fonts);
seatFonts = null;
styleMetrics = null;
documentDefaultStyle = dominantTextStyle(whole);
currentCell = null;
currentCellWidth = Double.NaN;
Expand Down Expand Up @@ -5043,6 +5055,27 @@ private FontLibrary measuredFonts() {
return seatFonts;
}

/**
* The height the layout gives a line set in a style, in points; 0 for no style, and for a
* face the measured fonts do not hold — an export whose layout failed is written without
* one, and measuring here must not fail it in its stead.
*/
private double styleLineHeight(DocumentTextStyle style) {
if (style == null) {
return 0;
}
if (styleMetrics == null) {
styleMetrics = new com.demcha.compose.document.layout.ChartTextMetricsSupport(
new com.demcha.compose.engine.measurement.FontLibraryTextMeasurementSystem(
measuredFonts(), com.demcha.compose.engine.render.pdf.PdfFont.class));
}
try {
return styleMetrics.lineHeight(style);
} catch (RuntimeException unknownFace) {
return 0;
}
}

/**
* The foot of a shape container's content, measured up from the foot of its page, or
* {@code NaN} when it is not laid out on one page. Its bottom padding is owed below it
Expand Down Expand Up @@ -5534,9 +5567,79 @@ private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node, boolean
heldAbove = 0;
}
styleTheMark(para, markStyle);
// A paragraph the layout laid out none of — composed in a table cell, a page zone's, or
// one of an export without a layout — has its style's line.
double pageLine = pictures.pageLine() > 0 ? pictures.pageLine()
: pictures.pageLine() == 0 ? styleLineHeight(node.textStyle()) : 0;
if (pageLine > 0 && pictures.reach() >= pageLine - PICTURE_FILLS_ITS_LINE) {
letThePicturesSetTheLine(para);
}
seatInTheLine(para, node, runsBefore, lineTopAbove + heldAbove, cutToTheInk);
}

/**
* Lets a line of pictures and no text be as tall as its pictures, where Word sizes the line.
*
* <p>A line Word sizes itself — no exact or least height written, as a paragraph the layout
* laid out no line of is written — is its tallest run's height, and the paragraph's mark is
* a run: its font's depth below the baseline went under the picture. {@code SlateOrange}'s
* skills, a 12.4pt icon in a cell of its own beside each label, came out 0.2pt taller each in
* Word, its tenth 2.5pt low and the column under them with it. The mark and the picture runs
* are set at a point, whose depth is a fraction of one. A paragraph holding anything else —
* a letter, a break, a tab, a field, a link's text — keeps its sizes.</p>
*
* <p>Only where the pictures fill the page's line ({@link #PICTURE_FILLS_ITS_LINE}): there
* the line is the pictures' height on the page too. A smaller picture stands in a line the
* page makes as tall as its font, and that line is the mark's: {@code VioletGrid}'s bullet
* dots, their lines cut to the dots, set every bullet 5.8pt high.</p>
*/
private static void letThePicturesSetTheLine(XWPFParagraph para) {
CTPPr properties = para.getCTP().getPPr();
if (!holdsOnlyPictures(para)
|| properties != null && properties.isSetSpacing() && properties.getSpacing().isSetLineRule()
&& properties.getSpacing().getLineRule() != STLineSpacingRule.AUTO) {
return;
}
if (properties == null) {
properties = para.getCTP().addNewPPr();
}
org.openxmlformats.schemas.wordprocessingml.x2006.main.CTParaRPr mark =
properties.isSetRPr() ? properties.getRPr() : properties.addNewRPr();
BigInteger point = BigInteger.valueOf(ONE_POINT_IN_HALF_POINTS);
(mark.sizeOfSzArray() > 0 ? mark.getSzArray(0) : mark.addNewSz()).setVal(point);
(mark.sizeOfSzCsArray() > 0 ? mark.getSzCsArray(0) : mark.addNewSzCs()).setVal(point);
for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run : runsIn(para)) {
CTRPr size = run.isSetRPr() ? run.getRPr() : run.addNewRPr();
(size.sizeOfSzArray() > 0 ? size.getSzArray(0) : size.addNewSz()).setVal(point);
(size.sizeOfSzCsArray() > 0 ? size.getSzCsArray(0) : size.addNewSzCs()).setVal(point);
}
}

/**
* Whether a paragraph's runs, a link's included, hold pictures and nothing else: no
* letter, break, tab, symbol or field. An internal link's runs are not among the
* paragraph's own, so its text is not in {@link XWPFParagraph#getText()}.
*/
private static boolean holdsOnlyPictures(XWPFParagraph para) {
if (para.getCTP().sizeOfFldSimpleArray() > 0) {
return false;
}
boolean picture = false;
for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run : runsIn(para)) {
if (run.sizeOfBrArray() > 0 || run.sizeOfTabArray() > 0 || run.sizeOfSymArray() > 0
|| run.sizeOfFldCharArray() > 0 || run.sizeOfInstrTextArray() > 0) {
return false;
}
for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText text : run.getTArray()) {
if (text.getStringValue() != null && !text.getStringValue().isEmpty()) {
return false;
}
}
picture |= run.sizeOfDrawingArray() > 0 || run.sizeOfPictArray() > 0;
}
return picture;
}

/** Whether a paragraph laid out a line holding text, which its seat is read from. */
private boolean holdsText(ParagraphNode node) {
return layout.lines(node).stream().anyMatch(line -> line.spans().stream().anyMatch(
Expand Down
Loading
Loading