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

### Public API

- **A DOCX export ends and turns a drawing's outline as the page strokes it.** An outline was
written with no cap and no join, which Word 16 and LibreOffice draw with flat ends and round
corners where the page ends a stroke butt and mitres it: a rectangle's 10pt frame came out with
rounded corners, a star's points 4pt short, a triangle's apex 13pt short, and a path's or a
line's round or square caps ended flat, 5pt short at each end of a 10pt stroke. Each outline now
states the page's cap — flat, round or square — and join — mitred up to ten times the stroke's
width, as the page mitres, round or bevelled — on shapes, lines, polygons, paths, shape-container
outlines and the drawings a table's cells hold. Measured on one-drawing pages against the page's
ink, Word draws every cap and join where the page does, and LibreOffice too but for a mitred
corner sharper than 30 degrees, which it bevels; neither reads the miter limit, Word's own being
the page's. A drawn path's and a drawn line's report notes no longer name caps and joins; a line
written as a paragraph border still names the caps it cannot end in. 29 of the corpus's 62 DOCX change, each in its outlines alone
(146 flat, 60 round and 2 square caps; 148 mitred and 60 round joins); their reports go from 1340
notes to 1332, the 8 on paths in table cells that named only their caps gone, and 39 other path
notes no longer naming them.
- **A DOCX export's report names what it draws as a picture, or draws without, where it said nothing
or more than was lost.** An inline shape — a dot, an arrow, a checkbox — and an SVG icon that
states no text were drawn into pictures with no note, though Word could hold a shape in the line;
Expand Down
Binary file modified assets/readme/examples/word-export-companion.docx
Binary file not shown.
8 changes: 4 additions & 4 deletions docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

20 changes: 12 additions & 8 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -1070,15 +1070,22 @@ the outer cell's top. Word sets a panel's content below its whole top border and
its whole bottom border, where the page strokes both on the panel's edge; what
of them reaches past the panel's box is taken, or named, as "What a panel
keeps and loses" says. The body's
shapes stand above the page backgrounds, which LibreOffice stacks together with them. Each of these limits is named in the report:
shapes stand above the page backgrounds, which LibreOffice stacks together with them. An
outline ends and turns its corners as the page strokes it — a butt, round or square cap; a
mitred, round or bevelled join, mitred up to ten times the stroke's width as the page mitres.
Written with neither, Word and LibreOffice end an outline flat and round its corners: a
rectangle's 10pt frame came out with rounded corners, and a star's points 4pt short. Neither
editor reads the miter limit: Word's own is the page's, so it mitres and bevels where the page
does, and LibreOffice bevels a corner sharper than 30 degrees. What a drawing does not carry is
named in the report:

- A transform is not carried: a rotated or scaled shape is drawn upright at its size, and so
is what a turned shape container holds — named with its outline, or on its own where the
outline draws nothing.
- A drawing carries no link, no outline entry and no bookmark: a link to a drawn shape's
anchor points at none, and a page reference to it is a fixed number. A gradient fill,
unequal corners (drawn at the largest radius) and a line's dash pattern (drawn solid) are
not carried either, and a line's caps are not written: Word and LibreOffice end it flat.
not carried either.
A rule — a line or a thin bar written as a paragraph border — keeps its anchor; its note
names a lost outline entry, caps (a border ends flat) or a bar's link, a line's link being
reported as `rule link`.
Expand All @@ -1090,12 +1097,9 @@ shapes stand above the page backgrounds, which LibreOffice stacks together with

