open-standards/wai/extensions/graphics-vector.md
Transaction Science 4084a31bc6
All checks were successful
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 51s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m25s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m25s
standards workspaces build + test / aac-rs (pull_request) Successful in 2m28s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 56s
standards workspaces build + test / map-rs (pull_request) Successful in 54s
standards workspaces build + test / mesh-rs (pull_request) Successful in 53s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m28s
standards workspaces build + test / openpay (pull_request) Successful in 2m51s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 27s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 1m55s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 1m45s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 7m26s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 6m32s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m32s
wai-vector vector graphics / macos-arm64 (pull_request) Successful in 1m33s
lockfiles / every Cargo.lock is current (pull_request) Successful in 37s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m8s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m59s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m38s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m10s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m11s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m19s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m10s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m22s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 4m5s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m8s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 13s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 8m43s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 27s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 49s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 2m2s
WAI section references / cited sections exist (pull_request) Successful in 10s
wai-vector vector graphics / linux-x86_64 (pull_request) Successful in 1m32s
wai-vector vector graphics / wasm32-wasip2 (wasmtime) (pull_request) Successful in 39s
wai-vector vector graphics / wasm32 under Node (pull_request) Successful in 21s
wai-vector vector graphics / wasm32 in three browser engines (pull_request) Successful in 2m20s
wai-vector vector graphics / fuzz, peak allocation, comparison count (pull_request) Successful in 16s
wai-vector vector graphics / independent checker (Python) (pull_request) Successful in 2m11s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 17m4s
graphics-vector 5.4: the worked example states each block's string and writing mode
The independent checker, written from the text alone, could not rebuild the
example's input from its list of blocks: the strings appeared only in the
printed output, and no block stated its writing mode. Each bullet now does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 02:46:34 -04:00

118 KiB
Raw Permalink Blame History

WAI Extension: Vector Graphics (wai.graphics.vector)

Status: Draft. This extension defines wai.graphics.vector: titles, credits and other authored graphics carried as integer drawing instructions, which the sink rasterises into an overlay layer. This revision defines:

  • the model, the conformance criteria and what they cover (§1);
  • the payload format WVG1, the outline-set format WOS1, the digest sidecar, and the order in which a sink refuses a payload (§2);
  • the arithmetic: the clock, track evaluation, placement, the load-time interval check and cost, flattening, coverage and compositing, written as the raster stage family RS1–RS9 (§3);
  • outline sets: pinning and carriage, the notes and the licence they carry, the default stroke set and the test repertoire (§4), the stroke source format WSF1 and its integer expansion (Annex A), and the default stroke set's digest (Annex B);
  • text and accessibility: what an encoder does to pre-shape text, the purpose vocabulary, clusters, the projection and the transcript with their exact JSON, exposure to the platform's accessibility interface, the chain that ties the words to the glyphs, and captions under the viewer's control (§5).

Section 6 is a heading in this revision. Its text lands in a later revision of this Draft: packages and floors. Where §1–§5 depend on it, they say what they depend on.

What a reader can build from this text. A sink that loads a WVG1 payload and the WOS1 sets it names, refuses exactly the payloads §2.7 refuses with the code §2.7 gives, renders each listed size, rate and frame to the bytes §3 defines, and computes the projection at each frame and the transcript to the bytes §5.4 defines; and a verifier that checks rendered frames against a digest sidecar (§2.10).

The string wai.graphics.vector is not registered in this revision: SPEC §5 registers it once its conformance corpus is complete (§1.8). Reference implementation: the wai-vector crate (wai/wai-vector). Conformance: wai/vector-conformance/. This Draft precedes both.

The keywords MUST, MUST NOT, SHOULD and MAY are as defined in RFC 2119 and RFC 8174.

1. Scope and conformance

1.1 Model

A wai.graphics.vector payload is a WVG1 (§2.2–§2.4): a fixed-width file of drawing instructions over a tick clock. It draws filled paths and runs of pre-shaped glyphs under animated tracks, inside isolated groups that may carry a clip path. Glyph outlines come from outline sets in the WOS1 format (§2.1). The payload names each set by SHA-256 and exact length, and either carries it (inline) or leaves the sink to obtain it (held).

The sink rasterises the instructions with integer arithmetic alone (§3) into an overlay layer: a premultiplied RGBA buffer, RGBA8P (§2.9), at a size the payload lists, for a frame rate it lists, at one frame. The payload also carries its words: text blocks in reading order, each with a language, a direction, a writing mode and a purpose, and clusters that map each run's glyphs to the bytes of its block (§2.3). The sink never shapes, reorders or breaks text; the authoring tool does (§5).

Composition. The overlay is a separate object. A sink draws it into a surface of its own, above whatever it accompanies, and the platform composites that surface with source-over.

  • The overlay never reads a pixel of anything below it.
  • The payload defines no blend with anything below its own layer: no operation reads another plane, and there is no colour-management, scaling or display directive.
  • Where video is decoded on a protected path that the application cannot read, the overlay layer stays outside that path. The composite is inside it, and is never claimed (§1.4).

Out of scope

  • How the platform scales, blends, colour-manages or presents the overlay layer, and when a frame reaches the screen.
  • Shaping, bidirectional reordering and line breaking (§5).
  • Any outline format other than WOS1: the sink never parses a font file (§4).

1.2 Conformance targets

This extension states requirements for three things.

  • A sink: a program that loads a WVG1 and renders its frames. It conforms when it refuses every payload §2.7 refuses, reporting the first code §2.7 gives, and when, for every payload it accepts, its decode is overlay-equivalent and text-equivalent (§1.3).
  • An encoder: a program that writes a WVG1. It conforms when every payload it writes passes every check of §2.7 at every size it lists, and meets the encoder rules of §2.5 and §5.
  • A verifier: a program that checks a payload, its sets and its digest sidecar. It conforms when it reports what a conforming sink reports, and in addition the encoder rules a sink does not check (§2.5, §5).

1.3 Overlay-equivalence and text-equivalence (normative)

A sink's decode of a WVG1 payload is overlay-equivalent when, for each listed size z, listed rate r and frame g of the document that it renders, the bytes it produces are exactly R(P, S, z, r, g), the function §3.10 defines over the payload P and the outline sets S that P names. The bytes are the buffer RGBA8P (§2.9) at the size's width and height. Overlay-equivalence covers that buffer and nothing after it.

A sink is text-equivalent when the projection it computes for every listed rate r and frame g, and the transcript it computes, are byte-identical to the JSON that §5 defines over P.

Exposure is a separate requirement (§5). A presenting sink MUST expose the projection to the platform's accessibility interface. What the platform's assistive technology then says, and when, is not claimed.

Once registered (§1.8), the capability's conformance is SPEC §7's bit-exact decode-equivalence for a StandardDefined capability. For this capability, that criterion is the two equivalences above, over the overlay layer and the computed text only.

1.4 What is covered

stage covered
instructions → outlines → coverage → the overlay's own compositing → RGBA8P at a listed size, rate and frame yes (overlay-equivalence)
RGBA8S, the straight-alpha view of the same buffer (§2.9) yes, as a second named output
the projection and the transcript, as computed JSON (§5) yes (text-equivalence)
exposure of the projection to the platform's accessibility interface a requirement (§5); what assistive technology says is not covered
the hand-off of the buffer to the platform's graphics interfaces no
scaling of the layer for presentation, blending over video or anything else below it, colour management, any mapping of the layer to a high-dynamic-range display, the panel no
when frame g reaches the screen no
a frame in which the sink suppressed caption items (§5) no: it is not the published frame, and the sink's receipt says so (§2.10)
an unlisted size or rate not rendered (size_unlisted, rate_unlisted, §2.7)

What is claimed, and what is not

claimed not claimed
the instruction bytes, by content hash the composite with video, or with anything else below the layer
each outline set, by SHA-256 over its whole bytes the hand-off to the platform's graphics interfaces, which may store a premultiplied buffer lossily
the RGBA8P buffer of each rendered frame: checked at the probe frames of a digest sidecar, and at every frame in verify mode (§2.10) scaling, colour management and display mapping of the layer
the projection and the transcript (§5) when a frame reached the screen, or its timing against the video
anything about the video's frames

The wording rule. Every statement of this capability's exactness names the overlay layer. None describes the screen, the picture, or what a viewer sees.

1.5 Fabrics

This revision states the exact decode on two fabrics: native CPU and WebAssembly. Every stage of §3 has a fixed width (§3.15), so a GPU can compute the same integers; a frame a GPU renders that matches the conformance corpus is evidence of that, not a claim this revision makes. A later revision states the GPU fabric.

1.6 Outline sets named in the payload

  • No parameter set. Once registered as StandardDefined (§1.8), wai.graphics.vector is not one of the classes that decode against a parameter set (SPEC §3.1). A prior pin on an envelope that names it is governed by SPEC §3.1's rule for such a pin: an encoder MUST NOT write one, a verifier reports it (prior_not_applicable), and a sink ignores it. This extension adds no refusal for it.
  • The payload names its sets. Each SET record (§2.3) names one outline set by SHA-256 over its whole bytes, and states its exact length. Those digests govern the decode.
  • A set is content. An outline set is like a font subset embedded in a document: it is not a model, a parameter set or code. The sink parses a fixed outline format and runs nothing from it. Carrying a set inline is therefore payload data, and the rule of the SPEC preamble that a model is never transported in the envelope that decodes against it does not reach it.
  • Never substitute. A glyph ID means nothing without its exact set, so a sink MUST NOT render a run from any set other than the one its SET record names. A set it cannot obtain refuses the payload (outline_not_held, §2.7).

1.7 What the decode reads

The decode reads the payload, and the outline sets the payload names by SHA-256 inside its own bytes. It reads no clock, random source, platform setting or any other input. The output is a function of the payload's bytes and of bytes whose digests the payload fixes (§3.11, P6).

1.8 Registration (pending)

SPEC §5 registers wai.graphics.vector once the conformance corpus is complete. The registration will record:

  • class StandardDefined, fidelity Lossless, intent replicate, and role standalone;
  • a new informational media value, graphics: vector graphics and text that the sink draws from instructions. SPEC §8 lets a MINOR revision add a value of an informational field. The overlay is not video, so no companion that binds to video applies to it;
  • three statements:
    1. No post-decode directive (determinism-tiers §2, What a tier covers). The payload carries no post-decode processing directive. It has no operation that reads another plane, no blend with anything below the overlay layer, and no colour-management, scaling or display directive.
    2. What the decode reads (§1.7). The decode reads the payload and the outline sets it names by SHA-256 inside its own bytes. It decodes against no parameter set (SPEC §3.1).
    3. The intent is replicate. The instructions are the authored source, and §3's rendering is their defined reconstruction.

Until then, no envelope carries wai.graphics.vector as a registered capability, and SPEC's registry does not list it.

2. Payload formats

2.0 Common rules (both formats)

  • Integers are little-endian and fixed-width, two's complement for the i* types. There are no varints, no floating-point values, and no padding beyond the fields listed.
  • Zero fields. Every field named zero, and every bit a field reserves, MUST be 0.
  • Tables are consumed in order, and no record carries an offset. An owner states a count, and the records it owns are the next ones in their table. No offset can point anywhere it should not, and no two records share bytes.
  • The total length is exact. It is computed from the header's counts before anything is allocated, and it MUST equal the payload's length. Trailing bytes are refused.
  • Caps bound every count before anything is allocated, as integer-operators §3 does. A count past its cap is too_large.
  • Every refusal is named (§2.7). The checks run in the stated order, and the first failure is reported.
    • Each step finishes over everything it reads before the next step starts.
    • Within a step, failures are found in file order: table order, then record order, then field offset.
    • Within one field, its own range is checked first, then any rule that relates it to earlier fields of its record. So a rule between two fields of one record is checked at the later field.
    • A rule that reads another record or another table is checked only after every record has passed its own checks.
  • Points. A point is a pair of i16 coordinates in outline units: twice the parent font's units for a glyph, or the path's own units for a path. y points down.
  • Notation is §3.0's: integer-operators §2, and R_rat. 2^k is 2 to the power k.

2.1 WOS1: the outline set

The sink reads this format, never a font file.

Layout: header (32 bytes) · glyph table (16 × n_glyphs) · contour ends (2 × contours_total) · points (5 × points_total) · notes (notes_len). The exact length is 32 + 16·n_glyphs + 2·contours_total + 5·points_total + notes_len. At the caps that is 31,457,312 bytes, below 2^25.

Header (32 bytes)

off size field rule
0 4 magic the ASCII bytes WOS1
4 4 format u32 = 1
8 2 upem2 u16: twice the parent's units per em. Informative: the sink never reads it, and any value is accepted
10 2 flags u16. Bit 0: overlaps were removed (informative). Bits 1–15 are reserved
12 4 n_glyphs u32, 1 ..= 65,536
16 4 points_total u32, ≤ 2^22
20 4 contours_total u32, ≤ 2^22
24 4 notes_len u32, 1 ..= 2^20
28 4 zero

Glyph record (16 bytes)

off size field rule
0 2 gid u16: the parent font's glyph ID. Strictly increasing across records
2 2 n_contours u16, ≤ n_points; at least 1 when n_points ≥ 1
4 2 n_points u16, 0 ..= 16,383
6 2 zero
8 8 xmin, ymin, xmax, ymax i16 each: exactly the minimum and maximum, over all of the glyph's points, on-curve and control alike, of x and of y. All four are 0 for a glyph with no points

Contour ends. For each glyph in glyph-record order, n_contours values, u16: the index, within the glyph, of each contour's last point. They strictly increase, and the last equals n_points − 1. Contour i of a glyph is its points from one past the previous contour's end (0 for the first) to its own end.

