diff --git a/CHANGELOG.md b/CHANGELOG.md index 56957c0d..8c64ec8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,86 @@ All notable changes to GraphCompose are documented here. Versions follow semantic versioning; release dates are ISO 8601. +## v1.6.3 — in progress + +Bug fix patch. Closes two independent hyperlink clickable-area +defects that surfaced on CV gallery presets and made the LinkedIn / +GitHub contact rows hijack each other's clicks (paragraph-level +link path) or drift past their visible text (span-level link path +through multi-space separators). **No public API change** — engine, +DSL, themes, templates, and backend records all stay +source-compatible with v1.6.2. + +### Engine + +- **Paragraph-level link annotations now hug rendered text.** + `PdfFixedLayoutBackend` used to emit a paragraph's `linkOptions` + as a single rectangle covering the entire fragment box + (`fragment.x()` + `fragment.width()`), ignoring `TextAlign.RIGHT` + / `TextAlign.CENTER`. Stacked right-aligned contact paragraphs + (e.g. one per LinkedIn / GitHub icon row in Timeline Minimal / + Sidebar Portrait / Monogram Sidebar) therefore produced + full-column-wide rects that overlapped the empty alignment gap of + neighbouring rows — hovering over GitHub clicked the LinkedIn row. + The backend now emits one per-line rect tight to `line.width()` + positioned at the alignment-aware `lineX`, matching how + inline-span links already worked. Span-level link emission, table + / shape / barcode payload links, and bookmark anchoring are + unchanged. +- **Glyph sanitizer preserves all author whitespace.** + `PdfFont.sanitizeForRender` used to collapse any run of consecutive + spaces into a single space, both for whitespace-only tokens (the + `" "` halves of a `" | "` separator) and for inter-word gaps + in spaced-caps strings (`spacedUpper("ARTEM DEMCHYSHYN")` produces + `"A R T E M D E M C H Y S H Y N"` with deliberate triple-spaces + between words). The collapse shrank the rendered glyph stream + under measurement, drifting inline-link rectangles ~8pt per + `" | "` separator past their visible labels and visually + merging spaced-caps titles back into a single run (`"A R T E M D E + M C H Y S H Y N"` — no word boundary). The sanitizer no longer + collapses adjacent spaces; newlines / NBSP / non-tab control + characters still resolve to a single space each, but author + whitespace is now preserved verbatim so wrap geometry, + link-rectangle emission, and `showText(...)` all see the same + string. Layout snapshot baselines for five CV presets and one + nested-list document widened to reflect the recovered whitespace — + the deliberate visual change is the bug fix. + +### Templates + +- **Boxed Sections projects render as title + indented description.** + The "Projects" module now renders each bullet-list or + `IndentedBlock` item as two stacked paragraphs — bullet plus bold + project name (with an optional tech-stack chunk in parentheses) on + the first line, then a hanging-indented description below aligned + to the project name (not the bullet). The previous single-line + rendering ran the project name and description together. Bullet + marker, hanging-indent, and surrounding modules are unchanged. + Example data in `ExampleDataFactory.sampleCvSpecV2` and + `PresetVisualGalleryTest` now ships tech-stack chunks (`"Java 21, + PDFBox, Maven, JMH"`) so the gallery PDFs reflect the new layout. + +### Tests + +- New regression in `PdfFixedLayoutBackendFeaturesTest` — + `shouldTightlyHugRightAlignedParagraphLinkRectangles` — stacks + three right-aligned link paragraphs and asserts each clickable + rect hugs its rendered label width (≤ 150pt), sits flush against + the inner right margin, and does not overlap the Y-band of + neighbouring rows. +- New regression in `PdfFixedLayoutBackendFeaturesTest` — + `shouldKeepCenteredInlineLinkRectanglesAlignedAcrossMultiSpaceSeparators` + — renders a centered contact line built with `" | "` separators + and asserts the three resulting link rectangles preserve + left-to-right order with non-overlapping X ranges and a sane + per-separator gap (5..40pt), pinning the bug where collapsed + whitespace pushed later rects past the line. +- New regression in `PdfFontSanitizerTest` — + `sanitizeForRender_preservesWhitespaceOnlyTokensVerbatim` — pins + the whitespace-only short-circuit so render width stays in + lockstep with `getTextWidth` for tokenised contact-line + separators. + ## v1.6.2 — 2026-05-20 Robustness patch. Closes four engine defects surfaced while building diff --git a/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java b/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java index 9b3a4af8..39d71b7f 100644 --- a/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java +++ b/examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java @@ -298,19 +298,23 @@ public static CvSpec sampleCvSpecV2() { + "pattern matching, virtual threads.")))) .module(CvModule.of("Projects", new BulletListBlock(List.of( - "**GraphCompose** - Declarative Java PDF layout engine. " - + "Semantic DSL, slot-based templates, snapshot testing. " - + "Powers production CV / invoice / proposal pipelines " - + "for hiring tools and billing systems. *(Open source)*", - "**Template Studio** - Internal tool for evaluating CV, proposal, " - + "and invoice output across 14 design presets. PNG " + "**GraphCompose (Java 21, PDFBox, Maven, JMH)** - " + + "Declarative Java PDF layout engine. Semantic DSL, " + + "slot-based templates, snapshot testing. Powers " + + "production CV / invoice / proposal pipelines for " + + "hiring tools and billing systems. *(Open source)*", + "**Template Studio (Kotlin, Compose Desktop, PDFBox PNG diff)** - " + + "Internal tool for evaluating CV, proposal, and " + + "invoice output across 14 design presets. PNG " + "diffing, side-by-side layout, baseline freezing.", - "**LayoutLint** - Static analyser that flags fragile authoring " - + "patterns (deeply nested rows, untyped offsets, " - + "implicit page breaks) before they ship to production.", - "**ChromeForge** - Editorial-magazine document toolkit built on " - + "GraphCompose: cinematic covers, pull quotes, multi-" - + "column flow, sidebar callouts.")))) + "**LayoutLint (Java 21, JavaParser, Spoon)** - Static analyser " + + "that flags fragile authoring patterns (deeply " + + "nested rows, untyped offsets, implicit page " + + "breaks) before they ship to production.", + "**ChromeForge (Java, GraphCompose, Pandoc bridge)** - " + + "Editorial-magazine document toolkit built on " + + "GraphCompose: cinematic covers, pull quotes, " + + "multi-column flow, sidebar callouts.")))) .module(CvModule.of("Professional Experience", new MultiParagraphBlock(List.of( "**Senior Platform Engineer**, Northwind Systems | " diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java index 6e138474..1d8710b9 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackend.java @@ -327,10 +327,14 @@ private void finishRenderedFragment(PlacedFragment fragment, boolean guideLines, Map> ownerBounds) throws Exception { if (payload instanceof ParagraphFragmentPayload paragraphPayload) { - addParagraphSpanLinks(fragment, paragraphPayload, environment); + addParagraphLinks(fragment, paragraphPayload, environment); } if (payload instanceof PdfSemanticFragmentPayload semanticPayload) { - if (semanticPayload.linkOptions() != null) { + // Paragraph-level link emission is handled above with per-line + // rects tight to the rendered text (alignment-aware). Other + // semantic payloads (shapes, table rows) still use the full + // fragment rect as their clickable area. + if (semanticPayload.linkOptions() != null && !(payload instanceof ParagraphFragmentPayload)) { PdfLinkAnnotationWriter.addUriLink( environment.document().getPage(fragment.pageIndex()), new PdfLinkAnnotationWriter.PlacedPdfRect(fragment.x(), fragment.y(), fragment.width(), fragment.height()), @@ -345,9 +349,10 @@ private void finishRenderedFragment(PlacedFragment fragment, } } - private void addParagraphSpanLinks(PlacedFragment fragment, - ParagraphFragmentPayload payload, - PdfRenderEnvironment environment) throws Exception { + private void addParagraphLinks(PlacedFragment fragment, + ParagraphFragmentPayload payload, + PdfRenderEnvironment environment) throws Exception { + var paragraphLink = payload.linkOptions(); double innerX = fragment.x() + payload.padding().left(); double innerWidth = Math.max(0.0, fragment.width() - payload.padding().horizontal()); double contentTop = fragment.y() + fragment.height() - payload.padding().top(); @@ -362,6 +367,23 @@ private void addParagraphSpanLinks(PlacedFragment fragment, case CENTER -> innerX + (innerWidth - line.width()) / 2.0; case LEFT -> innerX; }; + + // Paragraph-level link covers each rendered line tightly. Without + // this, right- or center-aligned paragraphs leaked clickable area + // across the empty alignment gap, so neighbouring contact rows + // (LinkedIn / GitHub icon paragraphs) hijacked each other's + // clicks. + if (paragraphLink != null && line.width() > 0.0) { + PdfLinkAnnotationWriter.addUriLink( + environment.document().getPage(fragment.pageIndex()), + new PdfLinkAnnotationWriter.PlacedPdfRect( + lineX, + lineTop - resolvedLineHeight, + line.width(), + resolvedLineHeight), + paragraphLink); + } + double spanX = lineX; for (ParagraphSpan span : line.spans()) { if (span.linkOptions() != null && span.width() > 0.0) { diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java index dc8d8e6f..eb0282e0 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java @@ -185,6 +185,32 @@ private void addSectionBanner(SectionBuilder section, String title) { private void addModuleBody(SectionBuilder section, CvModule module) { section.spacing(4) .padding(new DocumentInsets(4, 4, 0, 4)); + // Projects render with a dedicated two-line layout — bold + // project name (with optional tech stack in parens) on the + // first line behind a bullet, then a hanging-indented + // description below — instead of the flat single-line bullet + // used for general bullet lists. Matches the canonical CV + // visual where "what the project is" stands apart from "what + // it did". Honours both shapes the data layer ships: a + // {@link BulletListBlock} with "**Name (tech)** - Description" + // strings and an {@link IndentedBlock} with separate title / + // body fields. + if (isProjectsModule(module.title())) { + if (module.body() instanceof BulletListBlock projects) { + for (String item : projects.items()) { + renderProjectItem(section, parseProjectItem(safe(item).trim())); + } + return; + } + if (module.body() instanceof IndentedBlock indented) { + for (IndentedBlock.Item item : indented.items()) { + renderProjectItem(section, + new ProjectParts(safe(item.title()).trim(), + safe(item.body()).trim())); + } + return; + } + } renderBody(section, module.body()); } @@ -266,6 +292,54 @@ private void renderBulletItem(SectionBuilder section, String rawLine) { .rich(rich -> appendMarkdown(rich, text, base))); } + /** + * Renders one project entry as two stacked paragraphs: + * + *
+         * • Name (tech stack)
+         *   Description text wrapped under the title, hanging-indented
+         *   so it lines up with the project name (not the bullet).
+         * 
+ * + *