Polygons and paths — a star, a chevron, an SVG icon's layers, a portrait drawn as paths —
are drawn the same way, as custom geometry through the same points and curves, their fill
and stroke colours carried; the dash pattern and the clip round an SVG icon are not, and
the outline is written with no cap and no join, which Word and LibreOffice draw with flat
ends and round joins. A path's note names a dash, a cap that is not flat where the path has
an open end and a join that is not round where two of its segments meet, and a clip that
cuts the icon's art, parked
outside its box, is named in the report. A shape filled only with a gradient paint shows nothing the export
and stroke colours carried, and the outline's caps and joins as above; the dash pattern and
the clip round an SVG icon are not. A path's note names a gradient or a dash it loses, and a
clip that cuts the icon's art, parked outside its box, is named in the report. A shape filled only with a gradient paint shows nothing the export
carries and is **skipped**, and the report names each one. In the flow a drawn or skipped
shape still takes its room:
its placed height and margins are owed as space above what follows, and so are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.demcha.compose.document.layout.payloads.ShapeFragmentPayload;
import com.demcha.compose.document.layout.payloads.SideBorders;
import com.demcha.compose.document.style.DocumentCornerRadius;
import com.demcha.compose.document.style.DocumentLineCap;
import com.demcha.compose.document.style.DocumentLineJoin;
import com.demcha.compose.document.style.DocumentPathSegment;
import com.demcha.compose.engine.components.content.shape.Stroke;
import org.apache.poi.util.Units;
Expand All @@ -27,7 +29,8 @@
* wrote none of them, since none is text, and a CV came out as its words on a bare page. Each is
* a floating DrawingML shape at the layout's position, behind the text, in the order
* the page paints them — a rectangle, a rectangle with rounded corners, an ellipse or a line,
* filled and outlined as on the page.</p>
* filled and outlined as on the page, the outline ending and turning its corners as the page's
* does.</p>
*
* <p>A shape standing beside a paragraph's text is anchored in that paragraph and placed down from
* its top ({@link #drawingInParagraph}, or {@link #drawingInCell} for one in a table cell): a
Expand All @@ -42,6 +45,12 @@
*/
final class DocxDrawings {

/**
* The page's miter limit, PDF's default of ten times the stroke's width, in the thousandths of
* a percent DrawingML counts it in.
*/
private static final int MITER_LIMIT = 1_000_000;

private DocxDrawings() {
}

Expand Down Expand Up @@ -84,19 +93,25 @@ enum Kind {
* or a line of text laid over the flow — or {@code null}
* @param textAtTop whether the paragraph is set from the box's top-left corner, as the page
* sets a line in its box, rather than centred in the shape
* @param lineCap how its outline ends, as the page strokes it; butt where the page has none
* @param lineJoin how its outline turns a corner, as the page strokes it; mitred where the
* page has none
*/
record Shape(Kind kind, double x, double top, double width, double height, Color fill,
Color stroke, double strokeWidth, double radius, boolean flipH, int page,
List<DocumentPathSegment> path, boolean front, String picture, String text, boolean textAtTop) {
List<DocumentPathSegment> path, boolean front, String picture, String text, boolean textAtTop,
DocumentLineCap lineCap, DocumentLineJoin lineJoin) {
Shape {
path = path == null ? List.of() : List.copyOf(path);
lineCap = lineCap == null ? DocumentLineCap.BUTT : lineCap;
lineJoin = lineJoin == null ? DocumentLineJoin.MITER : lineJoin;
}

Shape(Kind kind, double x, double top, double width, double height, Color fill,
Color stroke, double strokeWidth, double radius, boolean flipH, int page,
List<DocumentPathSegment> path, boolean front, String picture) {
this(kind, x, top, width, height, fill, stroke, strokeWidth, radius, flipH, page, path, front, picture,
null, false);
null, false, null, null);
}

Shape(Kind kind, double x, double top, double width, double height, Color fill,
Expand All @@ -119,7 +134,7 @@ record Shape(Kind kind, double x, double top, double width, double height, Color
/** The same shape in front of the text rather than behind it. */
Shape inFront() {
return new Shape(kind, x, top, width, height, fill, stroke, strokeWidth, radius, flipH, page, path, true,
picture, text, textAtTop);
picture, text, textAtTop, lineCap, lineJoin);
}

/**
Expand All @@ -129,7 +144,18 @@ Shape inFront() {
*/
Shape holding(String paragraph) {
return new Shape(kind, x, top, width, height, fill, stroke, strokeWidth, radius, flipH, page, path, front,
picture, paragraph, false);
picture, paragraph, false, lineCap, lineJoin);
}

/**
* The same shape with its outline ending and turning its corners as the page strokes it.
*
* @param cap how the outline ends; {@code null} for butt
* @param join how it turns a corner; {@code null} for mitred
*/
Shape strokedWith(DocumentLineCap cap, DocumentLineJoin join) {
return new Shape(kind, x, top, width, height, fill, stroke, strokeWidth, radius, flipH, page, path, front,
picture, text, textAtTop, cap, join);
}

/**
Expand All @@ -147,7 +173,7 @@ Shape holding(String paragraph) {
*/
static Shape textBox(double x, double top, double width, double height, int page, String paragraph) {
return new Shape(Kind.RECT, x, top, width, height, null, null, 0, 0, false, page, List.of(), true,
null, paragraph, true);
null, paragraph, true, null, null);
}

/**
Expand Down Expand Up @@ -202,11 +228,12 @@ static List<Shape> of(PlacedFragment fragment, double pageHeight) {
// other diagonal flips it.
boolean flipH = (x2 - x1) * (y2 - y1) < 0;
add(shapes, visible(new Shape(Kind.LINE, Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1),
Math.abs(y2 - y1), null, colourOf(line.stroke()), widthOf(line.stroke()), 0, flipH, page)));
Math.abs(y2 - y1), null, colourOf(line.stroke()), widthOf(line.stroke()), 0, flipH, page)
.strokedWith(line.lineCap(), null)));
} else if (fragment.payload() instanceof PathFragmentPayload path) {
add(shapes, visible(new Shape(Kind.CUSTOM, fragment.x(), top, fragment.width(), fragment.height(),
path.fillColor(), colourOf(path.stroke()), widthOf(path.stroke()), 0, false, page,
path.segments())));
path.segments()).strokedWith(path.lineCap(), path.lineJoin())));
} else if (fragment.payload() instanceof PolygonFragmentPayload polygon) {
add(shapes, visible(new Shape(Kind.CUSTOM, fragment.x(), top, fragment.width(), fragment.height(),
polygon.fillColor(), colourOf(polygon.stroke()), widthOf(polygon.stroke()), 0, false, page,
Expand Down Expand Up @@ -404,19 +431,43 @@ private static String shapeGraphic(Shape shape, long cx, long cy) {
String fill = filled
? "<a:solidFill>" + colour(shape.fill()) + "</a:solidFill>"
: "<a:noFill/>";
String outline = shape.stroke() == null || shape.stroke().getAlpha() == 0 || !(shape.strokeWidth() > 0)
? "<a:ln><a:noFill/></a:ln>"
: "<a:ln w=\"" + Units.toEMU(shape.strokeWidth()) + "\"><a:solidFill>"
+ colour(shape.stroke()) + "</a:solidFill></a:ln>";
return "<a:graphic><a:graphicData uri=\"http://schemas.microsoft.com/office/word/2010/wordprocessingShape\">"
+ "<wps:wsp><wps:cNvSpPr/><wps:spPr>"
+ "<a:xfrm" + (shape.flipH() ? " flipH=\"1\"" : "") + "><a:off x=\"0\" y=\"0\"/><a:ext cx=\"" + cx
+ "\" cy=\"" + cy + "\"/></a:xfrm>"
+ geometry + fill + outline
+ geometry + fill + outline(shape)
+ "</wps:spPr>" + textOf(shape) + "</wps:wsp>"
+ "</a:graphicData></a:graphic>";
}

/**
* A shape's outline, ending and turning its corners as the page strokes it.
*
* <p>Written with neither, Word and LibreOffice end an outline flat and round its corners: a
* rectangle's 10pt frame came out with rounded corners, and a star's points 4pt short. The
* page mitres a corner up to ten times the stroke's width and bevels a sharper one, as the
* limit written here says. Neither editor reads the limit: Word's own is the page's, so it
* mitres and bevels where the page does, and LibreOffice bevels a corner sharper than 30
* degrees.</p>
*/
private static String outline(Shape shape) {
if (shape.stroke() == null || shape.stroke().getAlpha() == 0 || !(shape.strokeWidth() > 0)) {
return "<a:ln><a:noFill/></a:ln>";
}
String cap = switch (shape.lineCap()) {
case BUTT -> "flat";
case ROUND -> "rnd";
case SQUARE -> "sq";
};
String join = switch (shape.lineJoin()) {
case MITER -> "<a:miter lim=\"" + MITER_LIMIT + "\"/>";
case ROUND -> "<a:round/>";
case BEVEL -> "<a:bevel/>";
};
return "<a:ln w=\"" + Units.toEMU(shape.strokeWidth()) + "\" cap=\"" + cap + "\"><a:solidFill>"
+ colour(shape.stroke()) + "</a:solidFill>" + join + "</a:ln>";
}

/**
* What a shape holds in its text body: a paragraph centred across and down, with no inset,
* so a badge's initials stand in its middle as the page sets them; or nothing.
Expand Down
Loading
Loading