Points. For each glyph in glyph-record order, n_points points of 5 bytes: x: i16, y: i16, tag: u8, where tag is 0 for an on-curve point, 1 for a quadratic control and 2 for a cubic control. The origin is the glyph's pen position on the baseline. An implied on-curve point of a quadratic outline (the midpoint of two consecutive quadratic controls) is an integer at twice font units, and is stored explicitly.

The contour grammar (normative; paths use it too, §2.3). Let a contour's points be P_0 … P_{m−1}, m ≥ 1, and let index m mean P_0 again, which closes the contour. P_0 MUST be on-curve. Walk from i = 0 while i < m. From the on-curve point P_i:

  • P_{i+1} on-curve: emit line(P_i, P_{i+1}), and continue from i + 1;
  • P_{i+1} a quadratic control: P_{i+2} MUST be on-curve. Emit quad(P_i, P_{i+1}, P_{i+2}), and continue from i + 2;
  • P_{i+1} a cubic control: P_{i+2} MUST be a cubic control and P_{i+3} on-curve. Emit cubic(P_i, P_{i+1}, P_{i+2}, P_{i+3}), and continue from i + 3.

Anything else is malformed. The walk never needs an index past m. A contour of one point emits nothing.

Orientation. Reversing every contour of a fill leaves its coverage unchanged (§3.11, P1). Reversing some contours changes which regions are holes, as under any non-zero rule, so a set orients its outer contours one way and its holes the other way (§4).

Notes. UTF-8 text inside the set's digest: key=value lines, a blank line, then the parent font's full licence text. §4 defines the keys. A sink checks only that the notes are valid UTF-8.

Load checks, in order

  1. Fewer than the header's 32 bytes; the magic, format, the zero field and the reserved bits of flags: outline_malformed.
  2. The header's counts n_glyphs, points_total, contours_total and notes_len, one at a time in field order, each against its whole range before the next: a count above its cap is too_large, and n_glyphs = 0 or notes_len = 0 is outline_malformed. So n_glyphs = 0 with points_total past its cap is outline_malformed.
  3. The exact length differs from the number of bytes in the set: outline_malformed.
  4. Each glyph record, in order: n_points ≤ 16,383 (points_limit); then n_contours ≤ n_points, n_contours ≥ 1 when n_points ≥ 1, and the zero field (outline_malformed).
  5. The gids strictly increase.
  6. The glyph records' n_points sum to points_total, and their n_contours to contours_total.
  7. Each glyph's contour ends strictly increase, and the last equals n_points − 1.
  8. Every tag is 0, 1 or 2.
  9. Every contour meets the contour grammar.
  10. Each glyph's box is exact.
  11. The notes are valid UTF-8 (RFC 3629).

Steps 5–11 report outline_malformed. A sink makes no other check of a set. Whether the notes hold what §4 requires is the authoring tool's obligation, which a verifier checks.

2.2 WVG1: header (80 bytes)

off size field rule
0 4 magic the ASCII bytes WVG1
4 4 format u32 = 1
8 4 tb u32: ticks per second, 1 ..= 2^20 (120,000 recommended, §3.1)
12 4 t0 u32: the document's first tick, 0 ..= 2^31 − 1
16 4 dur u32: the document's length in ticks, 1 ..= 2^31 − t0
20 2 design_w u16: the design canvas's width in design pixels, 16 ..= 4096
22 2 design_h u16: its height, 16 ..= 4096. (4·design_w·design_h ≤ 2^26 follows from the two ranges.)
24 1 n_sizes u8, 1 ..= 8
25 1 n_rates u8, 1 ..= 4
26 1 n_sets u8, 0 ..= 64
27 1 n_tables u8, 0 ..= 32
28 2 n_paints u16, 0 ..= 4096
30 2 n_texts u16, 0 ..= 65,535
32 2 n_tracks u16, 0 ..= 65,535
34 2 n_paths u16, 0 ..= 4096
36 4 n_items u32, 1 ..= 65,536
40 4 n_keys u32, ≤ 2^22
44 4 n_glyphs u32, ≤ 2^20
48 4 n_clusters u32, ≤ 2^20
52 4 path_contours u32, ≤ 2^20
56 4 path_points u32, ≤ 2^22
60 4 strings_len u32, ≤ 2^22
64 4 inline_len u32, ≤ 2^25: the bytes of the inline sets
68 4 held_len u32, ≤ 2^26: the bytes of the held sets, as their SET records state them. inline_len + held_len ≤ 2^26
72 8 zero

The exact length is

80 + 8·n_sizes + 8·n_rates + 40·n_sets + 68·n_tables + 4·n_paints + 8·n_tracks + 12·n_keys + 16·n_paths + 2·path_contours + 5·path_points + 20·n_texts + 48·n_items + 12·n_glyphs + 12·n_clusters + strings_len + inline_len

and it MUST be at most 2^26 (too_large). So a sink reads at most 2^26 bytes of payload, and obtains, hashes and parses at most 2^26 bytes of sets for one payload.

2.3 WVG1: tables, in file order

SIZES · RATES · SETS · TABLES · PAINTS · TRACKS · KEYS · PATHS · PATH_ENDS · PATH_POINTS · TEXTS · ITEMS · GLYPHS · CLUSTERS · STRINGS · INLINE

Each table holds the number of records its header count states: n_sizes SIZE records, n_rates RATE records, and so on; path_contours PATH_END records, path_points PATH_POINT records; strings_len bytes of STRINGS and inline_len bytes of INLINE. The fields of each record are listed in offset order.

record bytes fields and rules
SIZE 8 w: u16, h: u16: each 16 ..= 4096; 8·w ≥ design_w and w ≤ 4·design_w; w·design_h = h·design_w (the same aspect, exactly). cost: u32: the exact per-frame cost at this size (§3.4(b)). Strictly ascending by w, so a width names one entry. (4·w·h ≤ 2^26 follows from the side ranges.)
RATE 8 num: u32, den: u32: each 1 ..= 2^20; gcd(num, den) = 1; num ≤ 240·den; den·tb divisible by num, so P = den·tb/num ticks per frame is an integer. Strictly ascending by num/den, compared as num_a·den_b < num_b·den_a
SET 40 carriage: u8: 0 held, 1 inline. zero: [u8; 3]. length: u32: the exact byte length of the WOS1, 32 ..= 2^25, for both carriages. sha256: [u8; 32]: the SHA-256 (FIPS 180-4) of the whole WOS1. The lengths of the inline sets sum to inline_len, and those of the held sets to held_len
TABLE 68 E[0..17]: u32, an easing table in Q16: E[0] = 0, E[16] = 65536, and every E[i] ≤ 65536. A table need not be monotone
PAINT 4 r, g, b, a: u8, premultiplied and sRGB-encoded: r, g, b ≤ a
TRACK 8 kind: u8: 1 opacity, 2 translation, 3 matrix entry. zero: u8. zero: u16. n_keys: u32, 1 ..= 4096. The track's keys are the next n_keys records of KEYS, and the tracks' n_keys sum to the header's n_keys
KEY 12 t: u32: a tick, < 2^31. value: i32. interp: u8: 0 hold, 1 linear, or 2 + j for easing table j, with j < n_tables. zero: [u8; 3]. Within a track, keys strictly ascend in t, and the last key's interp is 0. value by the track's kind: opacity 0 ..= 255; translation −2^22 ..= 2^22, in design subpixels; matrix entry −2^20 ..= 2^20, in Q16
PATH 16 n_contours: u16, at least 1. n_points: u16, 1 ..= 16,383; then n_contours ≤ n_points, checked at n_points, the later field (§2.0). zero: u32. xmin, ymin, xmax, ymax: i16, exact over the path's points as in WOS1. Its contour ends are the next n_contours records of PATH_ENDS, and its points the next n_points records of PATH_POINTS. The paths' n_contours sum to path_contours, and their n_points to path_points
PATH_END 2 u16: as a WOS1 contour end, within its path
PATH_POINT 5 x: i16, y: i16, tag: u8: as a WOS1 point, in path units. A path's contours meet §2.1's contour grammar
TEXT 20 a text block, in reading order. purpose: u8, 1 ..= 255 (§5; 13 is decorative). dir: u8: 0 left-to-right, 1 right-to-left. lang_len: u8, 1 ..= 63. mode: u8, the writing mode: 0 horizontal-tb, 1 vertical-rl, 2 vertical-lr. group: u16: 0 ungrouped. ruby: u16: 0 none; ruby = r ≥ 1 annotates block r − 1, so r ≤ n_texts. ruby_start: u32 and ruby_len: u32: a byte range of the base block's string; both 0 when ruby = 0, and ruby_len ≥ 1 otherwise. str_len: u32, 1 ..= 4096. The blocks' lang_len + str_len sum to strings_len
ITEM 48 §2.4
GLYPH 12 gid: u16. zero: u16. x: i32, y: i32: the glyph's pen position in design subpixels, relative to the run's origin, each in [−2^23, 2^23). The run items, in item order, take their n_glyphs records in turn, in visual order. The runs' n_glyphs sum to the header's n_glyphs
CLUSTER 12 glyph_count: u16. zero: u16. byte_first: u32. byte_len: u32. The run items, in item order, take their n_clusters records in turn, in glyph order: a run's first cluster covers its first glyph_count glyphs, the next cluster the following ones, and so on. The runs' n_clusters sum to the header's n_clusters
STRINGS strings_len for each TEXT in order, its language tag (lang_len bytes), then its string (str_len bytes)
INLINE inline_len the inline WOS1 sets, concatenated in SET order: each inline set's bytes are the next length bytes

A text block's string is its words in logical order, UTF-8. Its language tag is a BCP 47 tag; und is allowed. §5 defines how a sink exposes blocks, and the purpose vocabulary. This section uses two of its codes: 0 is refused, and 13 is decorative, text used as ornament, which is never exposed.

2.4 ITEM (48 bytes) and the scene

Items are listed in paint order: the first is drawn first, at the bottom. An item is a group begin (kind 0), a group end (1), a path fill (2) or a run fill (3).

off size field group begin (0) group end (1) path fill (2) run fill (3)
0 1 kind 0 1 2 3
1 1 rule: 0 non-zero, 1 even-odd the clip's rule; 0 with no clip, checked at ref, the later field (§2.0) 0 the path's rule 0 (glyphs are non-zero)
2 2 zero
4 2 ref a clip path index (< n_paths), or 0xFFFF for no clip 0 a path index (< n_paths) a set index (< n_sets)
6 2 paint 0xFFFF 0 a paint index (< n_paints) a paint index (< n_paints)
8 2 text 0xFFFF 0 the path's text alternative, a block index (< n_texts), or 0xFFFF for none the block this run draws part of (< n_texts; required)
10 2 zero
12 2 n_glyphs 0 0 0 1 ..= 4096
14 2 n_clusters 0 0 0 1 ..= 8192
16 4 t_on a tick 0 a tick a tick
20 4 t_off t_on < t_off ≤ 2^31 0 as a group begin as a group begin
24 4 unit_scale the clip path's, 1 ..= 2^24; 0 with no clip 0 1 ..= 2^24 1 ..= 2^24
28 14 track slots m00, m01, m10, m11, tx, ty, op: u16 each, a track index (< n_tracks) or 0xFFFF for absent m00 … m11 absent; tx, ty and op each a track or absent all 0 each a track or absent each a track or absent
42 6 zero

A field whose rule for the item's kind is a fixed value (0, or 0xFFFF) and that holds another value is out of range (field_range, §2.7). Only the fields named zero are zero fields.

Rules

  • Slot typing. m00, m01, m10 and m11 name tracks of kind 3; tx and ty tracks of kind 2; op a track of kind 1. An absent slot means the identity for the matrix (65536 on the diagonal, m00 and m11; 0 off it), 0 for a translation, and 255 for the opacity.
  • Groups balance, and nest at most 4 deep. Each group end closes the innermost group begin still open; there is no group end with no group open, and no group is open after the last item. An outermost group is at depth 1, and no group begin is at depth 5. A group is isolated: its children composite into a cleared target, which is composited into its parent at the group's end (§3.9). A group carries a translation, an opacity and an optional clip path, and no matrix.
  • Activity. An item is active at tick τ when t_on ≤ τ < t_off. An inactive group begin skips everything up to its matching end. The document's window [t0, t0 + dur) bounds frames, not item windows.
  • unit_scale is Q16 design subpixels per outline unit, or per path unit for a path or a clip.
  • A run draws its n_glyphs glyphs from the set its ref names, at its GLYPH positions, and owns its n_clusters clusters. It renders part of exactly one block, its text. A block may be drawn by several runs: its lines, its bidirectional runs, and runs in other sets.
  • A path's text names its text alternative, for example a wordmark's words. It MUST NOT name a block of purpose 13 (decorative).

2.5 Canonical rules (one encoding per scene)

