Skip to main content

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

Mirrored from the canonical text at commit 4ab66d24 ().

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).

Sections 4 to 6 and Annexes A and B are headings in this revision. Their text lands in later revisions of this Draft: outline sets and the default stroke set (§4, Annexes A and B), text and accessibility (§5), and packages and floors (§6). Where §1–§3 depend on them, 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, and renders each listed size, rate and frame to the bytes §3 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.

Out of scope

1.2 Conformance targets

This extension states requirements for three things.

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

stagecovered
instructions → outlines → coverage → the overlay’s own compositing → RGBA8P at a listed size, rate and frameyes (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 interfacea requirement (§5); what assistive technology says is not covered
the hand-off of the buffer to the platform’s graphics interfacesno
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 panelno
when frame g reaches the screenno
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 ratenot rendered (size_unlisted, rate_unlisted, §2.7)

What is claimed, and what is not

claimednot claimed
the instruction bytes, by content hashthe composite with video, or with anything else below the layer
each outline set, by SHA-256 over its whole bytesthe 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

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:

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)

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)

offsizefieldrule
04magicthe ASCII bytes WOS1
44formatu32 = 1
82upem2u16: twice the parent’s units per em. Informative: the sink never reads it, and any value is accepted
102flagsu16. Bit 0: overlaps were removed (informative). Bits 1–15 are reserved
124n_glyphsu32, 1 ..= 65,536
164points_totalu32, ≤ 2^22
204contours_totalu32, ≤ 2^22
244notes_lenu32, 1 ..= 2^20
284zero

Glyph record (16 bytes)

offsizefieldrule
02gidu16: the parent font’s glyph ID. Strictly increasing across records
22n_contoursu16, ≤ n_points; at least 1 when n_points ≥ 1
42n_pointsu16, 0 ..= 16,383
62zero
88xmin, ymin, xmax, ymaxi16 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:

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)

offsizefieldrule
04magicthe ASCII bytes WVG1
44formatu32 = 1
84tbu32: ticks per second, 1 ..= 2^20 (120,000 recommended, §3.1)
124t0u32: the document’s first tick, 0 ..= 2^31 − 1
164duru32: the document’s length in ticks, 1 ..= 2^31 − t0
202design_wu16: the design canvas’s width in design pixels, 16 ..= 4096
222design_hu16: its height, 16 ..= 4096. (4·design_w·design_h ≤ 2^26 follows from the two ranges.)
241n_sizesu8, 1 ..= 8
251n_ratesu8, 1 ..= 4
261n_setsu8, 0 ..= 64
271n_tablesu8, 0 ..= 32
282n_paintsu16, 0 ..= 4096
302n_textsu16, 0 ..= 65,535
322n_tracksu16, 0 ..= 65,535
342n_pathsu16, 0 ..= 4096
364n_itemsu32, 1 ..= 65,536
404n_keysu32, ≤ 2^22
444n_glyphsu32, ≤ 2^20
484n_clustersu32, ≤ 2^20
524path_contoursu32, ≤ 2^20
564path_pointsu32, ≤ 2^22
604strings_lenu32, ≤ 2^22
644inline_lenu32, ≤ 2^25: the bytes of the inline sets
684held_lenu32, ≤ 2^26: the bytes of the held sets, as their SET records state them. inline_len + held_len ≤ 2^26
728zero

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.

recordbytesfields and rules
SIZE8w: 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.)
RATE8num: 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
SET40carriage: 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
TABLE68E[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
PAINT4r, g, b, a: u8, premultiplied and sRGB-encoded: r, g, b ≤ a
TRACK8kind: 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
KEY12t: 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
PATH16n_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_END2u16: as a WOS1 contour end, within its path
PATH_POINT5x: i16, y: i16, tag: u8: as a WOS1 point, in path units. A path’s contours meet §2.1’s contour grammar
TEXT20a 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
ITEM48§2.4
GLYPH12gid: 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
CLUSTER12glyph_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
STRINGSstrings_lenfor each TEXT in order, its language tag (lang_len bytes), then its string (str_len bytes)
INLINEinline_lenthe 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).

