diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d612f42..899ff657 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,11 +26,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Temurin JDK 21 + - name: Set up Temurin JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '17' cache: maven - name: Run guards @@ -51,11 +51,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Temurin JDK 21 + - name: Set up Temurin JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '17' cache: maven - name: Build and run tests @@ -76,11 +76,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Temurin JDK 21 + - name: Set up Temurin JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '17' cache: maven - name: Install root artifact @@ -114,11 +114,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Temurin JDK 21 + - name: Set up Temurin JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '17' cache: maven - name: Run coarse performance smoke benchmark @@ -148,11 +148,11 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Set up Temurin JDK 21 + - name: Set up Temurin JDK 17 uses: actions/setup-java@v4 with: distribution: temurin - java-version: '21' + java-version: '17' cache: maven - name: Restore benchmark history cache diff --git a/docs/production-rendering.md b/docs/production-rendering.md index 9c673317..c8ac162c 100644 --- a/docs/production-rendering.md +++ b/docs/production-rendering.md @@ -40,6 +40,16 @@ the session. `buildPdf(Path)` also uses the streaming path internally. +## Glyph Fallback + +When a PDF render uses a Standard 14 font such as Helvetica, some Unicode +glyphs are not encodable through PDFBox's WinAnsi path. GraphCompose now +degrades unsupported glyphs to `?` during both width measurement and PDF text +emission so rendering does not fail mid-document. + +If preserving the original glyph matters, register an embedded/custom font +family that contains the character instead of relying on the Standard 14 fonts. + ## Debug Output Use `GraphCompose.document(path).guideLines(true)` or diff --git a/examples/pom.xml b/examples/pom.xml index b3f76104..8bc3e258 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -14,7 +14,7 @@ ${project.version} 1.5.18 - 21 + 17 5.12.2 3.27.3 @@ -84,4 +84,4 @@ - \ No newline at end of file + diff --git a/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java b/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java index 5b80f37a..1db6decd 100644 --- a/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java +++ b/examples/src/main/java/com/demcha/examples/support/WeeklyScheduleRenderer.java @@ -607,16 +607,19 @@ private static List dayCells(DayShift dayShift, Theme theme) if (dayShift == null) { return List.of(emptyDayCell(theme)); } - return switch (dayShift) { - case DayShift.EmptyDay e -> - List.of(emptyDayCell(theme)); - case DayShift.FullStatus(ShiftStatus s) -> - List.of(fullStatusCell(s, theme)); - case DayShift.CrossMeal(Shift shift) -> - shiftAcrossDay(shift, theme); - case DayShift.Halves(Half lunch, Half dinner) -> - mergedHalfCells(lunch, dinner, theme); - }; + if (dayShift instanceof DayShift.EmptyDay) { + return List.of(emptyDayCell(theme)); + } + if (dayShift instanceof DayShift.FullStatus fullStatus) { + return List.of(fullStatusCell(fullStatus.status(), theme)); + } + if (dayShift instanceof DayShift.CrossMeal crossMeal) { + return shiftAcrossDay(crossMeal.shift(), theme); + } + if (dayShift instanceof DayShift.Halves halves) { + return mergedHalfCells(halves.lunch(), halves.dinner(), theme); + } + throw new IllegalStateException("Unsupported day shift: " + dayShift); } private static List mergedHalfCells(Half lunch, Half dinner, Theme theme) { @@ -633,11 +636,16 @@ private static List mergedHalfCells(Half lunch, Half dinner, } private static List halfCells(Half half, Theme theme) { - return switch (half) { - case Half.Empty e -> List.of(emptyHalfCell(theme)); - case Half.StatusFill(ShiftStatus s) -> List.of(statusHalfCell(s, theme)); - case Half.Working(Shift shift) -> shiftHalfCells(shift, theme); - }; + if (half instanceof Half.Empty) { + return List.of(emptyHalfCell(theme)); + } + if (half instanceof Half.StatusFill statusFill) { + return List.of(statusHalfCell(statusFill.status(), theme)); + } + if (half instanceof Half.Working working) { + return shiftHalfCells(working.shift(), theme); + } + throw new IllegalStateException("Unsupported half shift: " + half); } private static DocumentTableCell emptyDayCell(Theme theme) { diff --git a/pom.xml b/pom.xml index f1dd0317..01c22cfd 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ UTF-8 - 21 + 17 3.27.3 diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java index 780805c3..82fba7ff 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfParagraphFragmentRenderHandler.java @@ -89,7 +89,8 @@ private void renderLine(PDPageContentStream stream, try { for (ParagraphSpan span : spans) { if (span instanceof ParagraphTextSpan textSpan) { - String text = sanitize(textSpan.text()); + PdfFont font = fonts.getFont(textSpan.textStyle().fontName(), PdfFont.class).orElseThrow(); + String text = font.sanitizeForRender(textSpan.textStyle(), sanitize(textSpan.text())); if (text.isEmpty()) { cursorX += textSpan.width(); continue; @@ -99,7 +100,6 @@ private void renderLine(PDPageContentStream stream, stream.newLineAtOffset((float) cursorX, (float) baselineY); inTextBlock = true; } - PdfFont font = fonts.getFont(textSpan.textStyle().fontName(), PdfFont.class).orElseThrow(); stream.setFont(font.fontType(textSpan.textStyle().decoration()), (float) textSpan.textStyle().size()); stream.setNonStrokingColor(textSpan.textStyle().color()); stream.showText(text); diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeClipBeginRenderHandler.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeClipBeginRenderHandler.java index f8f8c6da..cbe4db6f 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeClipBeginRenderHandler.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfShapeClipBeginRenderHandler.java @@ -69,12 +69,16 @@ public void render(PlacedFragment fragment, if (policy == ClipPolicy.CLIP_BOUNDS) { stream.addRect(x, y, width, height); } else { // CLIP_PATH - switch (outline) { - case ShapeOutline.Ellipse ignored -> addEllipsePath(stream, x, y, width, height); - case ShapeOutline.RoundedRectangle r -> addRoundedRectanglePath( + if (outline instanceof ShapeOutline.Ellipse) { + addEllipsePath(stream, x, y, width, height); + } else if (outline instanceof ShapeOutline.RoundedRectangle r) { + addRoundedRectanglePath( stream, x, y, width, height, (float) Math.min(r.cornerRadius(), Math.min(width, height) / 2.0f)); - case ShapeOutline.Rectangle ignored -> stream.addRect(x, y, width, height); + } else if (outline instanceof ShapeOutline.Rectangle) { + stream.addRect(x, y, width, height); + } else { + throw new IllegalStateException("Unsupported shape outline: " + outline); } } // clip() pushes the current path onto the clipping path; the path diff --git a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfTableRowFragmentRenderHandler.java b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfTableRowFragmentRenderHandler.java index 7ef961ad..4f8cdc1b 100644 --- a/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfTableRowFragmentRenderHandler.java +++ b/src/main/java/com/demcha/compose/document/backend/fixed/pdf/handlers/PdfTableRowFragmentRenderHandler.java @@ -178,12 +178,13 @@ private void renderCellText(PDPageContentStream stream, stream.setFont(font.fontType(cell.style().textStyle().decoration()), (float) cell.style().textStyle().size()); stream.setNonStrokingColor(cell.style().textStyle().color()); for (ResolvedTextLine line : lines) { - if (line.text().isEmpty()) { + String rendered = font.sanitizeForRender(cell.style().textStyle(), line.text()); + if (rendered.isEmpty()) { continue; } stream.beginText(); stream.newLineAtOffset((float) line.x(), (float) line.baselineY()); - stream.showText(line.text()); + stream.showText(rendered); stream.endText(); } } finally { diff --git a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java index 98a81372..94744552 100644 --- a/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java +++ b/src/main/java/com/demcha/compose/document/layout/TextFlowSupport.java @@ -238,7 +238,7 @@ public static PreparedSplitResult splitList(PreparedNode pre return new PreparedSplitResult<>(head, tail); } - PreparedListItemLayout firstItem = layout.items().getFirst(); + PreparedListItemLayout firstItem = layout.items().get(0); PreparedParagraphLayout itemLayout = firstItem.paragraphLayout(); int maxLines = maxLinesThatFit( itemLayout.visualLines(), @@ -796,7 +796,7 @@ private static List wrapParagraph(List logicalLines, result.add(currentPrefix + chunks.get(index)); currentPrefix = continuationPrefix; } - currentLine = currentPrefix + chunks.getLast(); + currentLine = currentPrefix + chunks.get(chunks.size() - 1); hasContent = true; } @@ -1451,7 +1451,7 @@ private static int maxLinesThatFit(List lines, double lineGap, do if (lines.isEmpty()) { return 0; } - if (availableHeight + EPS < lines.getFirst().lineHeight()) { + if (availableHeight + EPS < lines.get(0).lineHeight()) { return 0; } diff --git a/src/main/java/com/demcha/compose/document/layout/definitions/ShapeContainerDefinition.java b/src/main/java/com/demcha/compose/document/layout/definitions/ShapeContainerDefinition.java index 16bbd65d..c6c45441 100644 --- a/src/main/java/com/demcha/compose/document/layout/definitions/ShapeContainerDefinition.java +++ b/src/main/java/com/demcha/compose/document/layout/definitions/ShapeContainerDefinition.java @@ -105,8 +105,9 @@ public List emitFragments(PreparedNode prepa } Color awtFill = node.fillColor() == null ? null : node.fillColor().color(); Stroke stroke = toStroke(node.stroke()); - LayoutFragment outlineFragment = switch (outline) { - case ShapeOutline.Ellipse ignored -> new LayoutFragment( + LayoutFragment outlineFragment; + if (outline instanceof ShapeOutline.Ellipse) { + outlineFragment = new LayoutFragment( placement.path(), 0, padLeft, @@ -114,7 +115,8 @@ public List emitFragments(PreparedNode prepa width, height, new EllipseFragmentPayload(awtFill, stroke, null, null)); - case ShapeOutline.Rectangle ignored -> new LayoutFragment( + } else if (outline instanceof ShapeOutline.Rectangle) { + outlineFragment = new LayoutFragment( placement.path(), 0, padLeft, @@ -122,7 +124,8 @@ public List emitFragments(PreparedNode prepa width, height, new ShapeFragmentPayload(awtFill, stroke, 0.0, null, null, null)); - case ShapeOutline.RoundedRectangle r -> new LayoutFragment( + } else if (outline instanceof ShapeOutline.RoundedRectangle r) { + outlineFragment = new LayoutFragment( placement.path(), 0, padLeft, @@ -130,7 +133,9 @@ public List emitFragments(PreparedNode prepa width, height, new ShapeFragmentPayload(awtFill, stroke, r.cornerRadius(), null, null, null)); - }; + } else { + throw new IllegalStateException("Unsupported shape outline: " + outline); + } List opening = new ArrayList<>(4); boolean hasTransform = !node.transform().isIdentity(); diff --git a/src/main/java/com/demcha/compose/document/templates/components/Module.java b/src/main/java/com/demcha/compose/document/templates/components/Module.java index 4ec37118..4234829d 100644 --- a/src/main/java/com/demcha/compose/document/templates/components/Module.java +++ b/src/main/java/com/demcha/compose/document/templates/components/Module.java @@ -158,16 +158,25 @@ private DocumentTextStyle bodyStyle(BusinessTheme theme) { } private DocumentNode renderBody(BusinessTheme theme, Spacing spacing) { - return switch (body) { - case ParagraphBlock p -> renderParagraph(p, theme, spacing); - case BulletListBlock b -> renderList(b.items(), ListMarker.bullet(), - "bullet", theme, spacing); - case NumberedListBlock n -> renderList(n.items(), ListMarker.custom("1."), - "numbered", theme, spacing); - case MultiParagraphBlock m -> renderMultiParagraph(m, theme, spacing); - case IndentedBlock i -> renderIndented(i, theme, spacing); - case KeyValueBlock k -> renderKeyValue(k, theme, spacing); - }; + if (body instanceof ParagraphBlock p) { + return renderParagraph(p, theme, spacing); + } + if (body instanceof BulletListBlock b) { + return renderList(b.items(), ListMarker.bullet(), "bullet", theme, spacing); + } + if (body instanceof NumberedListBlock n) { + return renderList(n.items(), ListMarker.custom("1."), "numbered", theme, spacing); + } + if (body instanceof MultiParagraphBlock m) { + return renderMultiParagraph(m, theme, spacing); + } + if (body instanceof IndentedBlock i) { + return renderIndented(i, theme, spacing); + } + if (body instanceof KeyValueBlock k) { + return renderKeyValue(k, theme, spacing); + } + throw new IllegalStateException("Unsupported block: " + body); } private ParagraphNode renderParagraph(ParagraphBlock block, BusinessTheme theme, Spacing spacing) { diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java index e83f22ac..25b4ec96 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java @@ -215,39 +215,33 @@ private void addModuleBody(SectionBuilder section, CvModule module) { } private void renderBody(SectionBuilder section, Block body) { - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - WorkEntry entry = parseWorkEntry(line); - if (entry != null) { - renderWorkEntry(section, entry); - } else { - renderParagraph(section, line); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + WorkEntry entry = parseWorkEntry(line); + if (entry != null) { + renderWorkEntry(section, entry); + } else { + renderParagraph(section, line); } } - case BulletListBlock b -> renderBulletList(section, b.items()); - case NumberedListBlock n -> { - for (String item : n.items()) { - renderParagraph(section, item); - } - } - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof BulletListBlock b) { + renderBulletList(section, b.items()); + } else if (body instanceof NumberedListBlock n) { + for (String item : n.items()) { + renderParagraph(section, item); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderKeyValueEntry(section, entry); - } + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderKeyValueEntry(section, entry); } } } 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 39b45e3c..dc8d8e6f 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 @@ -189,48 +189,40 @@ private void addModuleBody(SectionBuilder section, CvModule module) { } private void renderBody(SectionBuilder section, Block body) { - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - WorkEntry entry = parseWorkEntry(line); - if (entry != null) { - renderWorkEntry(section, entry); - } else { - renderParagraph(section, line); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + WorkEntry entry = parseWorkEntry(line); + if (entry != null) { + renderWorkEntry(section, entry); + } else { + renderParagraph(section, line); } } - case BulletListBlock b -> { - for (String item : b.items()) { - WorkEntry entry = parseWorkEntry(item); - if (entry != null) { - renderWorkEntry(section, entry); - } else { - renderBulletItem(section, item); - } + } else if (body instanceof BulletListBlock b) { + for (String item : b.items()) { + WorkEntry entry = parseWorkEntry(item); + if (entry != null) { + renderWorkEntry(section, entry); + } else { + renderBulletItem(section, item); } } - case NumberedListBlock n -> { - for (String item : n.items()) { - renderParagraph(section, item); - } - } - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof NumberedListBlock n) { + for (String item : n.items()) { + renderParagraph(section, item); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderParagraph(section, entry.key() + ": " + entry.value()); - } + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderParagraph(section, entry.key() + ": " + entry.value()); } } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java index f2bd57da..e7b7ca2d 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java @@ -216,39 +216,33 @@ private void renderBody(SectionBuilder section, CvModule module) { section.spacing(4) .padding(DocumentInsets.zero()); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - WorkEntry entry = parseWorkEntry(line); - if (entry != null) { - renderWorkEntry(section, entry); - } else { - renderParagraph(section, line); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + WorkEntry entry = parseWorkEntry(line); + if (entry != null) { + renderWorkEntry(section, entry); + } else { + renderParagraph(section, line); } } - case BulletListBlock b -> renderBulletList(section, b.items()); - case NumberedListBlock n -> { - for (String item : n.items()) { - renderParagraph(section, item); - } - } - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof BulletListBlock b) { + renderBulletList(section, b.items()); + } else if (body instanceof NumberedListBlock n) { + for (String item : n.items()) { + renderParagraph(section, item); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderKeyValueEntry(section, entry); - } + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderKeyValueEntry(section, entry); } } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java index b3a82725..e1868dc6 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/ClassicSerif.java @@ -349,12 +349,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case NumberedListBlock n -> n.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -368,11 +372,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java index 90137ad7..c29d9204 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/CompactMono.java @@ -299,12 +299,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case NumberedListBlock n -> n.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -318,11 +322,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java index f7328a6f..3666fc88 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java @@ -549,38 +549,30 @@ private void sectionHeader(PageFlowBuilder pageFlow, String name, String title, } private void renderBody(SectionBuilder section, Block body) { - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - renderParagraph(section, line); - } - } - case BulletListBlock b -> { - for (String item : b.items()) { - renderParagraph(section, item); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + renderParagraph(section, line); } - case NumberedListBlock n -> { - for (String item : n.items()) { - renderParagraph(section, item); - } + } else if (body instanceof BulletListBlock b) { + for (String item : b.items()) { + renderParagraph(section, item); } - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof NumberedListBlock n) { + for (String item : n.items()) { + renderParagraph(section, item); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderKeyValueEntry(section, entry); - } + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderKeyValueEntry(section, entry); } } } @@ -668,12 +660,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case NumberedListBlock n -> n.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -687,11 +683,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java index 64df179a..67618c71 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/EngineeringResume.java @@ -17,6 +17,7 @@ import com.demcha.compose.document.templates.blocks.IndentedBlock; import com.demcha.compose.document.templates.blocks.KeyValueBlock; import com.demcha.compose.document.templates.blocks.MultiParagraphBlock; +import com.demcha.compose.document.templates.blocks.NumberedListBlock; import com.demcha.compose.document.templates.blocks.ParagraphBlock; import com.demcha.compose.document.templates.cv.spec.CvHeader; import com.demcha.compose.document.templates.cv.spec.CvModule; @@ -444,11 +445,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -462,11 +468,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java index a34cc558..9772f811 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java @@ -189,30 +189,26 @@ private void addModule(PageFlowBuilder flow, CvModule module) { } private void renderBody(SectionBuilder section, Block body) { - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - renderParagraph(section, line); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + renderParagraph(section, line); } - case BulletListBlock b -> renderBulletList(section, b.items()); - case NumberedListBlock n -> renderNumberedList(section, n.items()); - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof BulletListBlock b) { + renderBulletList(section, b.items()); + } else if (body instanceof NumberedListBlock n) { + renderNumberedList(section, n.items()); + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderParagraph(section, entry.key() + ": " + entry.value()); - } - } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderParagraph(section, entry.key() + ": " + entry.value()); } } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java index 79a2cd50..de330c28 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java @@ -497,32 +497,34 @@ private static List moduleItems(CvModule module) { } List result = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> { - String t = safe(p.text()).trim(); - if (!t.isBlank()) { - result.add(t); - } + if (body instanceof ParagraphBlock p) { + String t = safe(p.text()).trim(); + if (!t.isBlank()) { + result.add(t); } - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> { + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> { String t = safe(line).trim(); if (!t.isBlank()) { result.add(t); } }); - case BulletListBlock b -> b.items().forEach(item -> { + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> { String t = safe(item).trim(); if (!t.isBlank()) { result.add(t); } }); - case NumberedListBlock n -> n.items().forEach(item -> { + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> { String t = safe(item).trim(); if (!t.isBlank()) { result.add(t); } }); - case IndentedBlock i -> i.items().forEach(item -> { + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -536,11 +538,8 @@ private static List moduleItems(CvModule module) { result.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - result.add(entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> result.add(entry.key() + ": " + entry.value())); } return result; } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java index be144b8b..774c6033 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/NordicClean.java @@ -17,6 +17,7 @@ import com.demcha.compose.document.templates.blocks.IndentedBlock; import com.demcha.compose.document.templates.blocks.KeyValueBlock; import com.demcha.compose.document.templates.blocks.MultiParagraphBlock; +import com.demcha.compose.document.templates.blocks.NumberedListBlock; import com.demcha.compose.document.templates.blocks.ParagraphBlock; import com.demcha.compose.document.templates.cv.spec.CvHeader; import com.demcha.compose.document.templates.cv.spec.CvModule; @@ -428,11 +429,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -446,11 +452,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // other block kinds intentionally not surfaced here. - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java index b0d68c32..8e1cfd17 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/Panel.java @@ -260,30 +260,26 @@ private void renderPanel(SectionBuilder section, String title, CvModule module) } private void renderBody(SectionBuilder section, Block body) { - switch (body) { - case ParagraphBlock p -> renderParagraph(section, p.text()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - renderParagraph(section, line); - } + if (body instanceof ParagraphBlock p) { + renderParagraph(section, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + renderParagraph(section, line); } - case BulletListBlock b -> renderBulletList(section, b.items()); - case NumberedListBlock n -> renderBulletList(section, n.items()); - case IndentedBlock i -> { - for (IndentedBlock.Item item : i.items()) { - String inline = (item.title().isBlank() ? "" : item.title()) - + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") - + (item.body().isBlank() ? "" : item.body()); - renderParagraph(section, inline); - } + } else if (body instanceof BulletListBlock b) { + renderBulletList(section, b.items()); + } else if (body instanceof NumberedListBlock n) { + renderBulletList(section, n.items()); + } else if (body instanceof IndentedBlock i) { + for (IndentedBlock.Item item : i.items()) { + String inline = (item.title().isBlank() ? "" : item.title()) + + (item.title().isBlank() || item.body().isBlank() ? "" : " - ") + + (item.body().isBlank() ? "" : item.body()); + renderParagraph(section, inline); } - case KeyValueBlock kv -> { - for (KeyValueBlock.Entry entry : kv.entries()) { - renderKeyValueEntry(section, entry); - } - } - default -> { - // ignore other block kinds + } else if (body instanceof KeyValueBlock kv) { + for (KeyValueBlock.Entry entry : kv.entries()) { + renderKeyValueEntry(section, entry); } } } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java index a417f453..97ae0345 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java @@ -498,20 +498,18 @@ private static List moduleParagraphs(CvModule module) { } List result = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> result.add(safe(p.text()).trim()); - case MultiParagraphBlock m -> { - for (String line : m.paragraphs()) { - String t = safe(line).trim(); - if (!t.isBlank()) { - result.add(t); - } + if (body instanceof ParagraphBlock p) { + result.add(safe(p.text()).trim()); + } else if (body instanceof MultiParagraphBlock m) { + for (String line : m.paragraphs()) { + String t = safe(line).trim(); + if (!t.isBlank()) { + result.add(t); } } - default -> { - for (String item : moduleItems(module)) { - result.add(item); - } + } else { + for (String item : moduleItems(module)) { + result.add(item); } } return result; @@ -523,32 +521,34 @@ private static List moduleItems(CvModule module) { } List result = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> { - String t = safe(p.text()).trim(); - if (!t.isBlank()) { - result.add(t); - } + if (body instanceof ParagraphBlock p) { + String t = safe(p.text()).trim(); + if (!t.isBlank()) { + result.add(t); } - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> { + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> { String t = safe(line).trim(); if (!t.isBlank()) { result.add(t); } }); - case BulletListBlock b -> b.items().forEach(item -> { + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> { String t = safe(item).trim(); if (!t.isBlank()) { result.add(t); } }); - case NumberedListBlock n -> n.items().forEach(item -> { + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> { String t = safe(item).trim(); if (!t.isBlank()) { result.add(t); } }); - case IndentedBlock i -> i.items().forEach(item -> { + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -562,11 +562,8 @@ private static List moduleItems(CvModule module) { result.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - result.add(entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> result.add(entry.key() + ": " + entry.value())); } return result; } diff --git a/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java b/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java index 5068cd93..d714efcf 100644 --- a/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java +++ b/src/main/java/com/demcha/compose/document/templates/cv/presets/TimelineMinimal.java @@ -426,12 +426,16 @@ private static List moduleLines(CvModule module) { } List lines = new ArrayList<>(); Block body = module.body(); - switch (body) { - case ParagraphBlock p -> addLines(lines, p.text()); - case MultiParagraphBlock m -> m.paragraphs().forEach(line -> addLines(lines, line)); - case BulletListBlock b -> b.items().forEach(item -> addLines(lines, item)); - case NumberedListBlock n -> n.items().forEach(item -> addLines(lines, item)); - case IndentedBlock i -> i.items().forEach(item -> { + if (body instanceof ParagraphBlock p) { + addLines(lines, p.text()); + } else if (body instanceof MultiParagraphBlock m) { + m.paragraphs().forEach(line -> addLines(lines, line)); + } else if (body instanceof BulletListBlock b) { + b.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof NumberedListBlock n) { + n.items().forEach(item -> addLines(lines, item)); + } else if (body instanceof IndentedBlock i) { + i.items().forEach(item -> { String title = safe(item.title()); String bodyText = safe(item.body()); if (title.isBlank() && bodyText.isBlank()) { @@ -445,11 +449,8 @@ private static List moduleLines(CvModule module) { lines.add(title + " | " + bodyText); } }); - case KeyValueBlock kv -> kv.entries().forEach(entry -> - addLines(lines, entry.key() + ": " + entry.value())); - default -> { - // ignore other block kinds - } + } else if (body instanceof KeyValueBlock kv) { + kv.entries().forEach(entry -> addLines(lines, entry.key() + ": " + entry.value())); } return List.copyOf(lines); } diff --git a/src/main/java/com/demcha/compose/engine/font/Font.java b/src/main/java/com/demcha/compose/engine/font/Font.java index 00e00b12..d6e87b8d 100644 --- a/src/main/java/com/demcha/compose/engine/font/Font.java +++ b/src/main/java/com/demcha/compose/engine/font/Font.java @@ -20,13 +20,16 @@ public interface Font { T strikethrough(); default T fontType(TextDecoration textDecoration) { + if (textDecoration == null) { + return defaultFont(); + } return switch (textDecoration) { case BOLD -> bold(); case ITALIC -> italic(); case UNDERLINE -> underline(); case BOLD_ITALIC -> boldItalic(); case STRIKETHROUGH -> strikethrough(); - case null, default -> defaultFont(); + default -> defaultFont(); }; } @@ -47,4 +50,3 @@ default T fontType(TextDecoration textDecoration) { ContentSize getTightBounds(String text, TextStyle style); } - diff --git a/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java b/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java index 52fcb8e9..0cb0fd14 100644 --- a/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java +++ b/src/main/java/com/demcha/compose/engine/layout/container/ContainerAligner.java @@ -17,6 +17,8 @@ import com.demcha.compose.engine.core.EntityManager; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -113,9 +115,11 @@ public void alignChildren(Entity parent, EntityManager entityManager) { * The reverse logic is handled by the ReverseLayoutStrategy. */ protected List getOrderedChildren(Entity parent, EntityManager entityManager) { - return parent.getChildren().stream() + List children = parent.getChildren().stream() .map(id -> entityManager.getEntity(id).orElseThrow()) - .collect(Collectors.toList()).reversed(); + .collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(children); + return children; } protected abstract void updateChildPosition(Entity child, Axes axes); @@ -242,7 +246,8 @@ public void alignChildren(Entity parent, EntityManager entityManager) { Align align = parent.getComponent(Align.class).orElseThrow(); List children = parent.getChildren().stream() .map(id -> entityManager.getEntity(id).orElseThrow()) - .collect(Collectors.toList()).reversed(); + .collect(Collectors.toCollection(ArrayList::new)); + Collections.reverse(children); double main = 0; double resolvedWidth = parent.getComponent(ContentSize.class) 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 6eb3957f..63d9316d 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 @@ -96,7 +96,7 @@ public double getTextWidth(TextStyle style, String text) { try { // ✅ IMPORTANT: preserve whitespace runs exactly boolean whitespaceOnly = text.chars().allMatch(Character::isWhitespace); - String measured = whitespaceOnly ? text : textSanitizer(text); // sanitizer must not strip trailing spaces ideally + String measured = whitespaceOnly ? text : sanitizeForFont(fontType(style.decoration()), textSanitizer(text)); double width = fontType(style.decoration()).getStringWidth(measured) / 1000d * size; return width; @@ -107,16 +107,26 @@ public double getTextWidth(TextStyle style, String text) { } - private String prepareForRender(String text) { - return textSanitizer(text); + public String sanitizeForRender(TextStyle style, String text) { + if (text == null || text.isEmpty()) { + return ""; + } + return sanitizeForFont(fontType(style.decoration()), textSanitizer(text)); } - /** Keeps spaces, only replaces characters the font can't encode. */ - private String sanitizeByFont(PDFont font, String s) { - StringBuilder sb = new StringBuilder(s.length()); - s.codePoints().forEach(cp -> { - // keep spaces/newlines logic correct for wrapping - if (cp == '\n' || cp == '\r') return; + /** + * Normalizes text for one concrete PDF font, preserving layout-safe whitespace + * while degrading unsupported glyphs to a printable fallback. + */ + public static String sanitizeForFont(PDFont font, String text) { + if (text == null || text.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(text.length()); + text.codePoints().forEach(cp -> { + if (cp == '\n' || cp == '\r') { + return; + } String ch = new String(Character.toChars(cp)); if (canEncode(font, ch)) sb.append(ch); @@ -125,7 +135,7 @@ private String sanitizeByFont(PDFont font, String s) { return sb.toString(); } - private boolean canEncode(PDFont font, String ch) { + private static boolean canEncode(PDFont font, String ch) { try { font.encode(ch); return true; diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfBlockTextRenderHandler.java b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfBlockTextRenderHandler.java index e82b0070..ce0bec05 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfBlockTextRenderHandler.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfBlockTextRenderHandler.java @@ -91,9 +91,9 @@ private boolean renderBlock(Entity entity, contentStream.setTextMatrix(new Matrix(1, 0, 0, 1, (float) line.x(), (float) line.y())); for (TextDataBody body : line.bodies()) { - setFont(entityManager, body, contentStream); + PdfFont pdfFont = setFont(entityManager, body, contentStream); try { - String text = BlockText.sanitizeText(body.text()); + String text = pdfFont.sanitizeForRender(body.textStyle(), BlockText.sanitizeText(body.text())); if (!text.isEmpty()) { contentStream.showText(text); } @@ -119,10 +119,11 @@ private boolean renderBlock(Entity entity, return true; } - private void setFont(EntityManager entityManager, TextDataBody body, PDPageContentStream contentStream) throws IOException { + private PdfFont setFont(EntityManager entityManager, TextDataBody body, PDPageContentStream contentStream) throws IOException { var style = body.textStyle(); PdfFont pdfFont = entityManager.getFonts().getFont(style.fontName(), PdfFont.class).orElseThrow(); contentStream.setFont(pdfFont.fontType(style.decoration()), (float) style.size()); + return pdfFont; } /** diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandler.java b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandler.java index 5ffc8574..92dfe646 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandler.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandler.java @@ -202,12 +202,13 @@ private void renderCellText(PDPageContentStream stream, stream.setFont(font.fontType(style.textStyle().decoration()), (float) style.textStyle().size()); stream.setNonStrokingColor(style.textStyle().color()); for (ResolvedTextLine line : lines) { - if (line.text().isEmpty()) { + String rendered = font.sanitizeForRender(style.textStyle(), line.text()); + if (rendered.isEmpty()) { continue; } stream.beginText(); stream.newLineAtOffset((float) line.x(), (float) line.baselineY()); - stream.showText(line.text()); + stream.showText(rendered); stream.endText(); } } finally { diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTextRenderHandler.java b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTextRenderHandler.java index 844a9b38..0e9a4cff 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTextRenderHandler.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/handlers/PdfTextRenderHandler.java @@ -67,6 +67,7 @@ public boolean render(EntityManager manager, TextComponent.ValidatedTextData data = TextComponent.validatedTextData(entity); Font font = manager.getFonts().getFont(data.style().fontName(), PdfFont.class).orElseThrow(); PDFont pdfFont = font.fontType(data.style().decoration()); + String renderedText = resolveRenderText(font, data.style(), data.textValue().value()); Padding padding = entity.getComponent(Padding.class).orElse(Padding.zero()); PdfFont.VerticalMetrics metrics = resolveVerticalMetrics(font, data.style()); @@ -81,7 +82,7 @@ public boolean render(EntityManager manager, contentStream.setNonStrokingColor(data.style().color()); contentStream.beginText(); contentStream.newLineAtOffset(baselineX, baselineY); - contentStream.showText(data.textValue().value()); + contentStream.showText(renderedText); contentStream.endText(); } finally { contentStream.restoreGraphicsState(); @@ -108,4 +109,13 @@ private PdfFont.VerticalMetrics resolveVerticalMetrics(Font font, com.de } throw new IllegalStateException("Expected PdfFont for PDF text rendering but got " + font.getClass().getName()); } + + private String resolveRenderText(Font font, + com.demcha.compose.engine.components.content.text.TextStyle style, + String text) { + if (font instanceof PdfFont pdfFont) { + return pdfFont.sanitizeForRender(style, text); + } + return text == null ? "" : text; + } } diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfHeaderFooterRenderer.java b/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfHeaderFooterRenderer.java index 3f42fe9a..91c7685d 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfHeaderFooterRenderer.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfHeaderFooterRenderer.java @@ -2,6 +2,7 @@ import com.demcha.compose.engine.components.content.header_footer.HeaderFooterConfig; import com.demcha.compose.engine.components.content.header_footer.HeaderFooterZone; +import com.demcha.compose.engine.render.pdf.PdfFont; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -86,6 +87,7 @@ private static void renderZone(PDPageContentStream cs, // Left text String leftText = HeaderFooterConfig.resolvePlaceholders(config.getLeftText(), currentPage, totalPages); if (leftText != null && !leftText.isEmpty()) { + leftText = PdfFont.sanitizeForFont(font, leftText); cs.beginText(); cs.setFont(font, fontSize); cs.newLineAtOffset(marginLeft, baseY); @@ -96,6 +98,7 @@ private static void renderZone(PDPageContentStream cs, // Center text String centerText = HeaderFooterConfig.resolvePlaceholders(config.getCenterText(), currentPage, totalPages); if (centerText != null && !centerText.isEmpty()) { + centerText = PdfFont.sanitizeForFont(font, centerText); float textWidth = font.getStringWidth(centerText) / 1000f * fontSize; float centerX = marginLeft + (usableWidth - textWidth) / 2f; cs.beginText(); @@ -108,6 +111,7 @@ private static void renderZone(PDPageContentStream cs, // Right text String rightText = HeaderFooterConfig.resolvePlaceholders(config.getRightText(), currentPage, totalPages); if (rightText != null && !rightText.isEmpty()) { + rightText = PdfFont.sanitizeForFont(font, rightText); float textWidth = font.getStringWidth(rightText) / 1000f * fontSize; float rightX = pageWidth - marginRight - textWidth; cs.beginText(); diff --git a/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfWatermarkRenderer.java b/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfWatermarkRenderer.java index d157f4e5..1dea003f 100644 --- a/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfWatermarkRenderer.java +++ b/src/main/java/com/demcha/compose/engine/render/pdf/helpers/PdfWatermarkRenderer.java @@ -2,6 +2,7 @@ import com.demcha.compose.engine.components.content.watermark.WatermarkConfig; import com.demcha.compose.engine.components.content.watermark.WatermarkPosition; +import com.demcha.compose.engine.render.pdf.PdfFont; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -73,7 +74,7 @@ private static void renderTextWatermark(PDPageContentStream cs, PDRectangle mediaBox) throws IOException { PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); float fontSize = config.getFontSize(); - String text = config.getText(); + String text = PdfFont.sanitizeForFont(font, config.getText()); float textWidth = font.getStringWidth(text) / 1000f * fontSize; float textHeight = fontSize; diff --git a/src/test/java/com/demcha/compose/devtool/GraphComposeDevTool.java b/src/test/java/com/demcha/compose/devtool/GraphComposeDevTool.java index 813f403d..0b9c5202 100644 --- a/src/test/java/com/demcha/compose/devtool/GraphComposeDevTool.java +++ b/src/test/java/com/demcha/compose/devtool/GraphComposeDevTool.java @@ -56,15 +56,9 @@ public final class GraphComposeDevTool extends Application { private final PreviewCompiler compiler = new PreviewCompiler(); private final float previewScale = PreviewScaleResolver.fromSystemProperties(); private final ExecutorService refreshExecutor = Executors.newSingleThreadExecutor( - Thread.ofPlatform() - .daemon() - .name("graphcompose-devtool-refresh-", 0) - .factory()); + runnable -> newDaemonThread(runnable, "graphcompose-devtool-refresh-0")); private final ScheduledExecutorService debounceExecutor = Executors.newSingleThreadScheduledExecutor( - Thread.ofPlatform() - .daemon() - .name("graphcompose-devtool-debounce-", 0) - .factory()); + runnable -> newDaemonThread(runnable, "graphcompose-devtool-debounce-0")); private final AtomicReference pendingRequest = new AtomicReference<>(); private final AtomicReference> pendingDebounce = new AtomicReference<>(); private final AtomicReference currentPreview = new AtomicReference<>(); @@ -90,6 +84,12 @@ public static void main(String[] args) { Application.launch(GraphComposeDevTool.class, args); } + private static Thread newDaemonThread(Runnable runnable, String name) { + Thread thread = new Thread(runnable, name); + thread.setDaemon(true); + return thread; + } + @Override public void start(Stage stage) { var root = new BorderPane(); diff --git a/src/test/java/com/demcha/compose/devtool/PreviewCompiler.java b/src/test/java/com/demcha/compose/devtool/PreviewCompiler.java index 16842f68..72247705 100644 --- a/src/test/java/com/demcha/compose/devtool/PreviewCompiler.java +++ b/src/test/java/com/demcha/compose/devtool/PreviewCompiler.java @@ -85,7 +85,7 @@ public CompilationResult compile(DevToolWorkspace workspace, long revision) { var options = new ArrayList(); options.add("--release"); - options.add("21"); + options.add("17"); options.add("-encoding"); options.add("UTF-8"); options.add("-classpath"); diff --git a/src/test/java/com/demcha/compose/devtool/RecursivePathWatcher.java b/src/test/java/com/demcha/compose/devtool/RecursivePathWatcher.java index a6ad982e..28f9a8bc 100644 --- a/src/test/java/com/demcha/compose/devtool/RecursivePathWatcher.java +++ b/src/test/java/com/demcha/compose/devtool/RecursivePathWatcher.java @@ -43,10 +43,8 @@ public void start() throws IOException { } running = true; - thread = Thread.ofPlatform() - .daemon() - .name("graphcompose-devtool-watch", 0) - .unstarted(this::processLoop); + thread = new Thread(this::processLoop, "graphcompose-devtool-watch"); + thread.setDaemon(true); thread.start(); } diff --git a/src/test/java/com/demcha/compose/document/api/DocumentListNodeTest.java b/src/test/java/com/demcha/compose/document/api/DocumentListNodeTest.java index f17248a2..766c6628 100644 --- a/src/test/java/com/demcha/compose/document/api/DocumentListNodeTest.java +++ b/src/test/java/com/demcha/compose/document/api/DocumentListNodeTest.java @@ -38,7 +38,7 @@ void listShortcutShouldBuildBulletListWithoutManualParagraphLoop() throws Except .build(); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(ListNode.class); + assertThat(root.children().get(0)).isInstanceOf(ListNode.class); List payloads = paragraphPayloads(session.layoutGraph()); assertThat(payloads).hasSize(3); @@ -108,13 +108,13 @@ void wrappedListItemShouldIndentContinuationWithoutRepeatingMarker() throws Exce .build(); List payloads = paragraphPayloads(session.layoutGraph()); - List firstItemLines = payloads.getFirst().lines().stream() + List firstItemLines = payloads.get(0).lines().stream() .map(ParagraphLine::text) .toList(); assertThat(firstItemLines).hasSizeGreaterThan(1); assertThat(countOccurrences(String.join("\n", firstItemLines), "\u2022")).isEqualTo(1); - assertThat(payloads.get(1).lines().getFirst().text()).isEqualTo("\u2022 Tail"); + assertThat(payloads.get(1).lines().get(0).text()).isEqualTo("\u2022 Tail"); } } @@ -137,15 +137,15 @@ void markerlessListShouldIndentWrappedContinuationWithoutIndentingEveryItem() th .build(); List payloads = paragraphPayloads(session.layoutGraph()); - List firstItemLines = payloads.getFirst().lines().stream() + List firstItemLines = payloads.get(0).lines().stream() .map(ParagraphLine::text) .toList(); assertThat(payloads).hasSize(2); assertThat(firstItemLines).hasSizeGreaterThan(1); - assertThat(firstItemLines.getFirst()).startsWith("Alpha"); + assertThat(firstItemLines.get(0)).startsWith("Alpha"); assertThat(firstItemLines.get(1)).startsWith(" "); - assertThat(payloads.get(1).lines().getFirst().text()).startsWith("Beta"); + assertThat(payloads.get(1).lines().get(0).text()).startsWith("Beta"); } } @@ -238,9 +238,9 @@ void listFragmentsShouldCarryPaddingThroughParagraphPayload() throws Exception { assertThat(fragment.width()).isCloseTo(listNode.placementWidth(), within(0.01)); }); - ParagraphFragmentPayload first = paragraphPayload(fragments.getFirst()); + ParagraphFragmentPayload first = paragraphPayload(fragments.get(0)); ParagraphFragmentPayload middle = paragraphPayload(fragments.get(1)); - ParagraphFragmentPayload last = paragraphPayload(fragments.getLast()); + ParagraphFragmentPayload last = paragraphPayload(fragments.get(fragments.size() - 1)); assertPadding(first.padding(), 3, 5, 0, 11); assertPadding(middle.padding(), 0, 5, 0, 11); @@ -270,7 +270,7 @@ private static void assertPadding(Padding padding, double top, double right, dou private static List firstLineTexts(List payloads) { return payloads.stream() - .map(payload -> payload.lines().getFirst().text()) + .map(payload -> payload.lines().get(0).text()) .toList(); } diff --git a/src/test/java/com/demcha/compose/document/api/DocumentSessionTest.java b/src/test/java/com/demcha/compose/document/api/DocumentSessionTest.java index be870c06..e829b0fd 100644 --- a/src/test/java/com/demcha/compose/document/api/DocumentSessionTest.java +++ b/src/test/java/com/demcha/compose/document/api/DocumentSessionTest.java @@ -129,8 +129,8 @@ void pageFlowShortcutShouldAttachRootWithoutDslFacade() throws Exception { assertThat(session.roots()).containsExactly(root); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(ParagraphNode.class); - assertThat(((ParagraphNode) root.children().getFirst()).text()).isEqualTo("Shortcut paragraph"); + assertThat(root.children().get(0)).isInstanceOf(ParagraphNode.class); + assertThat(((ParagraphNode) root.children().get(0)).text()).isEqualTo("Shortcut paragraph"); assertThat(session.layoutGraph().totalPages()).isEqualTo(1); } } @@ -148,13 +148,13 @@ void composeShortcutShouldBatchDslCallsWithoutManualBuilderLifecycle() throws Ex .addText("Composed paragraph"))); assertThat(session.roots()).hasSize(1); - assertThat(session.roots().getFirst()).isInstanceOf(ContainerNode.class); + assertThat(session.roots().get(0)).isInstanceOf(ContainerNode.class); - ContainerNode root = (ContainerNode) session.roots().getFirst(); + ContainerNode root = (ContainerNode) session.roots().get(0); assertThat(root.name()).isEqualTo("ComposeShortcut"); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(ParagraphNode.class); - assertThat(((ParagraphNode) root.children().getFirst()).text()).isEqualTo("Composed paragraph"); + assertThat(root.children().get(0)).isInstanceOf(ParagraphNode.class); + assertThat(((ParagraphNode) root.children().get(0)).text()).isEqualTo("Composed paragraph"); assertThat(session.layoutGraph().totalPages()).isEqualTo(1); } } @@ -175,13 +175,13 @@ void namedSectionShortcutShouldAvoidRepeatingSectionNameInsideNestedBuilder() th .build(); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(SectionNode.class); + assertThat(root.children().get(0)).isInstanceOf(SectionNode.class); - SectionNode section = (SectionNode) root.children().getFirst(); + SectionNode section = (SectionNode) root.children().get(0); assertThat(section.name()).isEqualTo("Profile"); assertThat(section.children()).hasSize(1); - assertThat(section.children().getFirst()).isInstanceOf(ParagraphNode.class); - assertThat(((ParagraphNode) section.children().getFirst()).text()).isEqualTo("Senior platform engineer"); + assertThat(section.children().get(0)).isInstanceOf(ParagraphNode.class); + assertThat(((ParagraphNode) section.children().get(0)).text()).isEqualTo("Senior platform engineer"); assertThat(session.layoutGraph().totalPages()).isEqualTo(1); } } @@ -210,14 +210,14 @@ void styledSectionShouldEmitDecorationBeforeChildFragments() throws Exception { LayoutGraph graph = session.layoutGraph(); assertThat(graph.fragments()).hasSize(2); - assertThat(graph.fragments().getFirst().path()).contains("InfoCard[0]"); - assertThat(graph.fragments().getFirst().payload()) + assertThat(graph.fragments().get(0).path()).contains("InfoCard[0]"); + assertThat(graph.fragments().get(0).payload()) .isInstanceOf(ShapeFragmentPayload.class); assertThat(graph.fragments().get(1).payload()) .isInstanceOf(ParagraphFragmentPayload.class); ShapeFragmentPayload payload = - (ShapeFragmentPayload) graph.fragments().getFirst().payload(); + (ShapeFragmentPayload) graph.fragments().get(0).payload(); assertThat(payload.fillColor()).isEqualTo(fill.color()); assertThat(payload.stroke().strokeColor().color()).isEqualTo(DocumentColor.ROYAL_BLUE.color()); assertThat(payload.stroke().width()).isEqualTo(0.8); @@ -338,13 +338,13 @@ void moduleShortcutShouldBuildTitledSemanticBlocks() throws Exception { .build(); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(SectionNode.class); + assertThat(root.children().get(0)).isInstanceOf(SectionNode.class); - SectionNode module = (SectionNode) root.children().getFirst(); + SectionNode module = (SectionNode) root.children().get(0); assertThat(module.name()).isEqualTo("TechnicalSkills"); assertThat(module.children()).hasSize(5); - assertThat(module.children().getFirst()).isInstanceOf(ParagraphNode.class); - assertThat(((ParagraphNode) module.children().getFirst()).text()).isEqualTo("Technical Skills"); + assertThat(module.children().get(0)).isInstanceOf(ParagraphNode.class); + assertThat(((ParagraphNode) module.children().get(0)).text()).isEqualTo("Technical Skills"); assertThat(module.children().get(1)).isInstanceOf(ParagraphNode.class); assertThat(module.children().get(2)).isInstanceOf(ListNode.class); assertThat(module.children().get(3)).isInstanceOf(ListNode.class); @@ -416,11 +416,11 @@ void tableConvenienceMethodsShouldBuildHeaderAndBulkRows() throws Exception { .build(); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(TableNode.class); + assertThat(root.children().get(0)).isInstanceOf(TableNode.class); - TableNode table = (TableNode) root.children().getFirst(); + TableNode table = (TableNode) root.children().get(0); assertThat(table.rows()).hasSize(3); - assertThat(table.rows().getFirst()) + assertThat(table.rows().get(0)) .extracting(DocumentTableCell::lines) .containsExactly(List.of("Role"), List.of("Owner"), List.of("Status")); assertThat(table.rows().get(1)) @@ -467,16 +467,16 @@ void paragraphShouldSplitAcrossPagesAndPdfShouldMatchPageCount() throws Exceptio LayoutGraph graph = session.layoutGraph(); assertThat(graph.totalPages()).isGreaterThan(1); assertThat(graph.nodes()).hasSize(1); - assertThat(graph.nodes().getFirst().startPage()).isEqualTo(0); - assertThat(graph.nodes().getFirst().endPage()).isGreaterThan(0); + assertThat(graph.nodes().get(0).startPage()).isEqualTo(0); + assertThat(graph.nodes().get(0).endPage()).isGreaterThan(0); assertThat(graph.fragments()).extracting(PlacedFragment::pageIndex).doesNotHaveDuplicates(); assertThat(graph.fragments()).allSatisfy(fragment -> assertThat(fragment.payload()).isInstanceOf(ParagraphFragmentPayload.class)); ParagraphFragmentPayload firstPayload = - (ParagraphFragmentPayload) graph.fragments().getFirst().payload(); + (ParagraphFragmentPayload) graph.fragments().get(0).payload(); ParagraphFragmentPayload lastPayload = - (ParagraphFragmentPayload) graph.fragments().getLast().payload(); + (ParagraphFragmentPayload) graph.fragments().get(graph.fragments().size() - 1).payload(); assertThat(firstPayload.padding().top()).isEqualTo(4.0); assertThat(firstPayload.padding().bottom()).isEqualTo(0.0); assertThat(lastPayload.padding().top()).isEqualTo(0.0); @@ -558,7 +558,7 @@ void paragraphShouldPreserveExplicitNewlinesInPreparedFragments() throws Excepti assertThat(graph.fragments()).hasSize(1); ParagraphFragmentPayload payload = - (ParagraphFragmentPayload) graph.fragments().getFirst().payload(); + (ParagraphFragmentPayload) graph.fragments().get(0).payload(); assertThat(payload.lines()) .extracting(ParagraphLine::text) .containsExactly("First line", "", "Second line"); @@ -832,7 +832,7 @@ void guideLinesShouldRenderLegacyMarginPaddingAndBoxColors() throws Exception { try (PDDocument document = Loader.loadPDF(pdfBytes)) { BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, RENDER_DPI); - PlacedFragment fragment = graph.fragments().getFirst(); + PlacedFragment fragment = graph.fragments().get(0); assertThat(hasColorNear( image, @@ -884,7 +884,7 @@ void sessionGuideLinesShouldApplyToConveniencePdfOutputWithoutInvalidatingLayout try (PDDocument document = Loader.loadPDF(pdfBytes)) { BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, RENDER_DPI); - PlacedFragment fragment = graph.fragments().getFirst(); + PlacedFragment fragment = graph.fragments().get(0); assertThat(hasColorNear( image, @@ -1113,4 +1113,3 @@ private boolean isCloseColor(Color actual, Color expected, int tolerancePerChann && Math.abs(actual.getBlue() - expected.getBlue()) <= tolerancePerChannel; } } - 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 473ff84d..b9d581cf 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 @@ -128,8 +128,8 @@ void shouldRenderCanonicalPdfChromeAndSemanticAnnotations() throws Exception { assertThat(firstChild.getTitle()).isEqualTo("Overview"); assertThat(document.getPage(0).getAnnotations()).isNotEmpty(); - assertThat(document.getPage(0).getAnnotations().getFirst()).isInstanceOf(PDAnnotationLink.class); - PDAnnotationLink annotation = (PDAnnotationLink) document.getPage(0).getAnnotations().getFirst(); + assertThat(document.getPage(0).getAnnotations().get(0)).isInstanceOf(PDAnnotationLink.class); + PDAnnotationLink annotation = (PDAnnotationLink) document.getPage(0).getAnnotations().get(0); assertThat(annotation.getAction()).isInstanceOf(PDActionURI.class); assertThat(((PDActionURI) annotation.getAction()).getURI()).isEqualTo("https://example.com/docs"); diff --git a/src/test/java/com/demcha/compose/document/dsl/AddLinkShortcutTest.java b/src/test/java/com/demcha/compose/document/dsl/AddLinkShortcutTest.java index 8fac1f28..97a1a912 100644 --- a/src/test/java/com/demcha/compose/document/dsl/AddLinkShortcutTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/AddLinkShortcutTest.java @@ -34,8 +34,8 @@ void addLinkRendersClickableUriAndVisibleText() throws Exception { try (PDDocument document = Loader.loadPDF(pdfBytes)) { assertThat(document.getPage(0).getAnnotations()).isNotEmpty(); - assertThat(document.getPage(0).getAnnotations().getFirst()).isInstanceOf(PDAnnotationLink.class); - PDAnnotationLink link = (PDAnnotationLink) document.getPage(0).getAnnotations().getFirst(); + assertThat(document.getPage(0).getAnnotations().get(0)).isInstanceOf(PDAnnotationLink.class); + PDAnnotationLink link = (PDAnnotationLink) document.getPage(0).getAnnotations().get(0); assertThat(link.getAction()).isInstanceOf(PDActionURI.class); assertThat(((PDActionURI) link.getAction()).getURI()).isEqualTo("https://example.com/docs"); assertThat(new PDFTextStripper().getText(document)).contains("Documentation"); @@ -60,7 +60,7 @@ void addLinkAcceptsDocumentLinkOptions() throws Exception { } try (PDDocument document = Loader.loadPDF(pdfBytes)) { - PDAnnotationLink link = (PDAnnotationLink) document.getPage(0).getAnnotations().getFirst(); + PDAnnotationLink link = (PDAnnotationLink) document.getPage(0).getAnnotations().get(0); assertThat(((PDActionURI) link.getAction()).getURI()).isEqualTo("mailto:author@example.dev"); } } diff --git a/src/test/java/com/demcha/compose/document/dsl/ListBuilderNestedTest.java b/src/test/java/com/demcha/compose/document/dsl/ListBuilderNestedTest.java index 3236a1e0..9fb39e01 100644 --- a/src/test/java/com/demcha/compose/document/dsl/ListBuilderNestedTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/ListBuilderNestedTest.java @@ -63,7 +63,7 @@ void nestedAddItemPromotesListToNestedRepresentationAndClearsFlatItems() { assertThat(node.items()).isEmpty(); assertThat(node.nestedItems()).hasSize(1); - ListItem parent = node.nestedItems().getFirst(); + ListItem parent = node.nestedItems().get(0); assertThat(parent.label()).isEqualTo("Parent"); assertThat(parent.children()).extracting(ListItem::label) .containsExactly("Child A", "Child B"); @@ -200,11 +200,11 @@ void markerOverridesAreBakedIntoNodeNestedItemsAtBuildTime() { .build(); assertThat(node.nestedItems()).hasSize(1); - ListItem top = node.nestedItems().getFirst(); + ListItem top = node.nestedItems().get(0); assertThat(top.marker()).isNotNull(); assertThat(top.marker().value()).isEqualTo("- "); assertThat(top.children()).hasSize(1); - assertThat(top.children().getFirst().marker().value()).isEqualTo("→ "); + assertThat(top.children().get(0).marker().value()).isEqualTo("→ "); } @Test @@ -251,10 +251,10 @@ void pageFlowBuilderWiresNestedListIntoContainerHierarchy() throws Exception { .build(); assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(ListNode.class); - ListNode listNode = (ListNode) root.children().getFirst(); + assertThat(root.children().get(0)).isInstanceOf(ListNode.class); + ListNode listNode = (ListNode) root.children().get(0); assertThat(listNode.nestedItems()).hasSize(1); - assertThat(listNode.nestedItems().getFirst().children()) + assertThat(listNode.nestedItems().get(0).children()) .extracting(ListItem::label) .containsExactly("a1"); } @@ -285,7 +285,7 @@ private static List firstLineTexts(DocumentSession session) { .map(PlacedFragment::payload) .filter(ParagraphFragmentPayload.class::isInstance) .map(ParagraphFragmentPayload.class::cast) - .map(payload -> payload.lines().getFirst().text()) + .map(payload -> payload.lines().get(0).text()) .toList(); } } diff --git a/src/test/java/com/demcha/compose/document/table/TableCellComposedContentTest.java b/src/test/java/com/demcha/compose/document/table/TableCellComposedContentTest.java index 313fa154..3283ab91 100644 --- a/src/test/java/com/demcha/compose/document/table/TableCellComposedContentTest.java +++ b/src/test/java/com/demcha/compose/document/table/TableCellComposedContentTest.java @@ -74,7 +74,7 @@ void composedCellEmitsChildParagraphFragmentInsideTableLayout() throws Exception assertThat(paragraphFragments).hasSize(1); ParagraphFragmentPayload childPayload = (ParagraphFragmentPayload) - paragraphFragments.getFirst().payload(); + paragraphFragments.get(0).payload(); // The paragraph wraps within the cell's inner width, so the // full sentence is split across two visual lines. Combine // them before checking the rendered text. diff --git a/src/test/java/com/demcha/compose/document/templates/support/common/SessionTemplateComposeTargetTest.java b/src/test/java/com/demcha/compose/document/templates/support/common/SessionTemplateComposeTargetTest.java index 2b2d6e0b..53687372 100644 --- a/src/test/java/com/demcha/compose/document/templates/support/common/SessionTemplateComposeTargetTest.java +++ b/src/test/java/com/demcha/compose/document/templates/support/common/SessionTemplateComposeTargetTest.java @@ -63,14 +63,14 @@ void addModuleShouldAttachSectionNodeInsteadOfFlatteningBlocks() throws Exceptio target.finishDocument(); assertThat(session.roots()).hasSize(1); - DocumentNode rootNode = session.roots().getFirst(); + DocumentNode rootNode = session.roots().get(0); assertThat(rootNode).isInstanceOf(ContainerNode.class); ContainerNode root = (ContainerNode) rootNode; assertThat(root.children()).hasSize(1); - assertThat(root.children().getFirst()).isInstanceOf(SectionNode.class); + assertThat(root.children().get(0)).isInstanceOf(SectionNode.class); - SectionNode module = (SectionNode) root.children().getFirst(); + SectionNode module = (SectionNode) root.children().get(0); assertThat(module.name()).isEqualTo("TechnicalSkills"); assertThat(module.children()).hasSize(3); assertThat(module.children().get(0)).isInstanceOf(ParagraphNode.class); @@ -111,8 +111,8 @@ void customBlocksInsideModuleShouldRenderIntoTheModuleSection() throws Exception Margin.zero())))))); target.finishDocument(); - ContainerNode root = (ContainerNode) session.roots().getFirst(); - SectionNode module = (SectionNode) root.children().getFirst(); + ContainerNode root = (ContainerNode) session.roots().get(0); + SectionNode module = (SectionNode) root.children().get(0); assertThat(module.children()).hasSize(2); assertThat(module.children().get(1)).isInstanceOf(ParagraphNode.class); diff --git a/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java b/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java index af57f151..5056fd28 100644 --- a/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java +++ b/src/test/java/com/demcha/compose/engine/core/EngineComposerHarnessLayoutSnapshotTest.java @@ -155,7 +155,7 @@ private com.demcha.compose.engine.components.content.text.LineTextData firstLine BlockTextData textData = blockEntity.getComponent(BlockTextData.class) .orElseThrow(() -> new AssertionError("Missing BlockTextData for " + entityName)); - return textData.lines().getFirst(); + return textData.lines().get(0); } private EntityManager entityManager(EngineComposerHarness composer) throws Exception { diff --git a/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfLinkRenderHandlerTest.java b/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfLinkRenderHandlerTest.java index 95ef4c24..85c330ee 100644 --- a/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfLinkRenderHandlerTest.java +++ b/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfLinkRenderHandlerTest.java @@ -42,9 +42,9 @@ void shouldCreatePageAndAttachUriAnnotation() throws Exception { assertThat(rendered).isTrue(); assertThat(document.getNumberOfPages()).isEqualTo(1); assertThat(document.getPage(0).getAnnotations()).hasSize(1); - assertThat(document.getPage(0).getAnnotations().getFirst()).isInstanceOf(PDAnnotationLink.class); + assertThat(document.getPage(0).getAnnotations().get(0)).isInstanceOf(PDAnnotationLink.class); - PDAnnotationLink annotation = (PDAnnotationLink) document.getPage(0).getAnnotations().getFirst(); + PDAnnotationLink annotation = (PDAnnotationLink) document.getPage(0).getAnnotations().get(0); assertThat(annotation.getAction()).isInstanceOf(PDActionURI.class); assertThat(((PDActionURI) annotation.getAction()).getURI()).isEqualTo("https://example.com/profile"); assertThat(annotation.getRectangle().getWidth()).isEqualTo(128.0f); diff --git a/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandlerTest.java b/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandlerTest.java index 520cd0f6..4c402eeb 100644 --- a/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandlerTest.java +++ b/src/test/java/com/demcha/compose/engine/render/pdf/handlers/PdfTableRowRenderHandlerTest.java @@ -141,9 +141,9 @@ void shouldResolveTopAndMiddleAnchorsForMultilineContent() { assertThat(topLines).hasSize(2); assertThat(middleLines).hasSize(2); - assertThat(topLines.getFirst().baselineY()).isGreaterThan(middleLines.getFirst().baselineY()); - assertThat(topLines.getFirst().baselineY() - topLines.get(1).baselineY()).isEqualTo(lineHeight); - assertThat(middleLines.getFirst().baselineY() - middleLines.get(1).baselineY()).isEqualTo(lineHeight); + assertThat(topLines.get(0).baselineY()).isGreaterThan(middleLines.get(0).baselineY()); + assertThat(topLines.get(0).baselineY() - topLines.get(1).baselineY()).isEqualTo(lineHeight); + assertThat(middleLines.get(0).baselineY() - middleLines.get(1).baselineY()).isEqualTo(lineHeight); } @Test @@ -171,6 +171,6 @@ void shouldApplyCellLineSpacingWhenResolvingMultilineContent() { double lineHeight = font.getLineHeight(cell.style().textStyle()); assertThat(lines).hasSize(2); - assertThat(lines.getFirst().baselineY() - lines.get(1).baselineY()).isEqualTo(lineHeight + lineSpacing); + assertThat(lines.get(0).baselineY() - lines.get(1).baselineY()).isEqualTo(lineHeight + lineSpacing); } } diff --git a/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java b/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java index e8ec0e1f..8359a815 100644 --- a/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java +++ b/src/test/java/com/demcha/compose/font/FontLibraryIntegrationTest.java @@ -2,6 +2,7 @@ import com.demcha.compose.GraphCompose; import com.demcha.compose.testsupport.EngineComposerHarness; +import com.demcha.compose.engine.components.content.text.TextStyle; import com.demcha.compose.engine.render.pdf.PdfFont; import com.demcha.compose.engine.render.word.WordFont; import org.apache.pdfbox.pdmodel.PDDocument; @@ -60,4 +61,16 @@ void shouldRegisterCustomFontFamilyFromFilePaths() throws Exception { assertThat(fonts.getFont(customFamily, WordFont.class)).isPresent(); } } + + @Test + void shouldDegradeUnsupportedGlyphsForStandard14PdfFonts() { + PdfFont helvetica = (PdfFont) DefaultFonts.standardLibrary() + .getFont(FontName.HELVETICA, PdfFont.class) + .orElseThrow(); + + String sanitized = helvetica.sanitizeForRender(TextStyle.DEFAULT_STYLE, "Alpha ▪ Beta"); + + assertThat(sanitized).isEqualTo("Alpha ? Beta"); + assertThat(helvetica.getTextWidth(TextStyle.DEFAULT_STYLE, "Alpha ▪ Beta")).isGreaterThan(0.0); + } } diff --git a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java index f07eb149..33d0efbe 100644 --- a/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java +++ b/src/test/java/com/demcha/compose/testsupport/engine/assembly/TableBuilderTest.java @@ -41,8 +41,8 @@ void shouldComputeFixedAndAutoColumnWidths() throws Exception { TableRowData rowData = firstRow.getComponent(TableRowData.class).orElseThrow(); assertThat(layoutData.columnWidths()).hasSize(2); - assertThat(layoutData.columnWidths().getFirst()).isEqualTo(120.0); - assertThat(rowData.cells().getFirst().width()).isEqualTo(120.0); + assertThat(layoutData.columnWidths().get(0)).isEqualTo(120.0); + assertThat(rowData.cells().get(0).width()).isEqualTo(120.0); assertThat(rowData.cells().get(1).width()).isEqualTo(layoutData.columnWidths().get(1)); assertThat(layoutData.finalWidth()).isEqualTo(layoutData.columnWidths().stream().mapToDouble(Double::doubleValue).sum()); } @@ -119,13 +119,13 @@ void shouldApplyStylePrecedenceDefaultThenColumnThenRow() throws Exception { .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); TableResolvedCell secondRowFirstCell = child(table, 1) .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); assertThat(firstRowFirstCell.style().fillColor()).isEqualTo(ComponentColor.RED); assertThat(firstRowFirstCell.style().padding()).isEqualTo(Padding.of(8)); @@ -181,7 +181,7 @@ void shouldApplyCellOverrideAfterDefaultColumnAndRowStyles() throws Exception { .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); assertThat(firstCell.style().fillColor()).isEqualTo(ComponentColor.GREEN); assertThat(firstCell.style().padding()).isEqualTo(Padding.of(8)); @@ -203,7 +203,7 @@ void shouldPreserveStringRowApiAsSingleLineCellSpec() throws Exception { .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); assertThat(firstCell.lines()).containsExactly("Alpha"); } @@ -228,13 +228,13 @@ void shouldMeasureMultilineCellsUsingLongestLineAndLineCount() throws Exception .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); TableResolvedCell singleLineCell = child(longestLineTable, 0) .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); double paddingVertical = TableCellLayoutStyle.DEFAULT.padding().vertical(); double expectedMultilineHeight = (2 * (singleLineCell.height() - paddingVertical)) + paddingVertical; @@ -271,12 +271,12 @@ void shouldIncludeCellLineSpacingInMultilineCellHeight() throws Exception { .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); TableResolvedCell singleLineCell = child(singleLineTable, 0) .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); assertThat(multilineCell.height()).isEqualTo((2 * singleLineCell.height()) + 2.5); } @@ -332,7 +332,7 @@ void shouldCreateStableRowAndCellNamesFromTableName() throws Exception { .build(); Entity row = child(table, 0); - TableResolvedCell cell = row.getComponent(TableRowData.class).orElseThrow().cells().getFirst(); + TableResolvedCell cell = row.getComponent(TableRowData.class).orElseThrow().cells().get(0); assertThat(row.getComponent(EntityName.class)).hasValue(new EntityName("Orders__row_0")); assertThat(cell.name()).isEqualTo("Orders__row_0__cell_0"); @@ -353,7 +353,7 @@ void shouldAssignBordersToCurrentCellsSoTrailingPageLinesStayVisible() throws Ex .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); TableResolvedCell firstRowSecondCell = child(table, 0) .getComponent(TableRowData.class) @@ -419,7 +419,7 @@ void shouldLetCellOwnTopBoundaryWhenPreviousRowStrokeIsInvisible() throws Except .getComponent(TableRowData.class) .orElseThrow() .cells() - .getFirst(); + .get(0); assertThat(secondRowCell.borderSides()).containsExactlyInAnyOrder(Side.TOP, Side.LEFT, Side.RIGHT, Side.BOTTOM); assertThat(secondRowCell.fillInsets().top()).isEqualTo(1.0); diff --git a/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java b/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java index 2ab5114f..23c0b528 100644 --- a/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java +++ b/src/test/java/com/demcha/testing/layout/LayoutSnapshotExtractorTest.java @@ -162,8 +162,8 @@ void shouldCaptureTotalPagesFromPlacementSpan() { assertThat(snapshot.totalPages()).isEqualTo(3); assertThat(snapshot.nodes()).hasSize(1); - assertThat(snapshot.nodes().getFirst().startPage()).isEqualTo(2); - assertThat(snapshot.nodes().getFirst().endPage()).isEqualTo(0); + assertThat(snapshot.nodes().get(0).startPage()).isEqualTo(2); + assertThat(snapshot.nodes().get(0).endPage()).isEqualTo(0); } @Test diff --git a/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java b/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java index ece28e44..716b0522 100644 --- a/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java +++ b/src/test/java/com/demcha/testing/visual/TableRowSpanDemoTest.java @@ -138,7 +138,7 @@ void middleColumnRowSpanDoesNotExposePageBackgroundAtCellJoin() throws Exception BufferedImage image = PdfVisualRegression.standard() .renderScale(1.0f) .renderPages(pdf) - .getFirst(); + .get(0); List rowFragments = graph.fragments().stream() .filter(fragment -> fragment.payload() instanceof TableRowFragmentPayload) @@ -146,7 +146,7 @@ void middleColumnRowSpanDoesNotExposePageBackgroundAtCellJoin() throws Exception PlacedFragment secondRow = rowFragments.get(1); TableRowFragmentPayload secondRowPayload = (TableRowFragmentPayload) secondRow.payload(); - TableResolvedCell leftCell = secondRowPayload.cells().getFirst(); + TableResolvedCell leftCell = secondRowPayload.cells().get(0); double rowTop = secondRow.y() + secondRow.height(); double leftCellRight = secondRow.x() + leftCell.x() + leftCell.width(); @@ -189,7 +189,7 @@ void middleColumnRowSpanDoesNotDrawSeparatorsOutsideTableEdge() throws Exception BufferedImage image = PdfVisualRegression.standard() .renderScale(1.0f) .renderPages(pdf) - .getFirst(); + .get(0); List rowFragments = graph.fragments().stream() .filter(fragment -> fragment.payload() instanceof TableRowFragmentPayload) 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 a61bae1d..bcece595 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" : 32.914, + "placementWidth" : 42.812, "placementHeight" : 38.85, "startPage" : 0, "endPage" : 0, - "contentWidth" : 32.914, + "contentWidth" : 42.812, "contentHeight" : 38.85, "margin" : { "top" : 0.0, @@ -55,11 +55,11 @@ "computedY" : 269.15, "placementX" : 12.0, "placementY" : 269.15, - "placementWidth" : 32.914, + "placementWidth" : 42.812, "placementHeight" : 38.85, "startPage" : 0, "endPage" : 0, - "contentWidth" : 32.914, + "contentWidth" : 42.812, "contentHeight" : 38.85, "margin" : { "top" : 0.0,