A payload has one encoding for each scene. A sink checks these rules at load (§2.7: rule 1 at steps 1, 3 and 4, rule 6 at step 5(f), and rules 2–5 at step 5(g)), and a verifier also checks them by a round trip: parse, write, compare.

  1. Zero fields are 0, and the length is exact.
  2. First-reference order.
    • Walk the items in order, skipping group ends. In each item take ref (as a path for kinds 0 and 2, as a set for kind 3; skipping 0xFFFF), then paint (skipping 0xFFFF), then the seven track slots in order, skipping 0xFFFF.
    • In each pool (paths, sets, paints and tracks), the entries' first references in that walk MUST appear in the order 0, 1, 2, …, and every entry of each pool MUST be referenced.
    • Then walk the tracks in index order, and each track's keys in order. Each key with interp ≥ 2 references table interp − 2. The tables' first references MUST appear in the order 0, 1, 2, …, and every table MUST be referenced.
  3. No duplicates. No two sets have the same sha256. No two paints are equal; no two tracks are equal (the same kind and the same sequence of keys); no two paths are equal (the same PATH record, contour ends and points); and no two tables are equal.
    • A sink MUST detect duplicates by sorting or hashing each pool, never by comparing every pair: the tracks a payload can hold within every cap make about 2^31 pairs.
  4. Text blocks keep reading order, so rule 2 does not order them. Instead:
    • every block MUST be named by the text of at least one run or path, since a block nothing draws could never be exposed;
    • group numbers are assigned in order of first use: a block's nonzero group is at most one more than the largest group of any earlier block;
    • the blocks of one nonzero group are contiguous in block order, so [1, 2, 1] is refused;
    • a ruby link r names an earlier block (r − 1 is less than the block's own index), and [ruby_start, ruby_start + ruby_len) lies within that block's string, with both ends on code-point boundaries.
  5. Clusters of a run.
    • Their glyph_counts sum to the run's n_glyphs.
    • Each range [byte_first, byte_first + byte_len) lies within the string of the run's block, with both ends on code-point boundaries.
    • glyph_count = 0 only with byte_len ≥ 1: characters that no glyph draws.
    • byte_len = 0 only with glyph_count ≥ 1: an inserted glyph, such as a hyphen at a break.
  6. Tags and strings.
    • A language tag is ASCII letters, digits and hyphens, with no leading, trailing or doubled hyphen. This is a syntax check only: well-formedness under RFC 5646 and validity against its registry are encoder rules a verifier checks.
    • A string is valid UTF-8 (RFC 3629) and holds no C0 control except U+000A, no U+007F and no C1 control (U+0080–U+009F).

Encoder rules a verifier checks and a sink does not are listed with the text they govern (§5): among them one bidirectional paragraph per block, one joining sequence per run, no zero-glyph cluster for a visible character, and every byte of a block's string covered by a cluster.

2.6 Caps and bounds at a glance

quantity cap purpose
payload ≤ 2^26 bytes, computed before allocation a bounded parse
sets, inline and held each length ≤ 2^25; inline_len + held_len ≤ 2^26 a bounded fetch, hash and parse
points per glyph and per path ≤ 16,383 the no-wrap theorem (§3.7)
glyphs per run ≤ 4,096 the run sum S ≤ 2^29 (§3.8)
fill instances (path items, group clips, glyph records) ≤ 65,536 items and ≤ 2^20 glyph records load work linear in them; a reused path or glyph counts once per instance in the cost
per-frame cost at each listed size ≤ 2^32 − 1, stated in SIZE.cost (§3.4(b)) bounded work per frame
design canvas and listed sizes each side 16 ..= 4096; 1/8 ≤ w/design_w ≤ 4 the tensor cap of integer-operators §3 on [4, h, w]; the placement widths (§3.3)
group depth ≤ 4 buffers per frame
track values opacity 0 ..= 255; translation ±2^22 design subpixels; matrix entry ±2^20 (Q16) the placement widths
unit_scale 1 ..= 2^24 (Q16) the placement widths
glyph positions −2^23 ..= 2^23 − 1 design subpixels the placement widths
ticks tb ≤ 2^20; every tick < 2^31, and t_off ≤ 2^31 the interpolation widths
block strings 1 ..= 4096 bytes each; all strings and tags ≤ 2^22 bytes bounded exposure
digest sidecar ≤ 2^17 bytes; 1 ..= 1,024 probes; ≤ 32 clips; every integer ≤ 2^31 − 1 a bounded parse (§2.10)

2.7 Refusals and their order

A sink refuses a payload at load, before the first frame, and reports the first failure in this order. Each check reads only the payload, the set bytes the sink holds, and values earlier steps have checked, so two conforming sinks that hold the same bytes report the same code.

The envelope. No refusal. A prior on an envelope that names this capability is governed by SPEC §3.1 (§1.6).

Load, once per payload

  1. payload_malformed: a payload shorter than the header's 80 bytes; the magic, format, or the header's zero field.

  2. too_large, in field order: a count or length field of the header above its cap (n_sizes, n_rates, n_sets, n_tables, n_paints, n_paths, n_items, n_keys, n_glyphs, n_clusters, path_contours, path_points, strings_len, inline_len, held_len); then inline_len + held_len > 2^26; then the computed exact length above 2^26.

  3. payload_malformed: the payload's length differs from the computed exact length.

  4. The record pass. One pass, in file order: first the header's remaining fields, then every record of every table, each record's fields in offset order.

    • The header: tb, t0, dur, design_w, design_h, and n_sizes, n_rates and n_items at least 1, each against its range in §2.2 (field_range).
    • Each record: a nonzero zero field or reserved bit is payload_malformed; an enumeration or a value out of its range is field_range; a PATH whose n_points exceeds 16,383 is points_limit.
    • The ranges include every index against the header count it indexes (ref, paint, text, the track slots, interp against n_tables, ruby against n_texts), purpose = 0, a PATH_POINT tag other than 0, 1 or 2, and §2.4's fixed values for the item's kind.

    A field's rule reads only the header and earlier fields of its own record. A SIZE's cost and a KEY's value have no rule in this pass.

  5. Cross-record structure, in this order, each check over all records in file order. Every check reports payload_malformed unless noted.

    • (a) The sum rules: the SET lengths, by carriage, to inline_len and held_len; the TRACK n_keys to n_keys; the PATH n_contours to path_contours and n_points to path_points; the TEXT lang_len + str_len to strings_len; the runs' n_glyphs to n_glyphs and n_clusters to n_clusters.
    • (b) Ordering: the SIZEs strictly ascend by w, and the RATEs by num/den; within each track, keys strictly ascend in t; the last key of each track has interp 0.
    • (c) Typing, field_range: each KEY's value lies in the range its track's kind allows (§2.3), over KEYS in order; then each track slot of each ITEM names a track of the kind the slot requires (§2.4), over ITEMS in order.
    • (d) Group balance and depth (§2.4).
    • (e) Each path's contour ends (strictly increasing within the path, the last equal to n_points − 1), its contour grammar (§2.1), and its exact box.
    • (f) STRINGS: each language tag's syntax, and each string's validity (§2.5 rule 6).
    • (g) The canonical rules of §2.5: rule 2 (first reference), rule 3 (duplicates), rule 4 (text blocks: referenced, group numbering, group contiguity, ruby links), rule 5 (clusters).
    • (h) A path item's text naming a block of purpose 13 (decorative).
  6. Sets, one at a time in SET order, each through these checks in this order before the next set:

    • outline_not_held: a held set the sink cannot obtain under its resolution policy (for example, only the sets bundled with the sink, a cache, or a fetch by digest);
    • outline_mismatch: the bytes obtained are not exactly length long, or do not hash to the record's sha256. A sink reads at most length + 1 bytes of a held set, and hashes a set before it parses it, whatever its source. An inline set is its length bytes of INLINE;
    • the WOS1 checks of §2.1, in their order (outline_malformed, too_large, points_limit).

    A later revision that lets a key log revoke a held set adds two checks before these, in the order SPEC §3.1 uses for parameter sets. Their names, outline_revoked and outline_not_authorised, are reserved for it.

  7. glyph_missing: a GLYPH record's gid names no glyph record of its run's set, over runs in item order and their glyphs in order.

  8. geometry_range: the load-time interval check (§3.4(a)) fails at some listed size.

  9. The cost (§3.4(b)), for each listed size in SIZE order: too_large if the computed cost exceeds 2^32 − 1; otherwise payload_malformed if it differs from SIZE.cost.

The digest sidecar (§2.10) is checked after the payload loads. Its codes, sidecar_malformed and sidecar_mismatch, never refuse the payload: the sink ignores the sidecar and presents the overlay.

Render. These are request errors, not refusals of the payload. A conforming sink renders only listed sizes and rates, and frames in range.

  • size_unlisted: a request for a size no SIZE lists.
  • rate_unlisted: a request for a rate no RATE lists.
  • frame_range: a frame g (a non-negative integer) outside the document at that rate. A frame is in range when ⌈t0/P⌉ ≤ g ≤ ⌊(t0 + dur − 1)/P⌋. The sink decides this by those divisions, never by forming g·P for an unchecked g, because P can reach 2^40.

The one render-time outcome. layer_mismatch: a sink that holds a valid digest sidecar computed a different SHA-256 for a probe frame rendered as published (§2.10). It MUST stop presenting the overlay layer, present its floor if it holds one (packaging §7; §6) and otherwise nothing, and report the code in its receipt.

Host policy (not conformance). A sink MAY decline at load a payload it cannot afford, as resource_declined, and present its floor. It decides by comparing the SIZE.cost of the size it would render with its own budget. It MAY decline on the stated value before computing anything; a sink that presents the payload MUST have verified that value (step 9). Declining is not a refusal of the payload, and it is never part of an exact replay.

The product rule. Every refusal of a payload happens before the first frame, and a conforming payload is refused at no listed size, rate or frame. The only render-time outcome, layer_mismatch, needs a sidecar that passed its own checks.

2.8 Versioning

Under SPEC §8, once a capability string is registered its payload format is fixed.

  • format = 1 is the only value wai.graphics.vector admits, and WOS1 format = 1 is fixed with it.
  • A successor (gradients, a sink stroker, group matrices, effects, image paints, colour glyphs) is a new string, which cites this core by reference and adds operators. It never widens this one, as wai.quantum.circuit2 is a new string beside wai.quantum.circuit (SPEC §5).
  • There are no feature bits. Capability advertisement and SPEC §4's fallback work on strings, and a string whose meaning depended on bits would make "supports wai.graphics.vector" ambiguous.

2.9 The canonical buffers

  • RGBA8P is the overlay layer's buffer: row-major, top row first, 4 bytes per pixel in the order R, G, B, A, with no row padding: 4·w·h bytes. The values are premultiplied and sRGB-encoded, never linearised. Every pixel satisfies R, G, B ≤ A (§3.9). A 1920×1080 frame is 8,294,400 bytes.

  • RGBA8S is a second named output, for consumers that take straight alpha:

    • (0, 0, 0, 0) where A = 0;
    • otherwise each colour channel is R_rat(255·c, A) = ⌊(255·c + ⌊A/2⌋)/A⌋, which lies in [0, 255], and alpha is unchanged.

    It is exact as a buffer. A platform that premultiplies it again afterwards rounds in its own way, which is outside the claim (§1.4).

  • Digests in conformance material. A conformance vector that states the SHA-256 of a frame names the buffer RGBA8P with its width and height, row-major, premultiplied, in the byte order R, G, B, A, as SPEC §7, Digests, requires for an output it does not otherwise name. A sequence of frames states each frame's digest and the clip digest SPEC §7 defines.

  • reconstruction-binding §2 defines its binding over RGB8. A binding over RGBA8P is not defined in this revision.

2.10 The digest sidecar

Frame digests travel in a digest sidecar beside the payload, never inside it, so adding a size or a rate changes the sidecar and never the payload's hash.

{"clips":[[1920,1080,25,1,"<64 hex>"]],"instr":"<64 hex>","probes":[[1920,1080,25,1,0,"<64 hex>"],[1920,1080,25,1,749,"<64 hex>"]],"schema":"wai.graphics.digests/1"}

The format (normative)

  • Form. One JSON object (RFC 8259) in UTF-8, with exactly the four members below, written in ascending byte order of their keys, with no whitespace. Integers are decimal, with no sign, fraction, exponent or leading zero. A digest is 64 lowercase hexadecimal digits. Nothing precedes or follows the object.
  • schema is the string wai.graphics.digests/1.
  • instr is the SHA-256 of the WVG1 payload's bytes.
  • clips is an array of 0 ..= 32 entries [w, h, num, den, clip], at most one per listed (size, rate), in strictly ascending order of (w, h, num, den). clip is the SHA-256 of the 32-byte SHA-256 digests of every frame of the document at that size and rate, concatenated in ascending order of g (SPEC §7, Digests).
  • probes is an array of 1 ..= 1,024 entries [w, h, num, den, g, digest], in strictly ascending order of (w, h, num, den, g), so with no duplicates. digest is the SHA-256 of frame g's RGBA8P at that size and rate.

Limits and binding

  • Size. The sidecar is at most 2^17 bytes. The largest canonical sidecar, with 1,024 probes and 32 clips at extreme values, is 111,715 bytes.
  • Integers. Every integer, as written, is at most 2^31 − 1, so no reader forms a value it cannot hold.
  • Each (w, h) MUST be a listed size, and each (num, den) a listed rate.
  • Each probe's g MUST be in the document's range at its rate, decided by division as in §2.7.
  • Binding. instr MUST equal the SHA-256 of the payload the sink loaded.

Sidecar checks run after the payload loads, in this order:

  1. sidecar_malformed: over 2^17 bytes; not this schema's canonical form; a count, an integer, a size, a rate or a g out of range; entries out of order or repeated.
  2. sidecar_mismatch: instr differs from the payload's SHA-256, as for a stale or a substituted sidecar.

On either code the sink ignores the sidecar, keeps presenting the overlay, and records the code in its receipt.

Sink rules

  • A sink that holds a valid sidecar MUST hash every probe frame it renders as published, and compare the digest with the probe's. At the first mismatch it stops presenting the overlay (layer_mismatch, §2.7).

  • It MUST NOT accept a frame on a 64-bit FNV-1a, or any digest other than SHA-256, alone (SPEC §7, Digests).

  • It MAY hash every frame it renders and check each clip digest it can complete: verify mode. Only a sink in verify mode writes a reconstruct-session claim (jwp-receipts §2.4.12), because that claim commits to every presented output by SHA-256.

  • Suppressed frames. A frame in which the sink suppressed items to honour the viewer's caption settings (§5) is not the published frame. For a probe at such a frame the sink MUST do one of two things:

    • (a) render the frame as published, off-screen, for the check alone; or
    • (b) skip the probe, and record probe_skipped, with the reason, in its receipt.

    It MUST NOT compare a suppressed frame with a published digest. Both outcomes conform, so exactness never overrides the viewer's caption settings.

The authoring tool's probe set. The tool builds the probe set deterministically, over the (size, rate) pairs it is asked to cover (by default every listed pair):

  • Candidates, in priority order, for each pair:
    1. the document's first frame ⌈t0/P⌉ and last frame ⌊(t0 + dur − 1)/P⌋, when the document has a frame at that rate, that is when the first is at most the last;
    2. for each item, in item order, whose effective window (§3.4(b)), the ticks a ≤ τ < b, holds at least one frame, that is ⌈a/P⌉ ≤ ⌈b/P⌉ − 1: the first frame inside it, ⌈a/P⌉, and the last, ⌈b/P⌉ − 1;
    3. for each key of each track, the first frame at or after the key's tick t, ⌈t/P⌉, if that frame is in range.
  • The tool takes level 1, then level 2, then level 3. Within a level it takes candidates in ascending order of (w, h, num, den, g), drops any it already holds, and stops at 1,024. The sidecar then lists the probes in its canonical order.
  • A document with no frame at any pair it covers has no candidate. probes holds at least one entry, so the tool writes no sidecar for it.

A probe set is detection, not proof.

Delivery

  • In a package, the sidecar is an entry named by its content hash, like every entry (packaging; §6), so a substituted sidecar is a package-level failure.
  • Outside a package, a sink SHOULD fetch the sidecar from the same authenticated origin as the payload.
  • A substituted sidecar whose instr is correct can only make a sink stop presenting the overlay (layer_mismatch). The floor then carries the words (§6).

Why probes, not every frame. At 1920×1080 and 25 fps, hashing every frame means hashing 207,360,000 bytes per second. The claim that every frame equals the reference is carried by the conformance corpus and its CI; probes catch a non-conforming renderer or a tampered set in the field.

2.11 Text digests

  • text_hash is the SHA-256 of the TEXTS table followed by the STRINGS area, exactly as carried. The format is canonical, so text_hash commits to every block's purpose, direction, writing mode, language, group, ruby link and string, in reading order. A receipt or a clearance record can bind the words of a whole document with it.
  • A block's identity is SHA-256("wai:text-block" ‖ 0x01 ‖ purpose ‖ dir ‖ mode ‖ lang_len ‖ the tag ‖ str_len as u32 little-endian ‖ the string), where the first term is those 14 ASCII bytes, and purpose, dir, mode and lang_len are one byte each.
    • It omits the positional fields (group and ruby), so one line has the same identity in every document that carries it.
  • Slicing a document into pieces gives each piece its own payload hash and text_hash (§6).

2.12 The JSON authoring form (informative)

The reference tool converts between WVG1 and an integer JSON form with one member per field, which makes vectors reviewable. The JSON form is never a payload, and a conforming sink never reads it.

3. Arithmetic (normative)

3.0 Notation, units and stages

The notation is integer-operators §2: ⌊a/b⌋ and ⌈a/b⌉ for b > 0, a mod b, R_half_up(p, s), isqrt, and fits iN. Every value is a mathematical integer, and no formula has an implicit width. This extension adds one function:

  • R_rat(n, d) = ⌊(2n + d)/(2d)⌋ for d > 0: the rational n/d rounded half up, a tie toward +∞. Equivalently, q + (2r ≥ d ? 1 : 0) with q = ⌊n/d⌋ and r = n − q·d.

R_rat and R_half_up are non-decreasing in their first argument, and map an exact multiple to its quotient. §3.2 and §3.4 rely on both.

unit meaning
outline unit (ou) a WOS1 or path coordinate, i16; y down
design subpixel (ds) 1/256 of a design pixel on the design_w × design_h canvas; y down; the origin at the top left
device subpixel (sp) 1/256 of an output pixel at a listed size (w, h). Pixel (x, y) is the square [256x, 256x + 256) × [256y, 256y + 256)

Stages and widths. Each stage computes at a fixed width, as the stage operators of integer-operators §7.4 do.

stage per work width runs on
driver item, frame the clock, tracks, placement, the load-time interval check and the cost i64 CPU, Wasm
geometry point, curve point placement; flattening (RS1) i64; flattening also i32, by an identity CPU, Wasm; a GPU later
coverage segment, cell, pixel the row split (RS2), the cell split and accumulation (RS3), the scan (RS4), quantisation (RS5), the glyph-wise union (RS6) i32 (u32 for the column product and the run sum) CPU, Wasm, GPU
composite pixel paint × coverage × opacity and source-over (RS7), the group pop (RS8), the straight-alpha view (RS9) i32 (every value < 2^24) CPU, Wasm, GPU

3.1 The clock

  • For a listed rate (num, den), P = den·tb/num ticks per frame, an integer by §2.3.
  • Frame g sits at tick τ(g) = g·P, on the same tick timeline as t0, the item windows and the keys. The document's frames at that rate are the g with t0 ≤ g·P < t0 + dur.
  • Frame digests are keyed by (size, rate, g). Because g is global, a piece cut from a document yields the same frame digests as the whole (§3.11, P7).
  • tb = 120000 makes P an integer for 24000/1001, 24, 25, 30000/1001, 30, 48, 50, 60000/1001, 60, 100, 120000/1001 and 120 fps. tb = 90000 does not for 24000/1001, 60000/1001 and 120000/1001.
  • Informative. A sink takes g from its media clock, for example g = ⌊(t_media − offset)·num/den⌋, clamped to the document. When a frame reaches the screen is the platform's, and is not claimed.

3.2 Track evaluation (driver, i64)

A track's value at tick τ, for its keys (t_i, interp_i, v_i), i = 0 … n − 1:

  • τ ≤ t_0 gives v_0, and τ ≥ t_{n−1} gives v_{n−1}.
  • Otherwise take the i with t_i ≤ τ < t_{i+1}, and let Δ = t_{i+1} − t_i, r = τ − t_i and δ = v_{i+1} − v_i. By interp_i:
    • hold (0): v_i;
    • linear (1): v_i + R_rat(δ·r, Δ);
    • table j (2 + j), with E = E_j: u = ⌊r·2^16/Δ⌋, which lies in [0, 65535]; k = ⌊u/4096⌋; φ = u mod 4096; e = E[k] + ⌊(E[k+1] − E[k])·φ/4096⌋, which lies in [0, 65536]; the value is v_i + R_half_up(δ·e, 16).

Properties

  • Convexity. Every value lies between v_i and v_{i+1}, and each key's value is reached exactly, because R_rat and R_half_up are non-decreasing and exact on multiples, and e ∈ [0, 65536]. So an opacity track stays in 0 ..= 255 and no clamp is needed, and the load-time interval check (§3.4(a)) can use the keys' range.
  • Integer speeds stay on the grid. With keys on frame ticks and δ = 256·k·N over N frames, frame m of the move lands exactly on v_i + 256·k·m.
  • Widths. Linear: 2·δ·r + Δ < 2^56. Table: r·2^16 < 2^47, the table step < 2^28, the value step < 2^40. All are i64, on the driver.

3.3 Placement (driver, i64)

At a listed size (w, h), with dw = design_w, for a fill item at tick τ:

  • M = (m00, m01, m10, m11), in Q16, from its matrix slots (§3.2; the identity where absent);
  • O = T_item + Σ T_g, in ds: the item's translation (tx, ty) plus the translations of every group that encloses it, an exact integer sum;
  • s = unit_scale.

The composed linear map, one per item per frame, in Q16 device subpixels per outline unit:

L_ij = R_rat(M_ij · s · w, dw · 2^16) for each of the four entries.

The origin of glyph record k, at its run-relative position (g_x, g_y) in ds:

  • t_k.x = R_rat((O_x·2^16 + m00·g_x + m01·g_y)·w, dw·2^16);
  • t_k.y = R_rat((O_y·2^16 + m10·g_x + m11·g_y)·w, dw·2^16).

A path is one glyph record at (0, 0), so its origin is R_rat(O·w, dw) on each axis.

A point p = (p_x, p_y), in outline units, of glyph record k lands at the device point

d = t_k + (R_half_up(L00·p_x + L01·p_y, 16), R_half_up(L10·p_x + L11·p_y, 16)).

A group's clip path is placed in the same way, with M the identity, O the sum of the translations of the group and of every group that encloses it, and s the group's unit_scale.

Why this form (informative)

  • Every glyph of a run shares one L, so equal glyphs at equal subpixel phase (t_k mod 256 on each axis) have identical device outlines up to a whole-pixel shift. A sink may therefore cache a glyph's coverage under the key (set digest, gid, L, t_k mod 256), and the cache cannot change a byte (§3.11, P2).
  • A run's positions and outlines transform rigidly together under M, each origin rounded once.
  • At the design size with M the identity, L = s and t = O: the design size is the identity map.

Widths at the caps of §2.6: L's numerator < 2^58; |L| ≤ 2^30, so L fits i32; the point sum, with its rounding term, < 2^47; the origin's numerator < 2^58. All are i64, on the driver.

3.4 The load-time interval check and the cost

(a) The interval check

At load, for every listed size, and for every path item, every glyph record of every run and every group with a clip, whether or not it is ever active, a sink computes the intervals below. It refuses the payload (geometry_range, §2.7 step 8) unless every end lies in [−2^22, 2^22 − 1].

  1. Track intervals. For each slot, [min, max] over its track's key values. An absent slot is its default, as a one-point interval.
  2. Origin. O's interval is the componentwise sum of the translation intervals of the item and of every group that encloses it (for a clip: of the group and of every group that encloses it).
  3. L. L_ij ∈ [R_rat(lo·s·w, dw·2^16), R_rat(hi·s·w, dw·2^16)], from M_ij ∈ [lo, hi]. For a clip, M is the identity.
  4. Per glyph record k of a run, or the path's or clip's single record at (0, 0), on each axis, with that axis's two matrix entries m_1, m_2 (m00, m01 for x; m10, m11 for y) and its two entries L_1, L_2 of L:
    • the origin numerator's interval is [O]·2^16 + [m_1]·g_x + [m_2]·g_y, with [O] the interval of O on that axis, where an interval times a scalar is the interval between the two end products, and a sum of intervals is the interval of the sums of the ends;
    • the origin's interval is R_rat(end·w, dw·2^16) at each end;
    • the point term's interval is the corner hull of [L_1] × [xmin, xmax] plus the corner hull of [L_2] × [ymin, ymax], then R_half_up(·, 16) at each end. The box is the glyph's exact box in its set (§2.1), or the path's (§2.3). The corner hull of two intervals is the interval from the least to the greatest of the four products of their ends;
    • the device interval on that axis is the origin's interval plus the point term's.
  5. Every end of every device interval MUST lie in [−2^22, 2^22 − 1].

Soundness. Five facts make the check sound:

  • (a) every evaluated track value lies in its key interval (§3.2);
  • (b) R_rat and R_half_up are non-decreasing;
  • (c) a product of two values lies in the corner hull of their intervals, and a sum in the sum of their intervals;
  • (d) every point of a glyph or path, on-curve or control, lies in its exact box;
  • (e) every flattened vertex is the floor of a convex combination of device control points, so it stays within their range on each axis (§3.5).

So every device coordinate of every frame lies in the checked range, and after the check every segment has |Δx|, |Δy| < 2^23.

Determinism. The rule is this interval computation, not the true range. It may refuse a document whose true range fits, and every sink refuses the same documents.

Authoring consequence (informative). At any listed size the range is ±16,384 output pixels. A long credit roll is therefore authored as blocks, each with its own short tracks and window, its keys covering only its time on and near the canvas; never as one long strip.

(b) The cost

The caps bound every allocation, but not the work of one frame: items may reuse one path, and every glyph record is its own fill. So each listed size states its per-frame cost, a function of the caps and of the intervals of (a) alone, computed once at load with no per-frame evaluation. For each listed size z = (w, h):

  1. The box of a fill instance. A fill instance is a path item, a group's clip, or one glyph record of a run. From its device interval [x_lo, x_hi] × [y_lo, y_hi] (step 4 of (a)):

    • c_lo = max(0, ⌊x_lo/256⌋), c_hi = min(w − 1, ⌊x_hi/256⌋), and bw = max(0, c_hi − c_lo + 1);
    • r_lo = max(0, ⌊y_lo/256⌋), r_hi = min(h − 1, ⌊y_hi/256⌋), and bh = max(0, r_hi − r_lo + 1).

    The box is empty when bw = 0 or bh = 0.

  2. A fill's cost is c(f) = p·(2^10 + bw + bh) + bw·bh, where p is the stored point count of its outline: the WOS1 glyph's n_points, or the PATH's. A glyph with no points costs bw·bh.

  3. An item's cost:

    • a path item: c(f);
    • a run item: the sum of c(f_k) over its glyph records, plus RW·RH, where RW = max c_hi − min c_lo + 1 and RH = max r_hi − min r_lo + 1 are taken over the glyph records whose box is not empty; RW·RH is 0 when every box is empty;
    • a group begin: w·h, plus c(f) of its clip if it has one;
    • a group end: 0.
  4. Effective windows. An item's effective window is its [t_on, t_off) intersected with the window of every group that encloses it and with [t0, t0 + dur). It may be empty.

  5. The cost of a size. cost(z) = w·h + max over every integer tick τ of Σ cost(item), the sum taken over the items whose effective window contains τ (0 when none does).

    • The maximum is over every tick, not only frame ticks, so the cost is the same at every rate.
    • It is computed by sorting the windows' edges. At equal ticks, ends come before starts, because windows are half-open.
    • Every quantity fits i64: a fill's cost is below 2^28, a run's below 2^40, and a sum over at most 2^20 + 2^17 instances below 2^58.
  6. The refusal. cost(z) MUST be at most 2^32 − 1 (too_large), and MUST equal SIZE.cost for that size (payload_malformed). These are §2.7 step 9.

The cost counts every instance's own points, so a path that many items reuse counts once per use.

What the cost bounds (informative). Suppose an implementation uses two freedoms §3.11 grants: it skips rows outside the canvas, and it folds the columns left of the canvas into one contribution per row piece. Then the procedure's per-frame operation count for a fill (its segments, row pieces and cell sub-pieces, and the pixels it scans, quantises and composites) is at most 3·c(f):

  • Segments σ ≤ 343·p. Lines number at most p. A quadratic uses 2 stored points and flattens to at most 2^9 segments; a cubic uses 3 and flattens to at most 2^10 (§3.5).
  • Row pieces number at most σ + p·(bh + 1). A placed, flattened outline has at most p y-monotone pieces (§3.7), so it crosses each of the bh + 1 row boundaries at most p times.
  • Sub-pieces number at most 2·(row pieces) + p·(bw + 1), by the same argument on x.
  • Pixels: bw·bh.

Summed, that is at most p·(1376 + 3·bh + bw) + bw·bh ≤ 3·c(f). An implementation that does not clip may do more work than the cost bounds. A GPU implementation lays out at most cost(z) records per frame, so u32 scratch offsets suffice.

Host policy. The ceiling makes every conforming document's work finite and the same at every sink. It does not say that a device can render the document at speed. A sink compares SIZE.cost with its own budget, set from its own measurements, and may decline (resource_declined, §2.7). The authoring tool reports cost(z) for every listed size.

3.5 Flattening (RS1)

Every curve of every contour is flattened into a polyline. All points here are device points (§3.3).

Level

  • A line is one segment.
  • A quadratic (P0, P1, P2): M = max over the two axes of |P0 − 2·P1 + P2|, and n is the least integer n ≥ 0 with M ≤ 64·4^n.
  • A cubic (P0, P1, P2, P3): M = max over the two axes and over i ∈ {0, 1} of |P_i − 2·P_{i+1} + P_{i+2}|, and n is the least integer n ≥ 0 with 3·M ≤ 64·4^n.
  • This bounds the chord deviation by 16 subpixels per axis, by the uniform-subdivision bound d(d − 1)/8 · M/4^n for a curve of degree d.
  • Inside the device range, n ≤ 9 for a quadratic and n ≤ 10 for a cubic. Flattening never refuses.

Vertices (the closed form). The curve becomes the polyline P0, V_1, …, V_{2^n}, with, for k = 1 … 2^n and on each axis:

  • quadratic, with B = P1 − P0 and A = P0 − 2·P1 + P2: V_k = P0 + ⌊(2^(n+1)·k·B + k²·A)/4^n⌋. The numerator is below 2^43;
  • cubic, with B = P1 − P0, C = P0 − 2·P1 + P2 and D = P3 − 3·P2 + 3·P1 − P0: V_k = P0 + ⌊(3·4^n·k·B + 3·2^n·k²·C + k³·D)/8^n⌋. The numerator is below 2^57.

V_{2^n} equals the end point exactly, so every contour closes. A contour's polyline is its first point, followed, for each line, quadratic or cubic the contour grammar emits (§2.1), in order, by the line's end point or the curve's vertices V_1 … V_{2^n}. It ends at its first point, and its segments join consecutive points.

The 32-bit form (an identity). A fabric without i64 MAY compute the same vertices by a quotient–remainder forward difference. Each numerator and each of its forward differences in k is held as a pair (q, r) with 0 ≤ r < 2^b, where b = 2n for a quadratic and 3n for a cubic; the initial pairs come from shifts and masks, and each step adds pairs and carries once. Every value fits i32, and the vertices equal the closed form.

3.6 Row split and cell split (RS2, RS3)

Rows (RS2). Each segment (x0, y0) → (x1, y1) of a polyline is split at the pixel-row boundaries y = 256·r.

  • A segment with y0 = y1 is skipped.
  • Otherwise let (xt, yt) be the endpoint with the smaller y and (xb, yb) the other, with dx = xb − xt and dy = yb − yt > 0. The x at a row boundary y is X(y) = xt + ⌊(y − yt)·dx/dy⌋: the floor of the exact rational, so the same from either endpoint and independent of the segment's direction. X(yt) = xt and X(yb) = xb.
  • For each pixel row r from ⌊yt/256⌋ to ⌊(yb − 1)/256⌋, the row piece runs from y_top = max(yt, 256·r) to y_bot = min(yb, 256·r + 256), with x from X. It keeps the segment's direction: from (X(y_top), y_top) to (X(y_bot), y_bot) when y0 < y1, and the other way when y0 > y1. Its row-local y is y − 256·r, so both ends' y lie in [0, 256] and differ.
  • The 32-bit form (an identity). The first boundary after yt is at most 256 from it, so ⌊(y_b1 − yt)·dx/dy⌋ forms a product below 2^31. Each later boundary adds ⌊256·dx/dy⌋ and carries a remainder in [0, dy). Every value fits i32.

Cells (RS3). A row piece from (xa, ya) to (xb, yb), in row-local y with ya ≠ yb, is split at the column boundaries.

  • If xa = xb, it is one sub-piece.

  • Otherwise it is split at every column boundary xc = 256·c strictly between xa and xb, taken in order from xa toward xb. The crossing's y is y(xc) = ya + ⌊(xc − xa)·(yb − ya)/(xb − xa)⌋, the fraction's signs normalised so that its denominator is positive. That is the floor of the exact y at xc on the line through the row piece's two ends, so it is the same from either end.

    • The 32-bit form (an identity). Form t = |xc − xa|·|yb − ya| and u = |xb − xa| in unsigned arithmetic, and take ya + ⌊t/u⌋ when yb > ya, or ya − ⌈t/u⌉ when yb < ya. The product is below 2^23·2^8 = 2^31.
  • The sub-pieces join (xa, ya), the crossings in order, and (xb, yb). A sub-piece from (x_i, y_i) to (x_{i+1}, y_{i+1}) lies in cell c = ⌊min(x_i, x_{i+1})/256⌋, with:

    • fa = x_i − 256·c and fb = x_{i+1} − 256·c, both in [0, 256];
    • cover = y_{i+1} − y_i, with |cover| ≤ 256;
    • area = cover·(fa + fb), with |area| ≤ 2^17.

    A sub-piece with cover = 0 contributes nothing.

3.7 Accumulation and scan (RS3, RS4), and why nothing wraps

One fill is one glyph record of a run (§3.8), one path item, or one group's clip.

The delta buffer. For one fill at a size (w, h), d[r][c] holds i32 values, zero to start, for 0 ≤ r < h and 0 ≤ c < w. For each sub-piece of the fill in a row r with 0 ≤ r < h:

  • c < 0: d[r][0] += 512·cover (the left fold);
  • 0 ≤ c < w: d[r][c] += 512·cover − area, and, if c + 1 < w, d[r][c + 1] += area;
  • c ≥ w: nothing.

Sub-pieces in rows outside [0, h) contribute nothing.

The scan (RS4). v[r][c] = Σ_{j ≤ c} d[r][j]. A full pixel is 512·256 = 2^17.

Density. For a fill and a row r, D_r = Σ over the fill's segments of |[yt, yb) ∩ [256·r, 256·r + 256)|: the y-length of each segment inside the row.

Theorem (no wrap, in any order). If D_r ≤ 2^22 − 1, then three kinds of value have magnitude at most 512·D_r ≤ 2^31 − 1: every partial sum of the contributions to one d[r][c], in any subset and any order; every final d[r][c]; and every sum of a contiguous range d[r][a..b], and so every prefix v and every partial sum that a tree or a sequential scan forms.

  • Sketch. The sub-pieces of one segment in one row share a sign, and their |cover| sums to δ_s ≤ 256. Each of the segment's contributions to one cell is bounded by 512·|cover| of its sub-piece, so they sum to at most 512·δ_s. The segment's share of v[r][c] lies in sgn·[0, 512·δ_s], so any contiguous range of its contributions is bounded by 512·δ_s. Summing over segments gives 512·D_r. Clipping on the right and the left fold change neither bound. ∎

The point cap makes the condition hold for every fill.

  1. A y-monotone polyline adds at most 256 to D_r in any row.
  2. The flattened polyline of a curve has no more changes of y-direction than its control polygon: sampling a monotone stretch at k/2^n keeps it monotone, and the floor is non-decreasing. Placement cannot add a change either, because its rounding is monotone: a strict rise can become flat, never a fall.
  3. A closed control polygon of p points has at most p changes of y-direction, whatever affine map placed it.
  4. So D_r ≤ 256·p. With p ≤ 16,383, D_r ≤ 2^22 − 256, and every partial sum stays within 512·D_r ≤ 2^31 − 2^17.

Consequences

  • No value wraps, in any order, on any fabric. The rule of integer-operators §6 that an implementation MUST NOT depend on a value wrapping is met with no exception.
  • Integer atomic addition on a GPU gives the true sum in any order.
  • Even-odd is exact: v is exact, so v mod 2^18 is.
  • No refusal and no validity rule depends on a frame's density.

3.8 Coverage: glyph runs, paths and clips (RS5, RS6)

A glyph run: the glyph-wise saturating union.

  • Each glyph record k of a run is its own fill: the contours of its glyph, placed at its origin (§3.3), filled under the non-zero rule. Its value at a canvas pixel is v_k, and c_k = min(|v_k|, 2^17).
  • v_k is defined by the procedure. v_k(x, y) is the value §3.6 and §3.7 give for the glyph's own fill on an unbounded grid: every row and every column, with no canvas clipping and no left fold.
    • Equivalently, v_k is 2^17 times the signed area, within the pixel, of the polygon whose vertices are the §3.6 crossings: the snapped polygon, not the outline.
    • For example, the triangle (0, 0)–(512, 3)–(0, 3) gives |v| = 1280 and 512 in its two pixels, where 2^17 times the exact area within each pixel is 1152 and 384. A sink that computes exact areas, in a glyph cache for example, produces other bytes, and does not conform.
  • On every canvas pixel, v_k equals the value §3.7's left fold computes over the canvas. The contributions of the columns left of the canvas sum into column 0, which is the prefix they would have formed, and the columns at or past w and the rows outside [0, h) never reach a canvas pixel. The left fold is how a sink normally computes v_k.
  • S = Σ_k c_k over the run's glyph records, at each canvas pixel. With at most 4,096 glyphs, S ≤ 2^29, which fits i32 and u32.
  • The run's coverage is α = R_half_up(255·min(S, 2^17), 17).

What the union does (informative).

  • Where glyphs abut, each covering part of a pixel, the sum gives the pixel its full coverage, and no seam appears.
  • Where glyphs overlap, the sum over-covers the partly covered edge pixels of the overlap: a darker knot. Exact union coverage needs a path boolean at the sink, which this capability does not have.
  • The union applies within one run only. Across runs, fills composite by source-over, so splitting a joining sequence across runs would bring back the seam where its glyphs abut. The encoder keeps each joining sequence in one run (§5).
  • Order and caching. Each c_k depends only on its glyph and its placement, so it can be cached, and a sum of non-negative terms is the same in any order.

A path is one fill over all its contours, under its rule:

  • non-zero: α = R_half_up(255·min(|v|, 2^17), 17);
  • even-odd: m = v mod 2^18, m′ = min(m, 2^18 − m), and α = R_half_up(255·m′, 17).

Every product is below 2^25. Both rules are even in v, so reversing every contour of a fill does not change its coverage (§3.11, P1). A full pixel gives 255, and half a pixel 128. The non-zero rule is the chain abs, clamp(0, 2^17), then rescale with multiplier 255, shift 17 and half_up (integer-operators §5.4).

A clip is a path fill under the group's rule, placed as §3.3 says. Its α is the mask m of §3.9.

3.9 Paint, opacity, compositing, groups and clips (RS7, RS8)

div255(x), for x ∈ [0, 65025], is ⌊257·(x + 128)/2^16⌋. It equals ⌊(2x + 255)/510⌋, the half-up rounding of x/255, over that whole range. It is the arithmetic of rescale with rq pre-bias 128, multiplier 257, shift 16 and floor (integer-operators §5.1, §5.4), restated here so that this family needs no graph. Its largest intermediate is below 2^24.

A fill at tick τ, with opacity o (its op track, §3.2) and premultiplied paint p, over coverage α at each pixel:

  • p′_ch = div255(p_ch·o) for ch ∈ {R, G, B, A}, once per item per frame;
  • s_ch = div255(p′_ch·α);
  • source-over into the current target T: T_ch ← s_ch + div255(T_ch·(255 − s_A)).

A group. Its begin pushes a target cleared to (0, 0, 0, 0), and its children composite into that target. At its end, with G the popped target, m the clip's coverage at each pixel (255 when the group has no clip) and o_g the group's opacity:

  • k = div255(m·o_g);
  • s_ch = div255(G_ch·k);
  • source-over into the parent, as for a fill.

The root starts at (0, 0, 0, 0) at every pixel, and is the frame's RGBA8P.

No clamp is needed. div255 is non-decreasing and div255(255·x) = x, so c ≤ a ≤ 255 holds through every product and every source-over.

The order is part of the definition. Other orders of these operations, such as applying the opacity after the coverage, give other bytes, so only this order conforms.

Blending space. sRGB-encoded 8-bit values. Blending in linear light is a successor string (§2.8).

3.10 The frame procedure

R(P, S, z, r, g), for a payload P, its sets S, a listed size z = (w, h), a listed rate r and a frame g in range (§2.7):

  1. τ = g·P_r.
  2. Clear the root to (0, 0, 0, 0).
  3. For each item in order:
    • group begin: if it is active at τ, push a cleared target; otherwise skip every item up to and including its matching end;
    • path fill or run fill: if it is active at τ, evaluate its tracks (§3.2), place it (§3.3), flatten (§3.5), split and accumulate each glyph record or the path (§3.6–§3.7), form α (§3.8), and composite (§3.9);
    • group end (of a group that was active): place the clip if there is one, form its α, pop, and composite (§3.9).
  4. Emit the root as RGBA8P (§2.9).

A group's translation and opacity, and its clip's placement, are evaluated at τ like any track.

3.11 Properties, freedoms and prohibitions

# property why
P1 Orientation independence of a whole fill. Reversing every contour of a fill leaves every α unchanged. Reversing one contour of a fill with several contours does not, in general: it changes which regions are holes the crossings are floors of exact rationals, the same from either end; every contribution negates; the quantisers are even in v
P2 Whole-pixel translation equivariance. Moving a fill by (256·kx, 256·ky) moves its coverage by (kx, ky) pixels exactly every rounding is a floor of a quantity whose integer part shifts by an integer
P3 Order independence. Accumulation, the scan and the run sum give the same integers in any order §3.7's theorem; the run's terms are non-negative
P4 Premultiplied closure: c ≤ a ≤ 255 §3.9
P5 Culling invariance. Skipping a glyph whose device box misses the canvas changes nothing each row of a closed contour wholly left of the canvas folds a zero sum into d[r][0]; everything else is outside [0, h) or at columns ≥ w
P6 The output is a function of (P, S, z, r, g) alone no clock, random source or platform input is read
P7 Slicing invariance frames are keyed by the global g, and inactive items draw nothing

An implementation MAY, as integer-operators §6 allows its operators:

  • cache any intermediate wherever the definition yields the same integers. Examples: a glyph's coverage under (set, gid, L, phase); a credit roll moving a whole number of pixels per frame, as a copy of a cached strip (P2).
    • A roll moving s design pixels per second lands on whole pixels at rate f and output width W exactly when s·W/(design_w·f) is an integer. Caching across a document's rates needs that at every listed rate and size, so where such caching matters an author writes one document per family of rates;
  • skip rows outside the canvas, and fold the columns left of the canvas into one contribution per row piece. These are the two freedoms under which §3.4(b)'s cost bounds the work;
  • partition work, reorder or regroup sums, skip zero contributions, compute in any width that holds the bounds (§3.12), cull, render later frames speculatively, and batch disjoint fills on a GPU.

An implementation MUST NOT:

  • use floating-point arithmetic for any value this section defines;
  • use an instruction whose result the instruction set leaves to the implementation; on Wasm, that means no relaxed SIMD;
  • depend on a value wrapping;
  • read any input outside (P, S, z, r, g).

3.12 Bounds

The caps of §2.6 fix every bound below; none depends on a parameter set.

value bound width stage basis
a WOS1 or path point |p| ≤ 2^15 i16 input §2.1
matrix keys, translations, unit_scale, glyph positions the caps of §2.6 i32 input §2.7
linear interpolation 2·δ·r + Δ < 2^56 i64 driver §3.2
table easing: r·2^16; the table step; the value step < 2^47; < 2^28; < 2^40 i64 driver §3.2
L's numerator; |L| < 2^58; ≤ 2^30 i64; i32 driver §3.3
the point sum L·p with its rounding term < 2^47 i64 driver §3.3
the origin's numerator < 2^58 i64 driver §3.3
the interval check's end products as the three rows above i64 driver §3.4(a)
a fill's cost c(f); a run's cost; cost(z) before its check < 2^28; < 2^40; < 2^58 i64 driver §3.4(b)
a sidecar's or a request's g against its range decided by division, never as g·P i64 driver §2.7
a device coordinate −2^22 ..= 2^22 − 1 i32 — §3.4(a)
the flattening numerators, quadratic and cubic < 2^43 (2^44 with the 4^n·P0 term); < 2^57 i64 RS1 §3.5
flattening, the 32-bit form < 2^31 i32 RS1 §3.5
the row crossing: closed form; 32-bit form < 2^46; < 2^31 i64; i32 RS2 §3.6
the column-crossing product < 2^31 (unsigned) u32 RS3 §3.6
cover; area ≤ 2^8; ≤ 2^17 i32 RS3 §3.6
the row density D_r ≤ 2^22 − 256 i32 RS3 §3.7
any partial sum into d, any partial sum of the scan, v ≤ 512·D_r ≤ 2^31 − 2^17 i32 RS3, RS4 §3.7
the run sum S ≤ 2^29 i32 / u32 RS6 §3.8
a quantiser's product < 2^25 i32 RS5 §3.8
compositing products; div255's intermediate ≤ 65,025; < 2^24 i32 RS7, RS8 §3.9
the straight-alpha numerator ≤ 255·255 + 127 i32 RS9 §2.9

3.13 What is exact, and what is merely deterministic

  • Exact: the integers of this section, and so RGBA8P; and the projection and the transcript (§5).
  • Deterministic, with a cost in quality (not a defect):
    • Curves are polylines. The flattened polygon is the shape. The chord deviation is at most 16 subpixels per axis, about 0.088 pixel in distance.
    • Over-coverage inside one fill, where two contours of one glyph overlap inside a partly covered pixel. The authoring tool removes overlaps inside a glyph where it can (§4).
    • Join knots. Glyphs of one run that overlap over-cover the partly covered edge pixels of the overlap (§3.8). Cursive joins and marks touching their bases are such overlaps.
    • The non-zero seam. Two contours of one fill that abut with opposite orientations cancel along the shared edge, so a pixel their union covers fully can read 0: two abutting rectangles reversed against each other give α = 0 at the seam. Sets orient outer contours and holes consistently (§4), and shapes are drawn as single contours.
    • The even-odd fringe. A contour duplicated under even-odd leaves the interior at 0, but its half-covered edge pixels quantise to 255: an opaque fringe one pixel wide around an invisible shape. The authoring tool removes duplicate contours, and a verifier flags a path with two equal contours.
    • Seams across runs. Fills of different runs composite by source-over (§3.8).
    • No hinting and no stem darkening are applied.
    • Blending is in sRGB-encoded space.
  • The mitigations live at authoring: overlap removal, consistent orientation, duplicate removal, one run per joining sequence, and heavier weights or optical-size instances for small text.

3.14 Strokes

There is no stroker at the sink. A stroke of an authored shape, such as a rule or a border, is expanded to a fill at authoring, and the fill is what the payload carries. The default stroke set ships expanded (§4). A normative integer stroker, needed for animated stroke parameters, is a successor string (§2.8).

3.15 The raster stage family RS1–RS9

The rasteriser's stages are a stage family of their own, defined in this section. They cite integer-operators §2 for the notation and the rounding functions, and its §6 for the implementation rules: nothing wraps, no floating point, no relaxed SIMD, and the same freedoms. RS1–RS9 are not operators of wai-ops/1, and they add nothing to it.

stage name what width refusal
RS1 vec_flatten §3.5's closed form, and its 32-bit identity i64; i32 by the identity none
RS2 vec_rows §3.6's row split, in closed form and by its 32-bit identity i64; i32 by the identity none
RS3 vec_cells the cell split and accumulation (§3.6–§3.7) i32 (u32 for the column product) none
RS4 vec_scan the row scan (§3.7) i32 none
RS5 vec_alpha the quantisers (§3.8) i32 none
RS6 vec_union S = Σ min(|v_k|, 2^17) and the run's α (§3.8) i32 / u32 none
RS7 vec_composite paint × opacity × coverage, then source-over (§3.9) i32 (every value < 2^24) none
RS8 vec_group the group pop (§3.9) i32 none
RS9 vec_unpremul the straight-alpha view (§2.9) i32 none

Every stage is total once the payload has loaded: the point cap, the range and the cost are load checks (§2.7), so no stage refuses. The driver's scalar work (the clock, tracks, placement, the interval check and the cost) is i64 on CPU and Wasm, and is not a stage.

Widths on a 32-bit fabric. RS3–RS9 fit i32, and RS1–RS2 do so through their identities. Every product in an i32 stage is below 2^31 (§3.12), so the family needs no product formed from 16-bit halves.

4. Outline sets

4.1 The sink reads WOS1 only

A sink never parses a font file and never runs a font program. It reads WOS1 (§2.1): fixed-width records and bounded counts, no recursion (composite glyphs are flattened before a set is written), no bytecode, subroutines, compression, bitmaps or variations, an exact box per glyph, and named refusals.

An authoring tool makes a set, from a font or from a stroke source (Annex A). How it does so is not sink conformance: a sink conforms by matching the set's digest, and a set is what its bytes are.

4.2 Pinning and carriage

  • Pinned means named by SHA-256 over the whole WOS1, notes included, in a SET record, which also states the set's exact length, whether the set is held or inline (§2.3).
  • Inline: the set's bytes are in INLINE. The sink hashes them and refuses the payload on a mismatch (§2.7 step 6). The payload is then self-contained.
  • Held: the sink obtains the bytes by digest under its resolution policy: for example, only the sets bundled with it, a cache, or a fetch by digest. It reads at most length + 1 bytes, refuses a length mismatch (outline_mismatch), and hashes the bytes before it parses them, whatever their source (§2.7 step 6).
  • Bundled: the default stroke set (§4.4) ships with every reference sink and is always held.
  • Never substitute (§1.6). A set the sink cannot obtain refuses the payload (outline_not_held), and the sink presents its floor.
  • Revocation of a held set is a later revision's (§2.7 step 6 reserves its names).

4.3 The notes: provenance and licence

The notes of a set made by the authoring tool are UTF-8 key=value lines, each ending in U+000A, then a blank line, then the full licence text of the set's source. A key that does not apply to a source is omitted.

key content
parent_sha256 the SHA-256 of the source: the font file, or the stroke source (Annex A), in lowercase hexadecimal
converter the tool's name and version, or for a stroke source the expansion's (Annex A)
licence the source's licence, as an SPDX identifier, for example Apache-2.0
licence_sha256 the SHA-256 of the licence text that follows the blank line
copyright the source's copyright lines, verbatim; repeatable
rfn each name the source's licence reserves, or none
rules for a set converted from a font: the canonicalisation, that is the ×2 grid, whether overlaps were removed (off, or the SHA-256 of the pinned module that removed them), and any down-shift
instance for a variable font: the axis tags and values the set was instanced at
source for a stroke source: where its drawing comes from
glyphs the source glyph IDs the set keeps, as ranges, and what they mean
metrics ascender,descender,cap_height,x_height in outline units, read by the authoring tool's checks and never by a sink
  • The licence travels inside the digest. A set served by digest from any origin carries its licence and notices in the pinned bytes, and no copy can drop them without changing the digest.
  • A set has no family-name field, so it never presents a reserved name as its own.
  • What checks what. A sink checks only that the notes are valid UTF-8 (§2.1). A verifier checks that licence_sha256 is the SHA-256 of the text after the blank line, and reports licence_mismatch when it is not.

4.4 The default stroke set

  • A stroke source, expanded once. The default stroke set is drawn in this repository as original centre lines, in the stroke source format WSF1 (Annex A). No outline is traced from any font. Annex A's integer expansion turns it, once, into an ordinary polygon-only WOS1, which is pinned and bundled. The sink has no stroker (§3.14).
  • Licence: Apache-2.0. The set's notes carry the licence text and say where the drawing comes from.
  • Coverage: printable ASCII, U+0020–U+007E. Each glyph ID is the code point the glyph draws. A set with further characters is a new set with its own digest, never a change to this one.
  • Carriage: an encoder names it with a held SET record stating Annex B's digest and length. A reference sink resolves it from the bytes bundled with it.
  • Layout is the authoring tool's. It places the glyphs from the advances in the source (Annex A); a sink reads none.
  • Where two stroke paths of one glyph overlap inside a partly covered pixel, the fill over-covers (§3.13).

4.5 The test repertoire

A second stroke source, drawn in this repository under Apache-2.0, is for the conformance corpus only. It is never bundled, and never a default for authors. It holds only what the corpus's test words need:

  • Arabic: the isolated, initial, medial and final forms of the letters of the test words, the lam-alef ligature as one glyph, and two marks;
  • Hebrew: the letters of the test words, among them one final form;
  • Devanagari: the consonants of the test words, each with its headline; one conjunct as a single glyph; the vowel sign i, which displays before the consonant it follows in logical order; and one mark above;
  • one monochrome glyph for U+1F600, for a visible character that a colour glyph would otherwise draw.

Its shaping is a small table in the corpus generator: Arabic forms chosen by Unicode joining type, the ligature, the reordered vowel sign, and authored mark positions. Each joining form's connector, and each Devanagari headline, runs to the edge of its glyph's advance, so the pen's round cap passes the edge and overlaps the neighbour, as joins in production fonts overlap; inside one run those overlaps over-cover their partly covered edge pixels (§3.8, §3.13).

With it, the corpus pins right-to-left visual order, contextual forms, a conjunct, a reordered vowel sign, marks, clusters and mixed direction on every fabric, with no third-party font. It does not test a shaper on a production font.

5. Text and accessibility

A payload carries its words beside its glyphs: text blocks in reading order, and clusters that tie each run's glyphs to the bytes of its block (§2.3). This section states:

  • what an encoder does before it writes them (§5.1–§5.3);
  • the two values a sink computes from them, the projection and the transcript, byte for byte (§5.4): this is text-equivalence (§1.3);
  • what a sink owes the platform's accessibility interface (§5.5) and the viewer's caption settings (§5.7);
  • how a third party checks that the words match the glyphs (§5.6).

5.1 Pre-shaped text

The sink never shapes, reorders or breaks text. An authoring tool does all of it before it writes the payload:

  1. Itemise the text by script, language, direction, writing mode and outline set. A fallback set is chosen here; the sink has none (§1.6).
  2. Resolve direction by the Unicode Bidirectional Algorithm (UAX #9), under the block's base direction.
  3. Break lines by the Unicode Line Breaking Algorithm (UAX #14), tailored by language.
  4. Shape each item, with the font instance its outline set was made from (§4.3).
  5. Justify and align. A glyph the layout adds, such as a hyphen at a break or an elongation, becomes an inserted cluster (§5.3).
  6. Reorder each line into visual order.
  7. Write the glyph records (gid, x, y), in design subpixels relative to the run's origin, with no snapping to pixels (§2.3). The subpixel phase is kept, so equal glyphs at different phases rasterise differently, and identically on every sink (§3.3).
  8. Write the text block and the clusters (§5.2, §5.3).

The payload fixes the result. Another shaper, or a later version of the same one, may write a different payload; a payload already written renders the same on every sink. Which shaper and which versions made a payload is recorded beside it, never in it (§5.6).

Encoder rules. An encoder MUST meet each rule below, and each rule of §5.3. A verifier checks them. A sink does not: it loads and renders a payload that breaks one, because none of them is a check of §2.7.

  • One base direction for a block. A block's dir is the direction the tool gave every paragraph of the block's string, set explicitly (UAX #9, rule HL1) and never detected from the first strong character (rule P3). So a U+000A inside a string never changes direction. A change of language, of direction or of writing mode starts a new block, in the same group.
  • One joining sequence in one run. A cursive joining sequence, and an Indic syllable, each lie within one run. Within a run, glyphs join by the glyph-wise union; across runs, fills composite by source-over, which would open a seam where the glyphs abut (§3.8). So a change of paint or of outline set never falls inside one.
  • Vertical text. The tool gives the glyphs of a vertical line vertical positions, puts text set sideways in a run of its own under a rotation matrix, and sets the block's mode: 1 for vertical-rl, 2 for vertical-lr. The sink draws every glyph where the payload puts it, whatever the mode. The mode is for the projection (§5.4), exposure (§5.5) and the text floor (§6).
  • Normalisation. A string is in Unicode Normalization Form C (UAX #15), so approved copy compares byte for byte with the string carried.
  • Language tags are well-formed under RFC 5646 and valid against its registry (§2.5 rule 6 checks their syntax only).

§5.4 adds a recommendation about windows (Where a block is).

5.2 Text blocks and the purpose vocabulary

A text block is one TEXT record and its bytes of STRINGS (§2.3). It carries:

  • the string: the words as authored, in logical order, UTF-8. It is the words themselves, never a description of them;
  • lang: a BCP 47 language tag; und is allowed;
  • dir: the base direction of every paragraph of the string (§5.1): 0 left-to-right, 1 right-to-left;
  • mode: the writing mode: 0 horizontal-tb, 1 vertical-rl, 2 vertical-lr;
  • purpose: what the words are for, from the vocabulary below;
  • group: the blocks of one lower third, or of one credit card, which are read together. 0 is no group. The blocks of a group are contiguous in block order (§2.5 rule 4);
  • ruby: optionally, the earlier block this block annotates, and the byte range of that block's string it annotates.

Reading order is block order: the order of the TEXT records, whatever the paint order of the items and wherever the glyphs sit on the canvas.

The purpose vocabulary

code purpose exposed as announced by default (within §5.5's limits)
1 title a heading politely
2 credit a list item, in its group no: the transcript carries it
3 lower_third grouped text politely
4 caption text; §5.7 applies no: the platform's caption renderer carries it
5 subtitle text; §5.7 applies no: the platform's caption renderer carries it
6 forced_narrative text; §5.7 applies politely
7 label text no
8 legal text politely
9 alert an alert assertively, within the limit of §5.5
10 logo the text alternative of a mark no
11 graphic the text alternative of a shape no
12 data text, such as a score or a reading no
13 decorative not exposed: text used as ornament never
0 none refused (field_range, §2.7)
14–255 unregistered as label no
  • An unregistered code is carried as it is. A sink exposes its block as it exposes a label, and writes the code itself in the JSON of §5.4, never 7.
  • The purpose is not an input to §3. No frame of R (§3.10) depends on it. A later revision that names an unregistered code therefore changes no frame and no projection: a sink of this revision goes on reading the code as label.
  • The last column is a sink's default, which the viewer can change, for example to have credits announced. A sink MAY announce less. The assignments are to be revised from tests with screen-reader users, and a revision of them changes no payload.

5.3 Clusters

A run's clusters follow its glyphs in order (§2.3): each cluster ties glyph_count consecutive glyphs of the run to the bytes [byte_first, byte_first + byte_len) of its block's string. A run's glyphs are in visual order, so in a right-to-left run the byte ranges descend.

case its cluster
a ligature: several code points, one glyph one cluster: one glyph, the bytes of all of them
a syllable whose vowel sign is drawn before its consonant, or split around it; a base with its marks one cluster: the syllable's glyphs, which are consecutive, and the syllable's bytes
right-to-left text clusters in visual order, byte_first descending
a glyph the layout inserts: a hyphen at a break, an elongation byte_len = 0, and byte_first the place in the string where it stands
characters that no glyph draws: a space at a line's end, a default-ignorable code point glyph_count = 0
a soft hyphen, U+00AD it stays in the string. Where a line breaks at it, the hyphen drawn is an inserted cluster
an emoji, or another character usually drawn in colour one cluster for its whole code point sequence, drawn by a monochrome outline glyph from a pinned set

Encoder rules (a verifier checks them; a sink checks only §2.5 rule 5):

  • Every byte in one cluster. In a block that at least one run draws, every byte of the string lies in the byte range of exactly one cluster, over all the clusters of all the runs that draw the block. Two code points may lie in no cluster: U+000A, which separates lines, and U+00AD, which is drawn only as an inserted hyphen. A block that only paths draw has no clusters.
  • No zero-glyph cluster for a visible character. A cluster with glyph_count = 0 covers only characters that are meant to draw nothing.
  • No colour glyphs. WOS1 has no colour layers, and layers drawn as several runs would break the rule above. An encoder draws a character that is usually drawn in colour with a monochrome outline glyph from a pinned set, or it does not write the payload. Colour glyphs are a successor string (§2.8).

What clusters are for (informative). With them a sink can find the character under a point, highlight a word as it is spoken or sung, and show progress through a line, all without shaping; and a verifier can compare a run with a fresh shaping of its words (§5.6).

5.4 The projection and the transcript

Definitions

  • An item's effective window is §3.4(b)'s: its [t_on, t_off), intersected with the window of every group that encloses it and with the document's window [t0, t0 + dur). It may be empty.

  • The items that draw a block are the run items whose text is the block's index, and the path items whose text is the block's index (§2.4).

  • A block's active set is the union of the effective windows of the items that draw it, written as a list of half-open tick intervals [on, off):

    • sorted in ascending order of on;
    • with intervals that overlap or touch merged, so [a, b) and [b, c) become [a, c), and no two intervals of the list share a tick or an end;
    • empty when the block is never active inside the document's window.

    Every interval has t0 ≤ on < off ≤ t0 + dur.

  • The projection at frame g of listed rate r, for a frame in the document's range at that rate (§2.7), is the list, in block order, of every block whose purpose is not 13 (decorative) and whose active set holds the frame's tick τ = g·P_r (§3.1): some interval has on ≤ τ < off.

  • The transcript is the list, in block order, of every block whose purpose is not 13, each with its active set. A block that is never active is in it, with an empty active set.

The projection follows windows, not pixels. An item's opacity, its position, a clip, and what other items cover do not enter it: a block is in the projection at a frame whenever the frame's tick lies in the effective window of an item that draws it, even at opacity 0 or off the canvas. An author ends a window where a fade ends.

A request for the projection at an unlisted rate is rate_unlisted, and at a frame outside the document at that rate frame_range: the request errors of §2.7, decided the same way.

The JSON (normative). A projection is one JSON array of node objects. A transcript is one JSON array of block objects. Both are in block order, and each is [] when it holds nothing.

A node object has these members, in this order, which is the ascending byte order of their keys:

key value
dir integer: the block's dir, 0 or 1
group integer: the block's group, 0 ..= 65,535
index integer: the block's index in TEXTS, counted from 0. Blocks of purpose 13 keep their indices, so the indices of a list need not be consecutive
lang string: the bytes of the block's language tag, as carried
mode integer: the block's mode, 0, 1 or 2
purpose integer: the block's purpose byte itself, 1 ..= 255 and never 13. An unregistered code is written as it is
ruby present only when the block's ruby is not 0: the object {"base":B,"len":L,"start":S}, its members in that order, where B = ruby − 1 is the index in TEXTS of the block it annotates, S = ruby_start and L = ruby_len. It is written whether or not block B is itself in the list
text string: the bytes of the block's string, as carried

A block object has the member active first, then the members of a node object. active is the block's active set: an array of two-element arrays [on,off] of integers, in ticks, in the order defined above; [] for a block that is never active.

The encoding

  • UTF-8, with no byte order mark, no whitespace anywhere, and nothing before or after the value.
  • An integer is written in decimal, with no sign and no leading zero; zero is 0. Every integer here is non-negative.
  • A string is ", then the bytes carried, then ", with exactly three replacements among those bytes: 0x22 is written \", 0x5C is written \\, and 0x0A is written \n. Nothing else is escaped: every other character is written as its UTF-8 bytes, a solidus and every character outside ASCII among them. No other control character occurs (§2.5 rule 6).
  • Members and array elements are separated by ,, and a key from its value by :.

The text record. A payload's text record is the following lines, each ended by one U+000A:

  1. the transcript;
  2. then, for each listed rate in RATE order, and for each frame g of the document at that rate in ascending order (§3.1), the object {"den":D,"g":G,"num":N,"projection":J}, its members in that order, where N and D are the rate's num and den, G is the frame, and J is the projection at that frame.

A rate at which the document has no frame adds no line. Two sinks are text-equivalent on a payload exactly when their text records are the same bytes, so conformance material states a payload's projections and transcript as its text record, or as the record's SHA-256.

A worked example. A document with tb = 120000, t0 = 0 and dur = 360000, listed at 25 fps, so P = 4800. It holds a lower third of three blocks, all in group 1:

  • block 0: purpose 3, ar, right-to-left, writing mode 0, the string باب البيت ("the door of the house"), drawn by a run active over [24000, 264000);
  • block 1: purpose 3, en, left-to-right, writing mode 0, the string Director of photography, drawn by a run active over the same window;
  • block 2: purpose 7, en, left-to-right, writing mode 0, the string DOP, a ruby annotation of the first 8 bytes of block 1's string (ruby = 2, ruby_start = 0, ruby_len = 8), drawn by a run active over [24000, 120000).

Its transcript, and the text record's line for frame 30, whose tick 144000 is past block 2's window:

[{"active":[[24000,264000]],"dir":1,"group":1,"index":0,"lang":"ar","mode":0,"purpose":3,"text":"باب البيت"},{"active":[[24000,264000]],"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Director of photography"},{"active":[[24000,120000]],"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]
{"den":1,"g":30,"num":25,"projection":[{"dir":1,"group":1,"index":0,"lang":"ar","mode":0,"purpose":3,"text":"باب البيت"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Director of photography"}]}

Text-equivalence (normative). A sink is text-equivalent on a payload when the transcript it computes, and the projection it computes for every listed rate and every frame of the document at that rate, are these JSON values byte for byte (§1.3). Suppressing caption items (§5.7) does not change either value: it changes what the sink draws and exposes, not what it computes.

Where a block is (encoder rule; a verifier warns). A block can be active while the runs that draw it are off the canvas, because a window may cover time before a line slides in.

  • A run meets the canvas at a frame when the box of at least one of its glyph records, placed at that frame at the design size (§3.3, with w = design_w, over the glyph's exact box, §2.1), overlaps the canvas: the design subpixels with 0 ≤ x < 256·design_w and 0 ≤ y < 256·design_h.
  • A verifier warns window_offcanvas for a run that is active at a frame of a listed rate at which it does not meet the canvas.
  • An encoder SHOULD cut each window to the frames at which its run meets the canvas, so a line that slides in is not announced before it appears.

5.5 Exposure

A sink that presents a WVG1 layer MUST expose the current projection to the platform's accessibility interface. It updates what it exposes when the projection changes, never on every frame. Exposure is a requirement of its own, beside text-equivalence (§1.3): what the platform's assistive technology then says, and when, is the platform's, and is not claimed.

  • What is exposed, for each node, in block order: its text; its language and its direction; the role its purpose gives it (§5.2); its group, as one group holding the group's blocks; its writing mode; and, for a ruby block, that it annotates the stated range of its base.
  • Two adjustments. The sink leaves out every block whose items it did not draw under §5.7, whose words the platform's caption renderer exposes from the timed-text component; and it keeps to the announcement limits below.
  • The transcript SHOULD also be reachable, so that a viewer can read the credits at a pace of their own.

Strings are text, never markup (normative). A string may hold <, & and any other character §2.5 rule 6 allows.

  • A sink that exposes through a document tree MUST insert a payload's strings only as text nodes, or as attribute values through the tree's attribute interface.
  • It MUST NOT build markup from them: no assignment to innerHTML or outerHTML, no insertAdjacentHTML, no document.write, and no template whose text is parsed as markup.
  • The label of the exposed region is the application's own string, never a payload's.

Announcement limits (normative maxima). A sink MAY announce less than these allow, and MUST NOT announce more.

  • A block is announced only when the interval of its active set that holds the current frame's tick is at least one second long: off − on ≥ tb. The transcript decides this, so it does not depend on playback.
  • A block is announced at most once for each interval of its active set.
  • At most one assertive announcement (an alert) is made in any 10 seconds of presentation, for each overlay. An alert that falls inside that time is announced politely.

So no payload can make assistive technology speak on every frame. The values are a first assignment, to be revised from tests with screen-reader users.

On the web (informative, except where it restates the rules above). A sink meets the requirement with a mirror in the document, beside the overlay's canvas:

  • A region (role="region") labelled by the page's own string, hidden from sight by clipping and never by display: none, which would hide it from assistive technology too.
  • Live regions: one aria-live="polite" container and one role="alert" container. When a block enters the projection, a node carrying lang, dir, the purpose, the mode and the text joins the container its purpose selects, within the limits above. It leaves when the block leaves.
  • A transcript section: a title as a heading, the credit blocks of a group as a list, a group as role="group", a ruby block as <ruby> with <rt>, and a logo as a text alternative.
  • Placement. The nodes MAY be placed over each block's glyphs, so that magnification and exploration by touch find them. The positions are not part of any claim.
  • Translation by the browser acts on the mirror, never on the layer. Nothing in the mirror changes a frame.

On other platforms (informative). One accessibility element for each block of the projection: its label the text, its language the tag, and a heading trait for a title; announcements carry the block's language, within the same limits. On a platform with no web engine this is the only route. Whether the screen readers of television web engines honour live regions and language tags inside an application is not established, and no statement about them is made here.

Checking exposure against the JSON. An accessibility tree normalises text, so a check that compares an exposed tree with a projection or a transcript compares text under this normalisation, and no other: every maximal run of U+0009, U+000A and U+0020 becomes one U+0020; leading and trailing U+0020 are removed; and U+00AD is removed. The order of the nodes, and each node's language, direction, purpose, mode, group and ruby structure, are compared as they are.

5.6 Whether the words match the glyphs

A payload could draw glyphs that say something other than its text block says, so that assistive technology reads one word while the layer shows another. A sink cannot detect that, because it does not shape. The words are tied to the layer by a chain, each link of which a third party can check:

link check by
parent font → WOS1 set derive the set again from the parent the notes name (parent_sha256), by the converter, rules and instance they record (§4.3); compare byte for byte a verifier
text block → glyph IDs shape each run again with the recorded shaper, Unicode version, script, direction, features and language, on the parent font instance. The glyph IDs and the clusters MUST equal the run's, inserted clusters aside; a difference is reported as glyph_text_mismatch a verifier
glyphs and positions → overlay layer §3, and the frame digests (§2.10) any sink
overlay layer → screen not covered (§1.4)

The authoring receipt. What the second link needs (the shaper's name and version, the Unicode version, and for each run its script, direction, features, language and byte range) travels in an authoring receipt beside the payload, which names the payload by its SHA-256. It is not in the payload, so the sink parses none of it, and the payload's hash does not change when only that record would. This revision states what the receipt records; its carriage as a receipt is a later revision's.

What the chain does not reach

  • Positions. Positions are layout, not shaping, so shaping a run again does not check them. Glyphs laid over one another to hide a word are not caught.
  • The shaper has to stay obtainable. The second link can be checked only while the recorded shaper can still be run.
  • No statement that a payload's pixels say its words is made without a verifier's report for that payload.

5.7 Captions stay under the viewer's control

Rules on access services require that the viewer control how captions are displayed, their size and font among other things (for example Directive (EU) 2019/882, Annex I, Section IV(b)(ii), and 47 CFR 79.103(c)). Glyphs already shaped and placed cannot be restyled without shaping at the sink. So:

  • Captions travel as timed text, a component of their own, which the platform renders under the viewer's settings (§6).
  • A WVG1 MAY also carry blocks of purpose caption, subtitle or forced_narrative, as the author's default rendering. The package rule: every such payload is published with a timed-text component in the same language that carries the same strings and timing. A package verifier checks it, and reports caption_without_track (§6).
  • A sink that cannot read the platform's caption preferences MUST NOT draw the items of caption or subtitle blocks, and MUST hand the timed-text component to the platform's caption renderer. A sink in a web page is in this case wherever the web platform gives it no access to those preferences.
  • A sink that can read them MUST NOT draw those items when the viewer has turned captions off, has chosen another caption language, or has set any caption styling.
  • For forced_narrative:
    • when the viewer has set caption styling, the sink MUST present the timed-text equivalent in that style, and not draw those items;
    • when the viewer has only turned captions off, the forced narrative is still drawn as authored, because it is not an access service;
    • a sink that cannot read the preferences draws it as authored, and SHOULD offer a control of its own that hands forced narratives to the platform's renderer.
  • The items of a block are the items that draw it (§5.4): its runs, and any path whose text alternative it is.
  • A frame drawn without those items is not the published frame. The sink MUST NOT claim a published digest for it. Its probes follow §2.10's rule for suppressed frames, its exposure leaves out the suppressed blocks (§5.5), and its receipt says that the viewer's caption settings applied.

Exactness never overrides the viewer's settings.

5.8 Two standing rules

  • No generative path. WVG1 has no generative operation. A sink MUST NOT put generated or recognised text in place of a block's string, and MUST NOT derive a string from pixels: it never reads text back out of a frame.
  • Never a page's own text. This capability is for titles and graphics that belong to a piece of media. A page's own text stays in the page's markup, where the user's settings reach it, and a sink on the web keeps the mirror of §5.5 beside every layer it presents.

6. Packages and floors

This section lands in a later revision of this Draft: graphics components and pieces in a package, slicing and its derivation record, and the text floor every graphics piece carries (packaging §7).

Annex A. WSF1, the stroke source format

A WSF1 is an authoring-side source: a sink never reads one. It holds, for each glyph, stroke paths: polylines of integer centre-line points in outline units, drawn with one round pen of radius ρ, with round caps and round joins. The expansion below turns it into a WOS1 (§2.1) with integer arithmetic alone. §2.0's common rules apply: little-endian fixed-width integers, no offsets, the exact length computed from the header, and caps checked before anything is allocated.

A.1 Layout

Header (32 bytes) · glyph table (8 × n_glyphs) · stroke ends (2 × strokes_total) · points (4 × points_total) · notes (notes_len). The exact length is 32 + 8·n_glyphs + 2·strokes_total + 4·points_total + notes_len.

Header (32 bytes)

off size field rule
0 4 magic the ASCII bytes WSF1
4 4 format u32 = 1
8 2 upem2 u16: twice the source's units per em, copied to the set's upem2
10 2 radius u16: the pen radius ρ in outline units, 1 ..= 4096
12 4 n_glyphs u32, 1 ..= 65,536
16 4 strokes_total u32, ≤ 2^14
20 4 points_total u32, ≤ 2^16
24 4 notes_len u32, 1 ..= 2^19
28 4 zero

Glyph record (8 bytes): gid: u16, strictly increasing across records. n_strokes: u16, ≤ 64, ≤ n_points, and at least 1 when n_points ≥ 1. n_points: u16, ≤ 256. advance: u16: the glyph's advance in outline units, which an authoring tool's layout reads and the set does not carry.

Stroke ends. For each glyph in record order, n_strokes values, u16: the index, within the glyph, of each stroke path's last point. They strictly increase, and the last equals n_points − 1.

Points. For each glyph in record order, n_points points of 4 bytes, x: i16 and y: i16: centre-line points in outline units, y down, the origin at the glyph's pen position on the baseline.

Notes. UTF-8, in §4.3's form; the expansion copies them into the set's notes.

Checks, in order. An expansion refuses a source at the first failure:

  1. fewer than 32 bytes; the magic, format or the zero field;
  2. radius, n_glyphs, strokes_total, points_total and notes_len, in field order, each against its range;
  3. the exact length;
  4. each glyph record: n_strokes and n_points against their caps, then n_strokes against n_points;
  5. the gids strictly increase;
  6. the records' n_strokes sum to strokes_total, and their n_points to points_total;
  7. each glyph's stroke ends;
  8. every point has −2^15 + ρ ≤ x, y ≤ 2^15 − 1 − ρ;
  9. within each stroke path, consecutive points differ, and no two consecutive segments point in exactly opposite directions (their cross product is 0 and their dot product negative);
  10. the notes are valid UTF-8 (RFC 3629).

Why the caps fit WOS1. A stroke path of m ≥ 2 points expands to at most 36·m − 4 vertices and one of one point to 64, so a glyph expands to at most 36·256 + 64·64 = 13,312 ≤ 16,383 points (§2.1), and a source to at most 36·2^16 + 64·2^14 < 2^22 points and 2^14 contours. Every vertex lies within ρ of its centre-line point on each axis, so by check 8 it fits i16.

A.2 The table of unit vectors

U is 64 unit vectors in Q15: U[k] = (round(2^15·cos(2πk/64)), round(2^15·sin(2πk/64))) for k = 0 … 63, each rounded once to the nearest integer. With y down, k increases from +x toward +y. These bytes are normative: for each k in order, the cos component, then the sin component, each an i32 little-endian, 512 bytes in all:

0080000000000000627f00008c0c00008a7d0000f91800007d7a000028250000
42760000fc300000e3700000573c00006e6a00001d470000f262000034510000
825a0000825a000034510000f26200001d4700006e6a0000573c0000e3700000
fc30000042760000282500007d7a0000f91800008a7d00008c0c0000627f0000
000000000080000074f3ffff627f000007e7ffff8a7d0000d8daffff7d7a0000
04cfffff42760000a9c3ffffe3700000e3b8ffff6e6a0000ccaefffff2620000
7ea5ffff825a00000e9dffff345100009295ffff1d4700001d8fffff573c0000
be89fffffc3000008385ffff282500007682fffff91800009e80ffff8c0c0000
0080ffff000000009e80ffff74f3ffff7682ffff07e7ffff8385ffffd8daffff
be89ffff04cfffff1d8fffffa9c3ffff9295ffffe3b8ffff0e9dffffccaeffff
7ea5ffff7ea5ffffccaeffff0e9dffffe3b8ffff9295ffffa9c3ffff1d8fffff
04cfffffbe89ffffd8daffff8385ffff07e7ffff7682ffff74f3ffff9e80ffff
000000000080ffff8c0c00009e80fffff91800007682ffff282500008385ffff
fc300000be89ffff573c00001d8fffff1d4700009295ffff345100000e9dffff
825a00007ea5fffff2620000ccaeffff6e6a0000e3b8ffffe3700000a9c3ffff
4276000004cfffff7d7a0000d8daffff8a7d000007e7ffff627f000074f3ffff

Their SHA-256 is 11c05a1323c8b6dde89a5627d29acecb9163b87dfd169054236fb1d1def66437. No trigonometry is evaluated anywhere else: the expansion reads only these integers.

A.3 The expansion (normative)

The notation is §3.0's (R_rat, R_half_up, isqrt), with cross(a, b) = a_x·b_y − a_y·b_x. Every value is an exact integer, and every one fits i64.

The set. One WOS1 glyph record for each source glyph, in order, with the same gid. One contour for each stroke path, in order, every point on-curve (tag 0). format 1; upem2 copied; flags 0, because overlaps are not removed; each glyph's box exact (§2.1). The notes are the line parent_sha256= followed by the source's SHA-256 in lowercase hexadecimal, the line converter=wsf1-expansion/1, each ending in U+000A, then the source's notes, byte for byte.

A segment's offset. For a segment d = (dx, dy), len = isqrt(dx² + dy²) and n(d) = (R_rat(−dy·ρ, len), R_rat(dx·ρ, len)). Check 9 makes len ≥ 1.

Arc vertices. The vertex of direction k at a point p is p + (R_half_up(ρ·U[k]_x, 15), R_half_up(ρ·U[k]_y, 15)). An arc at p, from an offset a, takes the directions u = U[k] that lie strictly between its ends in the negative sense (the sense of decreasing k):

  • a cap, from a to −a: every u with cross(a, u) < 0;
  • a join, from a to b: none unless cross(a, b) < 0; otherwise every u with cross(a, u) < 0 and cross(u, b) < 0.

The chosen directions lie in one open half-plane, and are taken in order from a: u before v when cross(u, v) < 0.

The contour of a stroke path p_0 … p_{m−1}:

  • m = 1, a dot: the vertices of every direction at p_0, for k = 0, 63, 62, …, 1.

  • m ≥ 2: let d_i = p_{i+1} − p_i and n_i = n(d_i) for i = 0 … m − 2, and at each interior point c_i = cross(d_{i−1}, d_i). The vertices are, in order:

    1. p_0 + n_0;
    2. for i = 1 … m − 2: p_i + n_{i−1}; if c_i < 0, the join from n_{i−1} to n_i at p_i; then p_i + n_i;
    3. p_{m−1} + n_{m−2}, then the cap from n_{m−2} at p_{m−1};
    4. p_{m−1} − n_{m−2};
    5. for i = m − 2 down to 1: p_i − n_i; if c_i > 0, the join from −n_i to −n_{i−1} at p_i; then p_i − n_{i−1};
    6. p_0 − n_0, then the cap from −n_0 at p_0.

    So the left offsets run forward, the end cap turns, the right offsets run back, and the start cap closes the contour. The outer side of each join takes the arc; the inner side takes its two offset points only, and the small loop they make lies inside the stroke's body, where the non-zero rule covers it.

  • Then a vertex equal to the one before it is dropped, and while the contour has more than one vertex and its last equals its first, the last is dropped.

The same source therefore gives the same set, byte for byte, on every machine.

Annex B. The default stroke set's digest

the set (WOS1) SHA-256 05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9, length 105,523 bytes
its source (WSF1) SHA-256 4eec759d394d021aec8ce99b41e4b25fecb36372c77459ef62df182b9eb8c783, length 19,181 bytes
coverage U+0020–U+007E; each glyph ID is the code point it draws
pen radius 80 outline units; upem2 2000
licence Apache-2.0, its text in the notes

Expanding the source by Annex A gives exactly the set's bytes. The conformance corpus holds both, as sources/stroke-ascii.wsf1 and sets/stroke-ascii.wos1, and every fabric leg checks the set's digest against this annex.