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

### Public API

- **A DOCX export's Word list items grow with their text where their face holds their marker.**
- A Word list's item, whose marker Word draws, was written at an exact height, which Word keeps
whatever its text holds.
- Word draws the marker as a run of the item's first line, in its level's face and size over its
paragraph mark's, and LibreOffice does too (measured in Word and LibreOffice 24.2: a bullet, a
white bullet and a small square in 8pt Lato, a bullet and an en dash in 9pt Fira Sans Condensed,
left exact and least lines as tall, with a space or a tab after it, its face its level's or its
mark's; a bullet at twice the size grew a least line by its height). A marker whose face holds no glyph for it is set in another face in each editor: Word
set an arrowhead beside 8pt Lato in Segoe UI Symbol, its least line 0.72pt taller.
- An item is now read with its marker, by the rule a paragraph's line grows by, where its level
writes letters its face holds, not a number or a picture. An item whose marker its face holds
no glyph for, or whose level is overridden or defined by a numbering style, stays exact. A
least item needs none of the lowering the space above gave an exact one Word set half a point
or more high, as `MidnightNavy`'s and `NavySidebar`'s duties were.
- The corpus writes 96 more list items at a least height, in eight documents; the 44 Word list
items still exact are set tighter than their faces' own line. In Word, two points more on
`Normal` leaves 923 of the 1832 paragraphs it grows on lines shorter than 1.1 times their
letters, from 1005, and 15 documents pass, from 14.
- Of the eight documents' lines, 53 stand nearer the page in Word and 26 further, of 682, and 53
nearer and 23 further in LibreOffice 24.2, of 678. The step from a line to the next differs
from the page's by more than 0.2pt for 80 of 456 steps in Word, from 95, and 88 of 482 in
LibreOffice, from 107.
- Some least items stand lower than they stood as exact lines: `OrangeOps`' duties and
`SerifHeadline`'s items, 8pt Lato on lines of 9.7 to 10.15pt in a table's cell, about 0.25pt
lower in both editors, the lowest 0.42pt in Word. The export reckons their least line within
0.06pt of the page, and Word and LibreOffice stand it about a quarter point lower; as exact
lines the export reckoned them as much high, and they stood nearer the page,
from 0.35pt high to 0.18pt low. The quarter-point check on a least line reads the export's
reckoning, so it does not hold these lines; each stands within half a point.

