Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
111bfd4
feat(api): in-PDF navigation — anchors and internal GoTo links
DemchaAV Jun 21, 2026
d8db151
feat(api): inline graphic internal links in RichText
DemchaAV Jun 21, 2026
46f5f83
feat(api): inline graphic internal links on ParagraphBuilder
DemchaAV Jun 21, 2026
7670d49
docs(recipes): in-PDF navigation recipe for anchors and internal links
DemchaAV Jun 21, 2026
6710470
feat(api): inline SVG-icon runs on the text baseline
DemchaAV Jun 21, 2026
8c6f6e4
feat(api): colour emoji by shortcode + graph-compose-emoji module
DemchaAV Jun 21, 2026
0791d3c
docs(examples): runnable colour-emoji shortcode showcase
DemchaAV Jun 21, 2026
5eeb2ce
fix(api): restore deprecated linkOptions() bridges for binary compati…
DemchaAV Jun 21, 2026
662cfd9
docs(examples): SVG-vs-PNG emoji comparison table
DemchaAV Jun 21, 2026
1a45e91
feat(svg): best-effort gradient import for real-world artwork
DemchaAV Jun 21, 2026
1f42687
feat(emoji): bundle the full Noto Emoji set via a gemoji shortcode index
DemchaAV Jun 21, 2026
2594a91
docs(examples): full-set emoji gallery catalogue
DemchaAV Jun 21, 2026
6b01739
feat(svg): keep translucent gradients (opaque stops) instead of flatt…
DemchaAV Jun 21, 2026
2764552
fix(svg): reject clip-path instead of painting unclipped overflow
DemchaAV Jun 21, 2026
9e0f59e
fix(svg): ignore clip-path instead of rejecting the whole icon
DemchaAV Jun 21, 2026
6dadee0
feat(svg): support clip-path and skip display:none subtrees
DemchaAV Jun 21, 2026
d12cdcf
fix(svg): drop same-colour translucent gradient overlays instead of p…
DemchaAV Jun 21, 2026
5669dc9
fix(svg): clip inline SVG icons to their viewBox
DemchaAV Jun 21, 2026
c04a34e
fix(svg): also drop single-stop translucent gradient overlays
DemchaAV Jun 21, 2026
3862687
fix(svg): clip block SVG icons to their viewBox
DemchaAV Jun 21, 2026
0431f63
feat(api): expose clipToBounds on the layer-stack DSL
DemchaAV Jun 21, 2026
da9f9af
ci: install graph-compose-emoji before building the examples module
DemchaAV Jun 21, 2026
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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,57 @@
All notable changes to GraphCompose are documented here. Versions
follow semantic versioning; release dates are ISO 8601.

## v1.9.0 — unreleased

In-document navigation. Rendered PDFs can now declare named **anchors** and
**internal links** that jump to them — clickable tables of contents,
`[text](#heading)`-style links, and bidirectional footnotes — emitted as native
PDF `GoTo` actions. External links are unchanged.

### Public API

- **In-PDF navigation: anchors + internal links** (`@since 1.9.0`). Every flow
and leaf builder gains `anchor(String)`, declaring a named destination at the
element's top-left — `section.anchor("intro")`, `paragraph.anchor("fn-1")`, and
the same on image / shape / ellipse / line / barcode / table builders. A link
targets an anchor instead of a URI via `RichText.linkTo(text, anchor)` /
`linkTo(text, style, anchor)`, `ParagraphBuilder.inlineLinkTo(text, anchor)` /
`linkTo(anchor)`, and `linkTo(anchor)` on the leaf builders. Inline graphics
inside a paragraph jump to anchors too via `RichText.imageLinkTo(...)` /
`shapeLinkTo(...)` (and the matching `ParagraphBuilder.inlineImageLinkTo(...)` /
`shapeLinkTo(...)`). Anchor resolution
is deferred to the end of the render pass, so a link may target an anchor that
appears later in the document (a forward reference). An unknown anchor renders
as ordinary styled text (no annotation) and logs a warning; a link whose text
wraps produces one annotation per line fragment; a duplicate anchor name keeps
the last registration. Backends without in-document navigation (DOCX) render an
internal link as plain text.
- **Unified `DocumentLinkTarget`** (`@since 1.9.0`). A new sealed
`DocumentLinkTarget` — `ExternalLinkTarget` (wrapping `DocumentLinkOptions`)
and `InternalLinkTarget` (an anchor name) — is now the link type carried
through semantic nodes and resolved layout fragments. `DocumentLinkOptions` is
unchanged and still accepted by every existing `link(DocumentLinkOptions)` and
inline-link DSL method (wrapped into an `ExternalLinkTarget` automatically), so
authoring code is source-compatible. The link accessor on the inline-run
records (`InlineTextRun` / `InlineImageRun` / `InlineShapeRun`) is now
`linkTarget()`; the former `linkOptions()` remains as a deprecated bridge that
returns the external options (or `null` for an internal link).

