|
3 | 3 | All notable changes to GraphCompose are documented here. Versions |
4 | 4 | follow semantic versioning; release dates are ISO 8601. |
5 | 5 |
|
| 6 | +## v1.9.0 — unreleased |
| 7 | + |
| 8 | +In-document navigation. Rendered PDFs can now declare named **anchors** and |
| 9 | +**internal links** that jump to them — clickable tables of contents, |
| 10 | +`[text](#heading)`-style links, and bidirectional footnotes — emitted as native |
| 11 | +PDF `GoTo` actions. External links are unchanged. |
| 12 | + |
| 13 | +### Public API |
| 14 | + |
| 15 | +- **In-PDF navigation: anchors + internal links** (`@since 1.9.0`). Every flow |
| 16 | + and leaf builder gains `anchor(String)`, declaring a named destination at the |
| 17 | + element's top-left — `section.anchor("intro")`, `paragraph.anchor("fn-1")`, and |
| 18 | + the same on image / shape / ellipse / line / barcode / table builders. A link |
| 19 | + targets an anchor instead of a URI via `RichText.linkTo(text, anchor)` / |
| 20 | + `linkTo(text, style, anchor)`, `ParagraphBuilder.inlineLinkTo(text, anchor)` / |
| 21 | + `linkTo(anchor)`, and `linkTo(anchor)` on the leaf builders. Inline graphics |
| 22 | + inside a paragraph jump to anchors too via `RichText.imageLinkTo(...)` / |
| 23 | + `shapeLinkTo(...)` (and the matching `ParagraphBuilder.inlineImageLinkTo(...)` / |
| 24 | + `shapeLinkTo(...)`). Anchor resolution |
| 25 | + is deferred to the end of the render pass, so a link may target an anchor that |
| 26 | + appears later in the document (a forward reference). An unknown anchor renders |
| 27 | + as ordinary styled text (no annotation) and logs a warning; a link whose text |
| 28 | + wraps produces one annotation per line fragment; a duplicate anchor name keeps |
| 29 | + the last registration. Backends without in-document navigation (DOCX) render an |
| 30 | + internal link as plain text. |
| 31 | +- **Unified `DocumentLinkTarget`** (`@since 1.9.0`). A new sealed |
| 32 | + `DocumentLinkTarget` — `ExternalLinkTarget` (wrapping `DocumentLinkOptions`) |
| 33 | + and `InternalLinkTarget` (an anchor name) — is now the link type carried |
| 34 | + through semantic nodes and resolved layout fragments. `DocumentLinkOptions` is |
| 35 | + unchanged and still accepted by every existing `link(DocumentLinkOptions)` and |
| 36 | + inline-link DSL method (wrapped into an `ExternalLinkTarget` automatically), so |
| 37 | + authoring code is source-compatible. The link accessor on the inline-run |
| 38 | + records (`InlineTextRun` / `InlineImageRun` / `InlineShapeRun`) is now |
| 39 | + `linkTarget()`; the former `linkOptions()` remains as a deprecated bridge that |
| 40 | + returns the external options (or `null` for an internal link). |
| 41 | +- **Inline SVG-icon runs** (`@since 1.9.0`). A parsed `SvgIcon` can now sit on |
| 42 | + the text baseline inside a paragraph via `RichText.svgIcon(icon, size)` and |
| 43 | + `ParagraphBuilder.svgIcon(icon, size)` (with `alignment` / `baselineOffset` / |
| 44 | + link overloads, plus a clickable form). `size` is the glyph's height in points; |
| 45 | + the width follows the icon's aspect ratio. The icon is drawn as crisp vector |
| 46 | + layers carrying their own colours — gradients included — so it renders |
| 47 | + independently of the active font's glyph coverage. This is the engine path for |
| 48 | + vector colour emoji (e.g. a Twemoji SVG dropped inline) and small vector marks. |
| 49 | + A new sealed `InlineRun` variant (`InlineSvgRun`) joins text / image / shape; |
| 50 | + the inline render reuses the existing SVG paint pipeline (shared with the block |
| 51 | + path fragment), so flat-colour output stays byte-identical. |
| 52 | +- **Colour emoji by shortcode** (`@since 1.9.0`). `RichText.emoji(":star:", size)` |
| 53 | + and `ParagraphBuilder.emoji(...)` resolve a GitHub-style shortcode to an inline |
| 54 | + vector colour glyph. Resolution is lenient — an unknown shortcode (or no emoji |
| 55 | + set on the classpath) is rendered as the literal text, the way GitHub treats an |
| 56 | + unrecognised `:code:`. The resolver is the new `EmojiLibrary` |
| 57 | + (`com.demcha.compose.document.emoji`): data-driven from the classpath layout |
| 58 | + `emoji/emoji-index.properties` (`shortcode=codepoint`) + `emoji/svg/<codepoint>.svg`, |
| 59 | + with `find(...)` (lenient `Optional`), `require(...)` (strict), `isAvailable()` |
| 60 | + and per-codepoint caching (a glyph using an SVG feature the parser rejects is |
| 61 | + treated as unresolved, so it falls back to text rather than failing the render). |
| 62 | + The glyphs ship in a new, independently-versioned **`graph-compose-emoji`** |
| 63 | + companion module (mirroring the `graph-compose-fonts` split): the engine carries |
| 64 | + no emoji art and has no Maven dependency on it. The module bundles the full |
| 65 | + **Noto Emoji** SVG set (~3.7k glyphs, SIL OFL 1.1) with a GitHub-style shortcode |
| 66 | + index (~1.6k shortcodes) generated from the gemoji database; both are rebuilt by |
| 67 | + `emoji/tools/build-emoji-set.py`. |
| 68 | +- **SVG gradient import is now best-effort** (`@since 1.9.0`). `stop-opacity` |
| 69 | + (which has no opaque-PDF-shading analogue) is ignored — the gradient renders |
| 70 | + with opaque stops — and a focal radial (`fx` / `fy`) approximates as a plain |
| 71 | + radial about the centre, instead of failing the whole icon. This lets |
| 72 | + real-world artwork import (keeps gradient scenes like `:framed_picture:` / |
| 73 | + `:city_sunrise:` looking like scenes rather than flat blobs); fully-opaque |
| 74 | + gradients are unchanged, byte for byte. |
| 75 | +- **SVG `clip-path` and `display:none` support** (`@since 1.9.0`). A |
| 76 | + `clip-path:url(#id)` (including the Adobe-Illustrator `<use>` + `clipPath` |
| 77 | + idiom, where the clipPath references a `<defs>` shape) is resolved to a clip |
| 78 | + region on each affected `SvgIcon.Layer` and honoured by the inline renderer, so |
| 79 | + glyphs that clip detail to a silhouette — hand gestures, body parts, the |
| 80 | + probing cane — render correctly instead of overflowing into halos. Hidden |
| 81 | + subtrees (`display:none`, e.g. an Illustrator guide layer of registration |
| 82 | + hatching) are skipped. Together these take the Noto Emoji set to essentially |
| 83 | + the whole bundled set rendering cleanly. |
| 84 | +- **Same-colour translucent gradients are dropped, not painted opaque.** A |
| 85 | + gradient whose stops are all the same RGB with at least one translucent stop |
| 86 | + carries no colour — it is a pure alpha overlay (a soft shadow or edge |
| 87 | + highlight, e.g. the hair-edge darkening on the vampire glyphs). With no |
| 88 | + shading-alpha in the backend, painting it opaque covered the art beneath (the |
| 89 | + vampire's face rendered as a solid hair blob); such layers are now dropped. |
| 90 | + Multi-colour gradients (real scenes — `:framed_picture:`, `:sunrise:`, |
| 91 | + `:city_sunset:`) are structural and keep rendering as gradients. |
| 92 | +- **Inline SVG icons are clipped to their viewBox.** Real-world icon art |
| 93 | + (notably Noto's working files) parks geometry outside the viewBox — a browser |
| 94 | + clips it to the viewBox, but the inline renderer was painting it, so an icon |
| 95 | + could smear copies of itself across adjacent glyphs (`:package:` rendered as |
| 96 | + several duplicated boxes overlapping its neighbours). The inline SVG render now |
| 97 | + clips each icon to its glyph box, matching SVG `viewBox` semantics. |
| 98 | +- **Block SVG icons are clipped to their viewBox too.** The same off-canvas art |
| 99 | + bled past the box on the block path (`addSvgIcon(icon, w)` / `SvgIcon.node(w)`), |
| 100 | + which had no viewBox clip. A block icon's layer stack now clips its layers to |
| 101 | + the icon box: `LayerStackNode` gains an opt-in `clipToBounds` (`@since 1.9.0`, |
| 102 | + default off so existing stacks stay byte-identical) and `SvgIcon.node(...)` |
| 103 | + sets it. It reuses the `ShapeContainer` clip pipeline — one paired |
| 104 | + begin/end marker per icon — so it matches the inline fix above. The same |
| 105 | + flag is exposed to the DSL as `LayerStackBuilder.clipToBounds()` — the |
| 106 | + `overflow: hidden` of a stacking box for any layer stack. |
| 107 | + |
| 108 | +### Documentation |
| 109 | + |
| 110 | +- New runnable example |
| 111 | + `examples/src/main/java/com/demcha/examples/features/navigation/InPdfNavigationExample.java` |
| 112 | + — a clickable table of contents plus a bidirectional footnote. |
| 113 | +- New runnable example |
| 114 | + `examples/src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java` |
| 115 | + — multi-colour vector glyphs (gold star, green check badge, violet gradient |
| 116 | + orb, info / warning marks) flowing inline with text, at several sizes. |
| 117 | +- New `graph-compose-emoji` module bundling the Noto Emoji SVG set (OFL 1.1) with |
| 118 | + `emoji/OFL.txt`, `emoji/NOTICE.md` and the `emoji/tools/build-emoji-set.py` |
| 119 | + generator that rebuilds the glyphs + shortcode index from noto-emoji + gemoji. |
| 120 | +- New runnable example |
| 121 | + `examples/src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java` |
| 122 | + — `:shortcode:` colour emoji flowing inline with text, the starter-set legend, |
| 123 | + the unknown-shortcode text fallback, and several glyph sizes. |
| 124 | +- New runnable example |
| 125 | + `examples/src/main/java/com/demcha/examples/features/text/EmojiSvgVsPngExample.java` |
| 126 | + — a `Shortcode | SVG (vector) | PNG (raster)` comparison table, drawing each |
| 127 | + starter glyph down both inline paths (`RichText.svgIcon` vs `RichText.image`). |
| 128 | +- New runnable example |
| 129 | + `examples/src/main/java/com/demcha/examples/features/text/EmojiGalleryExample.java` |
| 130 | + — a paginated catalogue of the entire bundled emoji set (every indexed glyph, |
| 131 | + drawn inline). |
| 132 | + |
| 133 | +### Tests |
| 134 | + |
| 135 | +- `InternalLinkAnchorTest` (PDFBox assertions): forward and backward references |
| 136 | + resolve to `GoTo`; an unknown anchor produces no annotation and no crash; the |
| 137 | + destination points at the correct page across a page break; a wrapped link |
| 138 | + emits an annotation per line fragment; external links still emit `URI`; a |
| 139 | + section anchor and a shape internal link are both navigable; a duplicate anchor |
| 140 | + keeps the last registration; plus a visual artifact write. |
| 141 | +- `InlineSvgRunTest` (run validation: null icon, non-finite / non-positive |
| 142 | + dimensions, alignment default, external-link wrapping) and `InlineSvgRenderTest` |
| 143 | + (PDFBox end-to-end: text preserved with no glyph substitution, the icon's fill |
| 144 | + colour and an inline gradient both rasterize onto the page, a linked icon emits |
| 145 | + a clickable annotation, and `svgIcon` sizes by aspect ratio). `InlineSvgRenderTest` |
| 146 | + also rasterizes off-canvas geometry to prove the inline glyph-box clip, and the |
| 147 | + new `BlockSvgRenderTest` does the same for the block path — off-canvas art does |
| 148 | + not bleed, in-box art still paints, the layer stack emits a balanced |
| 149 | + `CLIP_BOUNDS` begin/end pair, and a plain (non-icon) stack emits none. |
| 150 | +- `EmojiLibraryTest` (resolves shortcodes case-insensitively with/without colons, |
| 151 | + unknown → empty, `require` throws, an absent set reports unavailable and names |
| 152 | + the `graph-compose-emoji` artifact) and `EmojiRenderTest` (a known shortcode |
| 153 | + rasterizes a colour glyph, a gradient emoji paints its shading, an unknown |
| 154 | + shortcode falls back to literal text, and `RichText.emoji` yields an |
| 155 | + `InlineSvgRun` or a text run accordingly). |
| 156 | + |
6 | 157 | ## v1.8.0 — 2026-06-18 |
7 | 158 |
|
8 | 159 | Codenamed **"illustrative"**. Native vector charts (bar / line / pie, inline |
|
0 commit comments