Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<ParagraphLine> paragraphLines(LayoutGraph graph) {
return graph.fragments().stream()
.map(PlacedFragment::payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"), "<svg viewBox='0 0 10 10'/>");
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");
}
}
}