diff --git a/assets/readme/examples/cv-charcoal-gold-v2.pdf b/assets/readme/examples/cv-charcoal-gold-v2.pdf index f3190db15..a0ea137d2 100644 Binary files a/assets/readme/examples/cv-charcoal-gold-v2.pdf and b/assets/readme/examples/cv-charcoal-gold-v2.pdf differ diff --git a/assets/readme/examples/cv-midnight-navy-v2.pdf b/assets/readme/examples/cv-midnight-navy-v2.pdf index 91f11bbfe..931e5bfbe 100644 Binary files a/assets/readme/examples/cv-midnight-navy-v2.pdf and b/assets/readme/examples/cv-midnight-navy-v2.pdf differ diff --git a/assets/readme/examples/cv-navy-sidebar-v2.pdf b/assets/readme/examples/cv-navy-sidebar-v2.pdf index d9356c094..06b4ef5f2 100644 Binary files a/assets/readme/examples/cv-navy-sidebar-v2.pdf and b/assets/readme/examples/cv-navy-sidebar-v2.pdf differ diff --git a/assets/readme/examples/cv-professional-sidebar-v2.pdf b/assets/readme/examples/cv-professional-sidebar-v2.pdf index 59801874b..c61d2a66e 100644 Binary files a/assets/readme/examples/cv-professional-sidebar-v2.pdf and b/assets/readme/examples/cv-professional-sidebar-v2.pdf differ diff --git a/assets/readme/examples/cv-serif-headline-v2.pdf b/assets/readme/examples/cv-serif-headline-v2.pdf index b86f5b925..d105a3cd7 100644 Binary files a/assets/readme/examples/cv-serif-headline-v2.pdf and b/assets/readme/examples/cv-serif-headline-v2.pdf differ diff --git a/assets/readme/examples/cv-teal-pulse-v2.pdf b/assets/readme/examples/cv-teal-pulse-v2.pdf index 72277c2da..e1f9822a1 100644 Binary files a/assets/readme/examples/cv-teal-pulse-v2.pdf and b/assets/readme/examples/cv-teal-pulse-v2.pdf differ diff --git a/assets/readme/examples/cv-terracotta-rail-v2.pdf b/assets/readme/examples/cv-terracotta-rail-v2.pdf index f122afc92..e5457a93a 100644 Binary files a/assets/readme/examples/cv-terracotta-rail-v2.pdf and b/assets/readme/examples/cv-terracotta-rail-v2.pdf differ diff --git a/assets/readme/examples/cv-violet-grid-v2.pdf b/assets/readme/examples/cv-violet-grid-v2.pdf index 9d165c9c7..db0398ce5 100644 Binary files a/assets/readme/examples/cv-violet-grid-v2.pdf and b/assets/readme/examples/cv-violet-grid-v2.pdf differ diff --git a/assets/readme/examples/invoice-consulting-v2.pdf b/assets/readme/examples/invoice-consulting-v2.pdf index da323a244..cd77c060c 100644 Binary files a/assets/readme/examples/invoice-consulting-v2.pdf and b/assets/readme/examples/invoice-consulting-v2.pdf differ diff --git a/assets/readme/examples/invoice-luma-studio-v2.pdf b/assets/readme/examples/invoice-luma-studio-v2.pdf index 35493c777..c6dd269d1 100644 Binary files a/assets/readme/examples/invoice-luma-studio-v2.pdf and b/assets/readme/examples/invoice-luma-studio-v2.pdf differ 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..bf94b5a2e --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/TemplateTextLayerGateTest.java @@ -0,0 +1,219 @@ +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 com.demcha.compose.document.templates.rota.presets.CobaltRota; +import com.demcha.compose.document.templates.rota.presets.CobaltRotaFixtures; +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 brand qualifier is tracked; the rules that flank it are not. + * + *

Reached only when the brand carries no logo, which no committed + * preview does — so this is the one place the text lockup is drawn at all. + * The tracking belongs to the word: applying it to the whole run would + * spread the two rules away from the word they point at, which is a + * styling change rather than the text-layer fix, and centre alignment + * hides it from the layout snapshot.

