Skip to content

Native headless text layout

Optional server integration

Flutter applications and the playground already use Flutter text shaping. They do not need Pango. Pango is a native text-layout library for servers running Dart without Flutter; it handles font metrics, glyph placement and line wrapping. This adapter needs native shared libraries and cannot run inside a browser or a Cloudflare Worker. See the Pango project.

The pure Dart engine can generate and resolve diagrams without Flutter. For font-aware server rendering, explicitly import the optional Pango adapter:

dart
import 'package:vyuh_diagram_engine/vyuh_diagram_engine.dart';
import 'package:vyuh_diagram_engine/vyuh_diagram_pango.dart';

final textLayout = PangoDiagramTextLayoutEngine(
  fontFiles: ['/fonts/IBMPlexSans-Regular.ttf', '/fonts/IBMPlexSans-Bold.ttf'],
  defaultFontFamily: 'IBM Plex Sans',
);
final resolver = DiagramResolver(textLayout: textLayout);
try {
  final scene = resolver.resolve(document);
  // Pass this scene to DiagramSvgScene.encode, supplying the same font faces
  // as DiagramSvgResources when the output must be self-contained.
} finally {
  textLayout.dispose();
}

PangoDiagramTextLayoutEngine

Constructor argumentTypeContract
fontFilesIterable<String>Required, nonempty list of existing native font files. Snapshotted into backend-owned temporary files.
defaultFontFamilyStringRequired family name within those fonts; used when a run omits its family. Supply fallback fonts for every required script.
fallbackFontFamiliesIterable<String>Ordered fallback family names from supplied files. Retained in resolved lines and SVG, including symbol fonts needed by list markers.
nativeLibraryDirectoryString?Optional directory containing Pango, PangoFT2, Fontconfig, GLib and GObject shared libraries. Linux defaults to loader library names; Apple Silicon macOS defaults to /opt/homebrew/lib.
maximumCachedParagraphsint512; bounded least-recently-used measurement cache. 0 disables it.
maximumCachedCodeUnitsint262144; additional cache bound on retained canonical text.
blockSpacing, listIndentdoubleThe shared paragraph engine's spacing parameters.

measureBlock(block, region, availableWidth) returns immutable measured lines. layout(...) uses the existing paragraph/list placement pipeline; it does not introduce an export-only line wrapper. UTF-8 offsets from Pango are mapped to canonical UTF-16 offsets. Resolved lines retain the fallback font family, baseline, line height and wrapping for SVG, including list markers. Line height uses the shared 1.3 multiplier. Inline font size, weight, italic and OpenType super/subscript features participate in shaping; other marks remain output presentation.

The font map is isolated from process-global fonts. Missing glyphs reject with UnsupportedError instead of silently drawing tofu. Native coordinate limits and unsupported markup characters also reject. The supplied fonts must remain valid font data; the backend does not implement a font sanitizer.

dispose() releases native resources, cached measurements and private font-file snapshots. It is idempotent. Dispose the owning store before its text backend; measurement after disposal throws StateError. Resolved measurements contain no native pointers and remain readable after disposal.

runtimeVersion returns the loaded Pango version for deployment diagnostics and remains readable after disposal. Construction checks Pango's runtime ABI against 1.50.0 before loading the remaining backend libraries or allocating a font map. An incompatible runtime throws UnsupportedError with the loaded version.

Runtime requirements

This is an explicit native server rendering adapter, requiring Pango 1.50+ with its FreeType/Fontconfig backend. It is not imported by the browser library, and cannot run inside a Cloudflare Worker or a browser. Flutter canvases continue to use the Flutter text backend. This adapter currently supplies rendering metrics, not native interactive caret/hit-test geometry.

Native SVG preserves resolved line placement and embeds declared font resources; glyph rendering can still vary by SVG viewer.

Run the explicit native probe from the repository root. This command assumes the Noto fonts installed by Debian's fonts-noto-core package; on another host, substitute existing font paths and their internal family names:

sh
dart --packages=.dart_tool/package_config.json \
  packages/vyuh_diagram_codecs/tool/pango_smoke.dart \
  apps/demo/assets/fonts/IBMPlexSans-Regular.ttf \
  apps/demo/assets/fonts/IBMPlexSans-Bold.ttf \
  /tmp/diagram.svg \
  /usr/share/fonts/truetype/noto/NotoSansArabic-Regular.ttf 'Noto Sans Arabic' \
  /usr/share/fonts/truetype/noto/NotoSansDevanagari-Regular.ttf 'Noto Sans Devanagari' \
  /usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf 'Noto Sans Symbols 2'

