From 420d4202e132801baffe52520e26c9e6bac1df68 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 22 Jun 2026 16:26:19 +0100 Subject: [PATCH] test(dsl): cover unparseable emoji glyph and linked inline-SVG on a wrapped line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmojiLibraryTest: a set whose index points at an unrenderable glyph (an SVG with no drawable geometry). find() stays empty so callers fall back to text, and require() reports "indexed but could not be rendered" — the lenient resolver's previously-uncovered branch. - InlineSvgRenderTest: a linked inline SVG that wraps onto a later line; the click annotation still hugs the icon box, exercising spanLinkRectangle's alignment/height geometry off the paragraph's first line. --- .../document/dsl/InlineSvgRenderTest.java | 36 +++++++++++++++++++ .../document/emoji/EmojiLibraryTest.java | 24 +++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java b/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java index a820b1f4..da22e387 100644 --- a/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java @@ -342,6 +342,42 @@ void inlineSvgIconSplitAcrossPagesRendersAndPaints() throws Exception { } } + @Test + void linkedInlineSvgOnAWrappedLineKeepsTheAnnotationSizedToTheIcon() throws Exception { + // The single-line linked case is covered above; here the linked icon lands + // on a wrapped line, exercising spanLinkRectangle's alignment/height + // geometry when the icon's line is not the paragraph's first line. + double iconSize = 8.0; + byte[] pdf; + int lineCount; + try (DocumentSession session = GraphCompose.document() + .pageSize(168, 200) + .margin(14, 14, 14, 14) + .create()) { + session.dsl() + .pageFlow() + .name("Flow") + .addParagraph(p -> p + .inlineText("This label is intentionally long so the linked icon wraps onto a later line ") + .inlineSvgIcon(crimsonSquare(), iconSize, InlineImageAlignment.CENTER, + 0.0, new DocumentLinkOptions("https://example.com"))) + .build(); + lineCount = paragraphLines(session.layoutGraph()).size(); + pdf = session.toPdfBytes(); + } + + assertThat(lineCount).as("the linked icon sits on a wrapped paragraph").isGreaterThanOrEqualTo(2); + try (PDDocument document = Loader.loadPDF(pdf)) { + PDAnnotationLink link = (PDAnnotationLink) document.getPage(0).getAnnotations().stream() + .filter(PDAnnotationLink.class::isInstance) + .findFirst() + .orElseThrow(() -> new AssertionError("no link annotation for the wrapped inline SVG")); + assertThat((double) link.getRectangle().getHeight()) + .as("link rect still hugs the icon, not the line box, on a wrapped line") + .isCloseTo(iconSize, within(0.5)); + } + } + private static List paragraphLines(LayoutGraph graph) { return graph.fragments().stream() .map(PlacedFragment::payload) diff --git a/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java b/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java index 131a10f9..4081419b 100644 --- a/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java +++ b/src/test/java/com/demcha/compose/document/emoji/EmojiLibraryTest.java @@ -3,8 +3,12 @@ import com.demcha.compose.document.svg.SvgIcon; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -70,4 +74,24 @@ void absentEmojiSetReportsUnavailableAndNamesTheArtifact() throws Exception { .hasMessageContaining("graph-compose-emoji"); } } + + @Test + void indexedGlyphThatCannotBeParsedResolvesEmptyAndRequireExplains(@TempDir Path classpathRoot) throws Exception { + // A set whose index points at a glyph the SVG parser rejects (here an svg + // with no drawable geometry). find() must stay lenient — empty, so callers + // fall back to literal text — and require()'s message must distinguish + // "indexed but unrenderable" from "unknown shortcode". + Path svgDir = Files.createDirectories(classpathRoot.resolve("emoji/svg")); + Files.writeString(classpathRoot.resolve("emoji/emoji-index.properties"), "broken=0bad1\n"); + Files.writeString(svgDir.resolve("0bad1.svg"), ""); + try (URLClassLoader loader = new URLClassLoader(new URL[]{classpathRoot.toUri().toURL()}, null)) { + EmojiLibrary lib = new EmojiLibrary(loader); + + assertThat(lib.isAvailable()).isTrue(); + assertThat(lib.find("broken")).isEmpty(); + assertThatThrownBy(() -> lib.require("broken")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("could not be rendered"); + } + } }