offsizefieldgroup begin (0)group end (1)path fill (2)run fill (3)
01kind0123
11rule: 0 non-zero, 1 even-oddthe clip’s rule; 0 with no clip, checked at ref, the later field (§2.0)0the path’s rule0 (glyphs are non-zero)
22zero
42refa clip path index (< n_paths), or 0xFFFF for no clip0a path index (< n_paths)a set index (< n_sets)
62paint0xFFFF0a paint index (< n_paints)a paint index (< n_paints)
82text0xFFFF0the path’s text alternative, a block index (< n_texts), or 0xFFFF for nonethe block this run draws part of (< n_texts; required)
102zero
122n_glyphs0001 ..= 4096
142n_clusters0001 ..= 8192
164t_ona tick0a ticka tick
204t_offt_on < t_off ≤ 2^310as a group beginas a group begin
244unit_scalethe clip path’s, 1 ..= 2^24; 0 with no clip01 ..= 2^241 ..= 2^24
2814track slots m00, m01, m10, m11, tx, ty, op: u16 each, a track index (< n_tracks) or 0xFFFF for absentm00 … m11 absent; tx, ty and op each a track or absentall 0each a track or absenteach a track or absent
426zero

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

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

quantitycappurpose
payload≤ 2^26 bytes, computed before allocationa bounded parse
sets, inline and heldeach length ≤ 2^25; inline_len + held_len ≤ 2^26a bounded fetch, hash and parse
points per glyph and per path≤ 16,383the no-wrap theorem (§3.7)
glyphs per run≤ 4,096the run sum S ≤ 2^29 (§3.8)
fill instances (path items, group clips, glyph records)≤ 65,536 items and ≤ 2^20 glyph recordsload 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 sizeseach side 16 ..= 4096; 1/8 ≤ w/design_w ≤ 4the tensor cap of integer-operators §3 on [4, h, w]; the placement widths (§3.3)
group depth≤ 4buffers per frame
track valuesopacity 0 ..= 255; translation ±2^22 design subpixels; matrix entry ±2^20 (Q16)the placement widths
unit_scale1 ..= 2^24 (Q16)the placement widths
glyph positions−2^23 ..= 2^23 − 1 design subpixelsthe placement widths
tickstb ≤ 2^20; every tick < 2^31, and t_off ≤ 2^31the interpolation widths
block strings1 ..= 4096 bytes each; all strings and tags ≤ 2^22 bytesbounded exposure
digest sidecar≤ 2^17 bytes; 1 ..= 1,024 probes; ≤ 32 clips; every integer ≤ 2^31 − 1a 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.

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.

2.9 The canonical buffers

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)

Limits and binding

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

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):

A probe set is detection, not proof.

Delivery

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

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 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.

unitmeaning
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.

stageperworkwidthruns on
driveritem, framethe clock, tracks, placement, the load-time interval check and the costi64CPU, Wasm
geometrypoint, curvepoint placement; flattening (RS1)i64; flattening also i32, by an identityCPU, Wasm; a GPU later
coveragesegment, cell, pixelthe 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
compositepixelpaint × 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

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:

Properties

3.3 Placement (driver, i64)

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

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:

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)

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:

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):

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

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:

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.

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

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:

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.

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

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

A glyph run: the glyph-wise saturating union.

What the union does (informative).

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

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:

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:

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

#propertywhy
P1Orientation 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 holesthe crossings are floors of exact rationals, the same from either end; every contribution negates; the quantisers are even in v
P2Whole-pixel translation equivariance. Moving a fill by (256·kx, 256·ky) moves its coverage by (kx, ky) pixels exactlyevery rounding is a floor of a quantity whose integer part shifts by an integer
P3Order 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
P4Premultiplied closure: c ≤ a ≤ 255§3.9
P5Culling invariance. Skipping a glyph whose device box misses the canvas changes nothingeach 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
P6The output is a function of (P, S, z, r, g) aloneno clock, random source or platform input is read
P7Slicing invarianceframes are keyed by the global g, and inactive items draw nothing

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

