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

### Public API

- **A DOCX export keeps a colour's translucency where Word can hold it, and names where it
cannot.** A translucent colour (`DocumentColor.rgba(...)`, `withOpacity(...)`) was written at full
strength, without a note, on text, a table cell's shading, a panel's fill and borders, and a
table's rules. A rule was flattened against the panel around it or white, and a header's
separator against white, also without a note.
- **Text keeps its transparency**, as Word's text fill (`w14:textFill`), with `w:color` holding
the colour as authored: Word takes the colour from the fill and LibreOffice from `w:color`,
each with the fill's transparency, so both draw the page's tint.
- A translucent body colour is the Normal style's. A run, or a list's marker, of an opaque
colour writes an opaque fill of its own, so it does not take the style's.
- The fill is the last of a run's properties, and each part holding one — the body, the
styles, the numbering, a header or footer — marks its namespace `mc:Ignorable`.
- Word saves such text into a PDF as drawing, without a text layer.
- **Where Word holds an opaque colour only** — a cell's shading, a border, a rule — the colour
is flattened against what Word paints under it. Inside a panel or cell the export shaded, that
is the shading as written; on the page, it is read from the layout: the fills drawn before the
block at its centre, a page background included, a row's own fill (which is not written) left
out. Each is named in the report as `translucency`:
- a panel's fill and borders, once per panel;
- a table cell's fill and rules, once per table;
- a rule drawn as a paragraph border;
- a text header's or footer's separator, once per band, flattened against white, since it
runs across the page.
- A panel's borders and a cell's rules are flattened against the block's own fill, which the
page draws them over.
- A wholly transparent fill writes no shading; a wholly transparent border is drawn in the
colour under it, so it keeps its room in the row.
- A chip's shading is flattened the same way where its paragraph has no shading of its own,
and named on its `inline chip` note.
- On the page, where a picture, a barcode, a gradient or a fill under a transform is under the
block, or it is composed in a table cell, white stands in.
- Drawings, page backgrounds and pictures already kept their alpha.

Across the DOCX fidelity corpus one document's bytes change: `NavySidebar`'s four sidebar
rules, white at 115/255 over the sidebar's navy (32, 44, 59), are written `858B93` instead of
white. Word renders the rule at (133, 138, 146), where the PDF draws (131, 138, 146) and Word
drew (255, 255, 255) before. The report names the four rules: 955 notes, from 951. The other 61
documents are byte-identical.

