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.03.2.8
- 0.10.0
+ 0.11.0true
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/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.03.12.03.2.8
- 0.10.0
+ 0.11.0true
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.03.12.03.2.8
- 0.10.0
+ 0.11.0true
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