Skip to content

Commit 89e7ff8

Browse files
authored
refactor(api): name paragraph inline svg/emoji adders inlineSvgIcon/inlineEmoji (#221)
ParagraphBuilder's inline adders now follow the existing inlineText / inlineImage / inlineLink convention: svgIcon -> inlineSvgIcon and emoji -> inlineEmoji (all overloads). RichText keeps its bare svgIcon / emoji names, which are internally consistent there. The API is unreleased (@SInCE 1.9.0), so there is no deprecation bridge. Also adds two InlineSvgRenderTest cases for inline-SVG paths that only single-line paragraphs reached before: an icon that wraps onto a later line (asserting that line grows to the icon via lineHeight > textLineHeight) and an icon inside a paragraph that splits across a page break (it still paints on its head page).
1 parent 3b3390f commit 89e7ff8

9 files changed

Lines changed: 126 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ PDF `GoTo` actions. External links are unchanged.
4040
returns the external options (or `null` for an internal link).
4141
- **Inline SVG-icon runs** (`@since 1.9.0`). A parsed `SvgIcon` can now sit on
4242
the text baseline inside a paragraph via `RichText.svgIcon(icon, size)` and
43-
`ParagraphBuilder.svgIcon(icon, size)` (with `alignment` / `baselineOffset` /
43+
`ParagraphBuilder.inlineSvgIcon(icon, size)` (with `alignment` / `baselineOffset` /
4444
link overloads, plus a clickable form). `size` is the glyph's height in points;
4545
the width follows the icon's aspect ratio. The icon is drawn as crisp vector
4646
layers carrying their own colours — gradients included — so it renders
@@ -50,7 +50,7 @@ PDF `GoTo` actions. External links are unchanged.
5050
the inline render reuses the existing SVG paint pipeline (shared with the block
5151
path fragment), so flat-colour output stays byte-identical.
5252
- **Colour emoji by shortcode** (`@since 1.9.0`). `RichText.emoji(":star:", size)`
53-
and `ParagraphBuilder.emoji(...)` resolve a GitHub-style shortcode to an inline
53+
and `ParagraphBuilder.inlineEmoji(...)` resolve a GitHub-style shortcode to an inline
5454
vector colour glyph. Resolution is lenient — an unknown shortcode (or no emoji
5555
set on the classpath) is rendered as the literal text, the way GitHub treats an
5656
unrecognised `:code:`. The resolver is the new `EmojiLibrary`

examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public static Path generate() throws Exception {
5959
List<SvgIcon> chunk = glyphs.subList(start, Math.min(start + PER_PARAGRAPH, glyphs.size()));
6060
flow.addParagraph(p -> {
6161
for (SvgIcon icon : chunk) {
62-
p.svgIcon(icon, ICON_PT).inlineText(" ");
62+
p.inlineSvgIcon(icon, ICON_PT).inlineText(" ");
6363
}
6464
});
6565
}

examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
/**
1919
* Runnable showcase for colour emoji by shortcode ({@code @since 1.9.0}).
2020
*
21-
* <p>{@code RichText.emoji(":star:", size)} / {@code ParagraphBuilder.emoji(...)}
21+
* <p>{@code RichText.emoji(":star:", size)} / {@code ParagraphBuilder.inlineEmoji(...)}
2222
* resolve a GitHub-style shortcode to an inline vector colour glyph, drawn on the
2323
* text baseline — crisp at any zoom, no emoji font needed. Glyphs come from the
2424
* {@code graph-compose-emoji} companion artifact on the classpath (here, the

examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ private static byte[] rasterise(SvgIcon icon) throws Exception {
149149
byte[] glyphPdf;
150150
try (DocumentSession g = GraphCompose.document().pageSize(box, box).margin(0, 0, 0, 0).create()) {
151151
g.dsl().pageFlow().name("g")
152-
.addParagraph(p -> p.svgIcon(icon, box).margin(DocumentInsets.zero()))
152+
.addParagraph(p -> p.inlineSvgIcon(icon, box).margin(DocumentInsets.zero()))
153153
.build();
154154
glyphPdf = g.toPdfBytes();
155155
}

examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
* Runnable showcase for inline SVG-icon runs ({@code @since 1.9.0}).
2121
*
2222
* <p>Parsed {@link SvgIcon}s are placed on the text baseline with
23-
* {@code RichText.svgIcon(icon, size)} / {@code ParagraphBuilder.svgIcon(...)},
23+
* {@code RichText.svgIcon(icon, size)} / {@code ParagraphBuilder.inlineSvgIcon(...)},
2424
* so multi-colour vector glyphs flow inside a line of text — crisp at any zoom,
2525
* carrying their own colours, with no dependence on the active font's glyph
2626
* coverage. This is the engine path for vector colour emoji: a {@code :rocket:}

src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -554,8 +554,8 @@ public ParagraphBuilder shape(ShapeOutline outline,
554554
* @return this builder
555555
* @since 1.9.0
556556
*/
557-
public ParagraphBuilder svgIcon(SvgIcon icon, double size) {
558-
return svgIcon(icon, size, InlineImageAlignment.CENTER, 0.0, null);
557+
public ParagraphBuilder inlineSvgIcon(SvgIcon icon, double size) {
558+
return inlineSvgIcon(icon, size, InlineImageAlignment.CENTER, 0.0, null);
559559
}
560560

561561
/**
@@ -567,8 +567,8 @@ public ParagraphBuilder svgIcon(SvgIcon icon, double size) {
567567
* @return this builder
568568
* @since 1.9.0
569569
*/
570-
public ParagraphBuilder svgIcon(SvgIcon icon, double size, InlineImageAlignment alignment) {
571-
return svgIcon(icon, size, alignment, 0.0, null);
570+
public ParagraphBuilder inlineSvgIcon(SvgIcon icon, double size, InlineImageAlignment alignment) {
571+
return inlineSvgIcon(icon, size, alignment, 0.0, null);
572572
}
573573

574574
/**
@@ -584,7 +584,7 @@ public ParagraphBuilder svgIcon(SvgIcon icon, double size, InlineImageAlignment
584584
* @return this builder
585585
* @since 1.9.0
586586
*/
587-
public ParagraphBuilder svgIcon(SvgIcon icon,
587+
public ParagraphBuilder inlineSvgIcon(SvgIcon icon,
588588
double size,
589589
InlineImageAlignment alignment,
590590
double baselineOffset,
@@ -616,12 +616,12 @@ public ParagraphBuilder svgIcon(SvgIcon icon,
616616
* @return this builder
617617
* @since 1.9.0
618618
*/
619-
public ParagraphBuilder emoji(String shortcode, double size) {
620-
return emoji(shortcode, size, InlineImageAlignment.CENTER, 0.0, null);
619+
public ParagraphBuilder inlineEmoji(String shortcode, double size) {
620+
return inlineEmoji(shortcode, size, InlineImageAlignment.CENTER, 0.0, null);
621621
}
622622

623623
/**
624-
* Adds a colour emoji (see {@link #emoji(String, double)}) with explicit
624+
* Adds a colour emoji (see {@link #inlineEmoji(String, double)}) with explicit
625625
* vertical alignment, baseline offset and optional link metadata.
626626
*
627627
* @param shortcode emoji shortcode, with or without surrounding colons
@@ -632,14 +632,14 @@ public ParagraphBuilder emoji(String shortcode, double size) {
632632
* @return this builder
633633
* @since 1.9.0
634634
*/
635-
public ParagraphBuilder emoji(String shortcode,
635+
public ParagraphBuilder inlineEmoji(String shortcode,
636636
double size,
637637
InlineImageAlignment alignment,
638638
double baselineOffset,
639639
DocumentLinkOptions linkOptions) {
640640
SvgIcon icon = EmojiLibrary.getDefault().find(shortcode).orElse(null);
641641
if (icon != null) {
642-
return svgIcon(icon, size, alignment, baselineOffset, linkOptions);
642+
return inlineSvgIcon(icon, size, alignment, baselineOffset, linkOptions);
643643
}
644644
return inlineText(shortcode);
645645
}

src/main/java/com/demcha/compose/document/emoji/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* <p>The entry point is {@link com.demcha.compose.document.emoji.EmojiLibrary},
55
* which maps GitHub-style shortcodes (e.g. {@code ":star:"}) to parsed
66
* {@link com.demcha.compose.document.svg.SvgIcon} glyphs and backs the
7-
* {@code RichText.emoji(...)} / {@code ParagraphBuilder.emoji(...)} DSL. It is
7+
* {@code RichText.emoji(...)} / {@code ParagraphBuilder.inlineEmoji(...)} DSL. It is
88
* data-driven from the classpath layout {@code emoji/emoji-index.properties}
99
* + {@code emoji/svg/<codepoint>.svg} shipped by the independently-versioned
1010
* {@code graph-compose-emoji} companion artifact; the engine carries no emoji

src/test/java/com/demcha/compose/document/dsl/EmojiRenderTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class EmojiRenderTest {
2424

2525
@Test
2626
void knownShortcodeRendersAsInlineColourGlyph() throws Exception {
27-
byte[] pdf = render(p -> p.inlineText("Done ").emoji(":white_check_mark:", 14));
27+
byte[] pdf = render(p -> p.inlineText("Done ").inlineEmoji(":white_check_mark:", 14));
2828
try (PDDocument document = Loader.loadPDF(pdf)) {
2929
assertThat(new PDFTextStripper().getText(document)).contains("Done").doesNotContain("?");
3030
BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144);
@@ -39,7 +39,7 @@ void knownShortcodeRendersAsInlineColourGlyph() throws Exception {
3939

4040
@Test
4141
void secondColourEmojiAlsoResolvesAndPaints() throws Exception {
42-
byte[] pdf = render(p -> p.inlineText("Launch ").emoji(":rocket:", 14));
42+
byte[] pdf = render(p -> p.inlineText("Launch ").inlineEmoji(":rocket:", 14));
4343
try (PDDocument document = Loader.loadPDF(pdf)) {
4444
assertThat(new PDFTextStripper().getText(document)).contains("Launch").doesNotContain(":rocket:");
4545
BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144);
@@ -51,7 +51,7 @@ void secondColourEmojiAlsoResolvesAndPaints() throws Exception {
5151

5252
@Test
5353
void unknownShortcodeFallsBackToLiteralText() throws Exception {
54-
byte[] pdf = render(p -> p.inlineText("Ping ").emoji(":not_a_real_emoji:", 14));
54+
byte[] pdf = render(p -> p.inlineText("Ping ").inlineEmoji(":not_a_real_emoji:", 14));
5555
try (PDDocument document = Loader.loadPDF(pdf)) {
5656
assertThat(new PDFTextStripper().getText(document)).contains(":not_a_real_emoji:");
5757
}

src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import com.demcha.compose.GraphCompose;
44
import com.demcha.compose.document.api.DocumentSession;
5+
import com.demcha.compose.document.layout.LayoutGraph;
6+
import com.demcha.compose.document.layout.PlacedFragment;
7+
import com.demcha.compose.document.layout.payloads.ParagraphFragmentPayload;
8+
import com.demcha.compose.document.layout.payloads.ParagraphLine;
9+
import com.demcha.compose.document.layout.payloads.ParagraphSvgSpan;
510
import com.demcha.compose.document.node.DocumentLinkOptions;
611
import com.demcha.compose.document.node.InlineImageAlignment;
712
import com.demcha.compose.document.node.InlineSvgRun;
@@ -128,7 +133,7 @@ void linkedInlineSvgEmitsClickableAnnotationSizedToTheIconBox() throws Exception
128133
.name("Flow")
129134
.addParagraph(paragraph -> paragraph
130135
.inlineText("Home ")
131-
.svgIcon(crimsonSquare(), iconSize, InlineImageAlignment.CENTER,
136+
.inlineSvgIcon(crimsonSquare(), iconSize, InlineImageAlignment.CENTER,
132137
0.0, new DocumentLinkOptions("https://example.com")))
133138
.build();
134139
pdf = session.toPdfBytes();
@@ -192,7 +197,7 @@ private static byte[] renderAutoSized(boolean withIcon) throws Exception {
192197
.addParagraph(p -> {
193198
p.inlineText("Status complete now");
194199
if (withIcon) {
195-
p.svgIcon(wideBar, 10);
200+
p.inlineSvgIcon(wideBar, 10);
196201
}
197202
p.autoSize(24, 5);
198203
})
@@ -241,13 +246,111 @@ private static byte[] renderIconRow(SvgIcon icon) throws Exception {
241246
.addParagraph(paragraph -> paragraph
242247
.name("IconRow")
243248
.inlineText("Ship it ")
244-
.svgIcon(icon, 12)
249+
.inlineSvgIcon(icon, 12)
245250
.inlineText(" now"))
246251
.build();
247252
return session.toPdfBytes();
248253
}
249254
}
250255

256+
@Test
257+
void inlineSvgIconWrapsAcrossLinesAndDrivesLineHeight() throws Exception {
258+
// A tall (28pt) icon mid-paragraph on a narrow column: the paragraph must
259+
// wrap to several lines, the icon's line must carry a ParagraphSvgSpan, and
260+
// that line's height must be driven up by the icon (lineHeight > the plain
261+
// text-line height) — exercising the wrap + per-line max-graphic-height path
262+
// that the single-line tests above never reach.
263+
SvgIcon icon = crimsonSquare();
264+
try (DocumentSession session = GraphCompose.document()
265+
.pageSize(170, 240)
266+
.margin(14, 14, 14, 14)
267+
.create()) {
268+
session.dsl()
269+
.pageFlow()
270+
.name("Flow")
271+
.addParagraph(p -> p
272+
.name("WrappingIconParagraph")
273+
.inlineText("This sentence is intentionally long so that it wraps onto more "
274+
+ "than one line before it reaches the inline ")
275+
.inlineSvgIcon(icon, 28)
276+
.inlineText(" icon and then continues with yet more trailing text"))
277+
.build();
278+
279+
List<ParagraphLine> lines = paragraphLines(session.layoutGraph());
280+
assertThat(lines).as("the paragraph wraps to multiple lines").hasSizeGreaterThanOrEqualTo(2);
281+
282+
ParagraphLine iconLine = lines.stream()
283+
.filter(line -> line.spans().stream().anyMatch(ParagraphSvgSpan.class::isInstance))
284+
.findFirst()
285+
.orElseThrow(() -> new AssertionError("no wrapped line carries the inline SVG span"));
286+
ParagraphSvgSpan span = (ParagraphSvgSpan) iconLine.spans().stream()
287+
.filter(ParagraphSvgSpan.class::isInstance)
288+
.findFirst()
289+
.orElseThrow();
290+
assertThat(iconLine.lineHeight())
291+
.as("the icon's line grows to fit the icon")
292+
.isGreaterThanOrEqualTo(span.height());
293+
assertThat(iconLine.lineHeight())
294+
.as("the tall icon, not the text, drives its line's height")
295+
.isGreaterThan(iconLine.textLineHeight());
296+
297+
byte[] pdf = session.toPdfBytes();
298+
try (PDDocument document = Loader.loadPDF(pdf)) {
299+
BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144);
300+
assertThat(containsColorNear(image, 196, 30, 58, 45))
301+
.as("the wrapped inline SVG icon still paints its fill colour")
302+
.isTrue();
303+
assertThat(new PDFTextStripper().getText(document)).doesNotContain("?");
304+
}
305+
}
306+
}
307+
308+
@Test
309+
void inlineSvgIconSplitAcrossPagesRendersAndPaints() throws Exception {
310+
// The icon sits near the start of a paragraph whose body is long enough to
311+
// paginate. The split/continuation flow must keep the icon (it lands on the
312+
// head page) rather than drop or duplicate it across the page break.
313+
SvgIcon icon = crimsonSquare();
314+
StringBuilder body = new StringBuilder();
315+
for (int i = 0; i < 50; i++) {
316+
body.append("Filler sentence ").append(i).append(" that pads the paragraph. ");
317+
}
318+
try (DocumentSession session = GraphCompose.document()
319+
.pageSize(220, 130)
320+
.margin(12, 12, 12, 12)
321+
.create()) {
322+
session.dsl()
323+
.pageFlow()
324+
.name("Flow")
325+
.addParagraph(p -> p
326+
.inlineText("Status ")
327+
.inlineSvgIcon(icon, 12)
328+
.inlineText(" then a long body that must paginate: " + body))
329+
.build();
330+
331+
byte[] pdf = session.toPdfBytes();
332+
try (PDDocument document = Loader.loadPDF(pdf)) {
333+
assertThat(document.getNumberOfPages())
334+
.as("the paragraph splits across a page break")
335+
.isGreaterThanOrEqualTo(2);
336+
BufferedImage head = new PDFRenderer(document).renderImageWithDPI(0, 144);
337+
assertThat(containsColorNear(head, 196, 30, 58, 45))
338+
.as("the inline SVG icon paints on its (head) page within a paginating paragraph")
339+
.isTrue();
340+
assertThat(new PDFTextStripper().getText(document)).doesNotContain("?");
341+
}
342+
}
343+
}
344+
345+
private static List<ParagraphLine> paragraphLines(LayoutGraph graph) {
346+
return graph.fragments().stream()
347+
.map(PlacedFragment::payload)
348+
.filter(ParagraphFragmentPayload.class::isInstance)
349+
.map(ParagraphFragmentPayload.class::cast)
350+
.flatMap(payload -> payload.lines().stream())
351+
.toList();
352+
}
353+
251354
private static boolean containsColorNear(BufferedImage image, int r, int g, int b, int tolerance) {
252355
for (int y = 0; y < image.getHeight(); y++) {
253356
for (int x = 0; x < image.getWidth(); x++) {

0 commit comments

Comments
 (0)