Skip to content

SVG Rendering

Overview

Some converters include a render command that generates SVG diagrams from source files. This is separate from — and complementary to — the convert command that produces RDF.

flowchart LR
    subgraph source["Source file (.puml / .bpmn)"]
        src["diagram source"]
    end

    subgraph convert_path["convert command (primary)"]
        conv["Parser → Emitter → RDF"]
        trig["out/model.trig"]
    end

    subgraph render_path["render command (optional)"]
        rend["Parser → SVG layout engine"]
        svg["out/diagram.svg"]
    end

    subgraph downstream["Downstream (rdf2docs)"]
        rdf2docs["archvis: triples → generic SVG"]
    end

    src --> conv --> trig
    src --> rend --> svg
    trig --> downstream

Which converters support render

Converter convert render SVG source
ArchiMate ✓ ✗ Use Archi HTML export or rdf2docs
BPMN ✓ ✓ Reads BPMNDI geometry from XML (Camunda library)
PlantUML ✓ ✓ Delegates to PlantUML's built-in layout engine
Structurizr ✓ ✗ Use Structurizr's own rendering tools
Backstage ✓ ✗ Catalog data, no diagrams

PlantUML layout: dot or the bundled Smetana engine

Sequence diagrams are laid out by PlantUML itself. Class, component, use case, state and deployment diagrams need Graphviz's dot algorithm, supplied either by the dot binary or by Smetana, the pure-Java port bundled in the PlantUML JAR. --layout AUTO (the default) uses dot when present and falls back to Smetana with a warning, so no external install is required; --layout SMETANA pins the self-contained engine for reproducible CI output and --layout DOT fails rather than substituting. See PlantUML → Layout engine.

Nothing else is needed at runtime either: the PlantUML stdlib bundles (C4, AWS, Azure, Kubernetes, ArchiMate, Bootstrap icons and the rest) ship inside the same JAR, so !include <C4/C4_Container> resolves offline with no network access and no extra dependency.

Two approaches to SVG

1. Converter render command (notation-specific, high fidelity)

The converter reads the original source file and uses the notation's own rendering engine:

  • BPMN: Camunda model library → BPMNDI coordinates → proper gateway diamonds, event circles, task boxes, sequence flow arrows. --shape-config overrides fill, stroke and icon per element type; see BPMN → Render.
  • PlantUML: PlantUML MIT library → auto-layout → class boxes, arrows, packages

This produces notation-faithful SVGs — they look like what the authoring tool would produce.

# BPMN: render with proper BPMN notation symbols
java -jar bpmn2linkedarchi.jar render \
  process.bpmn -o svg/

# PlantUML: render with PlantUML auto-layout
java -jar plantuml2linkedarchi.jar render \
  diagram.puml -o svg/

2. rdf2docs (notation-agnostic, from RDF)

rdf2docs reads the archvis:ArchNode and archvis:Link triples from the TriG output and renders generic box-and-line diagrams:

  • Works for any notation (ArchiMate, BPMN, PlantUML, C4, custom)
  • Uses archvis:bounds-x/y/w/h for positioning
  • Uses archvis:source / archvis:target for connections
  • No notation-specific icons — plain rectangles and arrows

This produces uniform SVGs — consistent style across notations but without visual fidelity to the source notation.

When to use which

Scenario Approach
Documentation site with proper BPMN/UML notation Converter render
Consistent cross-notation architecture views rdf2docs
PlantUML diagrams (need auto-layout, no coordinates in source) Converter render (only option)
ArchiMate views (have full geometry in Exchange XML) rdf2docs (reads archvis: from TriG)
Quick preview in CI pipeline Converter render
Hyperlinked SVGs pointing to knowledge graph IRIs Converter render with --base-iri (see Hyperlinks)

The render command and diagram-index.yaml

Both convert and render share the same diagram-index.yaml file:

diagrams:
  - id: order-fulfillment
    file: order-fulfillment.bpmn
    status: publish
    title: "Order Fulfillment Process"
    author: "Platform Team"

