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-configoverrides 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/hfor positioning - Uses
archvis:source/archvis:targetfor 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.
Hyperlinks into the knowledge graph¶
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:
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
.pumlfile 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 withoutas 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.yamlfor both commands, so one file decides what gets converted and rendered, andidgives both the same model identity. - Shared IRI scheme —
rendermints element IRIs with the same{base}{notation}/{modelId}/element/{id}rule asconvert(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
convertcommand 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
rendercommands. 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
.pumlfile is text-only. PlantUML's layout engine computes positions at render time. There are noarchvis: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