clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / python / blockdiag-family / README.md
README.md 4.3K source

BlockDiag family renderer

One Cloudflare Python Worker renders BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, and RackDiag directly from pinned upstream Python libraries. The request hostname selects the engine. Request bodies cannot select a different engine.

The Worker imports renderer libraries directly and does not execute a command, spawn a process, call Java, or fetch the Fly compatibility origin. Remote and filesystem-backed images are rejected. SVG is sanitized before presentation metadata, padding, backgrounds, and framing are applied.

The six engines share one Worker because they form one compatible upstream dependency family. The four source trees provide the same parser, builder, SVG drawer, font, and node-renderer model. They retain six hostnames, so the hostname remains the only engine selector. The shared deployment avoids six copies of Pillow, the font, and the registration table. One request cannot select another engine.

Reproduce locally

Run these commands from this directory:

uv sync
uv run pytest -q
uv run pywrangler sync
uv run pywrangler dev --port 8788

The Worker build is Pyodide-based. pywrangler sync is therefore the important packaging check; ordinary uv sync only checks the CPython test environment. Deploy only after both checks pass:

uv run pywrangler deploy

For a local request, route the hostname to the development port, for example:

curl --resolve blockdiag.render.diagram.zip:8788:127.0.0.1 \
  -H 'Content-Type: application/json' \
  --data '{"source":"blockdiag { a -> b; }"}' \
  http://blockdiag.render.diagram.zip:8788/v1/svg

The request body must not contain an engine field. Wrangler's local custom domain emulation resolves every configured hostname as the first route. Use the CPython six-engine example suite locally. Smoke all six real hostnames after deployment. Do not treat a local Host header as proof of per-host dispatch.

Upstream parity reference

When an upstream comparison is needed, run the tagged Kroki image directly. It is an external reference and is not part of DiagramZip's build or runtime:

docker run --rm -p 8000:8000 yuzutech/kroki:0.32.1

Send the same fixtures to the reference image and to the Worker development port, then compare structural SVG coverage and record engine-specific losses. Pixel equality is not a gate for this extraction.

Pinned upstream revisions and their licenses are recorded in THIRD_PARTY_NOTICES.md and uv.lock.

Boundaries and known losses

The request protocol enforces a 1 MiB body limit and a 512 KiB source limit. It allows at most 64 options and 4 KiB per option. Padding ranges from 0 through 256. The sanitized SVG limit is 4 MiB. Only SVG output is exposed. Remote URLs and filesystem images are rejected before they can be fetched. Returned SVG is parsed with defusedxml; scripts, event handlers, active elements, external references, unsafe CSS, and unsafe embedded resources are removed.

Supported renderer options currently include size=WIDTHxHEIGHT, antialias, no-transparency, and no-doctype. The two raster-only flags, antialias and no-transparency, intentionally have no effect on SVG. The common protocol validates other upstream command-line options but does not pass them to a CLI. They remain documented loss until a direct API mapping is added. The Worker uses the bundled DejaVu Serif font path from the Docker reference. This path provides deterministic text metrics. Font fallback can differ on a larger host.

The vendored runtime replaces upstream importlib.metadata entry-point discovery with an explicit registration table. It installs the SVG drawer, all bundled node renderers (including PacketDiag), and only the built-in attributes, autoclass, and autolane plugins. This keeps the Worker reproducible and prevents arbitrary installed plugins from entering the rendering process.

The upstream libraries expose process-wide mutable renderer and plugin registries. Registration is one-time startup work. Each render and its cleanup are synchronous with no await boundary. Requests cannot interleave while one isolate changes those registries. Cloudflare can run multiple isolates. Each isolate has its own Python runtime and registries. Keep rendering synchronous until the registries become request-local.