- **A DOCX export's report names a paragraph or a list item the page reads as markdown.** A
session reads markdown unless it is told not to (`markdown(false)`). It reads a paragraph, or a
list item, of plain text holding a mark of emphasis or code:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ public static DocumentColor rgb(int red, int green, int blue) {
* surface: the PDF backend through a graphics-state alpha constant on
* shape fills and strokes, text runs, lines, side borders, and table
* paint; the PPTX backend natively in DrawingML. The DOCX backend
* currently renders the colour fully opaque.</p>
* keeps it on text, as Word's text fill, and on drawings and pictures;
* where Word holds an opaque colour only — a table cell's shading, a
* border, a rule — it flattens the colour against what the page paints
* under it and names it in its export report.</p>
*
* @param red red channel from 0 to 255
* @param green green channel from 0 to 255
Expand Down
12 changes: 6 additions & 6 deletions docs/architecture/backend-capability-matrix.md

Large diffs are not rendered by default.

61 changes: 52 additions & 9 deletions docs/recipes/docx-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ creation date is real metadata.

| Document node | DOCX output |
|---|---|
| Paragraphs | Word paragraphs with alignment, font, size, colour, bold/italic/underline; inline runs preserved; a `\n` in the text is a line break (`w:br`), which Word would otherwise read as a space; a table cell's `text("a\nb")` stays one line, as the page sets it; a `bulletOffset` of spaces is an indent — the wrapped lines (`FROM_SECOND_LINE`), the first (`FIRST_LINE`) or all of them start that far in, measured in the paragraph's style as the page measures it; a prefix with letters in it is not written, but the wrapped lines still start after the spaces the page covers it with; no prefix is applied to a paragraph written over the flow, as one of an overlay's left-and-right pair, as a badge's initials, or in a header or footer; the report names a prefix's letters, and the room a prefix sets lines in by where a path that writes none leaves it out and it moves a line — on the paragraph, or in a header or footer on the zone's note. A session reads markdown unless told not to (`markdown(false)`): it sets a paragraph of plain text's emphasis marks as the style of the text they mark, and drops its marks; the export writes the text as authored, marks and all, which it does not yet write as the page sets it, and the report names it — and, where the paragraph's lines are not read, composed in a table cell whose text the page set otherwise or with no layout, says whether the page reads its marks is not measured, where the page's parser drops one. An auto-sized paragraph (`autoSize`) is written at its style's size, not the one the page fits its text to, and the report names both sizes where Word, to its half point, holds them apart, on the paragraph or, in a header or footer, on the zone's note. A body paragraph the layout moves to a new page keeps the space the layout leaves above its text there — its own top edge, the edges of the containers opening with it, and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, since Word drops a paragraph's space above at the top of a page and keeps a line's height; the rest of the space owed stays above that line, so Word breaks the page where it did, and the gap between the paragraph's lines comes off the line as it would off its space above. A paragraph in a table cell, an overlay or a list is left to its container |
| Paragraphs | Word paragraphs with alignment, font, size, colour (a translucent one with its transparency, as Word's text fill — see "Translucent colours"), bold/italic/underline; inline runs preserved; a `\n` in the text is a line break (`w:br`), which Word would otherwise read as a space; a table cell's `text("a\nb")` stays one line, as the page sets it; a `bulletOffset` of spaces is an indent — the wrapped lines (`FROM_SECOND_LINE`), the first (`FIRST_LINE`) or all of them start that far in, measured in the paragraph's style as the page measures it; a prefix with letters in it is not written, but the wrapped lines still start after the spaces the page covers it with; no prefix is applied to a paragraph written over the flow, as one of an overlay's left-and-right pair, as a badge's initials, or in a header or footer; the report names a prefix's letters, and the room a prefix sets lines in by where a path that writes none leaves it out and it moves a line — on the paragraph, or in a header or footer on the zone's note. A session reads markdown unless told not to (`markdown(false)`): it sets a paragraph of plain text's emphasis marks as the style of the text they mark, and drops its marks; the export writes the text as authored, marks and all, which it does not yet write as the page sets it, and the report names it — and, where the paragraph's lines are not read, composed in a table cell whose text the page set otherwise or with no layout, says whether the page reads its marks is not measured, where the page's parser drops one. An auto-sized paragraph (`autoSize`) is written at its style's size, not the one the page fits its text to, and the report names both sizes where Word, to its half point, holds them apart, on the paragraph or, in a header or footer, on the zone's note. A body paragraph the layout moves to a new page keeps the space the layout leaves above its text there — its own top edge, the edges of the containers opening with it, and the gap before it where the gap did not fit at the foot of the page above — as an empty line that tall before it, kept with it, since Word drops a paragraph's space above at the top of a page and keeps a line's height; the rest of the space owed stays above that line, so Word breaks the page where it did, and the gap between the paragraph's lines comes off the line as it would off its space above. A paragraph in a table cell, an overlay or a list is left to its container |
| Lists | Real Word lists: a `numbering.xml` definition per list, `w:numPr` on each item, and the authored marker as the level's text. Nesting is a list level, so Enter continues the list and Tab demotes an item. See "What a list becomes" below for the kinds that stay plain paragraphs |
| Tables | Word tables, one cell per cell. Each cell states its own padding, on all four sides, so a row is as tall as the page draws it: as `w:tcMar`, and above and below partly in its paragraphs. Word and LibreOffice give every cell of a row the largest top and bottom margin of any cell in it, so a row's cells are written with its smallest, and the rest of a cell's padding above and below is space above its first paragraph and below its last (measured: a row whose day cells were padded 5.5pt above and 10.25pt below beside a label padded 0.75pt stood 60.3pt tall in both editors, where its tallest cell came to 46). A cell opening with a table has no paragraph above it to hold its padding, and a cell in a vertical merge has its bottom edge in another row: these keep their margins, and the row's comes down no lower than the largest of them. Its padding above and below gives up the room Word makes for the table's horizontal rules — half of a rule between two rows to each, the lower row's rule where the two differ, the rule above the table and the one below it whole to their row — which the page does not (measured: a 0.75pt rule made each row 0.75pt taller). A row held at the page's height is written less its margins and those rules too — a rule and a half in the first row and the last, two in a table of one row — since both editors read a row's written height as its cells' content (measured: held less one rule, a table ruled at 0.75pt stood 0.46pt taller in its first row and 0.36pt in its last). A cell that holds nothing but an empty line — a row that is only a rule, its thickness the empty cell's font — has that line cut to the room its row leaves it, the page's row less the cell's own margins and border: the page draws the rule's borders across the line, and Word and LibreOffice keep them outside it and grow the row (measured: `CobaltRota`'s two rules under a 0.9pt border stood 0.9pt taller each). A line with letters, a picture or a paragraph border of its own keeps its height. A table that states no rule is written with the engine's default 1pt black rule, as the page draws it, not left on Word's thinner grid. Its `textAnchor` becomes `w:vAlign` and the paragraph's `w:jc`, with the engine's default — the vertical middle, on the left — where Word's is the top, so a line beside a taller neighbour sits where the page puts it and an amount column stays right-aligned. A cell with no style of its own is set in the engine's default cell face rather than the document's Normal. A cell's lines are one paragraph with line breaks; when its style's `lineSpacing(...)` is above zero and it has more than one line, they are a paragraph each, with the spacing after every line but the last, since Word has no space between the lines of one paragraph but a taller line. A column sized to its content gets a point more than the page gives it, so the editor's font substitute cannot wrap its widest cell. The width is written when the document states one or every column is fixed; otherwise Word sizes the table — see "What falls back". A table breaks across pages where the layout breaks it: every row the layout placed is kept whole (`w:cantSplit`), `repeatHeader(n)` rows repeat on each page (`w:tblHeader`) and stay with the row under them. Two tables in a row — rows included, since a row is carried as a table — are kept apart by a paragraph a tenth of a point tall, holding the rest of the gap between them: an editor joins two tables with nothing between them into one. A table or a row the layout moves to a new page keeps its own top edge there, as the page does — written as a line that tall, kept with it, since Word drops a paragraph's space above at the top of a page — while the gap between it and the block before stays at the foot of the page above, where it fits there; a gap the layout carries onto the new page, because it did not fit at the foot of the page above, is not yet held above a table (body paragraphs and spacers: see their rows). A table's margin is its indent and the space round it, and its padding on the sides holds its rows in as the page draws them: its left side is in the indent too, and both are out of the room its columns are given |
| Composed cells (`DocumentTableCell.node(...)`) | Written by the same writers that write that node anywhere else, so a cell built from an image, a list or a table carries it. A nested table is a real `w:tbl` followed by the paragraph Word requires a cell to end with — a hairline, which the paragraph written next in the cell takes over, so no empty line opens under the table, and whose mark is hidden where it is left at the cell's end holding nothing and no space, since LibreOffice lays it out — and takes the width of the column it sits in, less its own margins and padding — the column's, not the one the page gives it, because the layout reports a composed cell's content under the owner's path |
Expand Down Expand Up @@ -643,13 +643,55 @@ see which phrase lost what — down to whether its left padding is unshaded spac
chip's fill, or not in the file.

A `w:shd` fill is opaque, so a translucent chip — `inlineCode(...)` is a fifth-opacity
grey — is flattened first against what the export wrote underneath it: the paragraph's own
shading, the cell's, or the page. Written at full strength the default code chip would be
a solid slab where the page has a tint; flattened, it is the colour the PDF shows. The
chip agrees with the file it is in rather than with the page the PDF drew — a translucent
*container* fill lands opaque too, and a chip on it composites over that. And the chip
stops being translucent: shade that paragraph another colour in Word and it keeps the
tint it was flattened to. That is recorded with the rest.
grey — is flattened first against what Word paints underneath it: the paragraph's own
shading, the cell's, or else the colour the page paints under the paragraph, a page
background included. Written at full strength the default code chip would be a solid slab
where the page has a tint; flattened, it is the colour the PDF shows. The chip agrees with
the file it is in — a translucent *container* fill is flattened too, and a chip on it
composites over what was written. And the chip stops being translucent: shade that
paragraph another colour in Word and it keeps the tint it was flattened to. That is
recorded with the rest.

## Translucent colours

A colour with an alpha below full (`DocumentColor.rgba(...)`, `withOpacity(...)`) keeps it
wherever Word holds an alpha, and is flattened where it does not:

- **Text** keeps it: the run's colour is written with its transparency as Word's text fill
(`w14:textFill`, Font → Text Effects → Transparency in Word), and `w:color` keeps the
colour as authored. Word takes the colour from the text fill and LibreOffice from
`w:color`, each with the fill's transparency, so both draw the page's tint (measured in
Word 16.0 and LibreOffice). A body colour that is translucent is the Normal style's, which
both editors take the fill from; a run of another, opaque colour writes an opaque text fill
of its own, as does a list's marker, so neither takes the style's. The fill is the last of
a run's properties, as Word writes it, and each part holding one — the body, the styles,
the numbering, a header or footer — marks Word 2010's namespace as one a reader that does
not know it may skip (`mc:Ignorable`). Word sets such text as drawing when it saves the
file as a PDF, so that PDF holds no text layer for it; the Word file holds the text.
- **Drawings and pictures** keep it: a shape's fill and outline carry their alpha in
DrawingML, and a page background's too; an inline shape, an icon and a barcode are
pictures with an alpha channel.
- **A cell's shading, a border and a rule** hold an opaque colour only. A translucent panel
fill, a table cell's fill and a rule drawn as a paragraph border are flattened against
what Word paints under them, so the file shows on first opening the colour the PDF shows.
Inside a panel or a cell the export shaded, that is the shading as written: a panel
flattened at its centre is one colour wherever its content stands. On the page, it is the
colour the page paints there — the fills the layout draws before the block, composited at
its centre: a white rule at half strength over a navy sidebar is a pale navy, not white. A
page background counts; a row's own fill does not, as the export does not write it (`row
paint`). A panel's borders and a cell's rules are flattened against the block's own fill
where it has one, since the page draws them over it. A wholly transparent fill is no
shading; a wholly transparent border is drawn in the colour under it, so it keeps its room
in the row.
- **Where the page's colour is not known** — a picture, a barcode or a gradient there, a fill
drawn under a transform, content composed in a table cell, or no layout at all — white
stands in for it. A text header's or footer's separator runs across the page, over
whatever it crosses, and is flattened against white.

Each flattened colour is named in the export report as `translucency` — once for a panel,
once for a table, for each rule, and once for each separator, however many headers it is
written into; a chip's on its `inline chip` note. What it stops being is translucent:
recolour what is under it in Word and it keeps the colour it was flattened to.

## What falls back

Expand Down Expand Up @@ -856,7 +898,8 @@ 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.
It flows with the text, and a reader moves or deletes it as a line of the
document. A dashed line keeps a dash, in Word's own lengths; a translucent one is
flattened against what lies under it, since a border is opaque. Three limits:
flattened against the colour the page paints under it, since a border is opaque, and the
report names it (see "Translucent colours"). Three limits:

- A line laid over something else — a layer in a layer stack of two or more layers
or a canvas, such as a skill meter's track and the fill over it — is not a rule in
Expand Down
Loading
Loading