- **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.
Expand All @@ -29,10 +59,10 @@ follow semantic versioning; release dates are ISO 8601.
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
- `TealPulse`'s duties stand 0.24pt lower in Word than as exact lines, and its skills 0.3pt
lower in LibreOffice, within half a point: the export reckons the duties' least line within
0.06pt of the page, and Word stands it lower, as it does the Word list items
above. `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.

Expand Down Expand Up @@ -63,8 +93,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 past its text (see above), and the paragraph is seated on its baseline, not a Word
list's item.
the line past its text, a Word list's marker in a face that holds it (see above), and the
paragraph is seated on its baseline.
- 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 @@ -141,18 +141,72 @@ static final class ShippedLines {

private final Map<FontName, FontFamilyDefinition> families;
private final Map<String, DocxNaturalLine> lines = new java.util.HashMap<>();
/** The file of each face with a line whose character map is not read yet. */
private final Map<String, byte[]> files = new java.util.HashMap<>();
/** Each face's character map, read the first time a text is asked of it; empty where unreadable. */
private final Map<String, java.util.Optional<org.apache.fontbox.ttf.CmapLookup>> characters =
new java.util.HashMap<>();

private ShippedLines(Map<FontName, FontFamilyDefinition> families, List<Embedded> embedded) {
this.families = families;
for (Embedded family : embedded) {
for (Face face : family.faces()) {
if (face.line() != null) {
lines.put(family.wordFamily() + '\n' + face.element(), face.line());
files.put(family.wordFamily() + '\n' + face.element(), face.font());
}
}
}
}

/**
* Whether a face the document ships, with a line, holds a glyph of its own for every letter
* of a text, so that an editor sets the text in it, not in a face it falls back to.
*
* @param fontName the face a style names
* @param slot which of its family's faces, as {@link #of(FontName, String)}
* @param text the letters
* @return whether it does; false where the document does not ship that face, it has no line,
* or its Unicode character map cannot be read
*/
boolean holds(FontName fontName, String slot, String text) {
String wordFamily = wordFamilyOf(fontName);
return wordFamily != null && holds(wordFamily, slot, text);
}

/**
* The same, by the family a run names in Word.
*
* @param wordFamily the family's name as the font table writes it
* @param slot which of its faces, as {@link #of(FontName, String)}
* @param text the letters
* @return whether it does, as {@link #holds(FontName, String, String)}
*/
boolean holds(String wordFamily, String slot, String text) {
String face = wordFamily + '\n' + slot;
java.util.Optional<org.apache.fontbox.ttf.CmapLookup> map = characters.get(face);
if (map == null) {
byte[] file = files.remove(face);
if (file == null) {
return false;
}
map = characterMapOf(file);
characters.put(face, map);
}
org.apache.fontbox.ttf.CmapLookup read = map.orElse(null);
return read != null && text.codePoints().allMatch(letter -> read.getGlyphId(letter) > 0);
}

/** A face's Unicode character map; empty where it holds none, or cannot be read. */
private static java.util.Optional<org.apache.fontbox.ttf.CmapLookup> characterMapOf(byte[] file) {
try (org.apache.fontbox.ttf.TrueTypeFont font = new org.apache.fontbox.ttf.TTFParser()
.parse(new org.apache.pdfbox.io.RandomAccessReadBuffer(file))) {
return java.util.Optional.ofNullable(font.getUnicodeCmapLookup(true));
} catch (IOException | RuntimeException unreadable) {
return java.util.Optional.empty();
}
}

/**
* The line a face is set in.
*
Expand All @@ -162,8 +216,14 @@ private ShippedLines(Map<FontName, FontFamilyDefinition> families, List<Embedded
* @return its line, or null where the document does not ship that face
*/
DocxNaturalLine of(FontName fontName, String slot) {
String wordFamily = wordFamilyOf(fontName);
return wordFamily == null ? null : of(wordFamily, slot);
}

/** The family a style's face is written as in Word, or null where none is known. */
private String wordFamilyOf(FontName fontName) {
FontFamilyDefinition family = fontName == null ? null : families.get(FontLibrary.resolveFamily(fontName));
return family == null ? null : of(family.wordFamily(), slot);
return family == null ? null : family.wordFamily();
}

/**
Expand Down Expand Up @@ -205,18 +265,20 @@ private record Embedded(String wordFamily, List<Face> faces) {
}

/**
* One face of a family: which slot it fills, its bytes, how the part refers to it, and the
* line an editor sets it in.
* One face of a family: which slot it fills, its file and the bytes the part stores, how the
* part refers to it, and the line an editor sets it in.
*/
private static final class Face {
private final String element;
private final byte[] font;
private final byte[] bytes;
private final String fontKey;
private final DocxNaturalLine line;
private String relationshipId;

Face(String element, byte[] bytes, String fontKey, DocxNaturalLine line) {
Face(String element, byte[] font, byte[] bytes, String fontKey, DocxNaturalLine line) {
this.element = element;
this.font = font;
this.bytes = bytes;
this.fontKey = fontKey;
this.line = line;
Expand All @@ -226,6 +288,12 @@ String element() {
return element;
}

/** The face's own file, as read. */
byte[] font() {
return font;
}

/** The file as the part stores it, obfuscated. */
byte[] bytes() {
return bytes;
}
Expand Down Expand Up @@ -384,7 +452,7 @@ private static void addFace(List<Face> faces,
}
}
DocxFontEmbedding.Obfuscated obfuscated = DocxFontEmbedding.obfuscate(bytes);
faces.add(new Face(slot.element(), obfuscated.bytes(), obfuscated.fontKey(), DocxNaturalLine.of(bytes)));
faces.add(new Face(slot.element(), bytes, obfuscated.bytes(), obfuscated.fontKey(), DocxNaturalLine.of(bytes)));
}

/**
Expand Down
Loading
Loading