From 6bc3027050fe2ab9f26b5319892210a28c25ab50 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 22 Jun 2026 14:53:23 +0100 Subject: [PATCH 1/3] refactor(api): name paragraph inline svg/emoji adders inlineSvgIcon/inlineEmoji 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). --- CHANGELOG.md | 4 +- .../features/text/EmojiGalleryExample.java | 2 +- .../features/text/EmojiShortcodeExample.java | 2 +- .../features/text/EmojiSvgVsPngExample.java | 2 +- .../features/text/InlineSvgIconExample.java | 2 +- .../document/dsl/ParagraphBuilder.java | 20 ++-- .../compose/document/emoji/package-info.java | 2 +- .../compose/document/dsl/EmojiRenderTest.java | 6 +- .../document/dsl/InlineSvgRenderTest.java | 109 +++++++++++++++++- 9 files changed, 126 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 770b1480..735b4b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ PDF `GoTo` actions. External links are unchanged. returns the external options (or `null` for an internal link). - **Inline SVG-icon runs** (`@since 1.9.0`). A parsed `SvgIcon` can now sit on the text baseline inside a paragraph via `RichText.svgIcon(icon, size)` and - `ParagraphBuilder.svgIcon(icon, size)` (with `alignment` / `baselineOffset` / + `ParagraphBuilder.inlineSvgIcon(icon, size)` (with `alignment` / `baselineOffset` / link overloads, plus a clickable form). `size` is the glyph's height in points; the width follows the icon's aspect ratio. The icon is drawn as crisp vector layers carrying their own colours — gradients included — so it renders @@ -50,7 +50,7 @@ PDF `GoTo` actions. External links are unchanged. the inline render reuses the existing SVG paint pipeline (shared with the block path fragment), so flat-colour output stays byte-identical. - **Colour emoji by shortcode** (`@since 1.9.0`). `RichText.emoji(":star:", size)` - and `ParagraphBuilder.emoji(...)` resolve a GitHub-style shortcode to an inline + and `ParagraphBuilder.inlineEmoji(...)` resolve a GitHub-style shortcode to an inline vector colour glyph. Resolution is lenient — an unknown shortcode (or no emoji set on the classpath) is rendered as the literal text, the way GitHub treats an unrecognised `:code:`. The resolver is the new `EmojiLibrary` diff --git a/examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java b/examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java index 4d3637bc..e87b7e9b 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java @@ -59,7 +59,7 @@ public static Path generate() throws Exception { List chunk = glyphs.subList(start, Math.min(start + PER_PARAGRAPH, glyphs.size())); flow.addParagraph(p -> { for (SvgIcon icon : chunk) { - p.svgIcon(icon, ICON_PT).inlineText(" "); + p.inlineSvgIcon(icon, ICON_PT).inlineText(" "); } }); } diff --git a/examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java b/examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java index 07a1671e..d72aab13 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java @@ -18,7 +18,7 @@ /** * Runnable showcase for colour emoji by shortcode ({@code @since 1.9.0}). * - *

{@code RichText.emoji(":star:", size)} / {@code ParagraphBuilder.emoji(...)} + *

{@code RichText.emoji(":star:", size)} / {@code ParagraphBuilder.inlineEmoji(...)} * resolve a GitHub-style shortcode to an inline vector colour glyph, drawn on the * text baseline — crisp at any zoom, no emoji font needed. Glyphs come from the * {@code graph-compose-emoji} companion artifact on the classpath (here, the diff --git a/examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java b/examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java index 396dd002..03b09463 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java @@ -149,7 +149,7 @@ private static byte[] rasterise(SvgIcon icon) throws Exception { byte[] glyphPdf; try (DocumentSession g = GraphCompose.document().pageSize(box, box).margin(0, 0, 0, 0).create()) { g.dsl().pageFlow().name("g") - .addParagraph(p -> p.svgIcon(icon, box).margin(DocumentInsets.zero())) + .addParagraph(p -> p.inlineSvgIcon(icon, box).margin(DocumentInsets.zero())) .build(); glyphPdf = g.toPdfBytes(); } diff --git a/examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java b/examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java index f79a1cc3..3b5f3754 100644 --- a/examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java +++ b/examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java @@ -20,7 +20,7 @@ * Runnable showcase for inline SVG-icon runs ({@code @since 1.9.0}). * *

Parsed {@link SvgIcon}s are placed on the text baseline with - * {@code RichText.svgIcon(icon, size)} / {@code ParagraphBuilder.svgIcon(...)}, + * {@code RichText.svgIcon(icon, size)} / {@code ParagraphBuilder.inlineSvgIcon(...)}, * so multi-colour vector glyphs flow inside a line of text — crisp at any zoom, * carrying their own colours, with no dependence on the active font's glyph * coverage. This is the engine path for vector colour emoji: a {@code :rocket:} diff --git a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java index 88522012..7b23dcbf 100644 --- a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java @@ -554,8 +554,8 @@ public ParagraphBuilder shape(ShapeOutline outline, * @return this builder * @since 1.9.0 */ - public ParagraphBuilder svgIcon(SvgIcon icon, double size) { - return svgIcon(icon, size, InlineImageAlignment.CENTER, 0.0, null); + public ParagraphBuilder inlineSvgIcon(SvgIcon icon, double size) { + return inlineSvgIcon(icon, size, InlineImageAlignment.CENTER, 0.0, null); } /** @@ -567,8 +567,8 @@ public ParagraphBuilder svgIcon(SvgIcon icon, double size) { * @return this builder * @since 1.9.0 */ - public ParagraphBuilder svgIcon(SvgIcon icon, double size, InlineImageAlignment alignment) { - return svgIcon(icon, size, alignment, 0.0, null); + public ParagraphBuilder inlineSvgIcon(SvgIcon icon, double size, InlineImageAlignment alignment) { + return inlineSvgIcon(icon, size, alignment, 0.0, null); } /** @@ -584,7 +584,7 @@ public ParagraphBuilder svgIcon(SvgIcon icon, double size, InlineImageAlignment * @return this builder * @since 1.9.0 */ - public ParagraphBuilder svgIcon(SvgIcon icon, + public ParagraphBuilder inlineSvgIcon(SvgIcon icon, double size, InlineImageAlignment alignment, double baselineOffset, @@ -616,12 +616,12 @@ public ParagraphBuilder svgIcon(SvgIcon icon, * @return this builder * @since 1.9.0 */ - public ParagraphBuilder emoji(String shortcode, double size) { - return emoji(shortcode, size, InlineImageAlignment.CENTER, 0.0, null); + public ParagraphBuilder inlineEmoji(String shortcode, double size) { + return inlineEmoji(shortcode, size, InlineImageAlignment.CENTER, 0.0, null); } /** - * Adds a colour emoji (see {@link #emoji(String, double)}) with explicit + * Adds a colour emoji (see {@link #inlineEmoji(String, double)}) with explicit * vertical alignment, baseline offset and optional link metadata. * * @param shortcode emoji shortcode, with or without surrounding colons @@ -632,14 +632,14 @@ public ParagraphBuilder emoji(String shortcode, double size) { * @return this builder * @since 1.9.0 */ - public ParagraphBuilder emoji(String shortcode, + public ParagraphBuilder inlineEmoji(String shortcode, double size, InlineImageAlignment alignment, double baselineOffset, DocumentLinkOptions linkOptions) { SvgIcon icon = EmojiLibrary.getDefault().find(shortcode).orElse(null); if (icon != null) { - return svgIcon(icon, size, alignment, baselineOffset, linkOptions); + return inlineSvgIcon(icon, size, alignment, baselineOffset, linkOptions); } return inlineText(shortcode); } diff --git a/src/main/java/com/demcha/compose/document/emoji/package-info.java b/src/main/java/com/demcha/compose/document/emoji/package-info.java index d5171eaa..b00a2762 100644 --- a/src/main/java/com/demcha/compose/document/emoji/package-info.java +++ b/src/main/java/com/demcha/compose/document/emoji/package-info.java @@ -4,7 +4,7 @@ *

The entry point is {@link com.demcha.compose.document.emoji.EmojiLibrary}, * which maps GitHub-style shortcodes (e.g. {@code ":star:"}) to parsed * {@link com.demcha.compose.document.svg.SvgIcon} glyphs and backs the - * {@code RichText.emoji(...)} / {@code ParagraphBuilder.emoji(...)} DSL. It is + * {@code RichText.emoji(...)} / {@code ParagraphBuilder.inlineEmoji(...)} DSL. It is * data-driven from the classpath layout {@code emoji/emoji-index.properties} * + {@code emoji/svg/.svg} shipped by the independently-versioned * {@code graph-compose-emoji} companion artifact; the engine carries no emoji diff --git a/src/test/java/com/demcha/compose/document/dsl/EmojiRenderTest.java b/src/test/java/com/demcha/compose/document/dsl/EmojiRenderTest.java index 09f60cea..7691fb62 100644 --- a/src/test/java/com/demcha/compose/document/dsl/EmojiRenderTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/EmojiRenderTest.java @@ -24,7 +24,7 @@ class EmojiRenderTest { @Test void knownShortcodeRendersAsInlineColourGlyph() throws Exception { - byte[] pdf = render(p -> p.inlineText("Done ").emoji(":white_check_mark:", 14)); + byte[] pdf = render(p -> p.inlineText("Done ").inlineEmoji(":white_check_mark:", 14)); try (PDDocument document = Loader.loadPDF(pdf)) { assertThat(new PDFTextStripper().getText(document)).contains("Done").doesNotContain("?"); BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144); @@ -39,7 +39,7 @@ void knownShortcodeRendersAsInlineColourGlyph() throws Exception { @Test void secondColourEmojiAlsoResolvesAndPaints() throws Exception { - byte[] pdf = render(p -> p.inlineText("Launch ").emoji(":rocket:", 14)); + byte[] pdf = render(p -> p.inlineText("Launch ").inlineEmoji(":rocket:", 14)); try (PDDocument document = Loader.loadPDF(pdf)) { assertThat(new PDFTextStripper().getText(document)).contains("Launch").doesNotContain(":rocket:"); BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144); @@ -51,7 +51,7 @@ void secondColourEmojiAlsoResolvesAndPaints() throws Exception { @Test void unknownShortcodeFallsBackToLiteralText() throws Exception { - byte[] pdf = render(p -> p.inlineText("Ping ").emoji(":not_a_real_emoji:", 14)); + byte[] pdf = render(p -> p.inlineText("Ping ").inlineEmoji(":not_a_real_emoji:", 14)); try (PDDocument document = Loader.loadPDF(pdf)) { assertThat(new PDFTextStripper().getText(document)).contains(":not_a_real_emoji:"); } 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 e067d4ac..a820b1f4 100644 --- a/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java +++ b/src/test/java/com/demcha/compose/document/dsl/InlineSvgRenderTest.java @@ -2,6 +2,11 @@ import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.layout.LayoutGraph; +import com.demcha.compose.document.layout.PlacedFragment; +import com.demcha.compose.document.layout.payloads.ParagraphFragmentPayload; +import com.demcha.compose.document.layout.payloads.ParagraphLine; +import com.demcha.compose.document.layout.payloads.ParagraphSvgSpan; import com.demcha.compose.document.node.DocumentLinkOptions; import com.demcha.compose.document.node.InlineImageAlignment; import com.demcha.compose.document.node.InlineSvgRun; @@ -128,7 +133,7 @@ void linkedInlineSvgEmitsClickableAnnotationSizedToTheIconBox() throws Exception .name("Flow") .addParagraph(paragraph -> paragraph .inlineText("Home ") - .svgIcon(crimsonSquare(), iconSize, InlineImageAlignment.CENTER, + .inlineSvgIcon(crimsonSquare(), iconSize, InlineImageAlignment.CENTER, 0.0, new DocumentLinkOptions("https://example.com"))) .build(); pdf = session.toPdfBytes(); @@ -192,7 +197,7 @@ private static byte[] renderAutoSized(boolean withIcon) throws Exception { .addParagraph(p -> { p.inlineText("Status complete now"); if (withIcon) { - p.svgIcon(wideBar, 10); + p.inlineSvgIcon(wideBar, 10); } p.autoSize(24, 5); }) @@ -241,13 +246,111 @@ private static byte[] renderIconRow(SvgIcon icon) throws Exception { .addParagraph(paragraph -> paragraph .name("IconRow") .inlineText("Ship it ") - .svgIcon(icon, 12) + .inlineSvgIcon(icon, 12) .inlineText(" now")) .build(); return session.toPdfBytes(); } } + @Test + void inlineSvgIconWrapsAcrossLinesAndDrivesLineHeight() throws Exception { + // A tall (28pt) icon mid-paragraph on a narrow column: the paragraph must + // wrap to several lines, the icon's line must carry a ParagraphSvgSpan, and + // that line's height must be driven up by the icon (lineHeight > the plain + // text-line height) — exercising the wrap + per-line max-graphic-height path + // that the single-line tests above never reach. + SvgIcon icon = crimsonSquare(); + try (DocumentSession session = GraphCompose.document() + .pageSize(170, 240) + .margin(14, 14, 14, 14) + .create()) { + session.dsl() + .pageFlow() + .name("Flow") + .addParagraph(p -> p + .name("WrappingIconParagraph") + .inlineText("This sentence is intentionally long so that it wraps onto more " + + "than one line before it reaches the inline ") + .inlineSvgIcon(icon, 28) + .inlineText(" icon and then continues with yet more trailing text")) + .build(); + + List lines = paragraphLines(session.layoutGraph()); + assertThat(lines).as("the paragraph wraps to multiple lines").hasSizeGreaterThanOrEqualTo(2); + + ParagraphLine iconLine = lines.stream() + .filter(line -> line.spans().stream().anyMatch(ParagraphSvgSpan.class::isInstance)) + .findFirst() + .orElseThrow(() -> new AssertionError("no wrapped line carries the inline SVG span")); + ParagraphSvgSpan span = (ParagraphSvgSpan) iconLine.spans().stream() + .filter(ParagraphSvgSpan.class::isInstance) + .findFirst() + .orElseThrow(); + assertThat(iconLine.lineHeight()) + .as("the icon's line grows to fit the icon") + .isGreaterThanOrEqualTo(span.height()); + assertThat(iconLine.lineHeight()) + .as("the tall icon, not the text, drives its line's height") + .isGreaterThan(iconLine.textLineHeight()); + + byte[] pdf = session.toPdfBytes(); + try (PDDocument document = Loader.loadPDF(pdf)) { + BufferedImage image = new PDFRenderer(document).renderImageWithDPI(0, 144); + assertThat(containsColorNear(image, 196, 30, 58, 45)) + .as("the wrapped inline SVG icon still paints its fill colour") + .isTrue(); + assertThat(new PDFTextStripper().getText(document)).doesNotContain("?"); + } + } + } + + @Test + void inlineSvgIconSplitAcrossPagesRendersAndPaints() throws Exception { + // The icon sits near the start of a paragraph whose body is long enough to + // paginate. The split/continuation flow must keep the icon (it lands on the + // head page) rather than drop or duplicate it across the page break. + SvgIcon icon = crimsonSquare(); + StringBuilder body = new StringBuilder(); + for (int i = 0; i < 50; i++) { + body.append("Filler sentence ").append(i).append(" that pads the paragraph. "); + } + try (DocumentSession session = GraphCompose.document() + .pageSize(220, 130) + .margin(12, 12, 12, 12) + .create()) { + session.dsl() + .pageFlow() + .name("Flow") + .addParagraph(p -> p + .inlineText("Status ") + .inlineSvgIcon(icon, 12) + .inlineText(" then a long body that must paginate: " + body)) + .build(); + + byte[] pdf = session.toPdfBytes(); + try (PDDocument document = Loader.loadPDF(pdf)) { + assertThat(document.getNumberOfPages()) + .as("the paragraph splits across a page break") + .isGreaterThanOrEqualTo(2); + BufferedImage head = new PDFRenderer(document).renderImageWithDPI(0, 144); + assertThat(containsColorNear(head, 196, 30, 58, 45)) + .as("the inline SVG icon paints on its (head) page within a paginating paragraph") + .isTrue(); + assertThat(new PDFTextStripper().getText(document)).doesNotContain("?"); + } + } + } + + private static List paragraphLines(LayoutGraph graph) { + return graph.fragments().stream() + .map(PlacedFragment::payload) + .filter(ParagraphFragmentPayload.class::isInstance) + .map(ParagraphFragmentPayload.class::cast) + .flatMap(payload -> payload.lines().stream()) + .toList(); + } + private static boolean containsColorNear(BufferedImage image, int r, int g, int b, int tolerance) { for (int y = 0; y < image.getHeight(); y++) { for (int x = 0; x < image.getWidth(); x++) { From c85ffd159421d5b51f51a76a00ab5f2b8829794a Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 22 Jun 2026 15:03:41 +0100 Subject: [PATCH 2/3] docs(api): add inline SVG/emoji recipes and document the blank-anchor throw - New "Inline SVG icons" and "Emoji / shortcodes" sections in the rich-text recipe, restoring the "every shipped feature has a recipe" guarantee; both recipe indexes updated. - @throws IllegalArgumentException on every link-by-anchor builder method (linkTo / inlineLinkTo / imageLinkTo / shapeLinkTo across RichText and the Paragraph / Barcode / Ellipse / Image / Line / Shape / Table builders), spelling out that a blank anchor throws while the anchor(...) setter clears. - Correct EmojiLibrary's class doc: it bundles the Noto Emoji set (SIL OFL 1.1), not a "small jdecked/twemoji starter set". --- docs/recipes.md | 2 +- docs/recipes/README.md | 2 +- docs/recipes/rich-text.md | 46 +++++++++++++++++++ .../compose/document/dsl/BarcodeBuilder.java | 1 + .../compose/document/dsl/EllipseBuilder.java | 1 + .../compose/document/dsl/ImageBuilder.java | 1 + .../compose/document/dsl/LineBuilder.java | 1 + .../document/dsl/ParagraphBuilder.java | 6 +++ .../demcha/compose/document/dsl/RichText.java | 6 +++ .../compose/document/dsl/ShapeBuilder.java | 1 + .../compose/document/dsl/TableBuilder.java | 1 + .../compose/document/emoji/EmojiLibrary.java | 4 +- 12 files changed, 68 insertions(+), 4 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 28a8aa8d..821748f8 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -19,7 +19,7 @@ authoring API; public application code should not import | [Layered page design](recipes/layered-page-design.md) | Choosing between page backgrounds, rows, layer stacks, and canvases | | [Absolute placement](recipes/absolute-placement.md) | `addCanvas` + `position(x, y)` for pixel-precise certificates and badges | | [Tables](recipes/tables.md) | Row span, zebra rows, totals row, repeated header on page break | -| [Rich text](recipes/rich-text.md) | `RichText` mixed-style runs, inline links/images/shapes, checkboxes | +| [Rich text](recipes/rich-text.md) | `RichText` mixed-style runs, inline links/images/shapes, SVG icons, emoji shortcodes, checkboxes | | [Lists](recipes/lists.md) | `addList`, marker customisation, nested lists with per-depth markers | | [Timelines](recipes/timelines.md) | `addTimeline`: markers on a connector rail, geometry and text-style controls | | [Barcodes](recipes/barcodes.md) | QR / Code 128 / EAN / UPC and friends, tinting, quiet zone | diff --git a/docs/recipes/README.md b/docs/recipes/README.md index c1b5e110..ec41e9ca 100644 --- a/docs/recipes/README.md +++ b/docs/recipes/README.md @@ -8,7 +8,7 @@ API, with copy-pasteable snippets verified against the current release. | Recipe | Covers | |---|---| | [charts.md](charts.md) | Native vector bar / line / area / pie-donut charts: data–spec–style layers, axis & grid toggles, point markers, value-label halos, legend placement, translucent area fills | -| [rich-text.md](rich-text.md) | `RichText` mixed-style runs in one paragraph: bold/accent/styled segments, inline links, inline images, inline shapes and checkboxes | +| [rich-text.md](rich-text.md) | `RichText` mixed-style runs in one paragraph: bold/accent/styled segments, inline links, inline images, inline SVG icons, emoji shortcodes, inline shapes and checkboxes | | [lists.md](lists.md) | `addList`: quick bulleted lists, marker customisation, nested lists with per-depth markers, spacing and styled items | | [timelines.md](timelines.md) | `addTimeline`: markers (dot / circle / numbered / square) on a connector rail, geometry and text-style controls, pagination opt-ins | | [keep-together.md](keep-together.md) | `keepTogether()` / `keepEntriesTogether()` — blocks that relocate whole instead of orphaning a heading at a page break | diff --git a/docs/recipes/rich-text.md b/docs/recipes/rich-text.md index c0c9ecc7..d3085415 100644 --- a/docs/recipes/rich-text.md +++ b/docs/recipes/rich-text.md @@ -98,6 +98,52 @@ relative to the surrounding line (`CENTER` by default); the full overload adds a `baselineOffset` and `DocumentLinkOptions` for a clickable inline image. +## Inline SVG icons + +A parsed `SvgIcon` sits on the text baseline like a word, drawn as crisp +vector layers that carry their own colours — so it renders independently +of the active font's glyph coverage. `size` is the glyph height in points; +the width follows the icon's aspect ratio. + +```java +import com.demcha.compose.document.svg.SvgIcon; + +SvgIcon star = SvgIcon.parse( + "" + + " " + + ""); + +section.addRich(rich -> rich + .plain("Rated ") + .svgIcon(star, 11) + .plain(" by reviewers.")); +``` + +On `ParagraphBuilder` the equivalent call is `inlineSvgIcon(icon, size)`; +both take `alignment` / `baselineOffset` / link overloads and a clickable +form via `DocumentLinkOptions`. `SvgIcon.parse(String)` reads inline SVG +markup; `SvgIcon.read(Path)` loads it from a file. + +## Emoji / shortcodes + +`emoji(":code:")` resolves a GitHub-style shortcode to an inline colour +glyph through `EmojiLibrary`. Resolution is lenient: an unknown shortcode — +or no emoji set on the classpath — falls back to the literal text, the way +GitHub renders an unrecognised `:code:`. + +```java +section.addRich(rich -> rich + .plain("Deploy ") + .emoji(":white_check_mark:", 11) + .plain(" succeeded ") + .emoji(":rocket:", 11)); +``` + +On `ParagraphBuilder` the call is `inlineEmoji(":code:", size)`. Glyphs ship +in the optional, independently-versioned `graph-compose-emoji` companion +artifact (Noto Emoji, SIL OFL 1.1) — add it to the classpath to resolve +shortcodes; the engine itself carries no emoji art. + ## Inline shapes and checkboxes Geometric figures drawn from geometry — not font glyphs — so they render diff --git a/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java b/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java index 0e20a2cd..38cf8969 100644 --- a/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/BarcodeBuilder.java @@ -246,6 +246,7 @@ public BarcodeBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public BarcodeBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java b/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java index 1ee1289d..8df48183 100644 --- a/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/EllipseBuilder.java @@ -158,6 +158,7 @@ public EllipseBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public EllipseBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java index d082d8f0..9baf3b5f 100644 --- a/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/ImageBuilder.java @@ -192,6 +192,7 @@ public ImageBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ImageBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java index 56a92698..0ecd83e8 100644 --- a/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/LineBuilder.java @@ -271,6 +271,7 @@ public LineBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public LineBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java index 7b23dcbf..1326c66a 100644 --- a/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/ParagraphBuilder.java @@ -188,6 +188,7 @@ public ParagraphBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder linkTo(String anchor) { @@ -247,6 +248,7 @@ public ParagraphBuilder inlineLink(String text, DocumentLinkOptions linkOptions) * @param text visible link text * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder inlineLinkTo(String text, String anchor) { @@ -340,6 +342,7 @@ public ParagraphBuilder inlineImage(DocumentImageData imageData, * @param height target height in points * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder inlineImageLinkTo(DocumentImageData imageData, double width, double height, String anchor) { @@ -357,6 +360,7 @@ public ParagraphBuilder inlineImageLinkTo(DocumentImageData imageData, double wi * @param baselineOffset extra vertical shift in points; positive moves up * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder inlineImageLinkTo(DocumentImageData imageData, @@ -653,6 +657,7 @@ public ParagraphBuilder inlineEmoji(String shortcode, * @param fill fill color * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder shapeLinkTo(ShapeOutline outline, DocumentColor fill, String anchor) { @@ -671,6 +676,7 @@ public ParagraphBuilder shapeLinkTo(ShapeOutline outline, DocumentColor fill, St * @param baselineOffset extra vertical shift in points; positive moves up * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ParagraphBuilder shapeLinkTo(ShapeOutline outline, diff --git a/src/main/java/com/demcha/compose/document/dsl/RichText.java b/src/main/java/com/demcha/compose/document/dsl/RichText.java index 5a771b67..2382bfb6 100644 --- a/src/main/java/com/demcha/compose/document/dsl/RichText.java +++ b/src/main/java/com/demcha/compose/document/dsl/RichText.java @@ -221,6 +221,7 @@ public RichText link(String text, String uri) { * @param text visible link text * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText linkTo(String text, String anchor) { @@ -235,6 +236,7 @@ public RichText linkTo(String text, String anchor) { * @param style explicit style for this run, or {@code null} for the link default * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText linkTo(String text, DocumentTextStyle style, String anchor) { @@ -343,6 +345,7 @@ public RichText image(DocumentImageData imageData, * @param height target height in points * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText imageLinkTo(DocumentImageData imageData, double width, double height, String anchor) { @@ -360,6 +363,7 @@ public RichText imageLinkTo(DocumentImageData imageData, double width, double he * @param baselineOffset extra vertical shift in points; positive moves up * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText imageLinkTo(DocumentImageData imageData, @@ -650,6 +654,7 @@ public RichText shape(ShapeOutline outline, * @param fill fill color * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText shapeLinkTo(ShapeOutline outline, DocumentColor fill, String anchor) { @@ -668,6 +673,7 @@ public RichText shapeLinkTo(ShapeOutline outline, DocumentColor fill, String anc * @param baselineOffset extra vertical shift in points; positive moves up * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public RichText shapeLinkTo(ShapeOutline outline, diff --git a/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java b/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java index 335c9d0a..435d7cb7 100644 --- a/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/ShapeBuilder.java @@ -192,6 +192,7 @@ public ShapeBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public ShapeBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java b/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java index 802632f7..198b75d4 100644 --- a/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java +++ b/src/main/java/com/demcha/compose/document/dsl/TableBuilder.java @@ -407,6 +407,7 @@ public TableBuilder linkTarget(DocumentLinkTarget linkTarget) { * * @param anchor target anchor name * @return this builder + * @throws IllegalArgumentException if {@code anchor} is blank * @since 1.9.0 */ public TableBuilder linkTo(String anchor) { diff --git a/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java b/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java index c9e75fa1..d5abce32 100644 --- a/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java +++ b/src/main/java/com/demcha/compose/document/emoji/EmojiLibrary.java @@ -26,8 +26,8 @@ *

The engine carries no emoji art and has no Maven dependency on the emoji * module — exactly like {@code DefaultFonts} and {@code graph-compose-fonts}. * This resolver is fully data-driven: any classpath providing that layout works, - * so the small bundled starter set can be replaced wholesale by the full - * jdecked/twemoji set with no code change.

+ * so the bundled Noto Emoji set (SIL OFL 1.1) can be swapped for another emoji + * set by changing the classpath alone, with no code change.

* *

Resolution is lenient by design — {@link #find(String)} returns an empty * {@link Optional} for an unknown shortcode or when no emoji set is on the From c923dffea38788ad6b868a50d5eba3d349b6189d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 22 Jun 2026 15:03:41 +0100 Subject: [PATCH 3/3] build(release): align central-publishing to 0.11.0, add publish-emoji workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump central-publishing-maven-plugin 0.10.0 -> 0.11.0 in the emoji, fonts and bundle standalone poms to match the engine pom (#219); they share no parent, so the literal is swept by hand. Add publish-emoji.yml mirroring publish-fonts.yml (emoji-v* tag, -f emoji/pom.xml, -P release) so graph-compose-emoji publishes to Central independently — matching the emoji-v* prose already in emoji/pom.xml. --- .github/workflows/publish-emoji.yml | 67 +++++++++++++++++++++++++++++ bundle/pom.xml | 2 +- emoji/pom.xml | 2 +- fonts/pom.xml | 2 +- 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/publish-emoji.yml diff --git a/.github/workflows/publish-emoji.yml b/.github/workflows/publish-emoji.yml new file mode 100644 index 00000000..f80dcefc --- /dev/null +++ b/.github/workflows/publish-emoji.yml @@ -0,0 +1,67 @@ +name: Publish emoji to Maven Central + +# Publishes the graph-compose-emoji companion artifact to Maven Central, +# independently of the engine. The bundled Noto Emoji glyphs change rarely, so +# they release on their OWN tag line (emoji-v*) — pushing an engine v* tag +# never touches them, and re-publishing emoji does not require an engine +# release. This mirrors publish-fonts.yml but targets emoji/pom.xml. +# +# Tagging: emoji-vX.Y.Z (e.g. emoji-v1.0.0). The emoji artifact carries its +# own independent version (see emoji/pom.xml) — keep it in sync with the tag. +# +# Human prerequisites are identical to publish.yml (GPG key + Central token +# secrets). See docs/contributing/release-process.md for the runbook. + +on: + push: + tags: + - 'emoji-v*' + workflow_dispatch: + inputs: + tag: + description: 'Existing emoji-v*-prefixed tag to (re-)publish' + required: true + type: string + +permissions: + contents: read + +jobs: + publish-emoji: + name: Publish ${{ github.ref_name }} to Maven Central + runs-on: ubuntu-latest + # Only ship plain semver tags (emoji-vX.Y.Z) to Central; pre-release + # suffixes ship nowhere from here. + if: | + github.event_name == 'workflow_dispatch' || + (!contains(github.ref, '-rc') && !contains(github.ref, '-alpha') && !contains(github.ref, '-beta') && !contains(github.ref, '-snapshot')) + env: + JAVA_TOOL_OPTIONS: -Djava.awt.headless=true + + steps: + - name: Check out repository at tag + uses: actions/checkout@v7 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Set up Temurin JDK 17 with Central credentials and GPG key + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + cache: maven + server-id: central + server-username: CENTRAL_USERNAME + server-password: CENTRAL_TOKEN + gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Publish emoji to Maven Central + # Activates the emoji module's release profile (sources + javadoc + + # gpg sign + central-publishing) and flips gpg.skip=false. Blocks + # until Sonatype's validator confirms validation. + run: ./mvnw -B -ntp -f emoji/pom.xml -P release -Dgpg.skip=false deploy + env: + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} diff --git a/bundle/pom.xml b/bundle/pom.xml index 06d5580b..c33c291e 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -67,7 +67,7 @@ 1.0.0 3.2.8 - 0.10.0 + 0.11.0 true diff --git a/emoji/pom.xml b/emoji/pom.xml index 14bc8871..de83840d 100644 --- a/emoji/pom.xml +++ b/emoji/pom.xml @@ -80,7 +80,7 @@ 3.4.0 3.12.0 3.2.8 - 0.10.0 + 0.11.0 true diff --git a/fonts/pom.xml b/fonts/pom.xml index 136d7a40..30fdc035 100644 --- a/fonts/pom.xml +++ b/fonts/pom.xml @@ -77,7 +77,7 @@ 3.4.0 3.12.0 3.2.8 - 0.10.0 + 0.11.0 true