Additional arguments are pairs of font path and internal family name. The first pair enables Arabic, the second Devanagari, and further pairs provide fallback coverage such as list symbols. Every supplied fallback is embedded in the SVG. IBM Plex Sans alone does not cover the probe's nested bullet marker. The two-font invocation therefore rejects that fixture; it is not a complete conformance command. Installing fonts globally does not make them available to the isolated backend: include their files and fallback family names explicitly.

For the Linux conformance image, run from the repository root:

sh
docker build -f tools/headless/Dockerfile -t diagram-headless .
docker run --name diagram-headless-check diagram-headless
docker cp diagram-headless-check:/tmp/diagram.svg ./diagram.svg
docker rm diagram-headless-check

This image compiles only the pure Dart packages and runs as user 65532. It bundles IBM Plex Sans and installs Noto Arabic, Devanagari and symbol fonts. It is a conformance executable, not a hosted service. Deployment owners must choose and record their own supported base-image and native-library versions.

The probe checks proportional measurement, wrapping, combining characters, empty lines, mixed runs, lists, nested lists, checklists, cache reuse, missing glyph rejection, disposal, native SVG text and embedded font resources.

Pango's layout API and font-map configuration are the native backend interfaces; serialized Pango debug output is not used as an application data format.

Native resource diagnostic

The conformance image also includes a lifecycle probe:

sh
docker run --rm --entrypoint /app/pango-lifecycle diagram-headless \
  /app/fonts/IBMPlexSans-Regular.ttf /app/fonts/IBMPlexSans-Bold.ttf

It defaults to 100 backend lifecycles; append an integer from 1 to 100000 after the font paths to choose a longer run. Each cycle measures 200 unique wrapping paragraphs. The probe checks cache reuse/eviction, validates retained measurements after disposal, rejects calls after disposal, and checks temporary-font cleanup after both success and invalid-font construction. Its JSON includes per-cycle RSS and elapsed time. RSS includes allocator and VM retention; this probe alone does not establish a leak-free service or a throughput guarantee.

Both native probes report the loaded Pango version in their JSON. To verify early rejection with deliberately incomplete incompatible libraries, run bun tools/headless/check-runtime-version.mjs from the repository root. This requires a C compiler and Dart on PATH (CC and DART may override them). The test compiles temporary fixtures, verifies rejection before loading other native dependencies, and removes the fixtures afterward.

The native adapter uses an owned PangoFT2 font map for measurement, with 72 DPI resolution. It does not load PangoCairo or clear process-global graphics caches.

Baseline geometry

DiagramMeasuredTextLine requires a finite, nonnegative baseline, measured from the visual line's top. Custom DiagramParagraphTextLayoutEngine shapers must provide it along with the line's width, height and offsets. Flutter obtains it from the shaped line metrics; the fixed-metrics bootstrap uses a synthetic baseline and does not promise font-accurate output.

Resolved propertyMeaning
ResolvedTextLine.baselineRetained alphabetic baseline offset from bounds.top.
ResolvedTextLine.baselineYBaseline position in the story owner's coordinate system.
ResolvedTextMarker.baselineAlphabetic baseline offset within the marker box.
ResolvedTextMarker.baselineYMarker baseline, aligned to its first content line.

Translations preserve the baseline offset and move its position with the line. A tall marker can reserve space above or below its paragraph; it cannot insert extra spacing between that paragraph's already-shaped visual lines. Empty paragraphs also retain a baseline. Export adapters must consume captured metrics and resources rather than infer baselines from font size. SVG export consumes these metrics; PDF output is not complete.

Custom line measurement admission

Content lines and independently shaped list markers pass through the same metric validation before a scene can publish.

DiagramMeasuredTextLine fieldRequirement
start, endOrdered UTF-16 offsets within the measured text; lines cannot overlap earlier ranges.
leftFinite offset within the line layout coordinate system.
width, intrinsicWidthFinite and nonnegative. A list marker requires positive intrinsic width.
heightFinite and strictly positive.
baselineFinite and nonnegative, relative to the line top.
fontSizeFinite and strictly positive.
geometryOptional retained interaction artifact. Its height must be finite, positive, and agree with height within 0.01 units.

A marker must resolve exactly one line covering its complete marker string. Malformed measurements throw before publication: the previous document, scene, selection, history and observers remain unchanged. A host can correct its shaping provider and retry; it must also follow the font/layout revision contract when changing already-cached measurements.