This means: - The same index controls which files are converted AND rendered - Every lifecycle state is converted and rendered, so a published image URL keeps resolving and an unreleased diagram still has a picture to review. --exclude-states withholds a state from both commands alike. See Lifecycle states - --no-render-archived stops drawing the archived ones, for an output directory that should hold only what is still in use. convert is unaffected, so the diagram keeps its IRIs — but any image URL already published for it stops resolving, which is why it is opt-in - The view ID names the .svg output — an SVG depicts one diagram — and any sub-directory of the index file: path is mirrored under the output directory. The model ID becomes the IRI path segment that convert mints elements under. In the legacy flat schema there is no view ID, so the model ID names the file and output paths are unchanged - Because those values are both published filenames and IRI segments, they are validated as slugs (^[a-z0-9][a-z0-9._-]*$) — see id rules and Models hold views - Metadata (title, author) is used in SVG annotations (BPMN) and provenance (TTL)

# Both commands use the same index
plantuml2linkedarchi convert models/*.puml \
  --diagrams-index models/diagram-index.yaml -o out/model.trig

plantuml2linkedarchi render models/*.puml \
  --diagrams-index models/diagram-index.yaml -o out/svg/

What every render command has in common

BaseRenderCommand in svg-core owns the whole loop, so render behaves the same whichever converter it belongs to. A converter supplies only its notation slug and a renderOne(); everything below is shared:

Concern Shared behaviour
File selection With --diagrams-index, every indexed entry is rendered whatever its lifecycle state (--exclude-states to leave some out, --no-render-archived to drop the archived ones). Resolved identically to convert, so a diagram in the graph has a picture. Without an index, every input given
Index lookup DiagramSelection in core, shared with convert: by path relative to --diagrams-root (or the index's own directory), falling back to an unambiguous bare file name. A warning when the index matched nothing. See How file: is matched
Model ID The index id, else the input filename without extension
Element IRIs {base}{notation}/{modelId}/element/ from --base-iri, matching IriMinting
Output path <viewId>.svg, falling back to <modelId>.svg where the index names no view, mirroring the sub-directory of the index file: path; flat for ad-hoc runs
Superseded names A copy under each id the entry declares under formerIds: for whichever level names the file, so a previously published image URL keeps resolving. See ADR 0003
Reporting Rendered: <file> → <path> per success, Also wrote superseded name: <name> per alias, [WARN] Skipped <file>: <reason> per failure
Failed file Leaves no output behind, so nothing in the output directory looks rendered when it is not
Exit codes 0 all rendered, 1 at least one file skipped, 2 setup failed before rendering started

Only genuinely notation-specific options live on the subclasses: --shape-config for BPMN, --layout for PlantUML.

With --base-iri, both renderers link each element to {base}{notation}/{modelId}/element/{id} — the same IRI convert mints for that element, so a click on a shape lands on the node that exists in the graph. Without --base-iri no links are emitted.

The two halves have to agree exactly. For playground/bpmn/order-fulfillment.bpmn — a view of model order-domain in that index — with --base-iri https://example.org/la/, render writes:

<a href="https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder">

and convert mints:

<https://example.org/la/bpmn/order-domain/graph/semantic> {
    <https://example.org/la/bpmn/order-domain/element/Task_ValidateOrder>
        a bpmn:UserTask , arch:Element , arch:ModelConcept ;
        arch:inModel <https://example.org/la/bpmn/order-domain> ;
        bpmn:name "Validate Order" .
}

Note the model id in both: an element belongs to the model, not to the view it is drawn in, so a shape in returns-handling.svg links into the same namespace.

Both are produced from the same IriMinting rule and the same model ID, which is why the model ID must not differ between the two runs — and why render and convert read one diagram index.

The two renderers get there differently, because one builds the SVG and the other does not:

Converter How the link is produced
BPMN The renderer writes the SVG itself, so it wraps each shape in <a href> directly
PlantUML PlantUML owns the output, so the renderer annotates a copy of the source with PlantUML's own link syntax ([[iri]]) on each element declaration and lets PlantUML emit the anchors

The PlantUML path has consequences worth knowing:

  • The .puml file on disk is never modified — the annotation happens in memory.
  • Links the author already wrote (class Foo [[https://…]]) are left alone.
  • The declaration is found by either form PlantUML accepts: a keyword (component Foo, class "Foo" as F) or a bracket shorthand ([Foo], (Use case), :Actor:, () Interface), with or without as Alias. Component and use case diagrams are usually written in the shorthand, so both have to be covered for their shapes to be clickable.
  • Elements that are only declared implicitly inside a relationship ([A] --> [B]) have no declaration line to annotate, so they render without a link. Relationship lines are never annotated: a link there would attach to the arrow.
  • If the annotated source no longer parses, the diagram is rendered without links and a warning is logged, rather than emitting a broken diagram. The check requires PlantUML to actually report links in the result, so an annotation that silently had no effect counts as a failure too. It asks only whether the diagram has any link, so the elements that got none are listed at debug level.

A caller embedding the result needs <object>, <iframe> or inline <svg>: an SVG loaded through <img src> (or a Markdown ![](…) image) is rendered without interactivity, so its anchors are not clickable no matter how they were emitted.

Design rationale

Why render is in the converter (not a separate tool)

  • Shared index — same diagram-index.yaml for both commands, so one file decides what gets converted and rendered, and id gives both the same model identity.
  • Shared IRI scheme — render mints element IRIs with the same {base}{notation}/{modelId}/element/{id} rule as convert (IriMinting), which is what makes the hyperlinks in the SVG resolve against the graph. A separate tool would have to reimplement that rule and keep it in step.
  • Hyperlinked SVGs — with --base-iri, each shape becomes an <a href> to its element IRI, because the renderer knows both the base IRI and the element IDs.
  • Single Docker image — one image, all capabilities.

Note that render does not share the parser with convert. BPMN parses twice with two different libraries (a StAX/CMOF parser for RDF, the Camunda model library for BPMNDI geometry), and PlantUML uses the same underlying library through a different entry point (PlantUmlParser for the model, SourceStringReader for layout). The duplication is deliberate: each side needs a different view of the file, and neither intermediate model carries what the other needs.

Why render is optional (not the primary output)

  • RDF is the contract — the convert command produces the primary artifact that flows into the knowledge graph. SVGs are a convenience output.
  • Not all notations have rendering — ArchiMate, Structurizr, and Backstage don't have render commands. The architecture doesn't depend on SVG generation.
  • rdf2docs exists — for notations that emit archvis: geometry in the TriG, generic SVG rendering can happen downstream without the converter.

Why not generate SVG from the RDF instead of the source?

For BPMN and PlantUML, the converter's render command reads the original source file (not the RDF output). This is because:

  • PlantUML has no coordinates — the .puml file is text-only. PlantUML's layout engine computes positions at render time. There are no archvis:bounds-* in the TriG to render from.
  • BPMN rendering needs the full model — the Camunda library reads BPMNDI and the semantic model together to produce correct notation (gateway markers, event types, subprocess nesting). The RDF only has the final coordinates, not the rendering context.

CI integration

In a source repo pipeline, both commands can run in sequence:

convert-and-render:
  image: linkedarchi-converters:latest
  script:
    # Primary artifact: RDF
    - plantuml2linkedarchi convert models/*.puml
        --diagrams-index models/diagram-index.yaml
        --format TRIG -o out/model.trig

    # Optional: SVG for documentation
    - plantuml2linkedarchi render models/*.puml
        --diagrams-index models/diagram-index.yaml
        --base-iri $BASE_IRI
        -o out/svg/

  artifacts:
    paths:
      - out/model.trig   # → consumed by archi-graph
      - out/svg/         # → consumed by docs site