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

### Public API

- **A DOCX export's lines holding a picture within their text grow with it.**
- A line holding a picture set in it, an icon or a drawn bullet beside its words, was written at
an exact height, which Word keeps whatever its text holds.
- Word lowers such a picture by its run's position, in a least line too; LibreOffice stands it a
tenth of a point above the baseline, whatever the position; and a picture run's own size sets
none of the line (measured in Word and LibreOffice 24.2: a 9pt icon lowered 1.5pt beside 8pt
Lato left a 9.95pt least line as tall in Word, and grew it to 10.75pt in LibreOffice).
- A line whose pictures stay within its text's own line where either editor stands them — no
higher than its text, its faces' line gap included, no lower than its text's descent — is now
written at a least height where its text alone would be, and stands its text there. A least
line is given no position to seat its text, which Word does not move it by. One whose
picture rises past its text in either editor, as `CharcoalGold`'s contact icons do in
LibreOffice, is written as before, and so are pictures in a paragraph of broken lines or among
spaces alone, a rating's dots.
- The corpus writes 92 more paragraphs at a least height, in seven documents. In Word, two
points more on `Normal` leaves 1005 of the 1832 paragraphs it grows on lines shorter than 1.1
times their letters, from 1041, and 14 documents pass, from 13.
- Of the seven documents' lines, 16 stand nearer the page in LibreOffice 24.2 and 9 further, of
880; in Word 9 nearer and 55 further, of 882. The step from a line to the next differs from the
page's by more than 0.2pt for 34 of 489 steps in LibreOffice, from 36, and 29 of 472 in Word,
from 31.
- Where such a line is taller than its text, the gap between lines written into it, and the
space above it does not take the difference, its text stands lower than as an exact line:
`TealPulse`'s duties 0.24pt lower in Word, inside the quarter point a least line is held to
there, and its skills 0.3pt lower in LibreOffice. `ConsultingInvoice`'s line items, whose
description now seats its row's lift differently, stand one step of Word's 0.12pt line grid
lower there and where they stood in LibreOffice.