An implementation MUST NOT:

3.12 Bounds

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

valueboundwidthstagebasis
a WOS1 or path point|p| ≤ 2^15i16input§2.1
matrix keys, translations, unit_scale, glyph positionsthe caps of §2.6i32input§2.7
linear interpolation 2·δ·r + Δ< 2^56i64driver§3.2
table easing: r·2^16; the table step; the value step< 2^47; < 2^28; < 2^40i64driver§3.2
L’s numerator; |L|< 2^58; ≤ 2^30i64; i32driver§3.3
the point sum L·p with its rounding term< 2^47i64driver§3.3
the origin’s numerator< 2^58i64driver§3.3
the interval check’s end productsas the three rows abovei64driver§3.4(a)
a fill’s cost c(f); a run’s cost; cost(z) before its check< 2^28; < 2^40; < 2^58i64driver§3.4(b)
a sidecar’s or a request’s g against its rangedecided by division, never as g·Pi64driver§2.7
a device coordinate−2^22 ..= 2^22 − 1i32—§3.4(a)
the flattening numerators, quadratic and cubic< 2^43 (2^44 with the 4^n·P0 term); < 2^57i64RS1§3.5
flattening, the 32-bit form< 2^31i32RS1§3.5
the row crossing: closed form; 32-bit form< 2^46; < 2^31i64; i32RS2§3.6
the column-crossing product< 2^31 (unsigned)u32RS3§3.6
cover; area≤ 2^8; ≤ 2^17i32RS3§3.6
the row density D_r≤ 2^22 − 256i32RS3§3.7
any partial sum into d, any partial sum of the scan, v≤ 512·D_r ≤ 2^31 − 2^17i32RS3, RS4§3.7
the run sum S≤ 2^29i32 / u32RS6§3.8
a quantiser’s product< 2^25i32RS5§3.8
compositing products; div255’s intermediate≤ 65,025; < 2^24i32RS7, RS8§3.9
the straight-alpha numerator≤ 255·255 + 127i32RS9§2.9

3.13 What is exact, and what is merely deterministic

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.

stagenamewhatwidthrefusal
RS1vec_flatten§3.5’s closed form, and its 32-bit identityi64; i32 by the identitynone
RS2vec_rows§3.6’s row split, in closed form and by its 32-bit identityi64; i32 by the identitynone
RS3vec_cellsthe cell split and accumulation (§3.6–§3.7)i32 (u32 for the column product)none
RS4vec_scanthe row scan (§3.7)i32none
RS5vec_alphathe quantisers (§3.8)i32none
RS6vec_unionS = Σ min(|v_k|, 2^17) and the run’s α (§3.8)i32 / u32none
RS7vec_compositepaint × opacity × coverage, then source-over (§3.9)i32 (every value < 2^24)none
RS8vec_groupthe group pop (§3.9)i32none
RS9vec_unpremulthe straight-alpha view (§2.9)i32none

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

This section lands in a later revision of this Draft: how a set is made from a font (authoring only), pinning and carriage, the keys of a set’s notes and the licence they carry, and the default stroke set (Annexes A and B). §2.1 defines the format a sink reads.

5. Text and accessibility

This section lands in a later revision of this Draft: what the authoring tool does to pre-shape text, the purpose vocabulary (§2.3 uses codes 0 and 13), clusters, the projection and the transcript with their exact JSON (text-equivalence, §1.3), exposure on each platform, the chain that checks that the words match the glyphs, and how captions stay under the viewer’s control.

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

Lands with §4.

Annex B. The default stroke set’s digest

Lands with §4.