Input format: {@code "**Name (tech)** - Description"}. + * Both halves are optional — a project without a description + * renders only the title line; a project without bold markers + * around the name is treated as plain title text.

+ */ + private void renderProjectItem(SectionBuilder section, ProjectParts parts) { + if (parts.name().isBlank() && parts.description().isBlank()) { + return; + } + DocumentTextStyle base = style(BODY_FONT, 8.6, + DocumentTextDecoration.DEFAULT, INK); + DocumentTextStyle nameStyle = style(BODY_FONT, 8.6, + DocumentTextDecoration.BOLD, INK); + + section.addParagraph(paragraph -> paragraph + .textStyle(base) + .lineSpacing(1.4) + .align(TextAlign.LEFT) + .margin(DocumentInsets.top(2)) + .bulletOffset("• ") + .indentStrategy(DocumentTextIndent.ALL_LINES) + .rich(rich -> appendMarkdown(rich, parts.name(), nameStyle))); + + if (parts.description().isBlank()) { + return; + } + // Two-space prefix matches the bullet+space width inside the + // hanging-indent computation, so the description's first + // glyph sits under the project name rather than the bullet. + section.addParagraph(paragraph -> paragraph + .textStyle(base) + .lineSpacing(1.4) + .align(TextAlign.LEFT) + .margin(DocumentInsets.zero()) + .bulletOffset(" ") + .indentStrategy(DocumentTextIndent.ALL_LINES) + .rich(rich -> appendMarkdown(rich, parts.description(), base))); + } + private void renderWorkEntry(SectionBuilder section, WorkEntry entry) { DocumentTextStyle positionStyle = style(BODY_FONT, 9.2, DocumentTextDecoration.BOLD, INK); @@ -448,6 +522,30 @@ private static String stripBasicMarkdown(String value) { .replace("_", ""); } + private static boolean isProjectsModule(String title) { + if (title == null) { + return false; + } + String normalized = title.toLowerCase(Locale.ROOT).trim(); + return normalized.equals("projects") || normalized.startsWith("projects "); + } + + private static ProjectParts parseProjectItem(String item) { + // Split on " - " (space-hyphen-space, mirroring WorkEntry parsing) + // so an em-dash or hyphen inside the description is not eaten. + // Falls back to "title only" when no separator is present. + int sepIndex = item.indexOf(" - "); + if (sepIndex <= 0) { + return new ProjectParts(item.trim(), ""); + } + String name = item.substring(0, sepIndex).trim(); + String description = item.substring(sepIndex + 3).trim(); + return new ProjectParts(name, description); + } + + private record ProjectParts(String name, String description) { + } + private static String spacedUpper(String value) { String upper = safe(value).toUpperCase(Locale.ROOT); StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java b/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java index f42c6eac..8be81634 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/PdfFont.java @@ -180,8 +180,19 @@ public double getTextWidthNoSanitize(TextStyle style, String text) { } private @NotNull String textSanitizer(String text) { + // v1.6.3: preserve author-supplied whitespace verbatim. The + // previous implementation collapsed any run of resulting spaces + // (original + converted) into one, but downstream geometry + // (`PdfFont.getTextWidth`, paragraph layout, link-rect emission) + // measures against the input as written. The collapse therefore + // shrank the rendered string under measurement, drifting + // link annotations away from their glyphs and visually merging + // author-spaced strings like `spacedUpper("ARTEM DEMCHYSHYN")` + // (which inserts deliberate triple-spaces between words). + // Newlines / NBSP / non-tab control chars still resolve to a + // single space each \u2014 they no longer collapse adjacent author + // spaces. StringBuilder sanitized = new StringBuilder(text.length()); - boolean previousSpace = false; for (int offset = 0; offset < text.length(); ) { int codePoint = text.codePointAt(offset); offset += Character.charCount(codePoint); @@ -191,16 +202,7 @@ public double getTextWidthNoSanitize(TextStyle style, String text) { default -> Character.isISOControl(codePoint) && codePoint != '\t' ? ' ' : codePoint; }; - if (resolved == ' ') { - if (!previousSpace) { - sanitized.append(' '); - previousSpace = true; - } - continue; - } - sanitized.appendCodePoint(resolved); - previousSpace = false; } return sanitized.toString(); diff --git a/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendFeaturesTest.java b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendFeaturesTest.java index b9d581cf..4975621c 100644 --- a/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendFeaturesTest.java +++ b/src/test/java/com/demcha/compose/document/backend/fixed/pdf/PdfFixedLayoutBackendFeaturesTest.java @@ -16,6 +16,7 @@ import com.demcha.compose.document.node.DocumentBookmarkOptions; import com.demcha.compose.document.node.DocumentLinkOptions; import com.demcha.compose.document.node.ShapeNode; +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.DocumentStroke; @@ -173,6 +174,160 @@ void shouldRenderInlineParagraphLinksAsSeparateAnnotations() throws Exception { } } + @Test + void shouldTightlyHugRightAlignedParagraphLinkRectangles() throws Exception { + // v1.6.3 regression: paragraph-level link annotations used to inherit + // the full fragment rectangle, so right-aligned contact rows (LinkedIn + // / GitHub icon paragraphs in CV presets like TimelineMinimal) leaked + // clickable area across the entire left-side alignment gap and + // hijacked the row above's click. Each line's link rect must hug the + // rendered text so neighbouring rows stay independent. + byte[] pdfBytes; + try (DocumentSession document = GraphCompose.document() + .pageSize(400, 200) + .margin(20, 20, 20, 20) + .create()) { + document.dsl() + .pageFlow() + .name("RightLinkFlow") + .addParagraph(paragraph -> paragraph + .name("EmailRow") + .text("jordan@example.dev") + .textStyle(DocumentTextStyle.DEFAULT) + .align(TextAlign.RIGHT) + .link(new DocumentLinkOptions("mailto:jordan@example.dev"))) + .addParagraph(paragraph -> paragraph + .name("LinkedInRow") + .text("LinkedIn") + .textStyle(DocumentTextStyle.DEFAULT) + .align(TextAlign.RIGHT) + .link(new DocumentLinkOptions("https://linkedin.com/in/jordan"))) + .addParagraph(paragraph -> paragraph + .name("GitHubRow") + .text("GitHub") + .textStyle(DocumentTextStyle.DEFAULT) + .align(TextAlign.RIGHT) + .link(new DocumentLinkOptions("https://github.com/jordan"))) + .build(); + + pdfBytes = document.toPdfBytes(); + } + + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + List links = new ArrayList<>(); + for (var page : document.getPages()) { + for (var annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationLink link) { + links.add(link); + } + } + } + assertThat(links).hasSize(3); + + float innerLeft = 20.0f; + float pageRight = 380.0f; + for (PDAnnotationLink link : links) { + var rect = link.getRectangle(); + // The link must hug the rendered text near the right margin + // (page width 400 - margin 20 = inner right edge 380) and + // must not stretch back across the whole inner width. + assertThat(rect.getUpperRightX()) + .as("right edge close to inner right margin for %s", + ((PDActionURI) link.getAction()).getURI()) + .isBetween(pageRight - 1.0f, pageRight + 0.5f); + assertThat(rect.getLowerLeftX()) + .as("left edge does not extend back across the alignment gap for %s", + ((PDActionURI) link.getAction()).getURI()) + .isGreaterThan(innerLeft + 100.0f); + // The clickable width must approximately match the rendered + // label width — short labels (LinkedIn ≈ 39pt) should produce + // narrow rects, not 360pt full-fragment ones. + float width = rect.getUpperRightX() - rect.getLowerLeftX(); + assertThat(width).isLessThan(150.0f); + } + + // Neighbouring rows must not overlap vertically — the v1.6.2 bug + // surfaced because the wide rects of LinkedIn / GitHub rows sat + // close enough that PDF readers prioritised the upper rect even + // when the cursor hovered the lower text. With per-line rects + // each row stays in its own Y band. + links.sort((a, b) -> Float.compare(b.getRectangle().getLowerLeftY(), + a.getRectangle().getLowerLeftY())); + for (int i = 1; i < links.size(); i++) { + float lowerTop = links.get(i).getRectangle().getUpperRightY(); + float upperBottom = links.get(i - 1).getRectangle().getLowerLeftY(); + assertThat(upperBottom) + .as("row %d (upper) bottom must sit above row %d (lower) top", i - 1, i) + .isGreaterThanOrEqualTo(lowerTop); + } + } + } + + @Test + void shouldKeepCenteredInlineLinkRectanglesAlignedAcrossMultiSpaceSeparators() throws Exception { + // v1.6.3 regression: when a centered contact line stitches inline + // links with author-supplied multi-space separators (the " | " + // pattern used by BoxedSections / CenteredHeadline / etc.), the + // PDF render path used to collapse " " to " " while measurement + // kept the original width. That drift pushed each subsequent link + // rectangle to the right of its visible glyphs — the second link + // hovered over the third link's text, and the third sat on empty + // space past the line end. + byte[] pdfBytes; + try (DocumentSession document = GraphCompose.document() + .pageSize(595, 200) + .margin(36, 36, 36, 36) + .create()) { + document.dsl() + .pageFlow() + .name("CenteredContactFlow") + .addParagraph(paragraph -> paragraph + .name("ContactLine") + .textStyle(DocumentTextStyle.DEFAULT) + .align(TextAlign.CENTER) + .rich(rich -> rich + .link("Email", new DocumentLinkOptions("mailto:a@example.dev")) + .plain(" | ") + .link("LinkedIn", new DocumentLinkOptions("https://linkedin.com/in/a")) + .plain(" | ") + .link("GitHub", new DocumentLinkOptions("https://github.com/a")))) + .build(); + + pdfBytes = document.toPdfBytes(); + } + + try (PDDocument document = Loader.loadPDF(pdfBytes)) { + // Three inline links — order is preserved by the renderer. + List links = new ArrayList<>(); + for (var page : document.getPages()) { + for (var annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationLink l) { + links.add(l); + } + } + } + assertThat(links).hasSize(3); + // Rects must appear left-to-right with non-overlapping X + // ranges; the buggy collapsing pushed later rects past the + // line so neighbours overlapped or sat on empty whitespace. + links.sort((a, b) -> Float.compare(a.getRectangle().getLowerLeftX(), + b.getRectangle().getLowerLeftX())); + for (int i = 1; i < links.size(); i++) { + float previousRight = links.get(i - 1).getRectangle().getUpperRightX(); + float currentLeft = links.get(i).getRectangle().getLowerLeftX(); + assertThat(currentLeft) + .as("link %d left edge must sit after link %d right edge", i, i - 1) + .isGreaterThan(previousRight); + float gap = currentLeft - previousRight; + // The author wrote " | " — the gap must be wide enough + // to clear the separator and tight enough not to span half + // the page (the collapsed-whitespace bug produced 30pt+ + // overlap, not a gap). + assertThat(gap).isBetween(5.0f, 40.0f); + } + } + } + @Test void shouldRenderSectionFillAndStrokeFromCanonicalDsl() throws Exception { Path outputFile = VisualTestOutputs.preparePdf("section-fill-and-stroke", "clean", "document-backend"); diff --git a/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualGalleryTest.java b/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualGalleryTest.java index 00f48118..0b2cd780 100644 --- a/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualGalleryTest.java +++ b/src/test/java/com/demcha/compose/document/templates/cv/presets/PresetVisualGalleryTest.java @@ -69,9 +69,11 @@ private static CvSpec sampleSpec() { "Professional track | 2023"))))) .module(CvModule.of("Projects", new IndentedBlock(List.of( - new IndentedBlock.Item("GraphCompose", + new IndentedBlock.Item( + "GraphCompose (Java 21, PDFBox, Maven)", "Declarative PDF layout engine for reusable document generation"), - new IndentedBlock.Item("Template Studio", + new IndentedBlock.Item( + "Template Studio (Kotlin, Compose Desktop)", "Internal tool for evaluating CV, proposal, and invoice output"))))) .module(CvModule.of("Professional Experience", new MultiParagraphBlock(List.of( diff --git a/src/test/java/com/demcha/compose/engine/render/pdf/PdfFontSanitizerTest.java b/src/test/java/com/demcha/compose/engine/render/pdf/PdfFontSanitizerTest.java index 09a9f655..4ac014c3 100644 --- a/src/test/java/com/demcha/compose/engine/render/pdf/PdfFontSanitizerTest.java +++ b/src/test/java/com/demcha/compose/engine/render/pdf/PdfFontSanitizerTest.java @@ -59,14 +59,40 @@ void sanitizeForRender_preservesSingleSpaces() { } @Test - void sanitizeForRender_collapsesConsecutiveSpaces() { - // Pins existing textSanitizer behaviour: multiple spaces collapse - // to a single space. If this contract ever changes, wrap geometry - // assumptions across the engine must be revisited. + void sanitizeForRender_preservesConsecutiveSpaces() { + // v1.6.3: textSanitizer no longer collapses author whitespace. + // The previous collapse (multi-space → one space) shrank the + // rendered string under measurement and broke spaced-upper + // titles ("A R T E M D E M C H Y S H Y N" rendered without + // its inter-word gap) and " | " contact-row separators. + // Newlines / NBSP / control chars still resolve to a single + // space each, but adjacent author spaces are kept verbatim so + // wrap geometry, link-rect emission, and showText all see the + // same string. String input = "spaced out"; String output = helvetica.sanitizeForRender(TextStyle.DEFAULT_STYLE, input); - assertThat(output).isEqualTo("spaced out"); + assertThat(output).isEqualTo("spaced out"); + assertThat(helvetica.getTextWidth(TextStyle.DEFAULT_STYLE, input)) + .isEqualTo(helvetica.getTextWidth(TextStyle.DEFAULT_STYLE, output)); + } + + @Test + void sanitizeForRender_preservesWhitespaceOnlyTokensVerbatim() { + // v1.6.3 regression: paragraph tokenisation produces standalone + // whitespace-only tokens (e.g. the " " halves of a " | " + // contact-line separator). Their render width must match what + // getTextWidth measured so the PDF text matrix advances by the + // same amount — otherwise link annotation rectangles drift to the + // right of the glyphs actually drawn (visible on right-/center- + // aligned contact rows where LinkedIn / GitHub clickable areas + // ended up past their visible text). + String triple = " "; + String output = helvetica.sanitizeForRender(TextStyle.DEFAULT_STYLE, triple); + + assertThat(output).isEqualTo(" "); + assertThat(helvetica.getTextWidth(TextStyle.DEFAULT_STYLE, triple)) + .isEqualTo(helvetica.getTextWidth(TextStyle.DEFAULT_STYLE, output)); } @Test diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_boxed_sections.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_boxed_sections.json index 07d7100b..851bcc4d 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_boxed_sections.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_boxed_sections.json @@ -22,15 +22,15 @@ "depth" : 1, "layer" : 1, "computedX" : 15.0, - "computedY" : 264.97, + "computedY" : 234.18, "placementX" : 15.0, - "placementY" : 264.97, + "placementY" : 234.18, "placementWidth" : 570.276, - "placementHeight" : 561.92, + "placementHeight" : 592.71, "startPage" : 0, "endPage" : 0, "contentWidth" : 570.276, - "contentHeight" : 561.92, + "contentHeight" : 592.71, "margin" : { "top" : 0.0, "right" : 0.0, @@ -682,15 +682,15 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 466.1, + "computedY" : 435.31, "placementX" : 15.0, - "placementY" : 466.1, - "placementWidth" : 322.149, - "placementHeight" : 34.79, + "placementY" : 435.31, + "placementWidth" : 259.627, + "placementHeight" : 65.58, "startPage" : 0, "endPage" : 0, - "contentWidth" : 322.149, - "contentHeight" : 34.79, + "contentWidth" : 259.627, + "contentHeight" : 65.58, "margin" : { "top" : 0.0, "right" : 0.0, @@ -715,11 +715,11 @@ "computedY" : 483.495, "placementX" : 19.0, "placementY" : 483.495, - "placementWidth" : 314.149, + "placementWidth" : 68.852, "placementHeight" : 11.395, "startPage" : 0, "endPage" : 0, - "contentWidth" : 314.149, + "contentWidth" : 68.852, "contentHeight" : 11.395, "margin" : { "top" : 2.0, @@ -742,14 +742,44 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 466.1, + "computedY" : 468.1, + "placementX" : 19.0, + "placementY" : 468.1, + "placementWidth" : 251.627, + "placementHeight" : 11.395, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 251.627, + "contentHeight" : 11.395, + "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" : "BoxedSectionsRoot[0]/BoxedSectionsBody_3[9]/ParagraphNode[2]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "BoxedSectionsRoot[0]/BoxedSectionsBody_3[9]", + "childIndex" : 2, + "depth" : 3, + "layer" : 3, + "computedX" : 19.0, + "computedY" : 450.705, "placementX" : 19.0, - "placementY" : 466.1, - "placementWidth" : 302.806, + "placementY" : 450.705, + "placementWidth" : 74.252, "placementHeight" : 11.395, "startPage" : 0, "endPage" : 0, - "contentWidth" : 302.806, + "contentWidth" : 74.252, "contentHeight" : 11.395, "margin" : { "top" : 2.0, @@ -763,6 +793,36 @@ "bottom" : 0.0, "left" : 0.0 } + }, { + "path" : "BoxedSectionsRoot[0]/BoxedSectionsBody_3[9]/ParagraphNode[3]", + "entityName" : null, + "entityKind" : "ParagraphNode", + "parentPath" : "BoxedSectionsRoot[0]/BoxedSectionsBody_3[9]", + "childIndex" : 3, + "depth" : 3, + "layer" : 3, + "computedX" : 19.0, + "computedY" : 435.31, + "placementX" : 19.0, + "placementY" : 435.31, + "placementWidth" : 235.821, + "placementHeight" : 11.395, + "startPage" : 0, + "endPage" : 0, + "contentWidth" : 235.821, + "contentHeight" : 11.395, + "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" : "BoxedSectionsRoot[0]/BoxedSectionsBanner_4[10]", "entityName" : "BoxedSectionsBanner_4", @@ -772,9 +832,9 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 432.38, + "computedY" : 401.59, "placementX" : 15.0, - "placementY" : 432.38, + "placementY" : 401.59, "placementWidth" : 570.276, "placementHeight" : 22.72, "startPage" : 0, @@ -802,9 +862,9 @@ "depth" : 3, "layer" : 3, "computedX" : 20.0, - "computedY" : 437.38, + "computedY" : 406.59, "placementX" : 20.0, - "placementY" : 437.38, + "placementY" : 406.59, "placementWidth" : 560.276, "placementHeight" : 12.72, "startPage" : 0, @@ -832,9 +892,9 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 340.48, + "computedY" : 309.69, "placementX" : 15.0, - "placementY" : 340.48, + "placementY" : 309.69, "placementWidth" : 570.276, "placementHeight" : 84.9, "startPage" : 0, @@ -862,9 +922,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 398.06, + "computedY" : 367.27, "placementX" : 19.0, - "placementY" : 398.06, + "placementY" : 367.27, "placementWidth" : 562.276, "placementHeight" : 23.32, "startPage" : 0, @@ -892,9 +952,9 @@ "depth" : 4, "layer" : 4, "computedX" : 19.0, - "computedY" : 409.19, + "computedY" : 378.4, "placementX" : 19.0, - "placementY" : 409.19, + "placementY" : 378.4, "placementWidth" : 112.038, "placementHeight" : 12.19, "startPage" : 0, @@ -922,9 +982,9 @@ "depth" : 5, "layer" : 5, "computedX" : 19.0, - "computedY" : 409.19, + "computedY" : 378.4, "placementX" : 19.0, - "placementY" : 409.19, + "placementY" : 378.4, "placementWidth" : 112.038, "placementHeight" : 12.19, "startPage" : 0, @@ -952,9 +1012,9 @@ "depth" : 4, "layer" : 4, "computedX" : 409.259, - "computedY" : 398.06, + "computedY" : 367.27, "placementX" : 409.259, - "placementY" : 398.06, + "placementY" : 367.27, "placementWidth" : 172.017, "placementHeight" : 23.32, "startPage" : 0, @@ -982,9 +1042,9 @@ "depth" : 5, "layer" : 5, "computedX" : 409.259, - "computedY" : 398.06, + "computedY" : 367.27, "placementX" : 409.259, - "placementY" : 398.06, + "placementY" : 367.27, "placementWidth" : 172.017, "placementHeight" : 23.32, "startPage" : 0, @@ -1012,9 +1072,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 382.93, + "computedY" : 352.14, "placementX" : 19.0, - "placementY" : 382.93, + "placementY" : 352.14, "placementWidth" : 68.88, "placementHeight" : 11.13, "startPage" : 0, @@ -1042,9 +1102,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 355.61, + "computedY" : 324.82, "placementX" : 19.0, - "placementY" : 355.61, + "placementY" : 324.82, "placementWidth" : 562.276, "placementHeight" : 23.32, "startPage" : 0, @@ -1072,9 +1132,9 @@ "depth" : 4, "layer" : 4, "computedX" : 19.0, - "computedY" : 366.74, + "computedY" : 335.95, "placementX" : 19.0, - "placementY" : 366.74, + "placementY" : 335.95, "placementWidth" : 80.914, "placementHeight" : 12.19, "startPage" : 0, @@ -1102,9 +1162,9 @@ "depth" : 5, "layer" : 5, "computedX" : 19.0, - "computedY" : 366.74, + "computedY" : 335.95, "placementX" : 19.0, - "placementY" : 366.74, + "placementY" : 335.95, "placementWidth" : 80.914, "placementHeight" : 12.19, "startPage" : 0, @@ -1132,9 +1192,9 @@ "depth" : 4, "layer" : 4, "computedX" : 409.259, - "computedY" : 355.61, + "computedY" : 324.82, "placementX" : 409.259, - "placementY" : 355.61, + "placementY" : 324.82, "placementWidth" : 172.017, "placementHeight" : 23.32, "startPage" : 0, @@ -1162,9 +1222,9 @@ "depth" : 5, "layer" : 5, "computedX" : 409.259, - "computedY" : 355.61, + "computedY" : 324.82, "placementX" : 409.259, - "placementY" : 355.61, + "placementY" : 324.82, "placementWidth" : 172.017, "placementHeight" : 23.32, "startPage" : 0, @@ -1192,9 +1252,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 340.48, + "computedY" : 309.69, "placementX" : 19.0, - "placementY" : 340.48, + "placementY" : 309.69, "placementWidth" : 56.448, "placementHeight" : 11.13, "startPage" : 0, @@ -1222,9 +1282,9 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 306.76, + "computedY" : 275.97, "placementX" : 15.0, - "placementY" : 306.76, + "placementY" : 275.97, "placementWidth" : 570.276, "placementHeight" : 22.72, "startPage" : 0, @@ -1252,9 +1312,9 @@ "depth" : 3, "layer" : 3, "computedX" : 20.0, - "computedY" : 311.76, + "computedY" : 280.97, "placementX" : 20.0, - "placementY" : 311.76, + "placementY" : 280.97, "placementWidth" : 560.276, "placementHeight" : 12.72, "startPage" : 0, @@ -1282,9 +1342,9 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 264.97, + "computedY" : 234.18, "placementX" : 15.0, - "placementY" : 264.97, + "placementY" : 234.18, "placementWidth" : 295.085, "placementHeight" : 34.79, "startPage" : 0, @@ -1312,9 +1372,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 282.365, + "computedY" : 251.575, "placementX" : 19.0, - "placementY" : 282.365, + "placementY" : 251.575, "placementWidth" : 287.085, "placementHeight" : 11.395, "startPage" : 0, @@ -1342,9 +1402,9 @@ "depth" : 3, "layer" : 3, "computedX" : 19.0, - "computedY" : 264.97, + "computedY" : 234.18, "placementX" : 19.0, - "placementY" : 264.97, + "placementY" : 234.18, "placementWidth" : 273.962, "placementHeight" : 11.395, "startPage" : 0, diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_centered_headline.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_centered_headline.json index 71dde9e4..def29652 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_centered_headline.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_centered_headline.json @@ -295,11 +295,11 @@ "computedY" : 710.89, "placementX" : 15.0, "placementY" : 710.89, - "placementWidth" : 160.949, + "placementWidth" : 165.566, "placementHeight" : 11.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 160.949, + "contentWidth" : 165.566, "contentHeight" : 11.4, "margin" : { "top" : 0.0, @@ -415,11 +415,11 @@ "computedY" : 664.05, "placementX" : 15.0, "placementY" : 664.05, - "placementWidth" : 114.722, + "placementWidth" : 119.339, "placementHeight" : 11.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 114.722, + "contentWidth" : 119.339, "contentHeight" : 11.4, "margin" : { "top" : 0.0, @@ -535,11 +535,11 @@ "computedY" : 592.33, "placementX" : 15.0, "placementY" : 592.33, - "placementWidth" : 192.347, + "placementWidth" : 201.581, "placementHeight" : 11.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 192.347, + "contentWidth" : 201.581, "contentHeight" : 11.4, "margin" : { "top" : 0.0, @@ -835,11 +835,11 @@ "computedY" : 463.77, "placementX" : 15.0, "placementY" : 463.77, - "placementWidth" : 175.427, + "placementWidth" : 180.044, "placementHeight" : 11.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 175.427, + "contentWidth" : 180.044, "contentHeight" : 11.4, "margin" : { "top" : 0.0, @@ -1225,11 +1225,11 @@ "computedY" : 385.09, "placementX" : 15.0, "placementY" : 385.09, - "placementWidth" : 173.527, + "placementWidth" : 178.144, "placementHeight" : 11.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 173.527, + "contentWidth" : 178.144, "contentHeight" : 11.4, "margin" : { "top" : 0.0, diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_classic_serif.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_classic_serif.json index 3838e28a..f4a16152 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_classic_serif.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_classic_serif.json @@ -355,11 +355,11 @@ "computedY" : 660.222, "placementX" : 25.0, "placementY" : 660.222, - "placementWidth" : 78.209, + "placementWidth" : 83.177, "placementHeight" : 12.19, "startPage" : 0, "endPage" : 0, - "contentWidth" : 78.209, + "contentWidth" : 83.177, "contentHeight" : 12.19, "margin" : { "top" : 0.0, @@ -655,11 +655,11 @@ "computedY" : 567.687, "placementX" : 317.638, "placementY" : 567.687, - "placementWidth" : 128.754, + "placementWidth" : 133.722, "placementHeight" : 12.19, "startPage" : 0, "endPage" : 0, - "contentWidth" : 128.754, + "contentWidth" : 133.722, "contentHeight" : 12.19, "margin" : { "top" : 0.0, diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_monogram_sidebar.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_monogram_sidebar.json index 26650bd6..0ca30fbb 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_monogram_sidebar.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_monogram_sidebar.json @@ -22,15 +22,15 @@ "depth" : 1, "layer" : 1, "computedX" : 15.0, - "computedY" : 69.12, + "computedY" : 58.32, "placementX" : 15.0, - "placementY" : 69.12, + "placementY" : 58.32, "placementWidth" : 570.276, - "placementHeight" : 757.77, + "placementHeight" : 768.57, "startPage" : 0, "endPage" : 0, "contentWidth" : 570.276, - "contentHeight" : 757.77, + "contentHeight" : 768.57, "margin" : { "top" : 0.0, "right" : 0.0, @@ -52,15 +52,15 @@ "depth" : 2, "layer" : 2, "computedX" : 15.0, - "computedY" : 69.12, + "computedY" : 58.32, "placementX" : 15.0, - "placementY" : 69.12, + "placementY" : 58.32, "placementWidth" : 570.276, - "placementHeight" : 757.77, + "placementHeight" : 768.57, "startPage" : 0, "endPage" : 0, "contentWidth" : 570.276, - "contentHeight" : 757.77, + "contentHeight" : 768.57, "margin" : { "top" : 0.0, "right" : 0.0, @@ -82,15 +82,15 @@ "depth" : 3, "layer" : 3, "computedX" : 15.0, - "computedY" : 69.12, + "computedY" : 58.32, "placementX" : 15.0, - "placementY" : 69.12, + "placementY" : 58.32, "placementWidth" : 188.191, - "placementHeight" : 757.77, + "placementHeight" : 768.57, "startPage" : 0, "endPage" : 0, "contentWidth" : 188.191, - "contentHeight" : 757.77, + "contentHeight" : 768.57, "margin" : { "top" : 0.0, "right" : 0.0, @@ -622,15 +622,15 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 333.29, + "computedY" : 322.49, "placementX" : 28.0, - "placementY" : 333.29, + "placementY" : 322.49, "placementWidth" : 162.191, - "placementHeight" : 9.6, + "placementHeight" : 20.4, "startPage" : 0, "endPage" : 0, "contentWidth" : 162.191, - "contentHeight" : 9.6, + "contentHeight" : 20.4, "margin" : { "top" : 6.0, "right" : 0.0, @@ -652,9 +652,9 @@ "depth" : 4, "layer" : 4, "computedX" : 50.095, - "computedY" : 323.29, + "computedY" : 312.49, "placementX" : 50.095, - "placementY" : 323.29, + "placementY" : 312.49, "placementWidth" : 118.0, "placementHeight" : 1.0, "startPage" : 0, @@ -682,9 +682,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 298.17, + "computedY" : 287.37, "placementX" : 28.0, - "placementY" : 298.17, + "placementY" : 287.37, "placementWidth" : 162.191, "placementHeight" : 9.12, "startPage" : 0, @@ -712,9 +712,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 281.53, + "computedY" : 270.73, "placementX" : 28.0, - "placementY" : 281.53, + "placementY" : 270.73, "placementWidth" : 162.191, "placementHeight" : 8.64, "startPage" : 0, @@ -742,9 +742,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 258.41, + "computedY" : 247.61, "placementX" : 28.0, - "placementY" : 258.41, + "placementY" : 247.61, "placementWidth" : 162.191, "placementHeight" : 9.12, "startPage" : 0, @@ -772,9 +772,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 241.77, + "computedY" : 230.97, "placementX" : 28.0, - "placementY" : 241.77, + "placementY" : 230.97, "placementWidth" : 162.191, "placementHeight" : 8.64, "startPage" : 0, @@ -802,9 +802,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 218.17, + "computedY" : 207.37, "placementX" : 28.0, - "placementY" : 218.17, + "placementY" : 207.37, "placementWidth" : 162.191, "placementHeight" : 9.6, "startPage" : 0, @@ -832,9 +832,9 @@ "depth" : 4, "layer" : 4, "computedX" : 50.095, - "computedY" : 208.17, + "computedY" : 197.37, "placementX" : 50.095, - "placementY" : 208.17, + "placementY" : 197.37, "placementWidth" : 118.0, "placementHeight" : 1.0, "startPage" : 0, @@ -862,9 +862,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 188.29, + "computedY" : 177.49, "placementX" : 28.0, - "placementY" : 188.29, + "placementY" : 177.49, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -892,9 +892,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 170.41, + "computedY" : 159.61, "placementX" : 28.0, - "placementY" : 170.41, + "placementY" : 159.61, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -922,9 +922,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 152.53, + "computedY" : 141.73, "placementX" : 28.0, - "placementY" : 152.53, + "placementY" : 141.73, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -952,9 +952,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 134.65, + "computedY" : 123.85, "placementX" : 28.0, - "placementY" : 134.65, + "placementY" : 123.85, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -982,9 +982,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 116.77, + "computedY" : 105.97, "placementX" : 28.0, - "placementY" : 116.77, + "placementY" : 105.97, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -1012,9 +1012,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 98.89, + "computedY" : 88.09, "placementX" : 28.0, - "placementY" : 98.89, + "placementY" : 88.09, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -1042,9 +1042,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 81.01, + "computedY" : 70.21, "placementX" : 28.0, - "placementY" : 81.01, + "placementY" : 70.21, "placementWidth" : 162.191, "placementHeight" : 8.88, "startPage" : 0, @@ -1072,9 +1072,9 @@ "depth" : 4, "layer" : 4, "computedX" : 28.0, - "computedY" : 69.12, + "computedY" : 58.32, "placementX" : 28.0, - "placementY" : 69.12, + "placementY" : 58.32, "placementWidth" : 162.191, "placementHeight" : 3.89, "startPage" : 0, @@ -1225,11 +1225,11 @@ "computedY" : 630.221, "placementX" : 221.191, "placementY" : 630.221, - "placementWidth" : 152.478, + "placementWidth" : 156.852, "placementHeight" : 10.8, "startPage" : 0, "endPage" : 0, - "contentWidth" : 152.478, + "contentWidth" : 156.852, "contentHeight" : 10.8, "margin" : { "top" : 6.0, @@ -1315,11 +1315,11 @@ "computedY" : 557.071, "placementX" : 221.191, "placementY" : 557.071, - "placementWidth" : 166.194, + "placementWidth" : 170.568, "placementHeight" : 10.8, "startPage" : 0, "endPage" : 0, - "contentWidth" : 166.194, + "contentWidth" : 170.568, "contentHeight" : 10.8, "margin" : { "top" : 6.0, @@ -1405,11 +1405,11 @@ "computedY" : 512.831, "placementX" : 221.191, "placementY" : 512.831, - "placementWidth" : 240.478, + "placementWidth" : 258.46, "placementHeight" : 8.88, "startPage" : 0, "endPage" : 0, - "contentWidth" : 240.478, + "contentWidth" : 258.46, "contentHeight" : 8.88, "margin" : { "top" : 0.0, @@ -1465,11 +1465,11 @@ "computedY" : 479.591, "placementX" : 221.191, "placementY" : 479.591, - "placementWidth" : 315.958, + "placementWidth" : 341.133, "placementHeight" : 8.88, "startPage" : 0, "endPage" : 0, - "contentWidth" : 315.958, + "contentWidth" : 341.133, "contentHeight" : 8.88, "margin" : { "top" : 0.0, diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_sidebar_portrait.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_sidebar_portrait.json index 28006173..31eff814 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_sidebar_portrait.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_sidebar_portrait.json @@ -505,11 +505,11 @@ "computedY" : 367.596, "placementX" : 41.0, "placementY" : 367.596, - "placementWidth" : 75.319, + "placementWidth" : 80.568, "placementHeight" : 12.96, "startPage" : 0, "endPage" : 0, - "contentWidth" : 75.319, + "contentWidth" : 80.568, "contentHeight" : 12.96, "margin" : { "top" : 0.0, @@ -895,11 +895,11 @@ "computedY" : 647.015, "placementX" : 242.894, "placementY" : 647.015, - "placementWidth" : 189.624, + "placementWidth" : 195.456, "placementHeight" : 14.4, "startPage" : 0, "endPage" : 0, - "contentWidth" : 189.624, + "contentWidth" : 195.456, "contentHeight" : 14.4, "margin" : { "top" : 8.0, diff --git a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_timeline_minimal.json b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_timeline_minimal.json index 26b28429..5b53f6b7 100644 --- a/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_timeline_minimal.json +++ b/src/test/resources/layout-snapshots/canonical-templates/cv-v2/template_v2_cv_timeline_minimal.json @@ -85,11 +85,11 @@ "computedY" : 777.89, "placementX" : 15.0, "placementY" : 777.89, - "placementWidth" : 268.464, + "placementWidth" : 279.664, "placementHeight" : 49.0, "startPage" : 0, "endPage" : 0, - "contentWidth" : 268.464, + "contentWidth" : 279.664, "contentHeight" : 49.0, "margin" : { "top" : 0.0, @@ -115,11 +115,11 @@ "computedY" : 793.29, "placementX" : 15.0, "placementY" : 793.29, - "placementWidth" : 268.464, + "placementWidth" : 279.664, "placementHeight" : 33.6, "startPage" : 0, "endPage" : 0, - "contentWidth" : 268.464, + "contentWidth" : 279.664, "contentHeight" : 33.6, "margin" : { "top" : 0.0, diff --git a/src/test/resources/layout-snapshots/document/nested_list_three_levels.json b/src/test/resources/layout-snapshots/document/nested_list_three_levels.json index bcece595..90e3b20f 100644 --- a/src/test/resources/layout-snapshots/document/nested_list_three_levels.json +++ b/src/test/resources/layout-snapshots/document/nested_list_three_levels.json @@ -25,11 +25,11 @@ "computedY" : 269.15, "placementX" : 12.0, "placementY" : 269.15, - "placementWidth" : 42.812, + "placementWidth" : 54.488, "placementHeight" : 38.85, "startPage" : 0, "endPage" : 0, - "contentWidth" : 42.812, + "contentWidth" : 54.488, "contentHeight" : 38.85, "margin" : { "top" : 0.0, @@ -55,11 +55,11 @@ "computedY" : 269.15, "placementX" : 12.0, "placementY" : 269.15, - "placementWidth" : 42.812, + "placementWidth" : 54.488, "placementHeight" : 38.85, "startPage" : 0, "endPage" : 0, - "contentWidth" : 42.812, + "contentWidth" : 54.488, "contentHeight" : 38.85, "margin" : { "top" : 0.0, diff --git a/src/test/resources/visual-baselines/cv-v2/blue_banner-page-0.png b/src/test/resources/visual-baselines/cv-v2/blue_banner-page-0.png index ecfd63d7..a09f4bf1 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/blue_banner-page-0.png and b/src/test/resources/visual-baselines/cv-v2/blue_banner-page-0.png differ diff --git a/src/test/resources/visual-baselines/cv-v2/boxed_sections-page-0.png b/src/test/resources/visual-baselines/cv-v2/boxed_sections-page-0.png index 372588f2..6722c836 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/boxed_sections-page-0.png and b/src/test/resources/visual-baselines/cv-v2/boxed_sections-page-0.png differ diff --git a/src/test/resources/visual-baselines/cv-v2/centered_headline-page-0.png b/src/test/resources/visual-baselines/cv-v2/centered_headline-page-0.png index 4a550f55..17790408 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/centered_headline-page-0.png and b/src/test/resources/visual-baselines/cv-v2/centered_headline-page-0.png differ diff --git a/src/test/resources/visual-baselines/cv-v2/classic_serif-page-0.png b/src/test/resources/visual-baselines/cv-v2/classic_serif-page-0.png index dc573a6e..143ae923 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/classic_serif-page-0.png and b/src/test/resources/visual-baselines/cv-v2/classic_serif-page-0.png differ diff --git a/src/test/resources/visual-baselines/cv-v2/monogram_sidebar-page-0.png b/src/test/resources/visual-baselines/cv-v2/monogram_sidebar-page-0.png index c09bf062..fa300321 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/monogram_sidebar-page-0.png and b/src/test/resources/visual-baselines/cv-v2/monogram_sidebar-page-0.png differ diff --git a/src/test/resources/visual-baselines/cv-v2/sidebar_portrait-page-0.png b/src/test/resources/visual-baselines/cv-v2/sidebar_portrait-page-0.png index b3f1dd5f..6e139b6e 100644 Binary files a/src/test/resources/visual-baselines/cv-v2/sidebar_portrait-page-0.png and b/src/test/resources/visual-baselines/cv-v2/sidebar_portrait-page-0.png differ