+ */ + @Test + void onlyTheBrandQualifierIsTracked() throws Exception { + String extracted = extract(session -> + ConsultingInvoice.create().compose( + session, ConsultingInvoiceFixtures.logolessInvoice())); + + assertThat(extracted).contains("NORTHPOINT"); + assertThat(extracted).contains("CONSULTING"); + assertThat(spelledOutRuns(extracted)).isEmpty(); + } + + /** The rota family's only preset, which no committed preview renders. */ + @Test + void theRotaReadsBackAsWords() throws Exception { + String extracted = extract(session -> + CobaltRota.create().compose(session, CobaltRotaFixtures.canonicalRota())); + + 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/coverletter/presets/CoverLetterTextLayerGateTest.java b/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterTextLayerGateTest.java new file mode 100644 index 000000000..98b6489cf --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterTextLayerGateTest.java @@ -0,0 +1,122 @@ +package com.demcha.compose.document.templates.coverletter.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.coverletter.data.CoverLetterDocument; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Every shipped cover-letter preset says the words it draws. + * + *

A letter is read by a person and by an applicant tracking system, and the + * fields both look for — who wrote it, what they do — are exactly the ones a + * design is most tempted to letter-space.

+ */ +class CoverLetterTextLayerGateTest { + + /** Three or more single letters separated by any Unicode space separator. */ + private static final Pattern SPELLED_OUT = + Pattern.compile("(?> factory) + throws Exception { + String extracted = extract(factory.get()); + + assertThat(spelledOutRuns(extracted)) + .describedAs("%s spells something out letter by letter", slug) + .isEmpty(); + // The identity an ATS reads the letter by. + assertThat(extracted) + .describedAs("%s lost the sender's name from its text layer", slug) + .contains(CoverLetterV2VisualParityTest.canonicalLetter().identity().name().full()); + } + + /** No preset ships without appearing above. */ + @Test + void everyShippedPresetIsOnThisList() throws IOException { + Path dir = Path.of("..", "templates", "src", "main", "java", "com", "demcha", + "compose", "document", "templates", "coverletter", "presets"); + List shipped = new ArrayList<>(); + try (var files = Files.list(dir)) { + for (Path file : files.toList()) { + String name = file.getFileName().toString(); + if (name.endsWith(".java") + && Files.readString(file).contains("public static final String ID")) { + shipped.add(name.substring(0, name.length() - ".java".length())); + } + } + } + List covered = presets().map(a -> (String) a.get()[0]).toList(); + assertThat(shipped) + .describedAs("a shipped cover-letter preset is not rendered by this gate") + .allSatisfy(preset -> assertThat(covered).contains(preset)); + } + + private static Stream presets() { + return Stream.of( + letter("BlueBannerLetter", BlueBannerLetter::create), + letter("BoxedSectionsLetter", BoxedSectionsLetter::create), + letter("CenteredHeadlineLetter", CenteredHeadlineLetter::create), + letter("ClassicSerifLetter", ClassicSerifLetter::create), + letter("CompactMonoLetter", CompactMonoLetter::create), + letter("EditorialBlueLetter", EditorialBlueLetter::create), + letter("EngineeringResumeLetter", EngineeringResumeLetter::create), + letter("ExecutiveLetter", ExecutiveLetter::create), + letter("MintEditorialLetter", MintEditorialLetter::create), + letter("ModernProfessionalLetter", ModernProfessionalLetter::create), + letter("MonogramSidebarLetter", MonogramSidebarLetter::create), + letter("NordicCleanLetter", NordicCleanLetter::create), + letter("PanelLetter", PanelLetter::create), + letter("SidebarPortraitLetter", SidebarPortraitLetter::create), + letter("TimelineMinimalLetter", TimelineMinimalLetter::create)); + } + + private static Arguments letter(String slug, + Supplier> factory) { + return Arguments.of(slug, factory); + } + + 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 static String extract(DocumentTemplate template) throws IOException { + byte[] pdf; + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .create()) { + template.compose(session, CoverLetterV2VisualParityTest.canonicalLetter()); + pdf = session.toPdfBytes(); + } + try (PDDocument document = Loader.loadPDF(pdf)) { + return new PDFTextStripper().getText(document).replaceAll("\\s*\\R\\s*", " "); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterV2VisualParityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterV2VisualParityTest.java index 1623b60df..6f9dd181d 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterV2VisualParityTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/coverletter/presets/CoverLetterV2VisualParityTest.java @@ -129,7 +129,7 @@ private static Stream presets() { *

Kept inline (not pulled from the examples module) so the test * depends only on main + main-test code.

*/ - private static CoverLetterDocument canonicalLetter() { + static CoverLetterDocument canonicalLetter() { return CoverLetterDocument.builder() .identity(CvIdentity.builder() .name("Jordan", "Rivera") 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..686442f89 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,16 +36,29 @@ * {@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()); } + /** + * The same invoice with no logo, which is the only way to reach the text + * lockup — the brand name over its qualifier, flanked by two rules. Every + * other fixture supplies a logo, so that branch of the masthead is drawn + * by no committed preview. + */ + public static StructuredInvoiceDocumentSpec logolessInvoice() { + return StructuredInvoiceDocumentSpec.from(baseBuilder(serviceLines(5)) + .brand(new InvoiceBrand(null, "NORTHPOINT", "CONSULTING", + "Strategy. Solutions. Results.")) + .build()); + } + /** Enough service lines to push the table onto a second page. */ static StructuredInvoiceDocumentSpec overflowInvoice() { return StructuredInvoiceDocumentSpec.from(baseBuilder(serviceLines(26)).build()); diff --git a/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceTextLayerGateTest.java b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceTextLayerGateTest.java new file mode 100644 index 000000000..7bd4ecdef --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/invoice/presets/InvoiceTextLayerGateTest.java @@ -0,0 +1,145 @@ +package com.demcha.compose.document.templates.invoice.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.data.invoice.StructuredInvoiceDocumentSpec; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Every shipped invoice preset says the words it draws. + * + *

Two of these were found writing their tracking as characters. The reason + * the other nine went unexamined for so long is worth stating: the family's + * pixel gate renders only the two presets that take an {@code + * InvoiceDocumentSpec}, and the rest carry their own per-preset tests, so no + * single list ever named all eleven. {@link #everyShippedPresetIsOnThisList} + * is that list, and it fails if a preset is added without one.

+ */ +class InvoiceTextLayerGateTest { + + /** Three or more single letters separated by any Unicode space separator. */ + private static final Pattern SPELLED_OUT = + Pattern.compile("(? render) throws Exception { + String extracted = extract(render.get()); + + assertThat(spelledOutRuns(extracted)) + .describedAs("%s spells something out letter by letter", slug) + .isEmpty(); + // The fields an invoice is read by, whoever is reading it. + assertThat(extracted) + .describedAs("%s lost its document title", slug) + .containsIgnoringCase("invoice"); + } + + /** + * No preset may be shipped without appearing above. The package is the + * catalogue; this compares it against the list the gate actually renders. + */ + @Test + void everyShippedPresetIsOnThisList() throws IOException { + Path dir = Path.of("..", "templates", "src", "main", "java", "com", "demcha", + "compose", "document", "templates", "invoice", "presets"); + List shipped = new ArrayList<>(); + try (var files = Files.list(dir)) { + for (Path file : files.toList()) { + String name = file.getFileName().toString(); + if (!name.endsWith(".java")) { + continue; + } + if (Files.readString(file).contains("public static final String ID")) { + shipped.add(name.substring(0, name.length() - ".java".length())); + } + } + } + List covered = presets().map(a -> (String) a.get()[0]).toList(); + + assertThat(shipped) + .describedAs("a shipped invoice preset is not rendered by this gate") + .allSatisfy(preset -> assertThat(covered).contains(preset)); + } + + private static Stream presets() { + return Stream.of( + preset("ModernInvoice", () -> + render(ModernInvoice.create(), InvoicePresetFixtures.canonicalInvoice())), + preset("ClassicInvoice", () -> + render(ClassicInvoice.create(), InvoicePresetFixtures.canonicalInvoice())), + preset("ConsultingInvoice", () -> + render(ConsultingInvoice.create(), ConsultingInvoiceFixtures.canonicalInvoice())), + preset("LumaStudioInvoice", () -> + render(LumaStudioInvoice.create(), LumaStudioInvoiceFixtures.canonicalInvoice())), + preset("MerchantInvoice", () -> + render(MerchantInvoice.create(), MerchantInvoiceFixtures.invoice())), + preset("MeteredInvoice", () -> + render(MeteredInvoice.create(), MeteredInvoiceFixtures.invoice())), + preset("ObsidianInvoice", () -> + render(ObsidianInvoice.create(), ObsidianInvoiceFixtures.invoice())), + preset("PaymentsInvoice", () -> + render(PaymentsInvoice.create(), PaymentsInvoiceFixtures.canonicalInvoice())), + preset("PlatformInvoice", () -> + render(PlatformInvoice.create(), PlatformInvoiceFixtures.invoice())), + preset("SubscriptionInvoice", () -> + render(SubscriptionInvoice.create(), SubscriptionInvoiceFixtures.invoice())), + preset("WorkspaceInvoice", () -> + render(WorkspaceInvoice.create(), WorkspaceInvoiceFixtures.canonicalInvoice()))); + } + + private static Arguments preset(String slug, Supplier render) { + return Arguments.of(slug, render); + } + + private static byte[] render(DocumentTemplate template, S spec) { + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .create()) { + template.compose(session, spec); + return session.toPdfBytes(); + } catch (Exception exc) { + throw new IllegalStateException("render failed", exc); + } + } + + private static byte[] render(DocumentTemplate template, + com.demcha.compose.document.templates.data.invoice.StructuredInvoiceData data) { + return render(template, StructuredInvoiceDocumentSpec.from(data)); + } + + 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 static String extract(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + return new PDFTextStripper().getText(document).replaceAll("\\s*\\R\\s*", " "); + } + } +} 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/proposal/presets/ProposalTextLayerGateTest.java b/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalTextLayerGateTest.java new file mode 100644 index 000000000..5a5a03fa0 --- /dev/null +++ b/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalTextLayerGateTest.java @@ -0,0 +1,109 @@ +package com.demcha.compose.document.templates.proposal.presets; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Every shipped proposal preset says the words it draws. */ +class ProposalTextLayerGateTest { + + /** Three or more single letters separated by any Unicode space separator. */ + private static final Pattern SPELLED_OUT = + Pattern.compile("(? render) throws Exception { + String extracted = extract(render.get()); + + assertThat(spelledOutRuns(extracted)) + .describedAs("%s spells something out letter by letter", slug) + .isEmpty(); + assertThat(extracted) + .describedAs("%s lost its document title", slug) + .containsIgnoringCase("proposal"); + } + + /** No preset ships without appearing above. */ + @Test + void everyShippedPresetIsOnThisList() throws IOException { + List covered = presets().map(a -> (String) a.get()[0]).toList(); + assertThat(shippedPresets("proposal")) + .describedAs("a shipped proposal preset is not rendered by this gate") + .allSatisfy(preset -> assertThat(covered).contains(preset)); + } + + static List shippedPresets(String family) throws IOException { + Path dir = Path.of("..", "templates", "src", "main", "java", "com", "demcha", + "compose", "document", "templates", family, "presets"); + List shipped = new ArrayList<>(); + try (var files = Files.list(dir)) { + for (Path file : files.toList()) { + String name = file.getFileName().toString(); + if (name.endsWith(".java") + && Files.readString(file).contains("public static final String ID")) { + shipped.add(name.substring(0, name.length() - ".java".length())); + } + } + } + return shipped; + } + + private static Stream presets() { + return Stream.of( + Arguments.of("ModernProposal", (Supplier) () -> + render(ModernProposal.create(), ProposalV2VisualParityTest.canonicalProposal())), + Arguments.of("EditorialProposal", (Supplier) () -> + render(EditorialProposal.create(), EditorialProposalFixtures.canonicalProposal())), + Arguments.of("IndigoProposal", (Supplier) () -> + render(IndigoProposal.create(), IndigoProposalFixtures.canonicalProposal())), + Arguments.of("NorthlineProposal", (Supplier) () -> + render(NorthlineProposal.create(), NorthlineProposalFixtures.canonicalProposal()))); + } + + private static byte[] render(DocumentTemplate template, S spec) { + try (DocumentSession session = GraphCompose.document() + .pageSize(DocumentPageSize.A4) + .create()) { + template.compose(session, spec); + return session.toPdfBytes(); + } catch (Exception exc) { + throw new IllegalStateException("render failed", exc); + } + } + + 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 static String extract(byte[] pdf) throws IOException { + try (PDDocument document = Loader.loadPDF(pdf)) { + return new PDFTextStripper().getText(document).replaceAll("\\s*\\R\\s*", " "); + } + } +} diff --git a/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalV2VisualParityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalV2VisualParityTest.java index e00057cb5..6047671e0 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalV2VisualParityTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/proposal/presets/ProposalV2VisualParityTest.java @@ -78,7 +78,7 @@ private static Stream presets() { * emphasized total), the acceptance terms, and the footer. Kept inline * so the test depends only on main + main-test code. */ - private static ProposalDocumentSpec canonicalProposal() { + static ProposalDocumentSpec canonicalProposal() { return ProposalDocumentSpec.from(ProposalData.builder() .title("Proposal") .proposalNumber("GC-P-2026-014") 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") diff --git a/qa/src/test/java/com/demcha/compose/document/templates/rota/presets/CobaltRotaFixtures.java b/qa/src/test/java/com/demcha/compose/document/templates/rota/presets/CobaltRotaFixtures.java index fcdf1f1f7..8d2c19a62 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/rota/presets/CobaltRotaFixtures.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/rota/presets/CobaltRotaFixtures.java @@ -35,13 +35,13 @@ * invented. The shape is a real week's, because that is what the gates have to * measure against; the content is not anyone's.

*/ -final class CobaltRotaFixtures { +public final class CobaltRotaFixtures { private CobaltRotaFixtures() { } /** One week of a bar's rota. */ - static StructuredRotaDocumentSpec canonicalRota() { + public static StructuredRotaDocumentSpec canonicalRota() { return StructuredRotaDocumentSpec.from(StructuredRotaData.builder() // A short wordmark on purpose: the lockup is sized by a fixed // fraction of its column, so a long name wraps rather than 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/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/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/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/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/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/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. */ 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..b775dbac0 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,12 +2,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 +26,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 +34,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 +71,63 @@ 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() + DocumentTextStyle headingStyle = style(HEADING_SIZE, INK, DocumentTextDecoration.DEFAULT) + .withLetterSpacing(DocumentLetterSpacing.ofFontSize(TRACKING_EM)); + double band = HEADING_SIZE * TIGHT_LEADING; + + // 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)) - .horizontal(tailWidth) + .fill() .thickness(HAIRLINE_THICKNESS) - .color(RULE) - .margin(DocumentInsets.zero()) - .build() - : null; - - 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); + .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)); } /** 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; } 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..54cac28f1 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 @@ -115,8 +115,19 @@ private static void renderTextLockup(SectionBuilder section, InvoiceBrand brand) .lineSpacing(1.0) .margin(DocumentInsets.zero())) .addParagraph(paragraph -> paragraph - .text(brand.qualifier().isBlank() - ? "" : "— " + tracked(brand.qualifier()) + " —") + // Three runs, because only the word is tracked. The rules + // that flank it are punctuation set at the qualifier's own + // size; tracking them would spread the dashes off the word + // they point at. + .rich(rich -> { + if (brand.qualifier().isBlank()) { + return; + } + rich.style("— ", BRAND_QUALIFIER) + .style(brand.qualifier(), + BRAND_QUALIFIER.withLetterSpacing(HEADING_TRACKING)) + .style(" —", BRAND_QUALIFIER); + }) .textStyle(BRAND_QUALIFIER) .align(TextAlign.CENTER) .margin(DocumentInsets.zero())) @@ -152,8 +163,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 +217,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..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; @@ -98,8 +97,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..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; @@ -117,7 +116,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..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; @@ -156,7 +155,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 +172,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 +219,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..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,13 +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; @@ -35,21 +33,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 +56,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 +72,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 +154,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)); } }