- **A DOCX export's table text cells grow with their text where they are as tall as their faces' own.**
- A table's text cell was written at an exact line, which Word keeps whatever its text holds, and
its row's margins, its lines' heights or its text's position moved it onto the page's baseline.
Expand Down Expand Up @@ -35,7 +63,8 @@ follow semantic versioning; release dates are ISO 8601.
the faces' own tables: the tallest run's reach above the baseline and the deepest run's below,
the paragraph's mark among them. It is so where the runs are set in faces the document ships,
all the same depth below the baseline, left to right, none raised or a script, no picture in
the line, and the paragraph is seated on its baseline, not a Word list's item.
the line past its text (see above), and the paragraph is seated on its baseline, not a Word
list's item.
- Word and LibreOffice keep a least line's spare above its text and stand its baseline the
runs' descent above its foot (measured on the templates' faces, Word and LibreOffice 24.2, to
0.15pt). That is where the page sets it, so such a line needs no position to stand there; it
Expand Down
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 @@ -27,9 +27,18 @@
*
* <p>A text's position does not move it in a least line: Word, raising every run of a line, keeps
* the text on the line's foot until it passes the line's top, and LibreOffice moves it and grows
* the line. So only a paragraph whose runs hold no position, no picture set in the line and no raise
* of their own is read — a shape anchored to it takes no room in the line, and is passed over; the
* rest, and one set in a face the document does not ship, have no reach.</p>
* the line. So only a paragraph whose runs of text hold no position and no raise of their own is
* read — a shape anchored to it takes no room in the line, and is passed over; the rest, and one
* set in a face the document does not ship, have no reach.</p>
*
* <p>A picture set in the line counts by where each editor stands it: Word lowers it by its run's
* position, in a least line too, and LibreOffice stands it {@link #PICTURE_RISE} above the
* baseline, whatever the position; a picture run's own face sets none of the line. A least line's
* height is its runs' and pictures' reach, the tallest above the baseline and the deepest below
* (measured: a 9pt picture lowered 1.5pt beside 8pt Lato left a 9.95pt least line as tall in Word,
* and grew it to 10.75pt in LibreOffice). Only a line whose pictures stay within its text's own line
* in both places — no higher than its text, its faces' line gap included, no lower than its text's
* descent — stands its text where it would without them (see {@link Reach#holdsItsPictures}).</p>
*
* @author Artem Demchyshyn
*/
Expand Down Expand Up @@ -57,6 +66,18 @@ final class DocxGrownLines {
/** Half points in a point, as Word writes sizes. */
private static final double HALF_POINTS_PER_POINT = 2;
private static final double POINT_TO_TWIP = 20;
/** EMUs in a point, as a drawing's extent is written. */
private static final double EMU_PER_POINT = 12700;
/**
* How far above the baseline LibreOffice stands a picture set in a line, its run's position
* aside, in points (measured in LibreOffice 24.2: a tenth of a point beside 8pt and 10pt Lato,
* for pictures 6 to 11pt tall and lowered by 0 to 3pt). Word lowers it by the position. The
* export's own reckoning of a picture's reach stands LibreOffice's on the baseline
* (DocxSemanticBackend.PictureReach); the tenth counts here, in whether a least line holds it.
*/
static final double PICTURE_RISE = 0.1;
/** What a run holding pictures set in the line may hold besides them. */
private static final Set<String> PICTURE_RUN_PARTS = Set.of("rPr", "drawing");

private final DocxFontTable.ShippedLines faces;
private final DocumentTextStyle defaults;
Expand All @@ -76,8 +97,12 @@ final class DocxGrownLines {
* line, in points.
*
* @param paragraph the written paragraph
* @return the reach, or null where one of its runs is not read: a picture, a raise, a face the
* document does not ship, a run Word sets right to left, or a marker Word draws itself
* @return the reach, its pictures' among it, or null where one of its runs is not read: a raise,
* a face the document does not ship, a run Word sets right to left, a marker Word draws
* itself, a drawing anchored beside pictures or holding none, a picture with no extent
* written or whose run's position is not a number, or pictures in a
* paragraph whose lines are broken, one of which may hold a picture alone, or that holds
* no letter beside them
*/
Reach reachOf(CTP paragraph) {
CTPPr properties = paragraph.getPPr();
Expand All @@ -92,6 +117,10 @@ Reach reachOf(CTP paragraph) {
return null;
}
boolean anyRun = false;
boolean broken = false;
boolean lettered = false;
double pictureTop = 0;
double pictureFoot = Double.NEGATIVE_INFINITY;
try (XmlCursor cursor = paragraph.newCursor()) {
cursor.selectPath("declare namespace w='http://schemas.openxmlformats.org/wordprocessingml/2006/main' "
+ "./w:r | ./w:hyperlink/w:r");
Expand All @@ -106,17 +135,82 @@ Reach reachOf(CTP paragraph) {
// least line as tall, in Word and in LibreOffice).
continue;
}
if (text.sizeOfDrawingArray() > 0 && holdsOnly(text, PICTURE_RUN_PARTS)) {
// Its face sets none of the line either (measured: a 24pt picture run left an
// 8pt least line as tall, in both editors).
double[] pictures = picturesIn(text);
if (pictures == null) {
return null;
}
pictureTop = Math.max(pictureTop, pictures[0]);
pictureFoot = Math.max(pictureFoot, pictures[1]);
continue;
}
if (!holdsOnly(text, RUN_PARTS)) {
return null;
}
broken |= text.sizeOfBrArray() > 0;
for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText letter : text.getTList()) {
// A no-break space is a space too.
lettered |= letter.getStringValue() != null && letter.getStringValue().codePoints()
.anyMatch(c -> !Character.isWhitespace(c) && !Character.isSpaceChar(c));
}
reach = reachOf(text.isSetRPr() ? text.getRPr() : null, reach);
if (reach == null) {
return null;
}
anyRun = true;
}
}
return anyRun ? reach : null;
// Pictures among spaces alone, a rating's dots, LibreOffice sets on a taller line (measured:
// 6.4pt dots between runs of spaces in 8pt Barlow, written at a least 9.7pt, stood the
// languages under them 0.56pt low there, and a point lower for each row of dots more).
boolean pictured = pictureFoot > Double.NEGATIVE_INFINITY;
// A line wrapped where the page wraps it may leave a picture alone on a line too; the
// corpus has none, and a break is where one is known.
if (!anyRun || pictured && (broken || !lettered)) {
return null;
}
return new Reach(reach.above(), reach.below(), reach.shallowest(), pictureTop, pictureFoot);
}

/**
* How high above the baseline a run's pictures set in the line reach, the higher of Word's and
* LibreOffice's places of them, and how far below it Word lowers them, in points; null where a
* drawing in it is not a picture of a written size set in the line.
*/
private static double[] picturesIn(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run) {
double lowered = 0;
if (run.isSetRPr() && run.getRPr().sizeOfPositionArray() > 0) {
if (!(run.getRPr().getPositionArray(0).getVal() instanceof Number position)) {
return null;
}
lowered = -position.doubleValue() / HALF_POINTS_PER_POINT;
}
double top = 0;
for (org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline picture
: inlinesIn(run)) {
if (picture == null || picture.getExtent() == null) {
return null;
}
double height = picture.getExtent().getCy() / EMU_PER_POINT;
top = Math.max(top, Math.max(height - lowered, height + PICTURE_RISE));
}
return new double[]{top, lowered};
}

/** A run's drawings set in its line, one null for a drawing holding anything else. */
private static java.util.List<org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline> inlinesIn(
org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR run) {
java.util.List<org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline> inlines =
new java.util.ArrayList<>();
for (org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing drawing : run.getDrawingList()) {
if (drawing.sizeOfAnchorArray() > 0 || drawing.sizeOfInlineArray() == 0) {
inlines.add(null);
}
inlines.addAll(drawing.getInlineList());
}
return inlines;
}

/**
Expand All @@ -131,7 +225,8 @@ Reach reachOf(CTP paragraph) {
*
* @param paragraph the written paragraph
* @return whether its runs are read ({@link #reachOf}), reach no taller than its line, to
* {@link #EPSILON}, and share one descent
* {@link #EPSILON}, share one descent, and hold its pictures within them
* ({@link Reach#holdsItsPictures})
*/
boolean mayGrow(CTP paragraph) {
CTPPr properties = paragraph.getPPr();
Expand All @@ -140,8 +235,9 @@ boolean mayGrow(CTP paragraph) {
return false;
}
Reach reach = reachOf(paragraph);
return reach != null && twips.doubleValue() / POINT_TO_TWIP + EPSILON >= reach.height()
&& reach.below() - reach.shallowest() <= EPSILON;
double line = twips.doubleValue() / POINT_TO_TWIP;
return reach != null && line + EPSILON >= reach.height()
&& reach.below() - reach.shallowest() <= EPSILON && reach.holdsItsPictures();
}

/**
Expand All @@ -150,7 +246,8 @@ boolean mayGrow(CTP paragraph) {
*
* @param paragraph the written paragraph
* @return the line, or null where its height is the editor's own, or a least line's runs are
* not read ({@link #reachOf})
* not read ({@link #reachOf}) or its pictures reach past its text, where the two editors
* stand them apart ({@link Reach#holdsItsPictures})
*/
Line lineOf(CTP paragraph) {
CTPPr properties = paragraph.getPPr();
Expand All @@ -166,7 +263,11 @@ Line lineOf(CTP paragraph) {
return null;
}
Reach reach = reachOf(paragraph);
if (reach == null) {
// A picture past the text is set elsewhere by each editor; and a line holding pictures
// written shorter than its text's own is one grown to its pictures (see
// DocxSemanticBackend#makeRoomForPictures), not read here.
if (reach == null || reach.holdsPictures() && !(written + EPSILON >= reach.height()
&& reach.holdsItsPictures())) {
return null;
}
double height = Math.max(written, reach.height());
Expand All @@ -191,7 +292,7 @@ private Reach reachOf(XmlObject runProperties, Reach so) {
return null;
}
return new Reach(Math.max(so.above(), line.ascent() * size), Math.max(so.below(), line.descent() * size),
Math.min(so.shallowest(), line.descent() * size));
Math.min(so.shallowest(), line.descent() * size), so.pictureTop(), so.pictureFoot());
}

/**
Expand Down Expand Up @@ -234,19 +335,39 @@ private static boolean holdsOnly(XmlObject element, Set<String> parts) {
}

/**
* How far a line's text reaches above and below its baseline, in points.
* How far a line's text and the pictures set in it reach above and below its baseline, in
* points.
*
* @param above how far above, its faces' line gap included
* @param below how far below: its deepest run's descent
* @param shallowest its shallowest run's descent
* @param above how far its text reaches above, its faces' line gap included
* @param below how far its text reaches below: its deepest run's descent
* @param shallowest its shallowest run's descent
* @param pictureTop how far above its pictures reach, the higher of where Word and LibreOffice
* stand them; 0 for none
* @param pictureFoot how far below Word lowers its pictures; negative infinity for none
*/
record Reach(double above, double below, double shallowest) {
static final Reach NONE = new Reach(0, 0, Double.POSITIVE_INFINITY);
record Reach(double above, double below, double shallowest, double pictureTop, double pictureFoot) {
static final Reach NONE = new Reach(0, 0, Double.POSITIVE_INFINITY, 0, Double.NEGATIVE_INFINITY);

/** The line's height. */
/** The text's line's height. */
double height() {
return above + below;
}

/**
* Whether its pictures stay within its text's own line in both editors: none reaches above
* its text, its faces' line gap included, nor below its shallowest run's descent, so that
* neither editor grows a line as tall as its text's for them nor stands its text elsewhere.
* Read against the text's own line, not the line written: a line written taller for its
* pictures, {@code makeRoomForPictures}' in DocxSemanticBackend, would hold them too.
*/
boolean holdsItsPictures() {
return pictureTop <= above + EPSILON && pictureFoot <= shallowest + EPSILON;
}

/** Whether its line holds a picture set in it. */
boolean holdsPictures() {
return pictureFoot > Double.NEGATIVE_INFINITY;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9529,7 +9529,8 @@ private double takeFromTheLineAbove(double points, ParagraphNode pulled) {
? writtenTwips(spacing.getLine()) : null;
java.util.Optional<com.demcha.compose.document.layout.payloads.ParagraphLine> laid = layout.firstLine(above);
// The letters' reach off their own baseline, before any seat: the position written on
// the runs below carries the seat, as far as it was written.
// the runs below carries the seat, as far as it was written. A line holding a picture has
// none (DocxInk), and is not shortened: the picture would be cut where the letters are not.
double[] ink = laid.isEmpty() ? null : DocxInk.of(laid.get(), measuredFonts());
if (line == null || ink == null || spacing.isSetAfter()) {
return 0;
Expand Down Expand Up @@ -13160,8 +13161,10 @@ private double shiftToThePagesBaseline(XWPFParagraph para,
}
// The position is moved as before where the whole shift called for it, whatever the space
// above took off it: a line 0.6pt low with 0.2pt above it is left a tenth off by its
// position, not four tenths.
boolean moved = Math.abs(shift) >= LEAST_BASELINE_SHIFT_POINTS || cutToFit;
// position, not four tenths. Not a least line's: Word does not move its text by a
// position, and LibreOffice moves it and grows the line (DocxGrownLines).
boolean least = para.getCTP().getPPr().getSpacing().getLineRule() == STLineSpacingRule.AT_LEAST;
boolean moved = !least && (Math.abs(shift) >= LEAST_BASELINE_SHIFT_POINTS || cutToFit);
if (ownsItsLine && !cutToFit) {
// Under half a point the position moves nothing, so the cell's padding may.
shift -= raiseIntoTheSpaceAbove(para, staysOnItsPage, para.getCTP().getPPr().getSpacing(), shift, !moved);
Expand Down Expand Up @@ -13608,7 +13611,10 @@ private static void setTextBrokenAtLines(XWPFRun run, String text) {
/**
* Lets a line hold the pictures in it (see {@link PictureReach}).
*
* <p>A paragraph whose pictures stay within its text keeps its exact height. One holding a
* <p>A paragraph whose pictures stay within its text keeps its exact height here; it is
* written at a least height where they stay within it in both editors and Word then stands
* its text within a quarter point of the page's ({@link #letTheLinesGrow},
* {@link DocxGrownLines.Reach#holdsItsPictures}). One holding a
* picture that rises above the text is written with its lines <em>at least</em> the
* height the picture reaches where Word puts it instead: the editor then grows the line to
* the picture rather
Expand Down
Loading
Loading