### Documentation

- New runnable example
`examples/src/main/java/com/demcha/examples/features/navigation/InPdfNavigationExample.java`
— a clickable table of contents plus a bidirectional footnote.

### Tests

- `InternalLinkAnchorTest` (PDFBox assertions): forward and backward references
resolve to `GoTo`; an unknown anchor produces no annotation and no crash; the
destination points at the correct page across a page break; a wrapped link
emits an annotation per line fragment; external links still emit `URI`; a
section anchor and a shape internal link are both navigable; a duplicate anchor
keeps the last registration; plus a visual artifact write.

## v1.8.0 — 2026-06-18

Codenamed **"illustrative"**. Native vector charts (bar / line / pie, inline
Expand Down
1 change: 1 addition & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ authoring API; public application code should not import
| [Barcodes](recipes/barcodes.md) | QR / Code 128 / EAN / UPC and friends, tinting, quiet zone |
| [Images](recipes/images.md) | Sources, sizing precedence, fit modes, images in rows and cards |
| [PDF chrome](recipes/pdf-chrome.md) | Metadata, watermarks, running header/footer placeholders, protection, links, bookmarks |
| [In-PDF navigation](recipes/in-pdf-navigation.md) | Anchors + internal `linkTo` links: clickable contents, heading jumps, bidirectional footnotes, inline-graphic links |
| [Translucency](recipes/translucency.md) | `DocumentColor.rgba` / `withOpacity`, alpha coverage, layered tints |
| [DOCX export](recipes/docx-export.md) | Semantic export, node mapping, fallbacks and skipped kinds |
| [Snapshot testing](recipes/snapshot-testing.md) | Layout-snapshot regression testing in consumer projects |
Expand Down
1 change: 1 addition & 0 deletions docs/recipes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ API, with copy-pasteable snippets verified against the current release.
| [barcodes.md](barcodes.md) | QR / Code 128 / Code 39 / EAN / UPC / PDF417 / DataMatrix, tinting, quiet zone, card centring |
| [images.md](images.md) | Sources (bytes/path), sizing precedence, STRETCH/CONTAIN/COVER fit modes, images in rows and cards |
| [pdf-chrome.md](pdf-chrome.md) | Metadata, watermarks, running header/footer with `{page}/{pages}/{date}`, protection, links and outline bookmarks |
| [in-pdf-navigation.md](in-pdf-navigation.md) | Named `anchor(...)` destinations + internal `linkTo(...)` links: clickable tables of contents, `#heading`-style jumps, bidirectional footnotes, inline-graphic links — native PDF GoTo actions |
| [translucency.md](translucency.md) | `DocumentColor.rgba` / `withOpacity`: which primitives honour alpha, byte-identity for opaque colours, layered tints |
| [docx-export.md](docx-export.md) | Semantic DOCX export: 1:1 node mapping, chart/shape-container fallbacks, skipped kinds |
| [snapshot-testing.md](snapshot-testing.md) | Layout-snapshot regression testing in consumer projects, baseline update flow |
Expand Down
104 changes: 104 additions & 0 deletions docs/recipes/in-pdf-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# In-PDF navigation: anchors and internal links

A rendered PDF can link **within the same document**, not just to external
URLs — a clickable table of contents, a `#heading`-style jump, or a
bidirectional footnote. Declare a named **anchor** on any element, then point
an **internal link** at it; the PDF backend resolves each one to a native
go-to action.

## Anchors — named destinations

`anchor(name)` marks an element's top-left as a named destination. It is
available on every flow builder (`pageFlow`, `section`, `module`) and on the
leaf builders (paragraph, image, shape, ellipse, line, barcode, table):

```java
flow.addSection("Introduction", s -> s
.anchor("introduction")
.addParagraph(p -> p.text("Introduction body")));

flow.addParagraph(p -> p.text("Method").anchor("method"));
```

Anchor names are unique per document; a duplicate keeps the last registration,
and a blank name clears the anchor.

## Internal links — jump to an anchor

`RichText.linkTo(text, anchor)` is the in-document counterpart of
`link(text, uri)`:

```java
flow.addRich(rich -> rich
.plain("See the ")
.linkTo("introduction", "introduction")
.plain(" for context."));
```

Resolution is deferred to the end of the render pass, so a link may target an
anchor that appears **later** in the document (a forward reference). An unknown
anchor renders as ordinary styled text with no annotation; a link whose text
wraps produces one clickable rectangle per line.

Paragraph-level and leaf-element links target an anchor the same way:

```java
flow.addParagraph(p -> p.text("Back to the top").linkTo("top"));
flow.addImage(i -> i.source(logo).size(48, 48).linkTo("cover"));
```

## A clickable table of contents

```java
flow.addRich(rich -> rich.plain("1. ").linkTo("Overview", "overview"));
flow.addRich(rich -> rich.plain("2. ").linkTo("Details", "details"));

flow.addSection("Overview", s -> s.anchor("overview")
.addParagraph(p -> p.text("Overview")));
flow.addSection("Details", s -> s.anchor("details")
.addParagraph(p -> p.text("Details")));
```

## Bidirectional footnotes

Anchor the body reference and the note, then link each way with the inline
internal link `inlineLinkTo(text, anchor)`:

```java
flow.addParagraph(p -> p
.anchor("fnref-1")
.inlineText("A claim that needs evidence")
.inlineLinkTo("[1]", "fn-1"));

flow.addParagraph(p -> p
.anchor("fn-1")
.inlineLinkTo("[1]", "fnref-1")
.inlineText(" Supporting evidence for the claim."));
```

Click `[1]` in the body to jump to the note; click `[1]` in the note to jump
back to the citation.

## Inline graphics as links

Inline icons, figures, and images jump to anchors too — `imageLinkTo` /
`shapeLinkTo` on `RichText` (and the matching `inlineImageLinkTo` /
`shapeLinkTo` on `ParagraphBuilder`):

```java
import com.demcha.compose.document.style.ShapeOutline;

flow.addRich(rich -> rich
.plain("Legend ")
.shapeLinkTo(ShapeOutline.circle(7), brand, "notes")
.plain(" — click the dot to jump to the notes."));
```

External links are unchanged: `link(text, uri)` still emits a URI action, and
backends without in-document navigation (the semantic DOCX export) render an
internal link as plain text.

Runnable showcase:
[InPdfNavigationExample](../../examples/src/main/java/com/demcha/examples/features/navigation/InPdfNavigationExample.java)
— a clickable table of contents, a bidirectional footnote, and an inline-shape
link on one page.
5 changes: 5 additions & 0 deletions docs/recipes/rich-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ annotation on supporting backends; `with(text, style, linkOptions)`
combines an explicit style with link metadata. On `ParagraphBuilder`,
`inlineLink(text, options)` is the equivalent low-level call.

For in-document navigation, `linkTo(text, anchor)` points at a named
`anchor(...)` elsewhere in the document instead of a URL, and inline images
and shapes can link to an anchor too (`imageLinkTo` / `shapeLinkTo`). See
[in-pdf-navigation.md](in-pdf-navigation.md).

## Inline images

```java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.demcha.examples.features.tables.ComposedTableCellExample;
import com.demcha.examples.features.tables.TableAdvancedExample;
import com.demcha.examples.features.text.InlineShapesExample;
import com.demcha.examples.features.navigation.InPdfNavigationExample;
import com.demcha.examples.features.text.RichTextShowcaseExample;
import com.demcha.examples.features.text.SectionPresetsExample;
import com.demcha.examples.features.themes.CustomBusinessThemeExample;
Expand Down Expand Up @@ -145,6 +146,7 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + InlineShapesExample.generate());
System.out.println("Generated: " + RichTextShowcaseExample.generate());
System.out.println("Generated: " + SectionPresetsExample.generate());
System.out.println("Generated: " + InPdfNavigationExample.generate());

// Theming + chrome
System.out.println("Generated: " + CustomBusinessThemeExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package com.demcha.examples.features.navigation;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.dsl.RichText;
import com.demcha.compose.document.node.DocumentLinkOptions;
import com.demcha.compose.document.style.DocumentColor;
import com.demcha.compose.document.style.DocumentInsets;
import com.demcha.compose.document.style.DocumentTextStyle;
import com.demcha.compose.document.style.ShapeOutline;
import com.demcha.compose.document.theme.BusinessTheme;
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;

import java.nio.file.Path;

/**
* Runnable showcase for in-PDF navigation (v1.9): named {@code anchor(...)}
* destinations plus internal {@code linkTo(...)} links that jump to them.
*
* <p>Demonstrates the two patterns the feature unlocks: a clickable table of
* contents whose entries jump to section anchors, and a bidirectional footnote
* (body reference jumps to the note; the note's marker jumps back to the body).
* Open the rendered PDF in a viewer and click the blue links to navigate.</p>
*/
public final class InPdfNavigationExample {
private static final BusinessTheme THEME = BusinessTheme.modern();
private static final DocumentColor INK = DocumentColor.rgb(34, 38, 50);
private static final DocumentColor LINK = DocumentColor.rgb(20, 90, 170);
private static final DocumentColor PANEL = DocumentColor.rgb(244, 247, 252);

private static final DocumentTextStyle LINK_STYLE = DocumentTextStyle.builder()
.fontName(FontName.HELVETICA)
.size(10.5)
.color(LINK)
.build();

private InPdfNavigationExample() {
}

public static Path generate() throws Exception {
Path outputFile = ExampleOutputPaths.prepare("features/navigation", "in-pdf-navigation.pdf");

try (DocumentSession document = GraphCompose.document(outputFile)
.pageSize(DocumentPageSize.A4)
.pageBackground(THEME.pageBackground())
.margin(40, 40, 40, 40)
.create()) {

document.pageFlow()
.name("InPdfNavigation")
.spacing(14)
.addParagraph(p -> p
.text("In-PDF navigation")
.textStyle(THEME.text().h1())
.margin(DocumentInsets.zero()))

// Clickable table of contents — each entry is an internal link
// to a section anchor declared further down the page.
.addSection("Contents", section -> section
.softPanel(PANEL, 8, 14)
.spacing(5)
.addParagraph(p -> p
.text("Contents")
.textStyle(THEME.text().h3())
.margin(DocumentInsets.zero()))
.addRich(RichText.text("1. ").linkTo("Overview", LINK_STYLE, "overview"))
.addRich(RichText.text("2. ").linkTo("Details", LINK_STYLE, "details"))
.addRich(RichText.text("3. ").linkTo("Notes", LINK_STYLE, "notes")))

.addSection("Overview", section -> section
.anchor("overview")
.spacing(4)
.addParagraph(p -> p.text("Overview").textStyle(THEME.text().h2()))
.addParagraph(p -> p
.anchor("fnref-1")
.inlineText("GraphCompose now renders in-document navigation",
body())
.inlineLinkTo(" [1]", "fn-1")
.inlineText(" entirely with native PDF go-to actions.", body())))

.addSection("Details", section -> section
.anchor("details")
.spacing(4)
.addParagraph(p -> p.text("Details").textStyle(THEME.text().h2()))
.addParagraph(p -> p
.text("Anchors resolve in a deferred pass, so a link may target an "
+ "anchor that appears later in the document (a forward reference).")
.textStyle(body()))
.addRich(RichText.text("Jump back to the ")
.linkTo("contents", LINK_STYLE, "overview")
.plain("."))
.addRich(RichText.text("Inline graphics navigate too — ")
.shapeLinkTo(ShapeOutline.circle(7), LINK, "notes")
.plain(" click the dot to jump to the notes.")))

.addSection("Notes", section -> section
.anchor("notes")
.softPanel(PANEL, 8, 14)
.spacing(4)
.addParagraph(p -> p.text("Notes").textStyle(THEME.text().h3()))
.addParagraph(p -> p
.anchor("fn-1")
.inlineLinkTo("[1]", "fnref-1")
.inlineText(" Click the marker to jump back to the citation in the "
+ "overview.", body())))

.addSection("Footer", section -> section
.accentTop(THEME.palette().rule(), 0.6)
.padding(new DocumentInsets(8, 0, 0, 0))
.addRich(RichText.text("External links still work: ")
.link("project home",
new DocumentLinkOptions(
"https://github.com/DemchaAV/GraphCompose"))))
.build();

document.buildPdf();
}

return outputFile;
}

public static void main(String[] args) throws Exception {
System.out.println("Generated: " + generate());
}

private static DocumentTextStyle body() {
return DocumentTextStyle.builder().fontName(FontName.HELVETICA).size(10.5).color(INK).build();
}
}
Loading
Loading