From dc1c69e4fec7a267e391faef45ccae918d93ed76 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 02:15:13 +0100
Subject: [PATCH 1/9] fix(templates): track ProfessionalSidebar and NavySidebar
headings with the pen
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both presets faked tracking by writing a heading one character at a time with
a sized space run between each pair. The gaps were real space characters, so
the file said what it drew: PROFILE copied, searched and read back as
"P R O F I L E", and the name as "M A R T A L I N D Q V I S T".
Native tracking already exists and already solves this — a tracked run keeps
its string whole, moves the pen with Tc, and states its own ActualText so an
extractor cannot mistake the gaps for word breaks. These two presets simply
predated it.
- ProfessionalSidebarStyles.tracked and NavySidebarStyles.tracked now write one
run carrying DocumentLetterSpacing.ofFontSize(trackingEm).
- Drop SPACE_ADVANCE_EM from both: it existed only to size the spacer runs.
The advance is equivalent by construction — a space glyph at
trackingEm*size/SPACE_ADVANCE_EM advanced by trackingEm*size, which is what
ofFontSize(trackingEm) adds — with one measured difference: Tc also advances
after the final glyph, so each tracked run is one gap wider. Measured on the
ProfessionalSidebar snapshot: placementWidth/contentWidth on 10 headings grow
by +1.390, +1.357 and +1.457 pt (trackingEm x font size); x, y, page
assignment and pagination are unchanged, which is why the two layout
snapshots move and nothing else does.
Verified: extracted text via PDFBox 3.0.8 goes from 13 suspicious runs to 0
for cv-professional-sidebar-v2 and 9 to 0 for cv-navy-sidebar-v2; both visual
parity gates stay green within budget; 25 preset tests pass.
Co-Authored-By: Claude Opus 5
---
.../cv-v2/navy_sidebar_layout.json | 32 +++++++--------
.../cv-v2/professional_sidebar_layout.json | 40 +++++++++----------
.../cv/presets/NavySidebarStyles.java | 25 ++++--------
.../cv/presets/ProfessionalSidebarStyles.java | 25 ++++--------
4 files changed, 52 insertions(+), 70 deletions(-)
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/navy_sidebar_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/navy_sidebar_layout.json
index f467275c7..c87be121d 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/navy_sidebar_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/navy_sidebar_layout.json
@@ -265,11 +265,11 @@
"computedY" : 653.493,
"placementX" : 26.5,
"placementY" : 653.493,
- "placementWidth" : 69.473,
+ "placementWidth" : 71.856,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 69.473,
+ "contentWidth" : 71.856,
"contentHeight" : 14.4,
"margin" : {
"top" : 0.0,
@@ -475,11 +475,11 @@
"computedY" : 517.693,
"placementX" : 26.5,
"placementY" : 517.693,
- "placementWidth" : 86.807,
+ "placementWidth" : 89.304,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 86.807,
+ "contentWidth" : 89.304,
"contentHeight" : 14.4,
"margin" : {
"top" : 18.0,
@@ -805,11 +805,11 @@
"computedY" : 316.593,
"placementX" : 26.5,
"placementY" : 316.593,
- "placementWidth" : 47.366,
+ "placementWidth" : 49.692,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 47.366,
+ "contentWidth" : 49.692,
"contentHeight" : 14.4,
"margin" : {
"top" : 18.0,
@@ -925,11 +925,11 @@
"computedY" : 162.993,
"placementX" : 26.5,
"placementY" : 162.993,
- "placementWidth" : 87.863,
+ "placementWidth" : 90.36,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 87.863,
+ "contentWidth" : 90.36,
"contentHeight" : 14.4,
"margin" : {
"top" : 18.0,
@@ -1345,11 +1345,11 @@
"computedY" : 753.69,
"placementX" : 202.197,
"placementY" : 753.69,
- "placementWidth" : 187.31,
+ "placementWidth" : 190.719,
"placementHeight" : 16.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 187.31,
+ "contentWidth" : 190.719,
"contentHeight" : 16.2,
"margin" : {
"top" : 0.0,
@@ -1585,11 +1585,11 @@
"computedY" : 632.95,
"placementX" : 232.097,
"placementY" : 632.95,
- "placementWidth" : 89.402,
+ "placementWidth" : 91.956,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 89.402,
+ "contentWidth" : 91.956,
"contentHeight" : 14.4,
"margin" : {
"top" : 0.0,
@@ -3295,11 +3295,11 @@
"computedY" : 211.675,
"placementX" : 232.097,
"placementY" : 211.675,
- "placementWidth" : 114.332,
+ "placementWidth" : 117.0,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 114.332,
+ "contentWidth" : 117.0,
"contentHeight" : 14.4,
"margin" : {
"top" : 0.0,
@@ -3505,11 +3505,11 @@
"computedY" : 95.325,
"placementX" : 232.097,
"placementY" : 95.325,
- "placementWidth" : 122.041,
+ "placementWidth" : 124.824,
"placementHeight" : 14.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 122.041,
+ "contentWidth" : 124.824,
"contentHeight" : 14.4,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/professional_sidebar_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/professional_sidebar_layout.json
index 17e496e80..d1690216a 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/professional_sidebar_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/professional_sidebar_layout.json
@@ -265,11 +265,11 @@
"computedY" : 606.284,
"placementX" : 17.8,
"placementY" : 606.284,
- "placementWidth" : 47.723,
+ "placementWidth" : 49.113,
"placementHeight" : 10.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 47.723,
+ "contentWidth" : 49.113,
"contentHeight" : 10.2,
"margin" : {
"top" : 0.0,
@@ -535,11 +535,11 @@
"computedY" : 438.234,
"placementX" : 17.8,
"placementY" : 438.234,
- "placementWidth" : 32.312,
+ "placementWidth" : 33.669,
"placementHeight" : 10.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 32.312,
+ "contentWidth" : 33.669,
"contentHeight" : 10.2,
"margin" : {
"top" : 0.0,
@@ -1885,11 +1885,11 @@
"computedY" : 206.784,
"placementX" : 17.8,
"placementY" : 206.784,
- "placementWidth" : 59.505,
+ "placementWidth" : 60.962,
"placementHeight" : 10.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 59.505,
+ "contentWidth" : 60.962,
"contentHeight" : 10.2,
"margin" : {
"top" : 0.0,
@@ -2935,11 +2935,11 @@
"computedY" : 82.854,
"placementX" : 17.8,
"placementY" : 82.854,
- "placementWidth" : 60.253,
+ "placementWidth" : 61.71,
"placementHeight" : 10.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 60.253,
+ "contentWidth" : 61.71,
"contentHeight" : 10.2,
"margin" : {
"top" : 0.0,
@@ -3805,11 +3805,11 @@
"computedY" : 671.34,
"placementX" : 170.056,
"placementY" : 671.34,
- "placementWidth" : 176.476,
+ "placementWidth" : 187.319,
"placementHeight" : 44.04,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 176.476,
+ "contentWidth" : 187.319,
"contentHeight" : 44.04,
"margin" : {
"top" : 0.0,
@@ -3835,11 +3835,11 @@
"computedY" : 653.64,
"placementX" : 170.056,
"placementY" : 653.64,
- "placementWidth" : 163.438,
+ "placementWidth" : 164.246,
"placementHeight" : 11.4,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 163.438,
+ "contentWidth" : 164.246,
"contentHeight" : 11.4,
"margin" : {
"top" : 0.0,
@@ -3985,11 +3985,11 @@
"computedY" : 606.75,
"placementX" : 170.056,
"placementY" : 606.75,
- "placementWidth" : 42.147,
+ "placementWidth" : 43.584,
"placementHeight" : 10.44,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 42.147,
+ "contentWidth" : 43.584,
"contentHeight" : 10.44,
"margin" : {
"top" : 0.0,
@@ -4135,11 +4135,11 @@
"computedY" : 515.14,
"placementX" : 170.056,
"placementY" : 515.14,
- "placementWidth" : 62.533,
+ "placementWidth" : 64.078,
"placementHeight" : 10.44,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 62.533,
+ "contentWidth" : 64.078,
"contentHeight" : 10.44,
"margin" : {
"top" : 0.0,
@@ -4765,11 +4765,11 @@
"computedY" : 155.19,
"placementX" : 170.056,
"placementY" : 155.19,
- "placementWidth" : 50.795,
+ "placementWidth" : 52.268,
"placementHeight" : 10.44,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 50.795,
+ "contentWidth" : 52.268,
"contentHeight" : 10.44,
"margin" : {
"top" : 0.0,
@@ -5215,11 +5215,11 @@
"computedY" : 34.66,
"placementX" : 170.056,
"placementY" : 34.66,
- "placementWidth" : 64.021,
+ "placementWidth" : 65.566,
"placementHeight" : 10.44,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 64.021,
+ "contentWidth" : 65.566,
"contentHeight" : 10.44,
"margin" : {
"top" : 0.0,
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/NavySidebarStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/NavySidebarStyles.java
index 0584c1372..c4ccb783b 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/NavySidebarStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/NavySidebarStyles.java
@@ -4,6 +4,7 @@
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentTextDecoration;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.core.text.TextStyles;
import com.demcha.compose.font.FontName;
@@ -78,12 +79,6 @@ private NavySidebarStyles() {
static final double TRACKING_EM = 0.17;
- /**
- * The advance of a space in Lato, as a fraction of type size.
- * {@link #tracked} sizes its spacer runs against this.
- */
- static final double SPACE_ADVANCE_EM = 0.25;
-
// -- bands -----------------------------------------------------------
static final double HEADING_BAND_HEIGHT = BADGE_DIAMETER;
@@ -166,23 +161,19 @@ static DocumentTextStyle style(double size, DocumentColor color,
}
/**
- * Writes text letter by letter with a sized space between each pair,
- * which is how this design gets its letter-spacing: a text style carries
- * no tracking, so the gap is a run of its own whose type size is chosen
- * to advance by {@link #TRACKING_EM} of the surrounding size.
+ * Writes {@code text} as one run tracked by {@link #TRACKING_EM}.
+ *
+ * The design wants the letters held apart; it does not want the gaps to
+ * become part of the text. Native tracking keeps the run a single string,
+ * so a heading is drawn spaced but reads, copies and searches as the word
+ * it is.
*
* @param paragraph the paragraph being built
* @param text the text to space out
* @param style the style of the letters
*/
static void tracked(ParagraphBuilder paragraph, String text, DocumentTextStyle style) {
- DocumentTextStyle spacer = style.withSize(TRACKING_EM * style.size() / SPACE_ADVANCE_EM);
- for (int i = 0; i < text.length(); i++) {
- paragraph.inlineText(String.valueOf(text.charAt(i)), style);
- if (i < text.length() - 1) {
- paragraph.inlineText(" ", spacer);
- }
- }
+ paragraph.inlineText(text, style.withLetterSpacing(DocumentLetterSpacing.ofFontSize(TRACKING_EM)));
}
/** Strips a title down to the letters and digits a node name can carry. */
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ProfessionalSidebarStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ProfessionalSidebarStyles.java
index 37a76b04d..fdb6025e4 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ProfessionalSidebarStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ProfessionalSidebarStyles.java
@@ -4,6 +4,7 @@
import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentTextDecoration;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.core.text.TextStyles;
import com.demcha.compose.font.FontName;
@@ -122,12 +123,6 @@ private ProfessionalSidebarStyles() {
static final double HEADING_TRACKING_EM = 0.14;
static final double ROLE_TRACKING_EM = 0.18;
- /**
- * The advance of a space in the body face, as a fraction of type size.
- * {@link #tracked} sizes its spacer runs against this to hit a tracking
- * measured in ems.
- */
- static final double SPACE_ADVANCE_EM = 0.25;
// -- vertical rhythm -------------------------------------------------
@@ -197,10 +192,12 @@ static DocumentTextStyle style(FontName font, double size, DocumentColor color,
}
/**
- * Writes text letter by letter with a sized space between each pair,
- * which is how this design gets its letter-spacing: a text style carries
- * no tracking, so the gap is a run of its own whose type size is chosen
- * to advance by {@code trackingEm} of the surrounding size.
+ * Writes {@code text} as one run tracked by {@code trackingEm}.
+ *
+ * The design wants the letters held apart; it does not want the gaps to
+ * become part of the text. Native tracking keeps the run a single string,
+ * so the heading is drawn spaced but reads, copies and searches as the word
+ * it is — {@code PROFILE}, not {@code P R O F I L E}.
*
* @param paragraph the paragraph being built
* @param text the text to space out
@@ -209,13 +206,7 @@ static DocumentTextStyle style(FontName font, double size, DocumentColor color,
*/
static void tracked(ParagraphBuilder paragraph, String text,
DocumentTextStyle style, double trackingEm) {
- DocumentTextStyle spacer = style.withSize(trackingEm * style.size() / SPACE_ADVANCE_EM);
- for (int i = 0; i < text.length(); i++) {
- paragraph.inlineText(String.valueOf(text.charAt(i)), style);
- if (i + 1 < text.length()) {
- paragraph.inlineText(" ", spacer);
- }
- }
+ paragraph.inlineText(text, style.withLetterSpacing(DocumentLetterSpacing.ofFontSize(trackingEm)));
}
/** Strips a title down to the letters and digits a node name can carry. */
From 9c3c9f8c03ed18c27cce1d5acd98b35fb10bf147 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 02:35:27 +0100
Subject: [PATCH 2/9] fix(templates): track five more CV presets with the pen,
not with spaces
TealPulse, VioletGrid, CharcoalGold, MidnightNavy and TerracottaRail each
built their tracking by writing characters that are not in the text: a space
run between every letter, or a StringBuilder splicing spaces into the string
before it was ever authored. Whatever the shape, the gaps were real
characters, so a heading copied as "S K I L L S" and a name as
"O L I V E R B E N N E T T".
Each now carries DocumentLetterSpacing on the style and hands the engine the
word it means. The gap measures are preserved as named constants rather than
re-derived:
- TealPulse: tracked and knockedOut share one spacedBy(style, em); the spacer
run count and its style are gone.
- VioletGrid: the gap was authored in points, so it tracks in points.
- CharcoalGold, MidnightNavy: the gap was one space of the face, so 0.25 em.
- TerracottaRail: the hair and thin space characters were already tracking
measures - a tenth and a fifth of an em - so heading/headingWithDash take
the measure instead of the character, and the masthead's full space is
0.25 em.
Geometry, measured rather than assumed:
- TealPulse, TerracottaRail: width only, no x, y or page change.
- VioletGrid: the heading text grows 1.239 and the rule that follows it
shrinks by exactly 1.239, so placementX moves with the text and the row
total is conserved - the rule still starts where the words end.
- CharcoalGold: +1.926 on the job title, which is deliberately NOT one
trailing gap. The old helper set three spaces between words and one between
letters; tracking adds its gap after every glyph, so the word gaps tighten
while a trailing gap is added. The look moves slightly and stays inside the
visual parity budget.
- MidnightNavy: no snapshot movement at all, though its role line is centred.
Verified: the CV text-layer gate now covers all 26 presets and only
serif_headline still fails it, which is the coupled trackedWidth case and
lands next. 129 preset tests green, every visual parity gate green.
Co-Authored-By: Claude Opus 5
---
.../cv-v2/charcoal_gold_layout.json | 4 +-
.../cv-v2/teal_pulse_layout.json | 40 +++---
.../cv-v2/terracotta_rail_layout.json | 48 +++----
.../cv-v2/violet_grid_layout.json | 132 +++++++++---------
.../cv/presets/CharcoalGoldMain.java | 8 +-
.../cv/presets/CharcoalGoldStyles.java | 41 ++----
.../cv/presets/MidnightNavyAside.java | 7 +-
.../cv/presets/MidnightNavyWidgets.java | 34 +----
.../cv/presets/TealPulseWidgets.java | 28 ++--
.../cv/presets/TerracottaRailAside.java | 10 +-
.../cv/presets/TerracottaRailMain.java | 21 +--
.../cv/presets/TerracottaRailStyles.java | 18 +--
.../cv/presets/TerracottaRailWidgets.java | 58 ++------
.../cv/presets/VioletGridWidgets.java | 18 +--
14 files changed, 188 insertions(+), 279 deletions(-)
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/charcoal_gold_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/charcoal_gold_layout.json
index 0732e455a..171315c1b 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/charcoal_gold_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/charcoal_gold_layout.json
@@ -3625,11 +3625,11 @@
"computedY" : 704.83,
"placementX" : 214.01,
"placementY" : 704.83,
- "placementWidth" : 152.099,
+ "placementWidth" : 154.025,
"placementHeight" : 13.92,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 152.099,
+ "contentWidth" : 154.025,
"contentHeight" : 13.92,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json
index 0c6a6e2cc..98800da2f 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/teal_pulse_layout.json
@@ -235,11 +235,11 @@
"computedY" : 676.575,
"placementX" : 120.891,
"placementY" : 676.575,
- "placementWidth" : 340.363,
+ "placementWidth" : 340.604,
"placementHeight" : 74.676,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 340.363,
+ "contentWidth" : 340.604,
"contentHeight" : 74.676,
"margin" : {
"top" : 0.0,
@@ -265,11 +265,11 @@
"computedY" : 695.951,
"placementX" : 120.891,
"placementY" : 695.951,
- "placementWidth" : 340.363,
+ "placementWidth" : 340.604,
"placementHeight" : 55.3,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 340.363,
+ "contentWidth" : 340.604,
"contentHeight" : 55.3,
"margin" : {
"top" : 0.0,
@@ -295,11 +295,11 @@
"computedY" : 676.575,
"placementX" : 120.891,
"placementY" : 676.575,
- "placementWidth" : 205.712,
+ "placementWidth" : 225.213,
"placementHeight" : 19.376,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 205.712,
+ "contentWidth" : 225.213,
"contentHeight" : 19.376,
"margin" : {
"top" : 0.0,
@@ -625,11 +625,11 @@
"computedY" : 239.229,
"placementX" : 28.604,
"placementY" : 239.229,
- "placementWidth" : 118.648,
+ "placementWidth" : 121.642,
"placementHeight" : 378.25,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 118.648,
+ "contentWidth" : 121.642,
"contentHeight" : 378.25,
"margin" : {
"top" : 0.0,
@@ -685,11 +685,11 @@
"computedY" : 590.967,
"placementX" : 28.604,
"placementY" : 590.967,
- "placementWidth" : 110.685,
+ "placementWidth" : 121.642,
"placementHeight" : 12.48,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 110.685,
+ "contentWidth" : 121.642,
"contentHeight" : 12.48,
"margin" : {
"top" : 0.0,
@@ -985,11 +985,11 @@
"computedY" : 594.208,
"placementX" : 229.118,
"placementY" : 594.208,
- "placementWidth" : 144.93,
+ "placementWidth" : 152.393,
"placementHeight" : 15.24,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 144.93,
+ "contentWidth" : 152.393,
"contentHeight" : 15.24,
"margin" : {
"top" : 0.0,
@@ -1255,11 +1255,11 @@
"computedY" : 438.259,
"placementX" : 229.118,
"placementY" : 438.259,
- "placementWidth" : 160.457,
+ "placementWidth" : 168.85,
"placementHeight" : 15.24,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 160.457,
+ "contentWidth" : 168.85,
"contentHeight" : 15.24,
"margin" : {
"top" : 0.0,
@@ -1825,11 +1825,11 @@
"computedY" : 122.272,
"placementX" : 65.842,
"placementY" : 122.272,
- "placementWidth" : 51.84,
+ "placementWidth" : 57.17,
"placementHeight" : 11.52,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 51.84,
+ "contentWidth" : 57.17,
"contentHeight" : 11.52,
"margin" : {
"top" : 0.0,
@@ -2215,11 +2215,11 @@
"computedY" : 122.272,
"placementX" : 228.038,
"placementY" : 122.272,
- "placementWidth" : 77.117,
+ "placementWidth" : 84.491,
"placementHeight" : 11.52,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 77.117,
+ "contentWidth" : 84.491,
"contentHeight" : 11.52,
"margin" : {
"top" : 0.0,
@@ -2545,11 +2545,11 @@
"computedY" : 122.272,
"placementX" : 431.37,
"placementY" : 122.272,
- "placementWidth" : 123.773,
+ "placementWidth" : 134.418,
"placementHeight" : 11.52,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 123.773,
+ "contentWidth" : 134.418,
"contentHeight" : 11.52,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/terracotta_rail_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/terracotta_rail_layout.json
index 7f8208ff9..dbd376bfc 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/terracotta_rail_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/terracotta_rail_layout.json
@@ -355,11 +355,11 @@
"computedY" : 457.063,
"placementX" : 25.4,
"placementY" : 457.063,
- "placementWidth" : 106.829,
+ "placementWidth" : 107.671,
"placementHeight" : 210.926,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 106.829,
+ "contentWidth" : 107.671,
"contentHeight" : 210.926,
"margin" : {
"top" : 0.0,
@@ -385,11 +385,11 @@
"computedY" : 657.463,
"placementX" : 25.4,
"placementY" : 657.463,
- "placementWidth" : 101.553,
+ "placementWidth" : 107.671,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 101.553,
+ "contentWidth" : 107.671,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -835,11 +835,11 @@
"computedY" : 429.537,
"placementX" : 25.4,
"placementY" : 429.537,
- "placementWidth" : 52.0,
+ "placementWidth" : 55.172,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 52.0,
+ "contentWidth" : 55.172,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -1165,11 +1165,11 @@
"computedY" : 279.961,
"placementX" : 25.4,
"placementY" : 279.961,
- "placementWidth" : 77.553,
+ "placementWidth" : 82.689,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 77.553,
+ "contentWidth" : 82.689,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -1345,11 +1345,11 @@
"computedY" : 210.134,
"placementX" : 25.4,
"placementY" : 210.134,
- "placementWidth" : 131.334,
+ "placementWidth" : 139.089,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 131.334,
+ "contentWidth" : 139.089,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -2065,11 +2065,11 @@
"computedY" : 759.69,
"placementX" : 205.345,
"placementY" : 759.69,
- "placementWidth" : 321.748,
+ "placementWidth" : 331.296,
"placementHeight" : 62.2,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 321.748,
+ "contentWidth" : 331.296,
"contentHeight" : 62.2,
"margin" : {
"top" : 0.0,
@@ -2095,11 +2095,11 @@
"computedY" : 788.29,
"placementX" : 205.345,
"placementY" : 788.29,
- "placementWidth" : 321.748,
+ "placementWidth" : 331.296,
"placementHeight" : 33.6,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 321.748,
+ "contentWidth" : 331.296,
"contentHeight" : 33.6,
"margin" : {
"top" : 0.0,
@@ -2125,11 +2125,11 @@
"computedY" : 764.69,
"placementX" : 205.345,
"placementY" : 764.69,
- "placementWidth" : 169.468,
+ "placementWidth" : 174.083,
"placementHeight" : 15.6,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 169.468,
+ "contentWidth" : 174.083,
"contentHeight" : 15.6,
"margin" : {
"top" : 0.0,
@@ -2215,11 +2215,11 @@
"computedY" : 738.163,
"placementX" : 205.345,
"placementY" : 738.163,
- "placementWidth" : 133.212,
+ "placementWidth" : 147.378,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 133.212,
+ "contentWidth" : 147.378,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -2425,11 +2425,11 @@
"computedY" : 630.417,
"placementX" : 205.345,
"placementY" : 630.417,
- "placementWidth" : 143.475,
+ "placementWidth" : 159.602,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 143.475,
+ "contentWidth" : 159.602,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -3955,11 +3955,11 @@
"computedY" : 279.286,
"placementX" : 205.345,
"placementY" : 279.286,
- "placementWidth" : 104.79,
+ "placementWidth" : 116.996,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 104.79,
+ "contentWidth" : 116.996,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
@@ -5035,11 +5035,11 @@
"computedY" : 115.876,
"placementX" : 205.345,
"placementY" : 115.876,
- "placementWidth" : 60.632,
+ "placementWidth" : 67.61,
"placementHeight" : 10.526,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 60.632,
+ "contentWidth" : 67.61,
"contentHeight" : 10.526,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json
index f05e0141b..60902ae5d 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/violet_grid_layout.json
@@ -145,11 +145,11 @@
"computedY" : 754.683,
"placementX" : 29.905,
"placementY" : 754.683,
- "placementWidth" : 157.277,
+ "placementWidth" : 162.826,
"placementHeight" : 20.319,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 157.277,
+ "contentWidth" : 162.826,
"contentHeight" : 20.319,
"margin" : {
"top" : 0.0,
@@ -505,11 +505,11 @@
"computedY" : 650.396,
"placementX" : 29.905,
"placementY" : 650.396,
- "placementWidth" : 76.844,
+ "placementWidth" : 78.083,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 76.844,
+ "contentWidth" : 78.083,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -535,11 +535,11 @@
"computedY" : 650.396,
"placementX" : 29.905,
"placementY" : 650.396,
- "placementWidth" : 76.844,
+ "placementWidth" : 78.083,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 76.844,
+ "contentWidth" : 78.083,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -561,15 +561,15 @@
"childIndex" : 1,
"depth" : 3,
"layer" : 3,
- "computedX" : 106.749,
+ "computedX" : 107.988,
"computedY" : 656.562,
- "placementX" : 106.749,
+ "placementX" : 107.988,
"placementY" : 656.562,
- "placementWidth" : 458.622,
+ "placementWidth" : 457.383,
"placementHeight" : 8.617,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 458.622,
+ "contentWidth" : 457.383,
"contentHeight" : 8.617,
"margin" : {
"top" : 0.0,
@@ -591,15 +591,15 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 118.034,
+ "computedX" : 119.273,
"computedY" : 656.562,
- "placementX" : 118.034,
+ "placementX" : 119.273,
"placementY" : 656.562,
- "placementWidth" : 447.337,
+ "placementWidth" : 446.098,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 447.337,
+ "contentWidth" : 446.098,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -2065,11 +2065,11 @@
"computedY" : 532.469,
"placementX" : 29.905,
"placementY" : 532.469,
- "placementWidth" : 34.064,
+ "placementWidth" : 34.991,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 34.064,
+ "contentWidth" : 34.991,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -2095,11 +2095,11 @@
"computedY" : 532.469,
"placementX" : 29.905,
"placementY" : 532.469,
- "placementWidth" : 34.064,
+ "placementWidth" : 34.991,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 34.064,
+ "contentWidth" : 34.991,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -2121,15 +2121,15 @@
"childIndex" : 1,
"depth" : 3,
"layer" : 3,
- "computedX" : 63.969,
+ "computedX" : 64.895,
"computedY" : 538.635,
- "placementX" : 63.969,
+ "placementX" : 64.895,
"placementY" : 538.635,
- "placementWidth" : 501.402,
+ "placementWidth" : 500.475,
"placementHeight" : 8.617,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 501.402,
+ "contentWidth" : 500.475,
"contentHeight" : 8.617,
"margin" : {
"top" : 0.0,
@@ -2151,15 +2151,15 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 75.254,
+ "computedX" : 76.18,
"computedY" : 538.635,
- "placementX" : 75.254,
+ "placementX" : 76.18,
"placementY" : 538.635,
- "placementWidth" : 490.117,
+ "placementWidth" : 489.19,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 490.117,
+ "contentWidth" : 489.19,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -2245,11 +2245,11 @@
"computedY" : 477.259,
"placementX" : 29.905,
"placementY" : 477.259,
- "placementWidth" : 63.841,
+ "placementWidth" : 64.963,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 63.841,
+ "contentWidth" : 64.963,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -2275,11 +2275,11 @@
"computedY" : 477.259,
"placementX" : 29.905,
"placementY" : 477.259,
- "placementWidth" : 63.841,
+ "placementWidth" : 64.963,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 63.841,
+ "contentWidth" : 64.963,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -2301,15 +2301,15 @@
"childIndex" : 1,
"depth" : 3,
"layer" : 3,
- "computedX" : 93.746,
+ "computedX" : 94.868,
"computedY" : 483.425,
- "placementX" : 93.746,
+ "placementX" : 94.868,
"placementY" : 483.425,
- "placementWidth" : 471.625,
+ "placementWidth" : 470.503,
"placementHeight" : 8.617,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 471.625,
+ "contentWidth" : 470.503,
"contentHeight" : 8.617,
"margin" : {
"top" : 0.0,
@@ -2331,15 +2331,15 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 105.031,
+ "computedX" : 106.153,
"computedY" : 483.425,
- "placementX" : 105.031,
+ "placementX" : 106.153,
"placementY" : 483.425,
- "placementWidth" : 460.34,
+ "placementWidth" : 459.218,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 460.34,
+ "contentWidth" : 459.218,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -3985,11 +3985,11 @@
"computedY" : 231.717,
"placementX" : 29.905,
"placementY" : 231.717,
- "placementWidth" : 105.864,
+ "placementWidth" : 107.26,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 105.864,
+ "contentWidth" : 107.26,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -4015,11 +4015,11 @@
"computedY" : 231.717,
"placementX" : 29.905,
"placementY" : 231.717,
- "placementWidth" : 105.864,
+ "placementWidth" : 107.26,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 105.864,
+ "contentWidth" : 107.26,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -4041,15 +4041,15 @@
"childIndex" : 1,
"depth" : 3,
"layer" : 3,
- "computedX" : 135.769,
+ "computedX" : 137.165,
"computedY" : 237.883,
- "placementX" : 135.769,
+ "placementX" : 137.165,
"placementY" : 237.883,
- "placementWidth" : 429.602,
+ "placementWidth" : 428.206,
"placementHeight" : 8.617,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 429.602,
+ "contentWidth" : 428.206,
"contentHeight" : 8.617,
"margin" : {
"top" : 0.0,
@@ -4071,15 +4071,15 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 147.054,
+ "computedX" : 148.45,
"computedY" : 237.883,
- "placementX" : 147.054,
+ "placementX" : 148.45,
"placementY" : 237.883,
- "placementWidth" : 418.317,
+ "placementWidth" : 416.921,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 418.317,
+ "contentWidth" : 416.921,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -4675,11 +4675,11 @@
"computedY" : 101.109,
"placementX" : 29.905,
"placementY" : 101.109,
- "placementWidth" : 62.401,
+ "placementWidth" : 63.484,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 62.401,
+ "contentWidth" : 63.484,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -4701,15 +4701,15 @@
"childIndex" : 1,
"depth" : 3,
"layer" : 3,
- "computedX" : 103.591,
+ "computedX" : 104.673,
"computedY" : 107.276,
- "placementX" : 103.591,
+ "placementX" : 104.673,
"placementY" : 107.276,
- "placementWidth" : 171.131,
+ "placementWidth" : 170.064,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 171.131,
+ "contentWidth" : 170.064,
"contentHeight" : 1.0,
"margin" : {
"top" : 7.617,
@@ -4731,9 +4731,9 @@
"childIndex" : 2,
"depth" : 3,
"layer" : 3,
- "computedX" : 274.722,
+ "computedX" : 274.738,
"computedY" : 115.893,
- "placementX" : 274.722,
+ "placementX" : 274.738,
"placementY" : 115.893,
"placementWidth" : 39.497,
"placementHeight" : 0.0,
@@ -4761,15 +4761,15 @@
"childIndex" : 3,
"depth" : 3,
"layer" : 3,
- "computedX" : 314.219,
+ "computedX" : 314.235,
"computedY" : 101.109,
- "placementX" : 314.219,
+ "placementX" : 314.235,
"placementY" : 101.109,
- "placementWidth" : 63.291,
+ "placementWidth" : 64.374,
"placementHeight" : 14.783,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 63.291,
+ "contentWidth" : 64.374,
"contentHeight" : 14.783,
"margin" : {
"top" : 0.0,
@@ -4791,15 +4791,15 @@
"childIndex" : 4,
"depth" : 3,
"layer" : 3,
- "computedX" : 388.794,
+ "computedX" : 389.893,
"computedY" : 107.276,
- "placementX" : 388.794,
+ "placementX" : 389.893,
"placementY" : 107.276,
- "placementWidth" : 176.576,
+ "placementWidth" : 175.478,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 176.576,
+ "contentWidth" : 175.478,
"contentHeight" : 1.0,
"margin" : {
"top" : 7.617,
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldMain.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldMain.java
index d94a8882f..c4aecacd5 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldMain.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldMain.java
@@ -5,6 +5,7 @@
import com.demcha.compose.document.node.LayerAlign;
import com.demcha.compose.document.node.ListMarker;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentRowColumn;
import com.demcha.compose.document.templates.cv.components.SectionLookup;
import com.demcha.compose.document.templates.cv.data.CvEntry;
@@ -47,7 +48,7 @@
import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldStyles.SUMMARY_TO_EXPERIENCE;
import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldStyles.TITLE_TO_RULE;
import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldStyles.textStyle;
-import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldStyles.tracked;
+import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldStyles.JOB_TITLE_TRACKING_EM;
import static com.demcha.compose.document.templates.cv.presets.CharcoalGoldWidgets.mainHeading;
/**
@@ -102,8 +103,9 @@ private static void renderMasthead(SectionBuilder main, CvIdentity identity) {
.margin(0f, 0f, (float) MASTHEAD_TO_TITLE, 0f));
block.addParagraph(p -> p
.name("JobTitle")
- .text(tracked(identity.jobTitle()))
- .textStyle(textStyle(JOB_TITLE_SIZE, INK, false))
+ .text(identity.jobTitle())
+ .textStyle(textStyle(JOB_TITLE_SIZE, INK, false)
+ .withLetterSpacing(DocumentLetterSpacing.ofFontSize(JOB_TITLE_TRACKING_EM)))
.margin(0f, 0f, (float) TITLE_TO_RULE, 0f));
block.addLine(line -> line
.name("MastheadRule")
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldStyles.java
index a8c3eda27..85f32bec2 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CharcoalGoldStyles.java
@@ -45,6 +45,14 @@ private CharcoalGoldStyles() {
static final double NAME_GIVEN_SIZE = 35.7;
static final double NAME_FAMILY_SIZE = 45.5;
static final double JOB_TITLE_SIZE = 11.6;
+
+ /**
+ * Tracking on the masthead job title. The look used to be made by writing a
+ * space between each letter, so the gap this replaces is one space advance
+ * of the body face — a quarter of the type size.
+ */
+ static final double JOB_TITLE_TRACKING_EM = 0.25;
+
static final double MAIN_HEADING_SIZE = 11.0;
static final double SIDEBAR_HEADING_SIZE = 10.0;
static final double ROLE_SIZE = 10.5;
@@ -160,39 +168,6 @@ static double gap(double pitch, double size) {
return Math.max(0.0, pitch - size * LINE_FACTOR);
}
- /**
- * Letter-spacing by spaces rather than by sized runs: a space between
- * letters and three between words.
- *
- * This design tracks only one line — the job title — and does it
- * coarsely enough that real spaces reach it, which is simpler than the
- * per-letter spacer runs the other ported sheets need.
- *
- * @param text the text to space out
- * @return the spaced text
- */
- static String tracked(String text) {
- if (text == null || text.isEmpty()) {
- return "";
- }
- StringBuilder out = new StringBuilder(text.length() * 2);
- boolean startOfWord = true;
- for (int i = 0; i < text.length(); i++) {
- char ch = text.charAt(i);
- if (ch == ' ') {
- out.append(" ");
- startOfWord = true;
- continue;
- }
- if (!startOfWord) {
- out.append(' ');
- }
- out.append(ch);
- startOfWord = false;
- }
- return out.toString();
- }
-
/** Strips a title down to the letters and digits a node name can carry. */
static String compact(String text) {
return text == null ? "" : text.replaceAll("[^A-Za-z0-9]", "");
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyAside.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyAside.java
index c059b9849..f00898157 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyAside.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyAside.java
@@ -12,6 +12,7 @@
import com.demcha.compose.document.node.RowVerticalAlign;
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentRowColumn;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTextDecoration;
@@ -159,8 +160,10 @@ private static void renderNamePlate(SectionBuilder column, CvIdentity identity)
column.addParagraph(p -> p
.name("Role")
- .text(MidnightNavyWidgets.tracked(identity.jobTitle().toUpperCase(Locale.ROOT)))
- .textStyle(style(ROLE_SIZE, WHITE, DocumentTextDecoration.DEFAULT))
+ .text(identity.jobTitle().toUpperCase(Locale.ROOT))
+ .textStyle(style(ROLE_SIZE, WHITE, DocumentTextDecoration.DEFAULT)
+ .withLetterSpacing(DocumentLetterSpacing.ofFontSize(
+ MidnightNavyWidgets.ROLE_TRACKING_EM)))
.align(TextAlign.CENTER)
.margin(new DocumentInsets(px(13.1), 0, 0, 0)));
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyWidgets.java
index 438f8034c..3f6bf31e0 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MidnightNavyWidgets.java
@@ -109,37 +109,13 @@ static void nameDivider(SectionBuilder column, double width, double leftInset, d
}
/**
- * The role line's wide tracking, as spaces between characters.
+ * The role line's wide tracking.
*
- * There is no letter-spacing API, so the tracking is real spaces and its
- * width depends on the face's space advance: change the family and the
- * tracking has to be re-checked.
- *
- * @param text the line to track
- * @return the same line with a space between every pair of characters and
- * three between words
+ * The look used to be made by writing a space between each letter, so
+ * the gap this replaces is one space advance of the face — a quarter of the
+ * type size.
*/
- static String tracked(String text) {
- if (text == null || text.isEmpty()) {
- return "";
- }
- StringBuilder out = new StringBuilder(text.length() * 2);
- boolean startOfWord = true;
- for (int i = 0; i < text.length(); i++) {
- char ch = text.charAt(i);
- if (ch == ' ') {
- out.append(" ");
- startOfWord = true;
- continue;
- }
- if (!startOfWord) {
- out.append(' ');
- }
- out.append(ch);
- startOfWord = false;
- }
- return out.toString();
- }
+ static final double ROLE_TRACKING_EM = 0.25;
/** A label reduced to what can go in a node name. */
static String compact(String text) {
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java
index c2f35dfeb..c726c017c 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TealPulseWidgets.java
@@ -9,6 +9,7 @@
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.ClipPolicy;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
@@ -38,8 +39,6 @@
import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.SECTION_GAP;
import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.compact;
import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.gapRun;
-import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.spacerRuns;
-import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.spacerStyle;
import static com.demcha.compose.document.templates.cv.presets.TealPulseStyles.style;
/**
@@ -69,30 +68,21 @@ private TealPulseWidgets() {
*/
static void tracked(ParagraphBuilder paragraph, String text, DocumentTextStyle textStyle,
double trackingEm) {
- int runs = spacerRuns(trackingEm);
- DocumentTextStyle spacer = spacerStyle(textStyle, trackingEm, runs);
- for (int index = 0; index < text.length(); index++) {
- paragraph.inlineText(String.valueOf(text.charAt(index)), textStyle);
- if (index + 1 < text.length()) {
- paragraph.inlineText(" ".repeat(runs), spacer);
- }
- }
+ paragraph.inlineText(text, spacedBy(textStyle, trackingEm));
}
/**
- * The same, with the paper laid behind every run, so a rule the heading is
+ * The same, with the paper laid behind the run, so a rule the heading is
* drawn over shows only past the end of the words.
*/
static void knockedOut(ParagraphBuilder paragraph, String text,
DocumentTextStyle textStyle, double trackingEm) {
- int runs = spacerRuns(trackingEm);
- DocumentTextStyle spacer = spacerStyle(textStyle, trackingEm, runs);
- for (int index = 0; index < text.length(); index++) {
- knockedOutRun(paragraph, String.valueOf(text.charAt(index)), textStyle);
- if (index + 1 < text.length()) {
- knockedOutRun(paragraph, " ".repeat(runs), spacer);
- }
- }
+ knockedOutRun(paragraph, text, spacedBy(textStyle, trackingEm));
+ }
+
+ /** {@code textStyle} tracked by {@code trackingEm}, the letters left whole. */
+ private static DocumentTextStyle spacedBy(DocumentTextStyle textStyle, double trackingEm) {
+ return textStyle.withLetterSpacing(DocumentLetterSpacing.ofFontSize(trackingEm));
}
/** One knocked-out run. No horizontal padding, so neighbours abut. */
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java
index e72a7b111..128a25f8f 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java
@@ -34,7 +34,7 @@
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.RULE_THICKNESS;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_DASH_WIDTH;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_DIVIDER_GAP;
-import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_HEADING_SPACER;
+import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_HEADING_TRACKING_EM;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_PAD_LEFT;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_PAD_RIGHT;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.SIDEBAR_PAD_TOP;
@@ -189,10 +189,10 @@ private static void renderBulletedSkills(SectionBuilder side, SkillsSection sect
side.addSection(blockName, block -> {
block.spacing(0);
if (discBullet) {
- headingWithDash(block, section.title(), SIDEBAR_HEADING_SPACER,
+ headingWithDash(block, section.title(), SIDEBAR_HEADING_TRACKING_EM,
SIDEBAR_DASH_WIDTH);
} else {
- heading(block, section.title(), SIDEBAR_HEADING_SPACER);
+ heading(block, section.title(), SIDEBAR_HEADING_TRACKING_EM);
}
int index = 0;
for (SkillGroup group : section.groups()) {
@@ -216,7 +216,7 @@ private static void renderBulletedSkills(SectionBuilder side, SkillsSection sect
private static void renderCertifications(SectionBuilder side, EntriesSection section) {
side.addSection("Certifications", block -> {
block.spacing(0);
- headingWithDash(block, section.title(), SIDEBAR_HEADING_SPACER, SIDEBAR_DASH_WIDTH);
+ headingWithDash(block, section.title(), SIDEBAR_HEADING_TRACKING_EM, SIDEBAR_DASH_WIDTH);
List entries = section.entries();
for (int index = 0; index < entries.size(); index++) {
CvEntry entry = entries.get(index);
@@ -236,7 +236,7 @@ private static void renderCertifications(SectionBuilder side, EntriesSection sec
private static void renderFacts(SectionBuilder side, EntriesSection section) {
side.addSection("AdditionalInformation", block -> {
block.spacing(0);
- heading(block, section.title(), SIDEBAR_HEADING_SPACER);
+ heading(block, section.title(), SIDEBAR_HEADING_TRACKING_EM);
List entries = section.entries();
for (int i = 0; i < entries.size(); i++) {
CvEntry entry = entries.get(i);
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java
index fa43204bf..fe1e9988d 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java
@@ -36,7 +36,8 @@
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_DASH_WIDTH;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_DIVIDER_BOTTOM;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_DIVIDER_TOP;
-import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_HEADING_SPACER;
+import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_HEADING_TRACKING_EM;
+import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MASTHEAD_TRACKING_EM;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_PAD_LEFT;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_PAD_RIGHT;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailStyles.MAIN_PAD_TOP;
@@ -70,7 +71,7 @@
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailWidgets.entryLine;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailWidgets.ringMarker;
import static com.demcha.compose.document.templates.cv.presets.TerracottaRailWidgets.titleAndDate;
-import static com.demcha.compose.document.templates.cv.presets.TerracottaRailWidgets.trackedWide;
+import static com.demcha.compose.document.templates.cv.presets.TerracottaRailWidgets.spacedBy;
/**
* The wide column: the masthead, the summary, the roles held on a rail, the
@@ -123,13 +124,13 @@ private static void renderMasthead(SectionBuilder main, CvIdentity identity) {
block.spacing(0);
block.addParagraph(p -> p
.name("FullName")
- .text(trackedWide(identity.name().full().toUpperCase(Locale.ROOT)))
- .textStyle(text(NAME_SIZE, INK, true))
+ .text(identity.name().full().toUpperCase(Locale.ROOT))
+ .textStyle(spacedBy(text(NAME_SIZE, INK, true), MASTHEAD_TRACKING_EM))
.margin(0f, 0f, (float) NAME_TO_SUBTITLE_GAP, 0f));
block.addParagraph(p -> p
.name("JobTitle")
- .text(trackedWide(identity.jobTitle().toUpperCase(Locale.ROOT)))
- .textStyle(text(SUBTITLE_SIZE, ACCENT, true))
+ .text(identity.jobTitle().toUpperCase(Locale.ROOT))
+ .textStyle(spacedBy(text(SUBTITLE_SIZE, ACCENT, true), MASTHEAD_TRACKING_EM))
.margin(0f, 0f, (float) SUBTITLE_TO_RULE_GAP, 0f));
});
}
@@ -140,7 +141,7 @@ private static void renderMasthead(SectionBuilder main, CvIdentity identity) {
private static void renderSummary(SectionBuilder main, ParagraphSection summary) {
main.addSection("Summary", block -> {
block.spacing(0);
- headingWithDash(block, summary.title(), MAIN_HEADING_SPACER, MAIN_DASH_WIDTH);
+ headingWithDash(block, summary.title(), MAIN_HEADING_TRACKING_EM, MAIN_DASH_WIDTH);
List paragraphs = lines(summary.body());
for (int i = 0; i < paragraphs.size(); i++) {
int index = i;
@@ -164,7 +165,7 @@ private static void renderSummary(SectionBuilder main, ParagraphSection summary)
private static void renderExperience(SectionBuilder main, EntriesSection experience) {
main.addSection("Experience", block -> {
block.spacing(0);
- heading(block, experience.title(), MAIN_HEADING_SPACER);
+ heading(block, experience.title(), MAIN_HEADING_TRACKING_EM);
SectionBuilder holder = new SectionBuilder();
holder.name("ExperienceRailHolder");
holder.spacing(0);
@@ -232,7 +233,7 @@ private static void renderExperience(SectionBuilder main, EntriesSection experie
private static void renderProjects(SectionBuilder main, EntriesSection projects) {
main.addSection("SelectedProjects", block -> {
block.spacing(0);
- headingWithDash(block, projects.title(), MAIN_HEADING_SPACER, MAIN_DASH_WIDTH);
+ headingWithDash(block, projects.title(), MAIN_HEADING_TRACKING_EM, MAIN_DASH_WIDTH);
List entries = projects.entries();
for (int i = 0; i < entries.size(); i++) {
CvEntry entry = entries.get(i);
@@ -306,7 +307,7 @@ private static void renderProjects(SectionBuilder main, EntriesSection projects)
private static void renderEducation(SectionBuilder main, EntriesSection education) {
main.addSection("Education", block -> {
block.spacing(0);
- headingWithDash(block, education.title(), MAIN_HEADING_SPACER, MAIN_DASH_WIDTH);
+ headingWithDash(block, education.title(), MAIN_HEADING_TRACKING_EM, MAIN_DASH_WIDTH);
layeredRow(block, "EducationBand", 0.0, 0.0, band -> {
band.weights(EDUCATION_BAND_WEIGHT, 1.0 - EDUCATION_BAND_WEIGHT);
band.addSection("EducationEntries", cell -> {
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailStyles.java
index 89be2b0a5..31f105fe3 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailStyles.java
@@ -134,15 +134,17 @@ private TerracottaRailStyles() {
// -- tracking ----------------------------------------------------------
/**
- * The gaps a heading is letter-spaced with. A text style carries no
- * tracking, so a tracked line is written with a space character between
- * the letters, and which space is the whole choice: the sidebar takes a
- * hair space (a tenth of an em) because a full one would set ADDITIONAL
- * INFORMATION wider than the column and wrap it, and the main column,
- * being wider, takes a thin space (a fifth).
+ * The gaps a heading is letter-spaced with. These were hair and thin space
+ * characters written between the letters; they are the same measures as
+ * tracking. The sidebar takes a tenth of an em because a wider one sets
+ * ADDITIONAL INFORMATION past the column and wraps it, and the main column,
+ * being wider, takes a fifth.
*/
- static final char SIDEBAR_HEADING_SPACER = '\u200A';
- static final char MAIN_HEADING_SPACER = '\u2009';
+ static final double SIDEBAR_HEADING_TRACKING_EM = 0.10;
+ static final double MAIN_HEADING_TRACKING_EM = 0.20;
+
+ /** The masthead's own tracking \u2014 it used a full space between letters. */
+ static final double MASTHEAD_TRACKING_EM = 0.25;
// -- helpers ------------------------------------------------------------
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java
index 3e2e1d269..2f095ff80 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java
@@ -13,7 +13,9 @@
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentStroke;
+import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.table.DocumentTableCell;
import com.demcha.compose.document.table.DocumentTableColumn;
import com.demcha.compose.document.table.DocumentTableStyle;
@@ -61,63 +63,27 @@ private TerracottaRailWidgets() {
* @param spacer the space to set between letters
* @return the tracked string
*/
- static String tracked(String text, char spacer) {
- if (text == null || text.isEmpty()) {
- return "";
- }
- StringBuilder out = new StringBuilder(text.length() * 2);
- for (int index = 0; index < text.length(); index++) {
- char letter = text.charAt(index);
- if (letter == ' ') {
- out.append(spacer).append(' ').append(spacer);
- continue;
- }
- if (index > 0 && text.charAt(index - 1) != ' ') {
- out.append(spacer);
- }
- out.append(letter);
- }
- return out.toString();
- }
-
- /** The masthead's own tracking: a full space between letters. */
- static String trackedWide(String text) {
- if (text == null || text.isEmpty()) {
- return "";
- }
- StringBuilder out = new StringBuilder(text.length() * 2);
- boolean startOfWord = true;
- for (int index = 0; index < text.length(); index++) {
- char letter = text.charAt(index);
- if (letter == ' ') {
- out.append(" ");
- startOfWord = true;
- continue;
- }
- if (!startOfWord) {
- out.append(' ');
- }
- out.append(letter);
- startOfWord = false;
- }
- return out.toString();
+ /** {@code style} tracked by {@code trackingEm}, the letters left whole. */
+ static DocumentTextStyle spacedBy(DocumentTextStyle style, double trackingEm) {
+ return style.withLetterSpacing(DocumentLetterSpacing.ofFontSize(trackingEm));
}
/** A heading with nothing under it. */
- static void heading(SectionBuilder block, String title, char spacer) {
+ static void heading(SectionBuilder block, String title, double trackingEm) {
block.addParagraph(p -> p
.name("Heading_" + compact(title))
- .text(tracked(title, spacer))
- .textStyle(text(SECTION_HEADING_SIZE, INK, true))
+ .text(title)
+ .textStyle(spacedBy(text(SECTION_HEADING_SIZE, INK, true), trackingEm))
.margin(0f, 0f, (float) HEADING_TO_BODY_GAP, 0f));
}
/** A heading over a short terracotta dash. */
- static void headingWithDash(SectionBuilder block, String title, char spacer, double dashWidth) {
+ static void headingWithDash(SectionBuilder block, String title, double trackingEm,
+ double dashWidth) {
block.addParagraph(p -> p
.name("Heading_" + compact(title))
- .text(tracked(title, spacer))
- .textStyle(text(SECTION_HEADING_SIZE, INK, true))
+ .text(title)
+ .textStyle(spacedBy(text(SECTION_HEADING_SIZE, INK, true), trackingEm))
.margin(0f, 0f, (float) HEADING_TO_DASH_GAP, 0f));
block.addLine(line -> line
.name("AccentLine_" + compact(title))
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java
index 6b215eeab..0c17a0cf3 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/VioletGridWidgets.java
@@ -6,6 +6,7 @@
import com.demcha.compose.document.node.InlineImageAlignment;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentRowColumn;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.font.FontName;
@@ -22,7 +23,6 @@
import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_SIZE;
import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TRACKING;
import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.HEADING_TO_RULE;
-import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.SPACE_RATIO;
import static com.demcha.compose.document.templates.cv.presets.VioletGridStyles.style;
/**
@@ -57,21 +57,15 @@ private VioletGridWidgets() {
static ParagraphBuilder tracked(String name, String text, FontName font, double size,
DocumentColor color, boolean bold, double tracking) {
DocumentTextStyle glyph = style(font, size, color, bold);
+ if (tracking > 0.0) {
+ // The gap was authored in points, so the tracking is read in points.
+ glyph = glyph.withLetterSpacing(DocumentLetterSpacing.points(tracking));
+ }
ParagraphBuilder paragraph = new ParagraphBuilder();
paragraph.name(name);
paragraph.lineSpacing(0);
paragraph.textStyle(glyph);
- if (tracking <= 0.0) {
- paragraph.text(text);
- return paragraph;
- }
- DocumentTextStyle gap = style(BODY_FONT, tracking / SPACE_RATIO, color, bold);
- for (int index = 0; index < text.length(); index++) {
- paragraph.inlineText(String.valueOf(text.charAt(index)), glyph);
- if (index < text.length() - 1) {
- paragraph.inlineText(" ", gap);
- }
- }
+ paragraph.text(text);
return paragraph;
}
From 1dc84358ec293c28a9818aceacdf9aee3a4aea7c Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 03:02:56 +0100
Subject: [PATCH 3/9] fix(templates): let the engine measure SerifHeadline's
tracked headings
The heading band was a layer stack holding three absolutely positioned
children, and the rule that trails a title was sized by subtracting an
estimate of the letters from the band: trackedWidth summed three advance
classes for capitals, added (n-1) gaps, and padded the result by 3% so the
rule would never run under the words. That estimate was a second
implementation of text measurement, and its error is visible in the old
snapshot - the gap between the words and the rule came out 10.603, 11.538
and 10.607 on the three tailed headings, when one number was intended.
The band is now a row: a fixed column for the dash, an auto column for the
words, and a weight column whose line fills what is left. The engine
measures the tracked capitals and the rule takes the remainder, so nothing
here predicts a width. tracked() writes one run carrying
DocumentLetterSpacing, and trackedWidth, capAdvanceEm, the three
CAP_ADVANCE constants and TRACKED_WIDTH_SAFETY are gone.
A row may not nest directly in the body's column row, so the stack stays as
the host - which is also what gives the row a width for its last column to
resolve against. The stack carries the dash's overhang as a negative left
inset, which both hangs the dash into the margin and hands back the width it
borrows.
Structure changed (the snapshot grows 266 -> 290 nodes: a RowNode per
heading plus its cells). Geometry held:
- title x unchanged on all six headings (36.623 / 430.457); title y unchanged.
- dash x, width and thickness unchanged (14.533 / 408.367, 12.789, 1.744).
- band right edge unchanged to the point: 374.658 and 563.593.
- every rule still ends on that edge.
- the words-to-rule gap is now 11.045-11.046 on all three tailed headings,
where it was 10.603-11.538. The spread was the estimate; it is gone.
- title widths grow 1.174-1.466, the trailing tracking advance plus the old
spacer approximation's error.
- measured deltas: the dash and rule sit 0.484 higher, because cells align to
the row top and the row is centred in the 12.595 band rather than each
child being centred in it. Sub-half-point, inside the visual parity budget.
- one page before and after; no pagination change.
Verified: 65 tests green, including the visual parity gate and the CV
text-layer gate, which now covers all 26 presets.
Co-Authored-By: Claude Opus 5
---
.../cv-v2/serif_headline_layout.json | 1016 ++++++++++++++---
.../cv/presets/SerifHeadlineStyles.java | 59 +-
.../cv/presets/SerifHeadlineWidgets.java | 102 +-
3 files changed, 937 insertions(+), 240 deletions(-)
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/serif_headline_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/serif_headline_layout.json
index a743fc3bf..fa3c4a599 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/serif_headline_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/cv-v2/serif_headline_layout.json
@@ -145,11 +145,11 @@
"computedY" : 751.271,
"placementX" : 31.682,
"placementY" : 751.271,
- "placementWidth" : 172.592,
+ "placementWidth" : 174.011,
"placementHeight" : 15.502,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 172.592,
+ "contentWidth" : 174.011,
"contentHeight" : 15.502,
"margin" : {
"top" : 0.0,
@@ -801,21 +801,21 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 31.682,
+ "computedX" : 14.533,
"computedY" : 657.394,
- "placementX" : 31.682,
+ "placementX" : 14.533,
"placementY" : 657.394,
- "placementWidth" : 342.976,
+ "placementWidth" : 360.125,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 342.976,
+ "contentWidth" : 360.125,
"contentHeight" : 12.595,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -17.149
},
"padding" : {
"top" : 0.0,
@@ -824,17 +824,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/HeadingDash_Experience[0]",
- "entityName" : "HeadingDash_Experience",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]",
+ "entityName" : "SectionHeadingRow_Experience",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]",
"childIndex" : 0,
"depth" : 5,
"layer" : 5,
"computedX" : 14.533,
- "computedY" : 662.819,
+ "computedY" : 657.878,
+ "placementX" : 14.533,
+ "placementY" : 657.878,
+ "placementWidth" : 360.125,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 360.125,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 14.533,
+ "computedY" : 662.335,
"placementX" : 14.533,
- "placementY" : 662.819,
+ "placementY" : 662.335,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[0]/HeadingDash_Experience[0]",
+ "entityName" : "HeadingDash_Experience",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
+ "computedX" : 14.533,
+ "computedY" : 662.335,
+ "placementX" : 14.533,
+ "placementY" : 662.335,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -854,22 +914,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionTitle_Experience[1]",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]",
+ "childIndex" : 1,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 27.322,
+ "computedY" : 657.878,
+ "placementX" : 27.322,
+ "placementY" : 657.878,
+ "placementWidth" : 82.974,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 82.974,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[1]/SectionTitle_Experience[0]",
"entityName" : "SectionTitle_Experience",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]",
- "childIndex" : 1,
- "depth" : 5,
- "layer" : 5,
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
"computedX" : 36.623,
"computedY" : 657.878,
"placementX" : 36.623,
"placementY" : 657.878,
- "placementWidth" : 72.353,
+ "placementWidth" : 73.673,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 72.353,
+ "contentWidth" : 73.673,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -883,6 +973,36 @@
"bottom" : 0.0,
"left" : 0.0
}
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Experience[0]/SectionHeadingRow_Experience[0]",
+ "childIndex" : 2,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 110.297,
+ "computedY" : 663.556,
+ "placementX" : 110.297,
+ "placementY" : 663.556,
+ "placementWidth" : 11.045,
+ "placementHeight" : 5.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 11.045,
+ "contentHeight" : 5.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
}, {
"path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/ExperienceRail[1]",
"entityName" : "ExperienceRail",
@@ -2481,21 +2601,21 @@
"childIndex" : 2,
"depth" : 4,
"layer" : 4,
- "computedX" : 31.682,
+ "computedX" : 14.533,
"computedY" : 285.277,
- "placementX" : 31.682,
+ "placementX" : 14.533,
"placementY" : 285.277,
- "placementWidth" : 342.976,
+ "placementWidth" : 360.125,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 342.976,
+ "contentWidth" : 360.125,
"contentHeight" : 12.595,
"margin" : {
"top" : 23.605,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -17.149
},
"padding" : {
"top" : 0.0,
@@ -2504,17 +2624,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/HeadingDash_Projects[0]",
- "entityName" : "HeadingDash_Projects",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]",
+ "entityName" : "SectionHeadingRow_Projects",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]",
"childIndex" : 0,
"depth" : 5,
"layer" : 5,
"computedX" : 14.533,
- "computedY" : 290.703,
+ "computedY" : 285.762,
+ "placementX" : 14.533,
+ "placementY" : 285.762,
+ "placementWidth" : 360.125,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 360.125,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 14.533,
+ "computedY" : 290.218,
+ "placementX" : 14.533,
+ "placementY" : 290.218,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[0]/HeadingDash_Projects[0]",
+ "entityName" : "HeadingDash_Projects",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
+ "computedX" : 14.533,
+ "computedY" : 290.218,
"placementX" : 14.533,
- "placementY" : 290.703,
+ "placementY" : 290.218,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -2534,22 +2714,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionTitle_Projects[1]",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]",
+ "childIndex" : 1,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 27.322,
+ "computedY" : 285.762,
+ "placementX" : 27.322,
+ "placementY" : 285.762,
+ "placementWidth" : 69.317,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 69.317,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[1]/SectionTitle_Projects[0]",
"entityName" : "SectionTitle_Projects",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]",
- "childIndex" : 1,
- "depth" : 5,
- "layer" : 5,
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
"computedX" : 36.623,
"computedY" : 285.762,
"placementX" : 36.623,
"placementY" : 285.762,
- "placementWidth" : 58.622,
+ "placementWidth" : 60.016,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 58.622,
+ "contentWidth" : 60.016,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -2564,22 +2774,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/HeadingTail_Projects[2]",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]",
+ "childIndex" : 2,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 96.639,
+ "computedY" : 290.439,
+ "placementX" : 96.639,
+ "placementY" : 290.439,
+ "placementWidth" : 278.019,
+ "placementHeight" : 6.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 278.019,
+ "contentHeight" : 6.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[2]/HeadingTail_Projects[0]",
"entityName" : "HeadingTail_Projects",
"entityKind" : "LineNode",
- "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]",
- "childIndex" : 2,
- "depth" : 5,
- "layer" : 5,
- "computedX" : 105.848,
- "computedY" : 291.075,
- "placementX" : 105.848,
- "placementY" : 291.075,
- "placementWidth" : 268.81,
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/MainColumn[0]/SectionHeading_Projects[2]/SectionHeadingRow_Projects[0]/SectionNode[2]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
+ "computedX" : 107.685,
+ "computedY" : 290.439,
+ "placementX" : 107.685,
+ "placementY" : 290.439,
+ "placementWidth" : 266.974,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 268.81,
+ "contentWidth" : 266.974,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -3141,21 +3381,21 @@
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
- "computedX" : 413.009,
+ "computedX" : 408.367,
"computedY" : 657.394,
- "placementX" : 413.009,
+ "placementX" : 408.367,
"placementY" : 657.394,
- "placementWidth" : 150.584,
+ "placementWidth" : 155.226,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 150.584,
+ "contentWidth" : 155.226,
"contentHeight" : 12.595,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -4.642
},
"padding" : {
"top" : 0.0,
@@ -3164,17 +3404,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/HeadingDash_Education[0]",
- "entityName" : "HeadingDash_Education",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]",
+ "entityName" : "SectionHeadingRow_Education",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]",
"childIndex" : 0,
"depth" : 5,
"layer" : 5,
"computedX" : 408.367,
- "computedY" : 662.819,
+ "computedY" : 657.878,
+ "placementX" : 408.367,
+ "placementY" : 657.878,
+ "placementWidth" : 155.226,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 155.226,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 408.367,
+ "computedY" : 662.335,
+ "placementX" : 408.367,
+ "placementY" : 662.335,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[0]/HeadingDash_Education[0]",
+ "entityName" : "HeadingDash_Education",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
+ "computedX" : 408.367,
+ "computedY" : 662.335,
"placementX" : 408.367,
- "placementY" : 662.819,
+ "placementY" : 662.335,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -3194,22 +3494,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionTitle_Education[1]",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]",
+ "childIndex" : 1,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 421.156,
+ "computedY" : 657.878,
+ "placementX" : 421.156,
+ "placementY" : 657.878,
+ "placementWidth" : 80.888,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 80.888,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[1]/SectionTitle_Education[0]",
"entityName" : "SectionTitle_Education",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]",
- "childIndex" : 1,
- "depth" : 5,
- "layer" : 5,
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
"computedX" : 430.457,
"computedY" : 657.878,
"placementX" : 430.457,
"placementY" : 657.878,
- "placementWidth" : 70.23,
+ "placementWidth" : 71.587,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 70.23,
+ "contentWidth" : 71.587,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -3223,6 +3553,36 @@
"bottom" : 0.0,
"left" : 0.0
}
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Education[0]/SectionHeadingRow_Education[0]",
+ "childIndex" : 2,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 502.045,
+ "computedY" : 663.556,
+ "placementX" : 502.045,
+ "placementY" : 663.556,
+ "placementWidth" : 11.045,
+ "placementHeight" : 5.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 11.045,
+ "contentHeight" : 5.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
}, {
"path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/Degree_MSinComputerScience[1]",
"entityName" : "Degree_MSinComputerScience",
@@ -3441,21 +3801,21 @@
"childIndex" : 8,
"depth" : 4,
"layer" : 4,
- "computedX" : 413.009,
+ "computedX" : 408.367,
"computedY" : 525.864,
- "placementX" : 413.009,
+ "placementX" : 408.367,
"placementY" : 525.864,
- "placementWidth" : 150.584,
+ "placementWidth" : 155.226,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 150.584,
+ "contentWidth" : 155.226,
"contentHeight" : 12.595,
"margin" : {
"top" : 16.973,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -4.642
},
"padding" : {
"top" : 0.0,
@@ -3464,17 +3824,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/HeadingDash_Skills[0]",
- "entityName" : "HeadingDash_Skills",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]",
+ "entityName" : "SectionHeadingRow_Skills",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]",
"childIndex" : 0,
"depth" : 5,
"layer" : 5,
"computedX" : 408.367,
- "computedY" : 531.29,
+ "computedY" : 526.349,
+ "placementX" : 408.367,
+ "placementY" : 526.349,
+ "placementWidth" : 155.226,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 155.226,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 408.367,
+ "computedY" : 530.806,
"placementX" : 408.367,
- "placementY" : 531.29,
+ "placementY" : 530.806,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[0]/HeadingDash_Skills[0]",
+ "entityName" : "HeadingDash_Skills",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
+ "computedX" : 408.367,
+ "computedY" : 530.806,
+ "placementX" : 408.367,
+ "placementY" : 530.806,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -3491,25 +3911,55 @@
"top" : 0.0,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]",
+ "childIndex" : 1,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 421.156,
+ "computedY" : 526.349,
+ "placementX" : 421.156,
+ "placementY" : 526.349,
+ "placementWidth" : 48.82,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 48.82,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
}
}, {
- "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionTitle_Skills[1]",
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[1]/SectionTitle_Skills[0]",
"entityName" : "SectionTitle_Skills",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]",
- "childIndex" : 1,
- "depth" : 5,
- "layer" : 5,
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 7,
+ "layer" : 7,
"computedX" : 430.457,
"computedY" : 526.349,
"placementX" : 430.457,
"placementY" : 526.349,
- "placementWidth" : 38.052,
+ "placementWidth" : 39.518,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 38.052,
+ "contentWidth" : 39.518,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -3523,6 +3973,36 @@
"bottom" : 0.0,
"left" : 0.0
}
+ }, {
+ "path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SectionHeading_Skills[8]/SectionHeadingRow_Skills[0]",
+ "childIndex" : 2,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 469.976,
+ "computedY" : 532.026,
+ "placementX" : 469.976,
+ "placementY" : 532.026,
+ "placementWidth" : 11.045,
+ "placementHeight" : 5.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 11.045,
+ "contentHeight" : 5.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
}, {
"path" : "SerifHeadlineCv[0]/BodyColumns[3]/AsideColumn[1]/SkillCaption_Languages[9]",
"entityName" : "SkillCaption_Languages",
@@ -3535,11 +4015,11 @@
"computedY" : 506.519,
"placementX" : 413.009,
"placementY" : 506.519,
- "placementWidth" : 50.545,
+ "placementWidth" : 51.949,
"placementHeight" : 8.284,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 50.545,
+ "contentWidth" : 51.949,
"contentHeight" : 8.284,
"margin" : {
"top" : 11.061,
@@ -4075,11 +4555,11 @@
"computedY" : 438.072,
"placementX" : 413.009,
"placementY" : 438.072,
- "placementWidth" : 115.415,
+ "placementWidth" : 117.201,
"placementHeight" : 8.284,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 115.415,
+ "contentWidth" : 117.201,
"contentHeight" : 8.284,
"margin" : {
"top" : 12.605,
@@ -4735,11 +5215,11 @@
"computedY" : 359.064,
"placementX" : 413.009,
"placementY" : 359.064,
- "placementWidth" : 49.012,
+ "placementWidth" : 50.417,
"placementHeight" : 8.284,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 49.012,
+ "contentWidth" : 50.417,
"contentHeight" : 8.284,
"margin" : {
"top" : 12.605,
@@ -5155,11 +5635,11 @@
"computedY" : 301.178,
"placementX" : 413.009,
"placementY" : 301.178,
- "placementWidth" : 105.596,
+ "placementWidth" : 107.322,
"placementHeight" : 8.284,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 105.596,
+ "contentWidth" : 107.322,
"contentHeight" : 8.284,
"margin" : {
"top" : 12.605,
@@ -5815,11 +6295,11 @@
"computedY" : 222.169,
"placementX" : 413.009,
"placementY" : 222.169,
- "placementWidth" : 52.046,
+ "placementWidth" : 53.509,
"placementHeight" : 8.284,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 52.046,
+ "contentWidth" : 53.509,
"contentHeight" : 8.284,
"margin" : {
"top" : 12.605,
@@ -5961,21 +6441,21 @@
"childIndex" : 0,
"depth" : 3,
"layer" : 3,
- "computedX" : 31.682,
+ "computedX" : 14.533,
"computedY" : 171.346,
- "placementX" : 31.682,
+ "placementX" : 14.533,
"placementY" : 171.346,
- "placementWidth" : 531.911,
+ "placementWidth" : 549.06,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 531.911,
+ "contentWidth" : 549.06,
"contentHeight" : 12.595,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -17.149
},
"padding" : {
"top" : 0.0,
@@ -5984,17 +6464,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/HeadingDash_Certifications[0]",
- "entityName" : "HeadingDash_Certifications",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]",
+ "entityName" : "SectionHeadingRow_Certifications",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]",
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
"computedX" : 14.533,
- "computedY" : 176.772,
+ "computedY" : 171.831,
+ "placementX" : 14.533,
+ "placementY" : 171.831,
+ "placementWidth" : 549.06,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 549.06,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]",
+ "childIndex" : 0,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 14.533,
+ "computedY" : 176.287,
"placementX" : 14.533,
- "placementY" : 176.772,
+ "placementY" : 176.287,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[0]/HeadingDash_Certifications[0]",
+ "entityName" : "HeadingDash_Certifications",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 14.533,
+ "computedY" : 176.287,
+ "placementX" : 14.533,
+ "placementY" : 176.287,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -6014,22 +6554,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionTitle_Certifications[1]",
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]",
+ "childIndex" : 1,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 27.322,
+ "computedY" : 171.831,
+ "placementX" : 27.322,
+ "placementY" : 171.831,
+ "placementWidth" : 109.078,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 109.078,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[1]/SectionTitle_Certifications[0]",
"entityName" : "SectionTitle_Certifications",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]",
- "childIndex" : 1,
- "depth" : 4,
- "layer" : 4,
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
"computedX" : 36.623,
"computedY" : 171.831,
"placementX" : 36.623,
"placementY" : 171.831,
- "placementWidth" : 98.603,
+ "placementWidth" : 99.777,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 98.603,
+ "contentWidth" : 99.777,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -6044,22 +6614,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/HeadingTail_Certifications[2]",
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]",
+ "childIndex" : 2,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 136.4,
+ "computedY" : 176.508,
+ "placementX" : 136.4,
+ "placementY" : 176.508,
+ "placementWidth" : 427.193,
+ "placementHeight" : 6.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 427.193,
+ "contentHeight" : 6.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[2]/HeadingTail_Certifications[0]",
"entityName" : "HeadingTail_Certifications",
"entityKind" : "LineNode",
- "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]",
- "childIndex" : 2,
- "depth" : 4,
- "layer" : 4,
- "computedX" : 146.764,
- "computedY" : 177.144,
- "placementX" : 146.764,
- "placementY" : 177.144,
- "placementWidth" : 416.83,
+ "parentPath" : "SerifHeadlineCv[0]/Certifications[4]/SectionHeading_Certifications[0]/SectionHeadingRow_Certifications[0]/SectionNode[2]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 147.445,
+ "computedY" : 176.508,
+ "placementX" : 147.445,
+ "placementY" : 176.508,
+ "placementWidth" : 416.148,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 416.83,
+ "contentWidth" : 416.148,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
@@ -7281,21 +7881,21 @@
"childIndex" : 0,
"depth" : 3,
"layer" : 3,
- "computedX" : 31.682,
+ "computedX" : 14.533,
"computedY" : 97.242,
- "placementX" : 31.682,
+ "placementX" : 14.533,
"placementY" : 97.242,
- "placementWidth" : 531.911,
+ "placementWidth" : 549.06,
"placementHeight" : 12.595,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 531.911,
+ "contentWidth" : 549.06,
"contentHeight" : 12.595,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"bottom" : 0.0,
- "left" : 0.0
+ "left" : -17.149
},
"padding" : {
"top" : 0.0,
@@ -7304,17 +7904,77 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/HeadingDash_Achievements[0]",
- "entityName" : "HeadingDash_Achievements",
- "entityKind" : "LineNode",
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]",
+ "entityName" : "SectionHeadingRow_Achievements",
+ "entityKind" : "RowNode",
"parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]",
"childIndex" : 0,
"depth" : 4,
"layer" : 4,
"computedX" : 14.533,
- "computedY" : 102.668,
+ "computedY" : 97.727,
+ "placementX" : 14.533,
+ "placementY" : 97.727,
+ "placementWidth" : 549.06,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 549.06,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[0]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]",
+ "childIndex" : 0,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 14.533,
+ "computedY" : 102.183,
+ "placementX" : 14.533,
+ "placementY" : 102.183,
+ "placementWidth" : 12.789,
+ "placementHeight" : 7.17,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 12.789,
+ "contentHeight" : 7.17,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.426,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[0]/HeadingDash_Achievements[0]",
+ "entityName" : "HeadingDash_Achievements",
+ "entityKind" : "LineNode",
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[0]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 14.533,
+ "computedY" : 102.183,
"placementX" : 14.533,
- "placementY" : 102.668,
+ "placementY" : 102.183,
"placementWidth" : 12.789,
"placementHeight" : 1.744,
"startPage" : 0,
@@ -7334,22 +7994,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionTitle_Achievements[1]",
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[1]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]",
+ "childIndex" : 1,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 27.322,
+ "computedY" : 97.727,
+ "placementX" : 27.322,
+ "placementY" : 97.727,
+ "placementWidth" : 102.997,
+ "placementHeight" : 11.626,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 102.997,
+ "contentHeight" : 11.626,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 9.301
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[1]/SectionTitle_Achievements[0]",
"entityName" : "SectionTitle_Achievements",
"entityKind" : "ParagraphNode",
- "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]",
- "childIndex" : 1,
- "depth" : 4,
- "layer" : 4,
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[1]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
"computedX" : 36.623,
"computedY" : 97.727,
"placementX" : 36.623,
"placementY" : 97.727,
- "placementWidth" : 92.449,
+ "placementWidth" : 93.696,
"placementHeight" : 11.626,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 92.449,
+ "contentWidth" : 93.696,
"contentHeight" : 11.626,
"margin" : {
"top" : 0.0,
@@ -7364,22 +8054,52 @@
"left" : 0.0
}
}, {
- "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/HeadingTail_Achievements[2]",
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[2]",
+ "entityName" : null,
+ "entityKind" : "SectionNode",
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]",
+ "childIndex" : 2,
+ "depth" : 5,
+ "layer" : 5,
+ "computedX" : 130.319,
+ "computedY" : 102.404,
+ "placementX" : 130.319,
+ "placementY" : 102.404,
+ "placementWidth" : 433.274,
+ "placementHeight" : 6.949,
+ "startPage" : 0,
+ "endPage" : 0,
+ "contentWidth" : 433.274,
+ "contentHeight" : 6.949,
+ "margin" : {
+ "top" : 0.0,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 0.0
+ },
+ "padding" : {
+ "top" : 5.949,
+ "right" : 0.0,
+ "bottom" : 0.0,
+ "left" : 11.045
+ }
+ }, {
+ "path" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[2]/HeadingTail_Achievements[0]",
"entityName" : "HeadingTail_Achievements",
"entityKind" : "LineNode",
- "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]",
- "childIndex" : 2,
- "depth" : 4,
- "layer" : 4,
- "computedX" : 139.679,
- "computedY" : 103.04,
- "placementX" : 139.679,
- "placementY" : 103.04,
- "placementWidth" : 423.915,
+ "parentPath" : "SerifHeadlineCv[0]/Achievements[5]/SectionHeading_Achievements[0]/SectionHeadingRow_Achievements[0]/SectionNode[2]",
+ "childIndex" : 0,
+ "depth" : 6,
+ "layer" : 6,
+ "computedX" : 141.365,
+ "computedY" : 102.404,
+ "placementX" : 141.365,
+ "placementY" : 102.404,
+ "placementWidth" : 422.229,
"placementHeight" : 1.0,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 423.915,
+ "contentWidth" : 422.229,
"contentHeight" : 1.0,
"margin" : {
"top" : 0.0,
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineStyles.java
index 1d06ab064..61d7967cd 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineStyles.java
@@ -5,6 +5,7 @@
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextDecoration;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.templates.core.text.TextStyles;
import com.demcha.compose.font.FontName;
@@ -330,65 +331,19 @@ static double titleOffset(double leftSurfaceDistance) {
// -- tracked capitals ------------------------------------------------
- static final double CAP_ADVANCE_NARROW = 0.30;
- static final double CAP_ADVANCE_WIDE = 0.85;
- static final double CAP_ADVANCE_EM = 0.62;
-
- /** Headroom on the estimate, so a tail never runs under its heading. */
- static final double TRACKED_WIDTH_SAFETY = 1.03;
-
/**
- * Writes text letter by letter with a sized space between each pair,
- * which is how this design gets its letter-spacing: a text style carries
- * no tracking, so the gap is a run of its own whose type size is chosen
- * to advance by {@link #TRACKING_EM} of the surrounding size.
+ * Writes {@code text} as one run tracked by {@link #TRACKING_EM}.
+ *
+ * The letters stay one string, so a caption reads and copies as the
+ * word it is rather than as its letters with spaces between them.
*
* @param paragraph the paragraph being built
* @param text the text to space out
* @param style the style of the letters
*/
static void tracked(ParagraphBuilder paragraph, String text, DocumentTextStyle style) {
- DocumentTextStyle spacer = style.withSize(TRACKING_EM * style.size() / SPACE_ADVANCE_EM);
- for (int i = 0; i < text.length(); i++) {
- paragraph.inlineText(String.valueOf(text.charAt(i)), style);
- if (i < text.length() - 1) {
- paragraph.inlineText(" ", spacer);
- }
- }
- }
-
- /**
- * How wide a run of tracked capitals will be, estimated from per-letter
- * advances rather than measured.
- *
- * The heading rule that trails a title has to stop before the letters
- * start, and its width is authored — the engine offers no measurement at
- * compose time. Three advance classes are enough for capitals, and the
- * result is padded by {@link #TRACKED_WIDTH_SAFETY} so the estimate errs
- * towards a shorter rule.
- *
- * @param text the capitals to measure
- * @param size the type size
- * @return the estimated width in points
- */
- static double trackedWidth(String text, double size) {
- if (text.isEmpty()) {
- return 0.0;
- }
- double glyphs = 0.0;
- for (int i = 0; i < text.length(); i++) {
- glyphs += capAdvanceEm(text.charAt(i));
- }
- return (glyphs * size + (text.length() - 1) * size * TRACKING_EM)
- * TRACKED_WIDTH_SAFETY;
- }
-
- private static double capAdvanceEm(char glyph) {
- return switch (glyph) {
- case 'I', 'J' -> CAP_ADVANCE_NARROW;
- case 'M', 'W' -> CAP_ADVANCE_WIDE;
- default -> CAP_ADVANCE_EM;
- };
+ paragraph.inlineText(text, style.withLetterSpacing(
+ DocumentLetterSpacing.ofFontSize(TRACKING_EM)));
}
// -- bands -----------------------------------------------------------
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
index 8dd32fb0e..161e13f0a 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
@@ -3,11 +3,14 @@
import com.demcha.compose.document.dsl.ImageBuilder;
import com.demcha.compose.document.dsl.LineBuilder;
import com.demcha.compose.document.dsl.ParagraphBuilder;
+import com.demcha.compose.document.dsl.RowBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.dsl.ShapeContainerBuilder;
import com.demcha.compose.document.node.DocumentNode;
import com.demcha.compose.document.node.LayerAlign;
import com.demcha.compose.document.style.ClipPolicy;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
+import com.demcha.compose.document.style.DocumentRowColumn;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
@@ -24,6 +27,7 @@
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.PLATE;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.RULE;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.TAIL_GAP;
+import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.TRACKING_EM;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.TIGHT_LEADING;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.COLUMN_TAIL_GUTTER;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.columnInsets;
@@ -31,7 +35,6 @@
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.dashOffset;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.style;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.titleOffset;
-import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.trackedWidth;
import static com.demcha.compose.document.templates.cv.presets.SerifHeadlineStyles.tracked;
/**
@@ -69,46 +72,65 @@ static void sectionHeading(SectionBuilder section, String title, double width,
double dashAt = dashOffset(leftSurfaceDistance);
double titleAt = titleOffset(leftSurfaceDistance);
- DocumentTextStyle headingStyle = style(HEADING_SIZE, INK, DocumentTextDecoration.DEFAULT);
- DocumentNode dash = new LineBuilder()
- .name("HeadingDash_" + compact(title))
- .horizontal(DASH_WIDTH)
- .thickness(DASH_THICKNESS)
- .color(INK)
- .margin(DocumentInsets.zero())
- .build();
- ParagraphBuilder heading = new ParagraphBuilder()
- .name("SectionTitle_" + compact(title))
- .textStyle(headingStyle)
- .lineSpacing(TIGHT_LEADING);
- tracked(heading, caps, headingStyle);
- DocumentNode headingNode = heading.margin(DocumentInsets.zero()).build();
-
- // The rule is authored, so its width comes from an estimate of what
- // the letters will measure — see trackedWidth.
- double tailWidth = width - titleAt - trackedWidth(caps, HEADING_SIZE) - TAIL_GAP;
- DocumentNode tailNode = tail && tailWidth > 0
- ? new LineBuilder()
- .name("HeadingTail_" + compact(title))
- .horizontal(tailWidth)
- .thickness(HAIRLINE_THICKNESS)
- .color(RULE)
- .margin(DocumentInsets.zero())
- .build()
- : null;
+ DocumentTextStyle headingStyle = style(HEADING_SIZE, INK, DocumentTextDecoration.DEFAULT)
+ .withLetterSpacing(DocumentLetterSpacing.ofFontSize(TRACKING_EM));
+ double band = HEADING_SIZE * TIGHT_LEADING;
- section.addContainer(band -> {
- band.name("SectionHeading_" + compact(title))
- .rectangle(width, HEADING_SIZE * TIGHT_LEADING)
- .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
- .padding(DocumentInsets.zero())
- .margin(new DocumentInsets(gapAbove, 0, 0, 0))
- .position(dash, dashAt, 0, LayerAlign.CENTER_LEFT)
- .position(headingNode, titleAt, 0, LayerAlign.CENTER_LEFT);
- if (tailNode != null) {
- band.position(tailNode, 0, 0, LayerAlign.CENTER_RIGHT);
- }
- });
+ // A row, not a stack of positioned layers: the words take what they
+ // measure and the rule takes the rest, so nothing here has to predict
+ // how wide tracked capitals will be. The row hangs left by the dash
+ // offset — a negative inset, which also hands back the width it
+ // borrows, leaving the right edge where it was.
+ RowBuilder row = new RowBuilder();
+ {
+ row.name("SectionHeadingRow_" + compact(title));
+ row.spacing(0);
+ row.margin(DocumentInsets.zero());
+ row.columns(DocumentRowColumn.fixed(DASH_WIDTH),
+ DocumentRowColumn.auto(),
+ DocumentRowColumn.weight(1.0));
+ row.addSection(cell -> {
+ cell.spacing(0);
+ cell.padding((float) ((band - DASH_THICKNESS) / 2), 0f, 0f, 0f);
+ cell.addLine(line -> line
+ .name("HeadingDash_" + compact(title))
+ .horizontal(DASH_WIDTH)
+ .thickness(DASH_THICKNESS)
+ .color(INK));
+ });
+ row.addSection(cell -> {
+ cell.spacing(0);
+ cell.padding(0f, 0f, 0f, (float) (titleAt - dashAt - DASH_WIDTH));
+ cell.addParagraph(p -> p
+ .name("SectionTitle_" + compact(title))
+ .text(caps)
+ .textStyle(headingStyle)
+ .lineSpacing(TIGHT_LEADING));
+ });
+ row.addSection(cell -> {
+ cell.spacing(0);
+ cell.padding((float) ((band - HAIRLINE_THICKNESS) / 2), 0f, 0f, (float) TAIL_GAP);
+ if (tail) {
+ cell.addLine(line -> line
+ .name("HeadingTail_" + compact(title))
+ .fill()
+ .thickness(HAIRLINE_THICKNESS)
+ .color(RULE));
+ }
+ });
+ }
+
+ // The band stays a layer stack: a row may not nest directly inside the
+ // body's column row, and the stack is also what hands the row a width
+ // for its last column to take what the words leave. It is widened by
+ // the dash's overhang so the rule still ends on the column's edge.
+ section.addContainer(bandHost -> bandHost
+ .name("SectionHeading_" + compact(title))
+ .rectangle(width - dashAt, band)
+ .clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
+ .padding(DocumentInsets.zero())
+ .margin(new DocumentInsets(gapAbove, 0, 0, dashAt))
+ .position(row.build(), 0, 0, LayerAlign.CENTER_LEFT));
}
/**
From 0565c518c3438ab10d5c1cccfa14b84deaada6b8 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 09:16:51 +0100
Subject: [PATCH 4/9] fix(templates): track both invoice mastheads with the pen
The last two presets built tracking by writing something that is not the
text. ConsultingText.tracked spliced a space between every character of the
authored String, so "BILLED" was authored as "B I L L E D" and copied that
way. LumaStudioWidgets.tracked kept the String whole but pushed a 0.05pt
rectangle of the ground colour between each pair of letters, so the glyphs
sat far enough apart that an extractor invented the word breaks itself.
Different mechanisms, same result for a reader.
Consulting: all three call sites migrate to the semantic string plus
ConsultingStyles.HEADING_TRACKING, one space advance of whichever size the
run is set at, and the String helper is deleted. The section rule beneath a
heading is a fixed 30pt, so no width depended on the mutated text.
LumaStudio: the three overloads now build one run carrying
DocumentLetterSpacing.points(tracking) - the gaps were authored in points, so
the tracking is read in points. The six tracking measures and their differing
styles, alignments and vertical alignments are untouched. The ground colour
is gone from the signature and its seven call sites: it existed only to paint
the spacer rectangles, and there are none.
Geometry, measured on all four invoice snapshots: width only. Same node
count, same totalPages - 1, 2, 1 and 3 - and zero nodes changed page
ownership, including both overflow cases. 29 tests green, both visual parity
gates green.
Co-Authored-By: Claude Opus 5
---
.../invoice/consulting_invoice_layout.json | 20 +++----
.../consulting_invoice_overflow_layout.json | 20 +++----
.../invoice/luma_studio_invoice_layout.json | 56 +++++++++----------
.../luma_studio_invoice_overflow_layout.json | 56 +++++++++----------
.../invoice/presets/ConsultingMasthead.java | 14 ++---
.../invoice/presets/ConsultingStyles.java | 10 ++++
.../invoice/presets/ConsultingText.java | 17 ------
.../invoice/presets/LumaStudioClosing.java | 3 +-
.../invoice/presets/LumaStudioLines.java | 2 +-
.../invoice/presets/LumaStudioMasthead.java | 6 +-
.../invoice/presets/LumaStudioSidebar.java | 2 +-
.../invoice/presets/LumaStudioWidgets.java | 47 +++++++---------
12 files changed, 118 insertions(+), 135 deletions(-)
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_layout.json
index 71f81a1bc..01549e355 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_layout.json
@@ -565,11 +565,11 @@
"computedY" : 778.406,
"placementX" : 339.231,
"placementY" : 778.406,
- "placementWidth" : 128.001,
+ "placementWidth" : 139.263,
"placementHeight" : 32.984,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 128.001,
+ "contentWidth" : 139.263,
"contentHeight" : 32.984,
"margin" : {
"top" : 0.0,
@@ -1135,11 +1135,11 @@
"computedY" : 647.17,
"placementX" : 30.5,
"placementY" : 647.17,
- "placementWidth" : 56.469,
+ "placementWidth" : 61.266,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 56.469,
+ "contentWidth" : 61.266,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -1345,11 +1345,11 @@
"computedY" : 647.17,
"placementX" : 340.102,
"placementY" : 647.17,
- "placementWidth" : 109.748,
+ "placementWidth" : 116.508,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 109.748,
+ "contentWidth" : 116.508,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -1945,11 +1945,11 @@
"computedY" : 276.092,
"placementX" : 30.5,
"placementY" : 276.092,
- "placementWidth" : 139.604,
+ "placementWidth" : 147.672,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 139.604,
+ "contentWidth" : 147.672,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -2635,11 +2635,11 @@
"computedY" : 276.092,
"placementX" : 353.129,
"placementY" : 276.092,
- "placementWidth" : 36.122,
+ "placementWidth" : 39.611,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 36.122,
+ "contentWidth" : 39.611,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_overflow_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_overflow_layout.json
index fda4ec506..a89205baf 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_overflow_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/consulting_invoice_overflow_layout.json
@@ -565,11 +565,11 @@
"computedY" : 778.406,
"placementX" : 339.231,
"placementY" : 778.406,
- "placementWidth" : 128.001,
+ "placementWidth" : 139.263,
"placementHeight" : 32.984,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 128.001,
+ "contentWidth" : 139.263,
"contentHeight" : 32.984,
"margin" : {
"top" : 0.0,
@@ -1135,11 +1135,11 @@
"computedY" : 647.17,
"placementX" : 30.5,
"placementY" : 647.17,
- "placementWidth" : 56.469,
+ "placementWidth" : 61.266,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 56.469,
+ "contentWidth" : 61.266,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -1345,11 +1345,11 @@
"computedY" : 647.17,
"placementX" : 340.102,
"placementY" : 647.17,
- "placementWidth" : 109.748,
+ "placementWidth" : 116.508,
"placementHeight" : 12.236,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 109.748,
+ "contentWidth" : 116.508,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -1945,11 +1945,11 @@
"computedY" : 350.872,
"placementX" : 30.5,
"placementY" : 350.872,
- "placementWidth" : 139.604,
+ "placementWidth" : 147.672,
"placementHeight" : 12.236,
"startPage" : 1,
"endPage" : 1,
- "contentWidth" : 139.604,
+ "contentWidth" : 147.672,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
@@ -2635,11 +2635,11 @@
"computedY" : 350.872,
"placementX" : 353.129,
"placementY" : 350.872,
- "placementWidth" : 36.122,
+ "placementWidth" : 39.611,
"placementHeight" : 12.236,
"startPage" : 1,
"endPage" : 1,
- "contentWidth" : 36.122,
+ "contentWidth" : 39.611,
"contentHeight" : 12.236,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_layout.json
index 129d7aebb..a5d36c9cd 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_layout.json
@@ -385,11 +385,11 @@
"computedY" : 675.395,
"placementX" : 33.288,
"placementY" : 675.395,
- "placementWidth" : 82.984,
+ "placementWidth" : 83.966,
"placementHeight" : 9.524,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 82.984,
+ "contentWidth" : 83.966,
"contentHeight" : 9.524,
"margin" : {
"top" : 0.0,
@@ -1045,11 +1045,11 @@
"computedY" : 736.636,
"placementX" : 389.878,
"placementY" : 736.636,
- "placementWidth" : 164.751,
+ "placementWidth" : 164.817,
"placementHeight" : 55.553,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 164.751,
+ "contentWidth" : 164.817,
"contentHeight" : 55.553,
"margin" : {
"top" : 0.0,
@@ -1135,11 +1135,11 @@
"computedY" : 720.957,
"placementX" : 389.878,
"placementY" : 720.957,
- "placementWidth" : 57.062,
+ "placementWidth" : 58.497,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 57.062,
+ "contentWidth" : 58.497,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1225,11 +1225,11 @@
"computedY" : 703.458,
"placementX" : 389.878,
"placementY" : 703.458,
- "placementWidth" : 51.552,
+ "placementWidth" : 52.989,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 51.552,
+ "contentWidth" : 52.989,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1315,11 +1315,11 @@
"computedY" : 685.958,
"placementX" : 389.878,
"placementY" : 685.958,
- "placementWidth" : 43.963,
+ "placementWidth" : 45.405,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 43.963,
+ "contentWidth" : 45.405,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1405,11 +1405,11 @@
"computedY" : 668.458,
"placementX" : 389.878,
"placementY" : 668.458,
- "placementWidth" : 75.89,
+ "placementWidth" : 77.32,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 75.89,
+ "contentWidth" : 77.32,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1495,11 +1495,11 @@
"computedY" : 650.958,
"placementX" : 389.878,
"placementY" : 650.958,
- "placementWidth" : 46.267,
+ "placementWidth" : 47.709,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 46.267,
+ "contentWidth" : 47.709,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1645,11 +1645,11 @@
"computedY" : 611.326,
"placementX" : 160.808,
"placementY" : 611.326,
- "placementWidth" : 33.342,
+ "placementWidth" : 34.786,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 33.342,
+ "contentWidth" : 34.786,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1735,11 +1735,11 @@
"computedY" : 611.326,
"placementX" : 379.157,
"placementY" : 611.326,
- "placementWidth" : 35.173,
+ "placementWidth" : 36.618,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 35.173,
+ "contentWidth" : 36.618,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1885,11 +1885,11 @@
"computedY" : 248.821,
"placementX" : 348.204,
"placementY" : 248.821,
- "placementWidth" : 46.299,
+ "placementWidth" : 46.895,
"placementHeight" : 11.704,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 46.299,
+ "contentWidth" : 46.895,
"contentHeight" : 11.704,
"margin" : {
"top" : 0.0,
@@ -1975,11 +1975,11 @@
"computedY" : 230.712,
"placementX" : 348.204,
"placementY" : 230.712,
- "placementWidth" : 45.982,
+ "placementWidth" : 46.575,
"placementHeight" : 11.704,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 45.982,
+ "contentWidth" : 46.575,
"contentHeight" : 11.704,
"margin" : {
"top" : 0.0,
@@ -2065,11 +2065,11 @@
"computedY" : 204.85,
"placementX" : 348.204,
"placementY" : 204.85,
- "placementWidth" : 52.971,
+ "placementWidth" : 53.564,
"placementHeight" : 12.507,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 52.971,
+ "contentWidth" : 53.564,
"contentHeight" : 12.507,
"margin" : {
"top" : 0.0,
@@ -2365,11 +2365,11 @@
"computedY" : 165.271,
"placementX" : 188.974,
"placementY" : 165.271,
- "placementWidth" : 28.177,
+ "placementWidth" : 29.626,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 28.177,
+ "contentWidth" : 29.626,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -2665,11 +2665,11 @@
"computedY" : 165.271,
"placementX" : 380.304,
"placementY" : 165.271,
- "placementWidth" : 82.727,
+ "placementWidth" : 84.151,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 82.727,
+ "contentWidth" : 84.151,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
diff --git a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_overflow_layout.json b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_overflow_layout.json
index 7592ce06c..edc19a435 100644
--- a/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_overflow_layout.json
+++ b/qa/src/test/resources/layout-snapshots/canonical-templates/invoice/luma_studio_invoice_overflow_layout.json
@@ -385,11 +385,11 @@
"computedY" : 675.395,
"placementX" : 33.288,
"placementY" : 675.395,
- "placementWidth" : 82.984,
+ "placementWidth" : 83.966,
"placementHeight" : 9.524,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 82.984,
+ "contentWidth" : 83.966,
"contentHeight" : 9.524,
"margin" : {
"top" : 0.0,
@@ -1045,11 +1045,11 @@
"computedY" : 736.636,
"placementX" : 389.878,
"placementY" : 736.636,
- "placementWidth" : 164.751,
+ "placementWidth" : 164.817,
"placementHeight" : 55.553,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 164.751,
+ "contentWidth" : 164.817,
"contentHeight" : 55.553,
"margin" : {
"top" : 0.0,
@@ -1135,11 +1135,11 @@
"computedY" : 720.957,
"placementX" : 389.878,
"placementY" : 720.957,
- "placementWidth" : 57.062,
+ "placementWidth" : 58.497,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 57.062,
+ "contentWidth" : 58.497,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1225,11 +1225,11 @@
"computedY" : 703.458,
"placementX" : 389.878,
"placementY" : 703.458,
- "placementWidth" : 51.552,
+ "placementWidth" : 52.989,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 51.552,
+ "contentWidth" : 52.989,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1315,11 +1315,11 @@
"computedY" : 685.958,
"placementX" : 389.878,
"placementY" : 685.958,
- "placementWidth" : 43.963,
+ "placementWidth" : 45.405,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 43.963,
+ "contentWidth" : 45.405,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1405,11 +1405,11 @@
"computedY" : 668.458,
"placementX" : 389.878,
"placementY" : 668.458,
- "placementWidth" : 75.89,
+ "placementWidth" : 77.32,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 75.89,
+ "contentWidth" : 77.32,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1495,11 +1495,11 @@
"computedY" : 650.958,
"placementX" : 389.878,
"placementY" : 650.958,
- "placementWidth" : 46.267,
+ "placementWidth" : 47.709,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 46.267,
+ "contentWidth" : 47.709,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1645,11 +1645,11 @@
"computedY" : 611.326,
"placementX" : 160.808,
"placementY" : 611.326,
- "placementWidth" : 33.342,
+ "placementWidth" : 34.786,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 33.342,
+ "contentWidth" : 34.786,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1735,11 +1735,11 @@
"computedY" : 611.326,
"placementX" : 379.157,
"placementY" : 611.326,
- "placementWidth" : 35.173,
+ "placementWidth" : 36.618,
"placementHeight" : 9.765,
"startPage" : 0,
"endPage" : 0,
- "contentWidth" : 35.173,
+ "contentWidth" : 36.618,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -1885,11 +1885,11 @@
"computedY" : 159.439,
"placementX" : 348.204,
"placementY" : 159.439,
- "placementWidth" : 46.299,
+ "placementWidth" : 46.895,
"placementHeight" : 11.704,
"startPage" : 1,
"endPage" : 1,
- "contentWidth" : 46.299,
+ "contentWidth" : 46.895,
"contentHeight" : 11.704,
"margin" : {
"top" : 0.0,
@@ -1975,11 +1975,11 @@
"computedY" : 141.331,
"placementX" : 348.204,
"placementY" : 141.331,
- "placementWidth" : 45.982,
+ "placementWidth" : 46.575,
"placementHeight" : 11.704,
"startPage" : 1,
"endPage" : 1,
- "contentWidth" : 45.982,
+ "contentWidth" : 46.575,
"contentHeight" : 11.704,
"margin" : {
"top" : 0.0,
@@ -2065,11 +2065,11 @@
"computedY" : 115.468,
"placementX" : 348.204,
"placementY" : 115.468,
- "placementWidth" : 52.971,
+ "placementWidth" : 53.564,
"placementHeight" : 12.507,
"startPage" : 1,
"endPage" : 1,
- "contentWidth" : 52.971,
+ "contentWidth" : 53.564,
"contentHeight" : 12.507,
"margin" : {
"top" : 0.0,
@@ -2365,11 +2365,11 @@
"computedY" : 766.307,
"placementX" : 188.974,
"placementY" : 766.307,
- "placementWidth" : 28.177,
+ "placementWidth" : 29.626,
"placementHeight" : 9.765,
"startPage" : 2,
"endPage" : 2,
- "contentWidth" : 28.177,
+ "contentWidth" : 29.626,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
@@ -2665,11 +2665,11 @@
"computedY" : 766.307,
"placementX" : 380.304,
"placementY" : 766.307,
- "placementWidth" : 82.727,
+ "placementWidth" : 84.151,
"placementHeight" : 9.765,
"startPage" : 2,
"endPage" : 2,
- "contentWidth" : 82.727,
+ "contentWidth" : 84.151,
"contentHeight" : 9.765,
"margin" : {
"top" : 0.0,
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingMasthead.java
index 96ef659ae..7e7470cab 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingMasthead.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingMasthead.java
@@ -33,7 +33,7 @@
import static com.demcha.compose.document.templates.invoice.presets.ConsultingStyles.SMALL_BOLD;
import static com.demcha.compose.document.templates.invoice.presets.ConsultingStyles.SUPPLIER_WIDTH;
import static com.demcha.compose.document.templates.invoice.presets.ConsultingText.link;
-import static com.demcha.compose.document.templates.invoice.presets.ConsultingText.tracked;
+import static com.demcha.compose.document.templates.invoice.presets.ConsultingStyles.HEADING_TRACKING;
/**
* The masthead of the Consulting Invoice: the sender's lockup and contact
@@ -116,8 +116,8 @@ private static void renderTextLockup(SectionBuilder section, InvoiceBrand brand)
.margin(DocumentInsets.zero()))
.addParagraph(paragraph -> paragraph
.text(brand.qualifier().isBlank()
- ? "" : "— " + tracked(brand.qualifier()) + " —")
- .textStyle(BRAND_QUALIFIER)
+ ? "" : "— " + brand.qualifier() + " —")
+ .textStyle(BRAND_QUALIFIER.withLetterSpacing(HEADING_TRACKING))
.align(TextAlign.CENTER)
.margin(DocumentInsets.zero()))
.addParagraph(paragraph -> paragraph
@@ -152,8 +152,8 @@ static void renderInvoiceHeader(SectionBuilder section, StructuredInvoiceData da
.padding(0, 0, 0, 24)
.spacing(1)
.addParagraph(paragraph -> paragraph
- .text(tracked(masthead.title()))
- .textStyle(INVOICE_TITLE)
+ .text(masthead.title())
+ .textStyle(INVOICE_TITLE.withLetterSpacing(HEADING_TRACKING))
.margin(DocumentInsets.bottom(8)));
for (InvoiceMasthead.Entry entry : masthead.entries()) {
metadataBand(section, entry);
@@ -206,8 +206,8 @@ static void sectionHeading(SectionBuilder section, String heading) {
return;
}
section.addParagraph(paragraph -> paragraph
- .text(tracked(heading))
- .textStyle(SECTION_HEADING)
+ .text(heading)
+ .textStyle(SECTION_HEADING.withLetterSpacing(HEADING_TRACKING))
.lineSpacing(1.0)
.margin(DocumentInsets.zero()))
.addLine(line -> line
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingStyles.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingStyles.java
index 722332b7d..aca17f74a 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingStyles.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingStyles.java
@@ -3,6 +3,7 @@
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentStroke;
import com.demcha.compose.document.style.DocumentTextDecoration;
import com.demcha.compose.document.style.DocumentTextStyle;
@@ -98,6 +99,15 @@ private ConsultingStyles() {
style(SECTION_SIZE, DocumentTextDecoration.BOLD, ACCENT_PRIMARY);
static final DocumentTextStyle INVOICE_TITLE =
style(TITLE_SIZE, DocumentTextDecoration.BOLD, ACCENT_PRIMARY);
+
+ /**
+ * The heading treatment's tracking. It used to be a space written between
+ * every pair of letters, so the gap it replaces is one space advance — a
+ * quarter of whichever size the run carrying it is set at.
+ */
+ static final DocumentLetterSpacing HEADING_TRACKING =
+ DocumentLetterSpacing.ofFontSize(0.25);
+
static final DocumentTextStyle BRAND_NAME =
style(BODY_SIZE * 2.45, DocumentTextDecoration.BOLD, INK);
static final DocumentTextStyle BRAND_MARK =
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingText.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingText.java
index be22cabac..1bacab48c 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingText.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/ConsultingText.java
@@ -82,23 +82,6 @@ private static Font loadDescriptionBoldFont() {
return new Font("SansSerif", Font.BOLD, 1);
}
- /**
- * Spaces a value out into tracked capitals (the heading treatment).
- *
- * @param value the text, already in the case it is set in
- * @return the tracked text
- */
- static String tracked(String value) {
- StringBuilder tracked = new StringBuilder(value.length() * 2);
- for (int index = 0; index < value.length(); index++) {
- if (index > 0) {
- tracked.append(' ');
- }
- tracked.append(value.charAt(index));
- }
- return tracked.toString();
- }
-
/**
* Formats a quantity at two decimal places, without grouping.
*
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
index aed7054c3..0b85099cd 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
@@ -98,8 +98,7 @@ private static void totalsRow(SectionBuilder section, String label, String amoun
.clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.position(tracked("TotalsLabel", label,
emphasised ? TOTAL_DUE_LABEL : TOTAL_LABEL,
- TRACK_TOTAL_LABEL,
- emphasised ? PANEL_HIGHLIGHT : PAPER),
+ TRACK_TOTAL_LABEL),
TOTALS_LABEL_INSET, 0, LayerAlign.CENTER_LEFT)
.position(paragraph(amount,
emphasised ? TOTAL_DUE_AMOUNT : TOTAL_AMOUNT,
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
index d1a2fc6d3..2726af7ab 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
@@ -117,7 +117,7 @@ private static DocumentTableColumn column(int index) {
*/
private static DocumentTableCell headerCell(String text, TextAlign align) {
return DocumentTableCell.node(tracked("HeaderCell", text, TABLE_HEADER,
- TRACK_TABLE_HEADER, INK_SURFACE, align, TextVerticalAlign.DEFAULT));
+ TRACK_TABLE_HEADER, align, TextVerticalAlign.DEFAULT));
}
/**
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
index ab75a03b0..918a1d218 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
@@ -156,7 +156,7 @@ static void renderTitle(SectionBuilder section, InvoiceMasthead masthead) {
.rectangle(META_WIDTH, TITLE_BLOCK_HEIGHT)
.clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.position(tracked("InvoiceTitle", masthead.title(), INVOICE_TITLE,
- TRACK_TITLE, PAPER, TextVerticalAlign.TOP),
+ TRACK_TITLE, TextVerticalAlign.TOP),
0, TITLE_CAP_INSET, LayerAlign.TOP_LEFT)
.position(new LineBuilder()
.name("TitleRule")
@@ -173,7 +173,7 @@ static void renderTitle(SectionBuilder section, InvoiceMasthead masthead) {
.rectangle(META_WIDTH, META_ROW_HEIGHT)
.clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.centerLeft(tracked("MetaLabel", entry.label(), META_LABEL,
- TRACK_META_LABEL, PAPER))
+ TRACK_META_LABEL))
.position(paragraph(entry.value(), BODY, TextAlign.LEFT),
META_WIDTH * META_VALUE_SPLIT, 0, LayerAlign.CENTER_LEFT));
}
@@ -220,7 +220,7 @@ static void renderParty(SectionBuilder section, InvoiceRecipient party, boolean
section.padding(new DocumentInsets(0, 0, 0, TEXT_INSET));
}
section.add(tracked("PartyHeading", heading, SECTION_HEADING,
- TRACK_SECTION_HEADING, PAPER))
+ TRACK_SECTION_HEADING))
.addParagraph(p -> p
.name("PartyAddress")
.text(String.join(NEWLINE, addressOf(party)))
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioSidebar.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioSidebar.java
index 4d3d39b2b..d9e81bfab 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioSidebar.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioSidebar.java
@@ -108,7 +108,7 @@ private static DocumentNode lockup(InvoiceBrand brand) {
.build(),
LOCKUP_LEFT, LOCKUP_RULE_Y, LayerAlign.TOP_LEFT)
.position(tracked("Wordmark", brand.name(), WORDMARK,
- TRACK_WORDMARK, ACCENT, TextVerticalAlign.TOP),
+ TRACK_WORDMARK, TextVerticalAlign.TOP),
LOCKUP_LEFT, WORDMARK_Y, LayerAlign.TOP_LEFT)
.position(seated(brand.tagline(), TAGLINE),
LOCKUP_LEFT, TAGLINE_Y, LayerAlign.TOP_LEFT)
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
index ce754986a..df91ba552 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
@@ -12,6 +12,7 @@
import com.demcha.compose.document.style.ClipPolicy;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.style.ShapeOutline;
@@ -35,21 +36,20 @@ private LumaStudioWidgets() {
/**
* A run of letter-spaced text.
*
- * A text style carries no tracking, so the gap between letters is an
- * inline rectangle of the ground colour — invisible, and exactly as wide
- * as the spacing asks for. The rectangle is 0.05pt tall so it takes no
- * part in the line box.
+ * The letters stay one string and the pen carries the tracking, so a
+ * label reads and copies as the word it is. The gaps used to be inline
+ * rectangles of the ground colour, which is why this once took a ground
+ * to paint them in and no longer does.
*
* @param name the node name
* @param text the text to space out
* @param style the style of the letters
* @param tracking the gap in points; zero or less writes the text plainly
- * @param ground the colour behind the run, which the gaps are painted in
* @return the paragraph node
*/
static DocumentNode tracked(String name, String text, DocumentTextStyle style,
- double tracking, DocumentColor ground) {
- return tracked(name, text, style, tracking, ground, TextVerticalAlign.DEFAULT);
+ double tracking) {
+ return tracked(name, text, style, tracking, TextVerticalAlign.DEFAULT);
}
/**
@@ -59,14 +59,12 @@ static DocumentNode tracked(String name, String text, DocumentTextStyle style,
* @param text the text to space out
* @param style the style of the letters
* @param tracking the gap in points
- * @param ground the colour the gaps are painted in
* @param verticalAlign where the text sits in its box
* @return the paragraph node
*/
static DocumentNode tracked(String name, String text, DocumentTextStyle style,
- double tracking, DocumentColor ground,
- TextVerticalAlign verticalAlign) {
- return tracked(name, text, style, tracking, ground, TextAlign.LEFT, verticalAlign);
+ double tracking, TextVerticalAlign verticalAlign) {
+ return tracked(name, text, style, tracking, TextAlign.LEFT, verticalAlign);
}
/**
@@ -77,32 +75,25 @@ static DocumentNode tracked(String name, String text, DocumentTextStyle style,
* @param text the text to space out
* @param style the style of the letters
* @param tracking the gap in points
- * @param ground the colour the gaps are painted in
* @param align where the run sits across its box
* @param verticalAlign where the text sits in its box
* @return the paragraph node
*/
static DocumentNode tracked(String name, String text, DocumentTextStyle style,
- double tracking, DocumentColor ground, TextAlign align,
+ double tracking, TextAlign align,
TextVerticalAlign verticalAlign) {
- ParagraphBuilder paragraph = new ParagraphBuilder()
+ DocumentTextStyle run = tracking > 0
+ ? style.withLetterSpacing(DocumentLetterSpacing.points(tracking))
+ : style;
+ return new ParagraphBuilder()
.name(name)
- .textStyle(style)
+ .textStyle(run)
.align(align)
.verticalAlign(verticalAlign)
.lineSpacing(0)
- .margin(DocumentInsets.zero());
- if (tracking <= 0) {
- return paragraph.text(text).build();
- }
- return paragraph.rich(rich -> {
- for (int index = 0; index < text.length(); index++) {
- if (index > 0) {
- rich.shape(new ShapeOutline.Rectangle(tracking, 0.05), ground);
- }
- rich.style(String.valueOf(text.charAt(index)), style);
- }
- }).build();
+ .margin(DocumentInsets.zero())
+ .text(text)
+ .build();
}
/** A plain paragraph as a node, for anchoring inside a container. */
@@ -166,7 +157,7 @@ static void sectionHead(SectionBuilder section, String iconToken, String heading
.clipPolicy(ClipPolicy.OVERFLOW_VISIBLE)
.centerLeft(disc(SECTION_DISC_SIZE, iconToken, SECTION_DISC_ICON_SIZE))
.position(tracked("SectionHeading", heading, SECTION_HEADING,
- TRACK_SECTION_HEADING, PAPER),
+ TRACK_SECTION_HEADING),
SECTION_HEADING_OFFSET, 0, LayerAlign.CENTER_LEFT));
}
}
From 099991a9005a4df76febb06f9b5c6dff4ca3b66e Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 09:21:01 +0100
Subject: [PATCH 5/9] test(templates): hold the promoted templates' text layer
to the words they draw
Ten shipped presets spelled their headings out in the text layer and nothing
caught it, because the one gate that asserts this - CvPresetTextLayerTest -
listed the sixteen older CV presets and none of the promoted ones. That list
is why the old presets were fine and the new ones were not.
- CvPresetTextLayerTest now covers all 26 CV presets. OrangeOps names Oswald
and leaves registering it to the caller, so the session carries it, as its
own snapshot test does.
- TemplateTextLayerGateTest covers the families the CV gate cannot reach. It
pins BILLED, INVOICE and PROFILE by name, and keeps ModernReceipt as the
control: that preset has carried native spaced caps since it shipped, so if
a change to the detector ever reddens it, the detector is wrong.
The detector is asserted against fixtures of its own, which is the part worth
keeping. It matches a run of three or more single letters separated by any
Unicode space separator, not just U+0020 - one preset spelled its headings
with hair and thin spaces, and a plain-space class reads straight past that.
Four cases pin it: ordinary spaces caught, hair and thin spaces caught, a
word tracked with the pen passed, and prose left alone.
That last pair is the lesson from the first sweep of this defect, which ran on
a PDFBox that ignores ActualText and called twenty-two correct templates
broken. A detector nobody has tried to fool is a detector nobody should trust,
so this one is run against both classes before it is believed.
Three fixtures widen to public to be reachable from one gate rather than
three; ProfessionalSidebarFixtures already was.
Co-Authored-By: Claude Opus 5
---
.../templates/TemplateTextLayerGateTest.java | 187 ++++++++++++++++++
.../cv/presets/CvPresetTextLayerTest.java | 24 ++-
.../presets/ConsultingInvoiceFixtures.java | 4 +-
.../presets/LumaStudioInvoiceFixtures.java | 4 +-
.../receipt/presets/ReceiptFixtures.java | 4 +-
5 files changed, 216 insertions(+), 7 deletions(-)
create mode 100644 qa/src/test/java/com/demcha/compose/document/templates/TemplateTextLayerGateTest.java
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/TemplateTextLayerGateTest.java b/qa/src/test/java/com/demcha/compose/document/templates/TemplateTextLayerGateTest.java
new file mode 100644
index 000000000..c6c570506
--- /dev/null
+++ b/qa/src/test/java/com/demcha/compose/document/templates/TemplateTextLayerGateTest.java
@@ -0,0 +1,187 @@
+package com.demcha.compose.document.templates;
+
+import com.demcha.compose.GraphCompose;
+import com.demcha.compose.document.api.DocumentSession;
+import com.demcha.compose.document.style.DocumentLetterSpacing;
+import com.demcha.compose.document.style.DocumentTextStyle;
+import com.demcha.compose.document.templates.cv.presets.ProfessionalSidebar;
+import com.demcha.compose.document.templates.cv.presets.ProfessionalSidebarFixtures;
+import com.demcha.compose.document.templates.invoice.presets.ConsultingInvoice;
+import com.demcha.compose.document.templates.invoice.presets.ConsultingInvoiceFixtures;
+import com.demcha.compose.document.templates.invoice.presets.LumaStudioInvoice;
+import com.demcha.compose.document.templates.invoice.presets.LumaStudioInvoiceFixtures;
+import com.demcha.compose.document.templates.receipt.presets.ModernReceipt;
+import com.demcha.compose.document.templates.receipt.presets.ReceiptFixtures;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The text layer of the promoted templates says the words they draw.
+ *
+ * Ten shipped presets once made their tracking by writing something that is
+ * not the text — a space between every letter, a hair space, a run of sized
+ * spacers, an invisible rectangle — so a heading drawn as PROFILE came out of
+ * the file as {@code P R O F I L E} and a name could not be searched for. The
+ * engine tracks with the pen and states the run's own {@code ActualText}, so
+ * the words survive. This gate holds them to that.
+ *
+ * The CV family is covered preset by preset in
+ * {@code CvPresetTextLayerTest}; this one covers the other families the defect
+ * reached, and pins the three labels by name.
+ *
+ * The detector is asserted against fixtures of its own below. That matters
+ * more than it looks: the first sweep of this defect was run on a PDFBox that
+ * ignores {@code ActualText}, and it called twenty-two correct templates
+ * broken. A detector nobody has tried to fool is a detector nobody should
+ * trust.
+ */
+class TemplateTextLayerGateTest {
+
+ /**
+ * Three or more single letters, each separated by one space character.
+ *
+ * Any space separator counts, not just {@code U+0020}: one of the
+ * presets spelled its headings with hair and thin spaces, which a plain
+ * space class reads straight past.
+ */
+ private static final Pattern SPELLED_OUT =
+ Pattern.compile("(?
+ ConsultingInvoice.create().compose(
+ session, ConsultingInvoiceFixtures.canonicalInvoice()));
+
+ assertThat(extracted).contains("BILLED");
+ assertThat(extracted).doesNotContain("B I L L E D");
+ assertThat(spelledOutRuns(extracted)).isEmpty();
+ }
+
+ @Test
+ void theLumaStudioInvoiceSaysInvoice() throws Exception {
+ String extracted = extract(session ->
+ LumaStudioInvoice.create().compose(
+ session, LumaStudioInvoiceFixtures.canonicalInvoice()));
+
+ assertThat(extracted).contains("INVOICE");
+ assertThat(extracted).doesNotContain("I N V O I C E");
+ assertThat(spelledOutRuns(extracted)).isEmpty();
+ }
+
+ @Test
+ void theProfessionalSidebarSaysProfile() throws Exception {
+ String extracted = extract(session ->
+ ProfessionalSidebar.create().compose(
+ session, ProfessionalSidebarFixtures.canonicalCv()));
+
+ assertThat(extracted).contains("PROFILE");
+ assertThat(extracted).doesNotContain("P R O F I L E");
+ assertThat(spelledOutRuns(extracted)).isEmpty();
+ }
+
+ /**
+ * The control. This preset never faked its tracking — it has carried
+ * native spaced caps from the day it shipped — so it is the case that must
+ * keep passing. If a change to the detector ever reddens this one, the
+ * detector is wrong, not the receipt.
+ */
+ @Test
+ void theReceiptsNativeSpacedCapsStayClean() throws Exception {
+ String extracted = extract(session ->
+ ModernReceipt.create().compose(session, ReceiptFixtures.canonicalReceipt()));
+
+ assertThat(extracted).contains("AMOUNT COLLECTED");
+ assertThat(spelledOutRuns(extracted)).isEmpty();
+ }
+
+ // -- the detector itself -----------------------------------------------
+
+ @Test
+ void theDetectorCatchesLettersSpelledOutWithOrdinarySpaces() throws Exception {
+ String extracted = extract(session -> session.pageFlow(page -> page
+ .addParagraph("P R O F I L E")));
+
+ assertThat(spelledOutRuns(extracted)).contains("P R O F I L E");
+ }
+
+ /**
+ * The failure one preset actually shipped: the gaps were hair and thin
+ * spaces, so a detector written against {@code " "} alone reads the line
+ * as a single word and passes it.
+ */
+ @Test
+ void theDetectorCatchesLettersSpelledOutWithHairAndThinSpaces() throws Exception {
+ String hair = "P R O F I L E";
+ String thin = "B I L L E D";
+
+ assertThat(spelledOutRuns(hair)).isNotEmpty();
+ assertThat(spelledOutRuns(thin)).isNotEmpty();
+ }
+
+ @Test
+ void theDetectorPassesAWordTrackedWithThePen() throws Exception {
+ String extracted = extract(session -> session.pageFlow(page -> page
+ .addParagraph(p -> p
+ .text("PROFILE")
+ .textStyle(DocumentTextStyle.builder()
+ .size(18)
+ .letterSpacing(DocumentLetterSpacing.ofFontSize(0.3))
+ .build()))));
+
+ assertThat(extracted).contains("PROFILE");
+ assertThat(spelledOutRuns(extracted)).isEmpty();
+ }
+
+ @Test
+ void theDetectorLeavesOrdinaryProseAlone() {
+ // Nothing spelled out: headings, prose, and initials short enough to be
+ // words rather than a spelled-out run.
+ assertThat(spelledOutRuns("PROFILE EXPERIENCE EDUCATION")).isEmpty();
+ assertThat(spelledOutRuns("Nothing here is spelled out letter by letter")).isEmpty();
+ assertThat(spelledOutRuns("Ordinary prose with short words a b and c")).isEmpty();
+
+ // And it still fires on a genuine run buried in prose.
+ assertThat(spelledOutRuns("Delivery lead, a s p e c i a l case aside"))
+ .containsExactly("a s p e c i a l");
+ }
+
+ // -- helpers -----------------------------------------------------------
+
+ private static List spelledOutRuns(String text) {
+ Matcher matcher = SPELLED_OUT.matcher(text);
+ List runs = new ArrayList<>();
+ while (matcher.find()) {
+ runs.add(matcher.group());
+ }
+ return runs;
+ }
+
+ private interface Composition {
+ void compose(DocumentSession session);
+ }
+
+ private static String extract(Composition composition) throws IOException {
+ byte[] pdf;
+ try (DocumentSession session = GraphCompose.document().create()) {
+ composition.compose(session);
+ pdf = session.toPdfBytes();
+ }
+ try (PDDocument document = Loader.loadPDF(pdf)) {
+ // One line break is a wrapping decision, not a text-layer defect.
+ return new PDFTextStripper().getText(document).replaceAll("\\s*\\R\\s*", " ");
+ }
+ }
+}
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/CvPresetTextLayerTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/CvPresetTextLayerTest.java
index b26f46eb7..2eccef5cd 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/CvPresetTextLayerTest.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/CvPresetTextLayerTest.java
@@ -49,6 +49,9 @@ class CvPresetTextLayerTest {
private static final List PROBES =
List.of("Platform", "certification", "retired", "drafts", "fifteen");
+ /** Margin for presets whose design paints to the page edge. */
+ private static final double FULL_BLEED = 0.0;
+
@ParameterizedTest(name = "{0}")
@MethodSource("presets")
void theProfileTextIsInTheFileAsItWasWritten(
@@ -105,6 +108,9 @@ private static String renderText(DocumentTemplate template, double m
.pageSize(DocumentPageSize.A4)
.margin(m, m, m, m)
.create()) {
+ // OrangeOps names Oswald and leaves registering it to the caller, so
+ // every session here carries it; the other presets do not ask for it.
+ OrangeOpsTestFont.register(session);
template.compose(session, probeDocument());
pdf = session.toPdfBytes();
}
@@ -154,7 +160,23 @@ private static Stream presets() {
preset("engineering_resume", EngineeringResume.RECOMMENDED_MARGIN, EngineeringResume::create),
preset("monogram_sidebar", MonogramSidebar.RECOMMENDED_MARGIN, MonogramSidebar::create),
preset("sidebar_portrait", SidebarPortrait.RECOMMENDED_MARGIN, SidebarPortrait::create),
- preset("mint_editorial", MintEditorial.RECOMMENDED_MARGIN, MintEditorial::create));
+ preset("mint_editorial", MintEditorial.RECOMMENDED_MARGIN, MintEditorial::create),
+ // The promoted presets. They were absent from this list while each
+ // grew its own letter-by-letter tracker, which is exactly how ten
+ // of them shipped spelling their headings out in the text layer.
+ preset("charcoal_gold", CharcoalGold.RECOMMENDED_MARGIN, CharcoalGold::create),
+ preset("navy_sidebar", NavySidebar.RECOMMENDED_MARGIN, NavySidebar::create),
+ preset("professional_sidebar", ProfessionalSidebar.RECOMMENDED_MARGIN, ProfessionalSidebar::create),
+ preset("serif_headline", SerifHeadline.RECOMMENDED_MARGIN, SerifHeadline::create),
+ preset("terracotta_rail", TerracottaRail.RECOMMENDED_MARGIN, TerracottaRail::create),
+ // Full-bleed designs: they paint to the page edge and publish no
+ // recommended margin, so they are composed the way their own
+ // snapshot tests compose them.
+ preset("midnight_navy", FULL_BLEED, MidnightNavy::create),
+ preset("orange_ops", FULL_BLEED, OrangeOps::create),
+ preset("slate_orange", FULL_BLEED, SlateOrange::create),
+ preset("teal_pulse", FULL_BLEED, TealPulse::create),
+ preset("violet_grid", FULL_BLEED, VioletGrid::create));
}
private static Arguments preset(String slug, double margin,
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ConsultingInvoiceFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ConsultingInvoiceFixtures.java
index 17f39b942..a9a07edd6 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ConsultingInvoiceFixtures.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/ConsultingInvoiceFixtures.java
@@ -36,13 +36,13 @@
* {@code ConsultingInvoiceSampleData} — the two modules cannot share a
* source file, so a content change here belongs there too.
*/
-final class ConsultingInvoiceFixtures {
+public final class ConsultingInvoiceFixtures {
private ConsultingInvoiceFixtures() {
}
/** The single-page reference invoice. */
- static StructuredInvoiceDocumentSpec canonicalInvoice() {
+ public static StructuredInvoiceDocumentSpec canonicalInvoice() {
return StructuredInvoiceDocumentSpec.from(baseBuilder(serviceLines(5)).build());
}
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioInvoiceFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioInvoiceFixtures.java
index b0115f9bb..f40f24f36 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioInvoiceFixtures.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioInvoiceFixtures.java
@@ -21,7 +21,7 @@
* geometry shift the pixel budget absorbs still trips the exact snapshot,
* and vice versa.
*/
-final class LumaStudioInvoiceFixtures {
+public final class LumaStudioInvoiceFixtures {
/** The break the sheet stacks its multi-line notes on. */
private static final String NEWLINE = String.valueOf((char) 10);
@@ -34,7 +34,7 @@ private LumaStudioInvoiceFixtures() {
* exercising the sidebar lockup, both party blocks, the VAT column, the
* total-due band, the notes and payment pair, and the sign-off band.
*/
- static StructuredInvoiceDocumentSpec canonicalInvoice() {
+ public static StructuredInvoiceDocumentSpec canonicalInvoice() {
return StructuredInvoiceDocumentSpec.from(base().build());
}
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/receipt/presets/ReceiptFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/receipt/presets/ReceiptFixtures.java
index b43ff1516..94a096670 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/receipt/presets/ReceiptFixtures.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/receipt/presets/ReceiptFixtures.java
@@ -11,7 +11,7 @@
* Kept in the test tree and built only from main code, so the gates depend on the
* templates module and not on the examples module.
*/
-final class ReceiptFixtures {
+public final class ReceiptFixtures {
private ReceiptFixtures() {
}
@@ -23,7 +23,7 @@ private ReceiptFixtures() {
*
* @return the canonical receipt spec shared by every receipt gate
*/
- static ReceiptDocumentSpec canonicalReceipt() {
+ public static ReceiptDocumentSpec canonicalReceipt() {
return ReceiptDocumentSpec.of(receipt -> receipt
.documentTitle("Transfer confirmation")
.issuerName("Northwind Pay")
From 80611e12d83b9e610f5d64fa789e5670bc7f854d Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 09:23:23 +0100
Subject: [PATCH 6/9] chore(templates): drop the imports the tracking migration
left behind
The spacer runs are gone, so the ground colours they were painted in and the
builders that assembled them are unreferenced: PAPER and INK_SURFACE across
the LumaStudio sheet, DocumentColor and ShapeOutline in its widgets, and
ParagraphBuilder in SerifHeadlineWidgets now that the heading is a row.
Two imports in TerracottaRailWidgets are also unused - EllipseBuilder and
ENTRY_INDENT - but both were already unused before this branch, so they are
left where they are rather than folded into an unrelated change.
Co-Authored-By: Claude Opus 5
---
.../document/templates/cv/presets/SerifHeadlineWidgets.java | 1 -
.../document/templates/invoice/presets/LumaStudioClosing.java | 1 -
.../document/templates/invoice/presets/LumaStudioLines.java | 1 -
.../document/templates/invoice/presets/LumaStudioMasthead.java | 1 -
.../document/templates/invoice/presets/LumaStudioWidgets.java | 3 ---
5 files changed, 7 deletions(-)
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
index 161e13f0a..46da72dd3 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SerifHeadlineWidgets.java
@@ -2,7 +2,6 @@
import com.demcha.compose.document.dsl.ImageBuilder;
import com.demcha.compose.document.dsl.LineBuilder;
-import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.RowBuilder;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.dsl.ShapeContainerBuilder;
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
index 0b85099cd..371da5c82 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioClosing.java
@@ -38,7 +38,6 @@
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PAGE_MARGIN_LEFT;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PAGE_MARGIN_RIGHT;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PANEL_HIGHLIGHT;
-import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PAPER;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.RULE_THICKNESS;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SECTION_HEADING_OFFSET;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SMALL_BOLD;
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
index 2726af7ab..118ac4abd 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioLines.java
@@ -19,7 +19,6 @@
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.BODY_BOLD;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.CONTENT_WIDTH;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.HAIRLINE;
-import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.INK_SURFACE;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.ITEM_DESC;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.ITEM_INDEX;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.ITEM_TITLE;
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
index 918a1d218..114992963 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioMasthead.java
@@ -29,7 +29,6 @@
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.META_WIDTH;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.MASTHEAD_TO_RULE;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.MICRO_MUTED;
-import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PAPER;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PARTIES_DIVIDER_INSET;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.RULE_THICKNESS;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SECTION_HEADING;
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
index df91ba552..26881ddb8 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/invoice/presets/LumaStudioWidgets.java
@@ -10,14 +10,11 @@
import com.demcha.compose.document.node.TextAlign;
import com.demcha.compose.document.node.TextVerticalAlign;
import com.demcha.compose.document.style.ClipPolicy;
-import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentLetterSpacing;
import com.demcha.compose.document.style.DocumentTextStyle;
-import com.demcha.compose.document.style.ShapeOutline;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.ACCENT;
-import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.PAPER;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SECTION_DISC_ICON_SIZE;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SECTION_DISC_SIZE;
import static com.demcha.compose.document.templates.invoice.presets.LumaStudioStyles.SECTION_HEADING;
From d4e62b38267fbf1a42e21247ea85c52ef4853271 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 13 Sep 2026 09:29:17 +0100
Subject: [PATCH 7/9] chore(examples): re-render the ten previews the tracking
migration moved
Every preview whose template stopped writing its tracking as characters is
regenerated from the migrated engine: eight CV sheets and both invoices.
CommittedAssetDriftTest is green again.
Only those ten moved. A content hash over the page streams also flagged six
unrelated previews - arabic-article, both engine decks, hebrew-invoice,
linkedin-carousel and maven-banner - but the drift gate, which drops what the
machine writes rather than comparing bytes, reads them as unchanged. They are
left alone rather than committed as churn.
Also drops a bare block left around the row construction in
SerifHeadlineWidgets while it was being reshaped; it scoped nothing.
Co-Authored-By: Claude Opus 5
---
.../readme/examples/cv-charcoal-gold-v2.pdf | Bin 35766 -> 35843 bytes
.../readme/examples/cv-midnight-navy-v2.pdf | Bin 37022 -> 37075 bytes
assets/readme/examples/cv-navy-sidebar-v2.pdf | Bin 34375 -> 34553 bytes
.../examples/cv-professional-sidebar-v2.pdf | Bin 53171 -> 53438 bytes
.../readme/examples/cv-serif-headline-v2.pdf | Bin 83129 -> 83345 bytes
assets/readme/examples/cv-teal-pulse-v2.pdf | Bin 41472 -> 39420 bytes
.../readme/examples/cv-terracotta-rail-v2.pdf | Bin 40771 -> 41017 bytes
assets/readme/examples/cv-violet-grid-v2.pdf | Bin 45158 -> 45228 bytes
.../readme/examples/invoice-consulting-v2.pdf | Bin 91964 -> 92152 bytes
.../examples/invoice-luma-studio-v2.pdf | Bin 48610 -> 46408 bytes
.../cv/presets/SerifHeadlineWidgets.java | 70 +++++++++---------
11 files changed, 34 insertions(+), 36 deletions(-)
diff --git a/assets/readme/examples/cv-charcoal-gold-v2.pdf b/assets/readme/examples/cv-charcoal-gold-v2.pdf
index f3190db15471c5b4ad494789741ce09c894df8e7..a0ea137d29e1ebc1e8f5a49c771198a4c6cec37e 100644
GIT binary patch
delta 12414
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
JNL2>;e*qvHE~fwh
delta 12289
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
HIn@6J7O>%A
diff --git a/assets/readme/examples/cv-midnight-navy-v2.pdf b/assets/readme/examples/cv-midnight-navy-v2.pdf
index 91f11bbfebbc834c8b31b27a37f214fe45285cbb..931e5bfbea1f02a49de582c03942e8015a37d42a 100644
GIT binary patch
delta 10256
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
delta 10135
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
diff --git a/assets/readme/examples/cv-navy-sidebar-v2.pdf b/assets/readme/examples/cv-navy-sidebar-v2.pdf
index d9356c094ff606f9174608ea2f0665feb270e621..06b4ef5f24678926537018ff4d8e928bf26eb6c9 100644
GIT binary patch
delta 7030
zcma)gRZtvS(=G1K;O-inVQ>jfa3?@;0>Rw}hXiMUKyU~_f)DOGgS!NGcL{+6`E$PS
zzg71<+^YMuyLYedTDu>1uWEQa(nA7LvK2BPNEA{hKnjtP#RZ?eJxKz<3F#(=hTLn-Z6#^y`x^R=pN`d^R$mXI
zot?ROyZRa$de7HX1caDN7=;90T|ix5p5euJr$6Pqbc|=4FVE8(FRL5ox`Kn72&&$f
z_~u@ixjFOIV}lESbp`)C+io8;jLSDv#Ome1XNp``M!o2)W!O}nIi48MmI#{ZoI4X`
zi=Lujt8}t_mFu>i1ym+0g*YF!ci}4v&MftPsW$9PjW08`U(!0_Qul3A-mpDv&|z$_
zML^$)G4GWdl}|@eA^4z7<*#
z7nQz}PH9IrQ^ELD7UHN>Q#T2WL{4dE;d;G-MOe9&OeL