From 10ba6e9bcc1a60cb07b8a027dbd4647b9642a786 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:43:02 -0400 Subject: [PATCH 1/4] wai-vector: the text layer and section 5, text and accessibility (D7-7) graphics-vector.md section 5 (Draft): what a sink exposes to assistive technology and to a text consumer, and how. Each text block's active set (the effective windows of the items that draw it, merged), the projection at a frame and the transcript as exact JSON, the text record and its digest, ruby, purposes, writing modes and the cluster rule. - wai-vector: src/text.rs (no_std, no dependency), the a11y and text commands, and vectors verify recomputing every text record. - The corpus: a new a11y group, each payload with its text record (.a11y.jsonl) and its digest in the expected file; canonical and refusal vectors for TEXT. No existing expected file changes. - tests/text_layer.rs: each rule of section 5, with expected JSON written by hand from the text, each falsified once. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-vector.yml | 25 +- wai/CHANGELOG.md | 52 ++ wai/extensions/graphics-vector.md | 232 ++++++++- wai/vector-conformance/README.md | 67 ++- .../expected/a11y/alternatives.a11y.jsonl | 26 + .../expected/a11y/alternatives.json | 1 + .../expected/a11y/clipped.a11y.jsonl | 20 + .../expected/a11y/clipped.json | 1 + .../expected/a11y/decorative.a11y.jsonl | 26 + .../expected/a11y/decorative.json | 1 + .../expected/a11y/groups.a11y.jsonl | 26 + .../expected/a11y/groups.json | 1 + .../expected/a11y/intervals.a11y.jsonl | 26 + .../expected/a11y/intervals.json | 1 + .../expected/a11y/modes.a11y.jsonl | 6 + .../expected/a11y/modes.json | 1 + .../expected/a11y/nested-groups.a11y.jsonl | 51 ++ .../expected/a11y/nested-groups.json | 1 + .../expected/a11y/no-text.a11y.jsonl | 6 + .../expected/a11y/no-text.json | 1 + .../expected/a11y/purposes.a11y.jsonl | 6 + .../expected/a11y/purposes.json | 1 + .../expected/a11y/rates.a11y.jsonl | 71 +++ .../expected/a11y/rates.json | 1 + .../expected/a11y/ruby.a11y.jsonl | 26 + .../expected/a11y/ruby.json | 1 + .../expected/a11y/strings.a11y.jsonl | 6 + .../expected/a11y/strings.json | 1 + .../expected/a11y/windows.a11y.jsonl | 51 ++ .../expected/a11y/windows.json | 1 + .../expected/a11y/worked-example.a11y.jsonl | 76 +++ .../expected/a11y/worked-example.json | 1 + .../canonical/groups-with-ungrouped.json | 1 + .../canonical/ruby-ends-mid-character.json | 1 + .../expected/canonical/ruby-names-later.json | 1 + .../canonical/ruby-to-string-end.json | 1 + .../refusals/s4-path-before-text.json | 1 + .../refusals/s4-text-before-item.json | 1 + .../refusals/s4-text-ruby-len-no-ruby.json | 1 + .../refusals/s5c-typing-before-strings.json | 1 + .../refusals/s5g-block-before-sets.json | 1 + .../refusals/s5h-decorative-before-sets.json | 1 + .../vectors/a11y/alternatives.json | 86 +++ .../vectors/a11y/alternatives.wvg1 | Bin 0 -> 827 bytes .../vectors/a11y/clipped.json | 162 ++++++ .../vectors/a11y/clipped.wvg1 | Bin 0 -> 2083 bytes .../vectors/a11y/decorative.json | 69 +++ .../vectors/a11y/decorative.wvg1 | Bin 0 -> 750 bytes .../vectors/a11y/groups.json | 127 +++++ .../vectors/a11y/groups.wvg1 | Bin 0 -> 1610 bytes .../vectors/a11y/index.json | 16 + .../vectors/a11y/intervals.json | 207 ++++++++ .../vectors/a11y/intervals.wvg1 | Bin 0 -> 2778 bytes .../vectors/a11y/modes.json | 55 ++ .../vectors/a11y/modes.wvg1 | Bin 0 -> 575 bytes .../vectors/a11y/nested-groups.json | 215 ++++++++ .../vectors/a11y/nested-groups.wvg1 | Bin 0 -> 2878 bytes .../vectors/a11y/no-text.json | 34 ++ .../vectors/a11y/no-text.wvg1 | Bin 0 -> 186 bytes .../vectors/a11y/purposes.json | 271 ++++++++++ .../vectors/a11y/purposes.wvg1 | Bin 0 -> 3860 bytes .../vectors/a11y/rates.json | 56 ++ .../vectors/a11y/rates.wvg1 | Bin 0 -> 484 bytes wai/vector-conformance/vectors/a11y/ruby.json | 55 ++ wai/vector-conformance/vectors/a11y/ruby.wvg1 | Bin 0 -> 624 bytes .../vectors/a11y/strings.json | 153 ++++++ .../vectors/a11y/strings.wvg1 | Bin 0 -> 2101 bytes .../vectors/a11y/windows.json | 91 ++++ .../vectors/a11y/windows.wvg1 | Bin 0 -> 949 bytes .../vectors/a11y/worked-example.json | 108 ++++ .../vectors/a11y/worked-example.wvg1 | Bin 0 -> 1273 bytes .../canonical/groups-with-ungrouped.json | 43 ++ .../canonical/groups-with-ungrouped.wvg1 | Bin 0 -> 979 bytes .../vectors/canonical/index.json | 4 + .../canonical/ruby-ends-mid-character.json | 43 ++ .../canonical/ruby-ends-mid-character.wvg1 | Bin 0 -> 942 bytes .../vectors/canonical/ruby-names-later.json | 43 ++ .../vectors/canonical/ruby-names-later.wvg1 | Bin 0 -> 942 bytes .../vectors/canonical/ruby-to-string-end.json | 43 ++ .../vectors/canonical/ruby-to-string-end.wvg1 | Bin 0 -> 942 bytes .../vectors/refusals/index.json | 6 + .../vectors/refusals/s4-path-before-text.json | 64 +++ .../vectors/refusals/s4-path-before-text.wvg1 | Bin 0 -> 1178 bytes .../vectors/refusals/s4-text-before-item.json | 64 +++ .../vectors/refusals/s4-text-before-item.wvg1 | Bin 0 -> 1178 bytes .../refusals/s4-text-ruby-len-no-ruby.json | 64 +++ .../refusals/s4-text-ruby-len-no-ruby.wvg1 | Bin 0 -> 1178 bytes .../refusals/s5c-typing-before-strings.json | 64 +++ .../refusals/s5c-typing-before-strings.wvg1 | Bin 0 -> 1179 bytes .../refusals/s5g-block-before-sets.json | 65 +++ .../refusals/s5g-block-before-sets.wvg1 | Bin 0 -> 647 bytes .../refusals/s5h-decorative-before-sets.json | 65 +++ .../refusals/s5h-decorative-before-sets.wvg1 | Bin 0 -> 647 bytes wai/wai-vector/Cargo.toml | 4 +- wai/wai-vector/src/bin/wai-vector.rs | 96 +++- wai/wai-vector/src/lib.rs | 3 + wai/wai-vector/src/text.rs | 431 +++++++++++++++ wai/wai-vector/src/vectors.rs | 59 ++- wai/wai-vector/src/vectors/a11y.rs | 347 +++++++++++++ wai/wai-vector/src/vectors/canonical.rs | 14 + wai/wai-vector/src/vectors/refusals.rs | 24 + wai/wai-vector/src/vectors/text.rs | 20 +- wai/wai-vector/tests/corpus.rs | 19 +- wai/wai-vector/tests/fuzz_smoke.rs | 96 +++- wai/wai-vector/tests/text_layer.rs | 489 ++++++++++++++++++ 105 files changed, 4624 insertions(+), 74 deletions(-) create mode 100644 wai/vector-conformance/expected/a11y/alternatives.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/alternatives.json create mode 100644 wai/vector-conformance/expected/a11y/clipped.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/clipped.json create mode 100644 wai/vector-conformance/expected/a11y/decorative.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/decorative.json create mode 100644 wai/vector-conformance/expected/a11y/groups.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/groups.json create mode 100644 wai/vector-conformance/expected/a11y/intervals.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/intervals.json create mode 100644 wai/vector-conformance/expected/a11y/modes.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/modes.json create mode 100644 wai/vector-conformance/expected/a11y/nested-groups.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/nested-groups.json create mode 100644 wai/vector-conformance/expected/a11y/no-text.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/no-text.json create mode 100644 wai/vector-conformance/expected/a11y/purposes.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/purposes.json create mode 100644 wai/vector-conformance/expected/a11y/rates.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/rates.json create mode 100644 wai/vector-conformance/expected/a11y/ruby.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/ruby.json create mode 100644 wai/vector-conformance/expected/a11y/strings.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/strings.json create mode 100644 wai/vector-conformance/expected/a11y/windows.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/windows.json create mode 100644 wai/vector-conformance/expected/a11y/worked-example.a11y.jsonl create mode 100644 wai/vector-conformance/expected/a11y/worked-example.json create mode 100644 wai/vector-conformance/expected/canonical/groups-with-ungrouped.json create mode 100644 wai/vector-conformance/expected/canonical/ruby-ends-mid-character.json create mode 100644 wai/vector-conformance/expected/canonical/ruby-names-later.json create mode 100644 wai/vector-conformance/expected/canonical/ruby-to-string-end.json create mode 100644 wai/vector-conformance/expected/refusals/s4-path-before-text.json create mode 100644 wai/vector-conformance/expected/refusals/s4-text-before-item.json create mode 100644 wai/vector-conformance/expected/refusals/s4-text-ruby-len-no-ruby.json create mode 100644 wai/vector-conformance/expected/refusals/s5c-typing-before-strings.json create mode 100644 wai/vector-conformance/expected/refusals/s5g-block-before-sets.json create mode 100644 wai/vector-conformance/expected/refusals/s5h-decorative-before-sets.json create mode 100644 wai/vector-conformance/vectors/a11y/alternatives.json create mode 100644 wai/vector-conformance/vectors/a11y/alternatives.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/clipped.json create mode 100644 wai/vector-conformance/vectors/a11y/clipped.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/decorative.json create mode 100644 wai/vector-conformance/vectors/a11y/decorative.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/groups.json create mode 100644 wai/vector-conformance/vectors/a11y/groups.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/index.json create mode 100644 wai/vector-conformance/vectors/a11y/intervals.json create mode 100644 wai/vector-conformance/vectors/a11y/intervals.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/modes.json create mode 100644 wai/vector-conformance/vectors/a11y/modes.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/nested-groups.json create mode 100644 wai/vector-conformance/vectors/a11y/nested-groups.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/no-text.json create mode 100644 wai/vector-conformance/vectors/a11y/no-text.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/purposes.json create mode 100644 wai/vector-conformance/vectors/a11y/purposes.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/rates.json create mode 100644 wai/vector-conformance/vectors/a11y/rates.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/ruby.json create mode 100644 wai/vector-conformance/vectors/a11y/ruby.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/strings.json create mode 100644 wai/vector-conformance/vectors/a11y/strings.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/windows.json create mode 100644 wai/vector-conformance/vectors/a11y/windows.wvg1 create mode 100644 wai/vector-conformance/vectors/a11y/worked-example.json create mode 100644 wai/vector-conformance/vectors/a11y/worked-example.wvg1 create mode 100644 wai/vector-conformance/vectors/canonical/groups-with-ungrouped.json create mode 100644 wai/vector-conformance/vectors/canonical/groups-with-ungrouped.wvg1 create mode 100644 wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.json create mode 100644 wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.wvg1 create mode 100644 wai/vector-conformance/vectors/canonical/ruby-names-later.json create mode 100644 wai/vector-conformance/vectors/canonical/ruby-names-later.wvg1 create mode 100644 wai/vector-conformance/vectors/canonical/ruby-to-string-end.json create mode 100644 wai/vector-conformance/vectors/canonical/ruby-to-string-end.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s4-path-before-text.json create mode 100644 wai/vector-conformance/vectors/refusals/s4-path-before-text.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s4-text-before-item.json create mode 100644 wai/vector-conformance/vectors/refusals/s4-text-before-item.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.json create mode 100644 wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.json create mode 100644 wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s5g-block-before-sets.json create mode 100644 wai/vector-conformance/vectors/refusals/s5g-block-before-sets.wvg1 create mode 100644 wai/vector-conformance/vectors/refusals/s5h-decorative-before-sets.json create mode 100644 wai/vector-conformance/vectors/refusals/s5h-decorative-before-sets.wvg1 create mode 100644 wai/wai-vector/src/text.rs create mode 100644 wai/wai-vector/src/vectors/a11y.rs create mode 100644 wai/wai-vector/tests/text_layer.rs diff --git a/.github/workflows/wai-vector.yml b/.github/workflows/wai-vector.yml index 33c9e1447..c4e66defd 100644 --- a/.github/workflows/wai-vector.yml +++ b/.github/workflows/wai-vector.yml @@ -8,19 +8,26 @@ name: wai-vector vector graphics # - Linux x86_64, Linux arm64 and macOS arm64, natively, debug and release: the unit # tests, the refusal-order, canonical, clock, track, geometry, cost, held-set and # raster tests, the corpus tests (every frame of the rendering groups recomputed -# to its SHA-256), the in-house sets' tests (the default stroke set's SHA-256 is -# Annex B's, Annex A's table is the expansion's, and both sets re-derive from their -# WSF1 sources), and `wai-vector vectors verify`, which also regenerates the -# whole corpus and converts every payload's authoring form back to its bytes; and -# the sink's build, without the feature `author`, so with no stroker; +# to its SHA-256, and every text record of the a11y group recomputed byte for +# byte), the in-house sets' tests (the default stroke set's SHA-256 is Annex B's, +# Annex A's table is the expansion's, and both sets re-derive from their WSF1 +# sources), the text layer's tests (the projection and the transcript of section +# 5.4 against JSON written out by hand, and the worked example read from the +# extension), and `wai-vector vectors verify`, which also regenerates the whole +# corpus and converts every payload's authoring form back to its bytes; and the +# sink's build, without the feature `author`, so with no stroker; # - WASI (wasm32-wasip2) under wasmtime, a 32-bit target: the same tests and the same # `vectors verify`; # - a browser's engine (wasm32-unknown-unknown) under Node: the recomputation of -# every expected file and every authoring form, in a sink's build, and the default -# stroke set's SHA-256 against Annex B; +# every expected file and every authoring form, in a sink's build (an a11y +# payload's expected file states the SHA-256 of its text record, so the projection +# and the transcript are recomputed here too), and the default stroke set's +# SHA-256 against Annex B; # - the fuzz leg: the corpus-seeded fuzz of the WOS1 and WVG1 parsers at depth, each -# accepted payload of small cost rendered, the peak-allocation test and the -# duplicate-detection comparison count; +# accepted payload of small cost rendered and each accepted payload's text layer +# held (its transcript is JSON, and its projections list the blocks the frame +# procedure draws), the peak-allocation test and the duplicate-detection +# comparison count; # - the independent checker, tools/wai_vector_check.py: Python standard library only, # written from the extension text alone and sharing no code with wai-vector, which # recomputes every expected file byte for byte (every frame by the closed-form diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index bc5858dcf..fce771448 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,58 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Vector graphics (Draft): text and accessibility, §5 of `wai.graphics.vector` + +- **[graphics-vector](extensions/graphics-vector.md) §5 is written.** A payload carries + its words beside its glyphs, and this revision says what a sink computes from them and + what it owes the viewer: + - **The projection and the transcript, byte for byte (§5.4).** A block's active set is + the union of the effective windows of the runs and paths that draw it, cut to the + document's window, sorted, with windows that overlap or touch merged. The projection + at a frame is every block active at the frame's tick, in reading order; the + transcript is every block with its active set. Both are JSON with a fixed member + order and exactly three string escapes. A block's `purpose` is written as the byte + carried, an unregistered code included; `mode` is in every node; a ruby link is + written as the base block's index with the range's start and length; `decorative` + blocks are in neither. Opacity, position and clips do not enter either value. A + payload's *text record* is its transcript and every frame's projection at every + listed rate, one value a line: text-equivalence is byte equality of it. + - **Encoder rules (§5.1, §5.3)** that a verifier checks and a sink does not: one base + direction for a block, one joining sequence in one run, vertical text and its + `mode`, Normalization Form C, every byte of a string in exactly one cluster, no + zero-glyph cluster for a visible character, and no colour glyphs. + - **The purpose vocabulary (§5.2),** with how each purpose is exposed and whether it is + announced by default. + - **Exposure (§5.5).** A presenting sink exposes the current projection to the + platform's accessibility interface, updates it when the projection changes, inserts + strings as text and never as markup, and keeps within stated announcement limits. + What assistive technology then says is not claimed. + - **Whether the words match the glyphs (§5.6):** the chain from a parent font to an + outline set, from a text block to glyph IDs, and from glyphs to the overlay layer; + what an authoring receipt records; and what the chain does not reach. + - **Captions under the viewer's control (§5.7).** Captions travel as timed text. A + sink that cannot read the platform's caption preferences does not draw `caption` or + `subtitle` items, and a frame it draws without them is not the published frame. +- **`wai-vector`: the text layer.** `wai_vector::text` computes `text_hash`, block + identities, effective windows, active sets, the projection, the transcript and the + text record, in a sink's build, and a verifier's check of the cluster rule. The tool + gains `wai-vector a11y` (the text record, the transcript, or one frame's projection) + and `wai-vector text` (the text digests and each block as a reviewer reads it). The + corpus-seeded fuzz holds the text layer of every payload it accepts: the transcript + is JSON that gives back each tag and string, and a projection lists exactly the + blocks the frame procedure draws at that tick. +- **`vector-conformance`: the `a11y` group.** Each payload's expected file states the + SHA-256 of its text record, and the record is committed beside it, so every fabric leg + recomputes it: the worked example of §5.4, windows and a fade to nothing, a + `decorative` block, text alternatives of paths, reading groups, ruby over part of a + base, registered and unregistered purposes, merged and clipped intervals, windows cut + by enclosing groups, the three writing modes, four rates over one document, strings + that take each escape and strings that take none, and a payload with no text. The + `canonical` and `refusals` groups gain text-block cases: an ungrouped block between + groups, ruby links to a later block and ranges that end inside a character or at the + string's end, and pairs that pin where `TEXTS` sits in the record pass and that the + text-block rules come before any outline set is read. + ### wai-quantum 0.4.3: metered runs, placement by run, and the fabric wrapper Additive. The default features, `full`, both implementations of the WebGPU member, and diff --git a/wai/extensions/graphics-vector.md b/wai/extensions/graphics-vector.md index 3a4ce0e8c..507fb4e79 100644 --- a/wai/extensions/graphics-vector.md +++ b/wai/extensions/graphics-vector.md @@ -4,11 +4,12 @@ > - 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). +> - 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). > -> Sections 5 and 6 are headings in this revision. Their text lands in later revisions of this Draft: text and accessibility (§5), and packages and floors (§6). Where §1–§4 depend on them, they say what they depend on. +> 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, 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). +> **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. > @@ -872,7 +873,230 @@ With it, the corpus pins right-to-left visual order, contextual forms, a conjunc ## 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. +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, the words "the door of the house", drawn by a run active over `[24000, 264000)`; +- block 1: purpose 3, `en`, left-to-right, drawn by a run active over the same window; +- block 2: purpose 7, `en`, 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: + +```text +[{"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 `` with ``, 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 diff --git a/wai/vector-conformance/README.md b/wai/vector-conformance/README.md index a4b9f300f..a39821e49 100644 --- a/wai/vector-conformance/README.md +++ b/wai/vector-conformance/README.md @@ -5,20 +5,22 @@ Vectors for `wai.graphics.vector` outline set and the `WVG1` payload with the order in which a sink refuses them (§2), the arithmetic a sink loads with: the clock, track evaluation, the load-time interval check and the per-frame cost (§3.1–§3.4); and the raster stage family RS1–RS9 and the -frame procedure (§3.5–§3.10), as the SHA-256 of every frame's `RGBA8P`; and text runs +frame procedure (§3.5–§3.10), as the SHA-256 of every frame's `RGBA8P`; text runs drawn from the in-house outline sets (§4): the default stroke set and the test -repertoire. The capability is a Draft and is not registered; this corpus precedes its -registration. +repertoire; and the text layer (§5): the transcript and every frame's projection, as +each payload's text record. The capability is a Draft and is not registered; this +corpus precedes its registration. -**Provenance.** Generated here by `wai-vector vectors gen` from formulas and from centre lines drawn in this repository: every payload, outline set, stroke source and arithmetic input is integers and text written here, with no media and no third-party font. The synthetic outline sets are drawn from formulas in `wai-vector/src/vectors/sets.rs` and `raster.rs`. The two in-house sets are drawn here as integer centre lines, the ASCII stroke set in `wai-vector/src/vectors/stroke.rs` and the Arabic, Hebrew and Devanagari test repertoire in `wai-vector/src/vectors/repertoire.rs`; the generator writes them as `WSF1` sources under `sources/` and expands each by Annex A into `sets/stroke-ascii.wos1` and `sets/test-repertoire.wos1`. No outline is traced from any font. Every set's notes state where it comes from and that it is licensed Apache-2.0, and the two in-house sets' notes carry the licence text. All text in the blocks is written here. +**Provenance.** Generated here by `wai-vector vectors gen` from formulas and from centre lines drawn in this repository: every payload, outline set, stroke source and arithmetic input is integers and text written here, with no media and no third-party font. The synthetic outline sets are drawn from formulas in `wai-vector/src/vectors/sets.rs` and `raster.rs`. The two in-house sets are drawn here as integer centre lines, the ASCII stroke set in `wai-vector/src/vectors/stroke.rs` and the Arabic, Hebrew and Devanagari test repertoire in `wai-vector/src/vectors/repertoire.rs`; the generator writes them as `WSF1` sources under `sources/` and expands each by Annex A into `sets/stroke-ascii.wos1` and `sets/test-repertoire.wos1`. No outline is traced from any font. Every set's notes state where it comes from and that it is licensed Apache-2.0, and the two in-house sets' notes carry the licence text. All text in the blocks is written here; the `a11y` payloads' blocks, windows and strings are written in `wai-vector/src/vectors/a11y.rs`, and each text record is computed from its payload by `wai-vector/src/text.rs`. **Equivalence.** A sink conforms on this corpus when, for every payload, it reports the expected file's `code`: the first refusal §2.7 gives, or `accepted`; and for every accepted payload it computes the same `SIZE` costs (§3.4(b)), the same `text_hash` and block identities (§2.11), the same ticks per frame and frame range for each rate (§3.1, §2.7), and, where the expected file lists frames, the same `RGBA8P` bytes for -every frame at every listed size and rate (overlay-equivalence, §1.3). An outline-set -parser conforms when it reports §2.1's first code for every `.wos1` vector. +every frame at every listed size and rate (overlay-equivalence, §1.3); and, in the +group `a11y`, the same text record, byte for byte (text-equivalence, §1.3). An +outline-set parser conforms when it reports §2.1's first code for every `.wos1` vector. ## Layout @@ -47,7 +49,13 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. `[g, SHA-256 of RGBA8P]` and the clip digest, the SHA-256 of the frames' digests concatenated in order of `g` (SPEC §7, *Digests*); in `composite`, also each frame's `RGBA8S` digest (`rgba8s`); + - in the group `a11y`, `text_record`: the SHA-256 of the payload's text record + (§5.4), which is the file beside it; - an arithmetic input: its results, or the SHA-256 of a whole table. +- `expected/a11y/.a11y.jsonl` — an accepted `a11y` payload's text record (§5.4), + exactly as §5.4 writes it: the transcript on the first line, then one line for each + frame of the document at each listed rate, rates in `RATE` order and frames + ascending, each line ended by U+000A. - `sha256.json` — the digest sidecar SPEC §7, *Digests*, asks of a corpus: each entry names its buffer and states its SHA-256: the `div255` table, and for each rendering vector at each listed size and rate, every frame's digest and the clip digest. @@ -60,13 +68,18 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. spans of `2^31 − 1` ticks, value steps of `2^23` and tables that are not monotone. - `canonical` — accepted payloads of every structure, each of which writes back to its own bytes, and one refusal per canonical rule (§2.5): first-reference order, - duplicates and unreferenced entries in each pool, group numbering and contiguity, - ruby links, clusters, and a decorative path alternative. + duplicates and unreferenced entries in each pool; text blocks: group numbering, an + ungrouped block between groups (accepted), a group that resumes or is split + (refused), ruby links to the block itself and to a later block, and ruby ranges that + start or end inside a character, run past the string, or end where it ends + (accepted); clusters; and a decorative path alternative. - `refusals` — a payload or outline set per code and per check of §2.7 and §2.1, and pairs of faults whose code pins the order: steps in order, fields in offset order - within a record, the record pass before any cross-table check, each set's checks - before the next set's. Held sets come from `sets/`, through a resolver that refuses a - digest it does not hold and reads at most `length + 1` bytes. + within a record, tables in file order (`PATHS` before `TEXTS` before `ITEMS`), the + record pass before any cross-table check, typing before strings, the text-block + rules and the decorative path alternative before any set, each set's checks before + the next set's. Held sets come from `sets/`, through a resolver that refuses a digest + it does not hold and reads at most `length + 1` bytes. - `geometry` — the interval check (§3.4(a)) accepted with an end at `2^22 − 1` and refused at `2^22` on each axis, at a second listed size, for an item never active, for a clip, for enclosing groups' translations and for a glyph with no points; and a @@ -106,8 +119,20 @@ parser conforms when it reports §2.1's first code for every `.wos1` vector. drawn before its consonant, a mark above and a conjunct; an Arabic block holding a Latin word and digits, its runs from both sets; one block over three runs and lines, with a cluster of no glyphs and an inserted hyphen; a ruby annotation; a vertical-rl - block; and U+1F600 drawn by one monochrome glyph. The blocks' projection lands with - §5; these vectors pin runs, clusters, blocks and frames. + block; and U+1F600 drawn by one monochrome glyph. These vectors pin runs, clusters, + blocks and frames; `a11y` pins the projection and the transcript. +- `a11y` — the text layer (§5.4), as each payload's text record: §5.4's worked + example; frames before, between and after windows, and a title that stays projected + at opacity 0 until its window ends; a `decorative` block, left out; the text + alternatives of paths; reading groups; ruby links over part of a base, one of which + outlasts its base; every registered purpose and three unregistered ones, written as + the byte carried; windows that touch, overlap, nest, stay one tick apart and come + out of order; windows cut by the document's own; windows cut by enclosing groups, + one of which never opens; the three writing modes, whose block identities differ; one + document at four rates, with a frame on a window's first tick and one on its end; + strings that take each of the three escapes, and markup characters, a soft hyphen, + Hebrew, Devanagari and U+1F600, which take none; and a payload with no text. The + frames of this group are not rendered. ## Regenerating and verifying @@ -118,6 +143,16 @@ cargo run --release --features std --bin wai-vector -- vectors verify .. ``` `vectors verify` regenerates every file and requires the committed bytes, then -recomputes every expected file from its vector and `sets/` alone, and converts every -payload's authoring form back to its bytes. `wai-vector/wasm/verify.mjs` recomputes the -expected files under a browser's WebAssembly engine with Node. +recomputes every expected file and every text record from its vector and `sets/` +alone, and converts every payload's authoring form back to its bytes. +`wai-vector/wasm/verify.mjs` recomputes the expected files under a browser's +WebAssembly engine with Node, each `a11y` payload's `text_record` among them. + +```sh +cargo run --release --features std --bin wai-vector -- a11y ../vector-conformance/vectors/a11y/worked-example.wvg1 --sets ../vector-conformance/sets +cargo run --release --features std --bin wai-vector -- text ../vector-conformance/vectors/a11y/worked-example.wvg1 --sets ../vector-conformance/sets +``` + +`a11y` prints a payload's text record (or, with `transcript`, or a rate and a frame, +one value of it), and `text` its `text_hash` and each block's identity, fields, string +and active set. diff --git a/wai/vector-conformance/expected/a11y/alternatives.a11y.jsonl b/wai/vector-conformance/expected/a11y/alternatives.a11y.jsonl new file mode 100644 index 000000000..fdadbb7c8 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/alternatives.a11y.jsonl @@ -0,0 +1,26 @@ +[{"active":[[0,72000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"active":[[48000,120000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"active":[[0,24000],[96000,120000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":10,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"}]} +{"den":1,"g":20,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":11,"text":"A red square"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Both"}]} diff --git a/wai/vector-conformance/expected/a11y/alternatives.json b/wai/vector-conformance/expected/a11y/alternatives.json new file mode 100644 index 000000000..3a34bb18b --- /dev/null +++ b/wai/vector-conformance/expected/a11y/alternatives.json @@ -0,0 +1 @@ +{"blocks":["ca88458c4ee4dc28f9744245e897f4a3cd84010b5bec034f95fc7ed355ee46ee","ffd5280e18b2c2d5e0c45338cd3941b663db6ec966f7e701afc47b4dc064f7ac","aed6f806bac9925457759b007860245459a32bd3fe9ea93db520c999affe82b2"],"code":"accepted","instr":"0468e1e98c8efd8e0031ec37b71b2103173918ab39c780edcf6fcdfee3043661","rates":[[25,1,4800,0,24]],"sizes":[[640,360,1307915]],"text_hash":"7dfef93b07be30ecc6aff3c62375d5fe833ff6cac0890c28e5618a3d00bf6f6f","text_record":"379cbc9350ca8490d45bfd566a686ebbc8e2387ac6532542117683e32e98941f"} diff --git a/wai/vector-conformance/expected/a11y/clipped.a11y.jsonl b/wai/vector-conformance/expected/a11y/clipped.a11y.jsonl new file mode 100644 index 000000000..83a2fc1ac --- /dev/null +++ b/wai/vector-conformance/expected/a11y/clipped.a11y.jsonl @@ -0,0 +1,20 @@ +[{"active":[[50000,72000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"starts before"},{"active":[[120000,144000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"active":[[50000,144000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"active":[],"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"over by then"},{"active":[],"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"not yet"},{"active":[[50000,144000]],"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}] +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"starts before"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"starts before"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"starts before"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"starts before"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":20,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":25,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":26,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":27,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":28,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} +{"den":1,"g":29,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"ends after"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"spans it"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"two parts"}]} diff --git a/wai/vector-conformance/expected/a11y/clipped.json b/wai/vector-conformance/expected/a11y/clipped.json new file mode 100644 index 000000000..8f439f21d --- /dev/null +++ b/wai/vector-conformance/expected/a11y/clipped.json @@ -0,0 +1 @@ +{"blocks":["00c1e14bc07174a6d67d196fd8f1fab91f005c21c44337e13f70d1b5b5abe8a9","4cd240a8579588b991bcf0a6145e00149fbbe4375ef7daa113c4020b008dea85","9e0f835b50020956df86c248a30e3a80830e927646dda8d84142326f82889b6a","1b49953f3a5eb03a46882a8fc816ecb77fc10977f4997929ea4b408137f0fa2c","7174b9bff0e4b79d59fb5b311c93b55de384034d55dd09c0b68e804456103991","b9516e95c53a745095e2f474337c6bff1be9ce6b07753bb28debf545a5325cce"],"code":"accepted","instr":"0d83a17dd8953346935ec8ed1c98a688981ff5e0bccbdcb7172c8c789208b41a","rates":[[25,1,4800,11,29]],"sizes":[[640,360,6611664]],"text_hash":"b5dd11f2e4c5abc7445c6894edb176a192b17d7a779b0c7eb7e5ab23324d6227","text_record":"ce79f2a3366392d806682f1cc0b0d67120a4a6b3f759484ee28d80b4f9d8d1c2"} diff --git a/wai/vector-conformance/expected/a11y/decorative.a11y.jsonl b/wai/vector-conformance/expected/a11y/decorative.a11y.jsonl new file mode 100644 index 000000000..e8c5da35b --- /dev/null +++ b/wai/vector-conformance/expected/a11y/decorative.a11y.jsonl @@ -0,0 +1,26 @@ +[{"active":[[0,120000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"active":[[0,120000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":20,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"Scores"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":12,"text":"3 - 1"}]} diff --git a/wai/vector-conformance/expected/a11y/decorative.json b/wai/vector-conformance/expected/a11y/decorative.json new file mode 100644 index 000000000..5a9d7f3f7 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/decorative.json @@ -0,0 +1 @@ +{"blocks":["9b2a6dbea8a24042136f3aef42473b9cc989d40e430f9fd792893a8bc6b43932","77f81a2ce213db8ffe9a08c327feba022518b4de24faef66613f50b2e00fc153","d6a6376deac7414d078f9b7041805661e0d8b9783255e89b52707947b7d0640f"],"code":"accepted","instr":"149aeef7ab2313c3e7b6b9311eeaed7fb55c1924bc9edc756cfe6774f5ccff8a","rates":[[25,1,4800,0,24]],"sizes":[[640,360,2924252]],"text_hash":"10d923bf436c63195e13e0a30bcd56b428a297682cd18c687970bdec421a6f14","text_record":"111295c8003c566b13df153b1f63af638d57a64cb213c2e415c5ff6004affa4d"} diff --git a/wai/vector-conformance/expected/a11y/groups.a11y.jsonl b/wai/vector-conformance/expected/a11y/groups.a11y.jsonl new file mode 100644 index 000000000..36083ba81 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/groups.a11y.jsonl @@ -0,0 +1,26 @@ +[{"active":[[0,48000]],"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"active":[[0,48000]],"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"},{"active":[[48000,72000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"},{"active":[[72000,120000]],"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"active":[[72000,120000]],"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"active":[[96000,120000]],"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":1,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Camera"},{"dir":0,"group":1,"index":1,"lang":"en","mode":0,"purpose":3,"text":"Second unit"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"Interval"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"}]} +{"den":1,"g":20,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":2,"index":3,"lang":"en","mode":0,"purpose":2,"text":"Editing"},{"dir":0,"group":2,"index":4,"lang":"en","mode":0,"purpose":2,"text":"Sound"},{"dir":0,"group":2,"index":5,"lang":"en","mode":0,"purpose":2,"text":"Music"}]} diff --git a/wai/vector-conformance/expected/a11y/groups.json b/wai/vector-conformance/expected/a11y/groups.json new file mode 100644 index 000000000..1885da8b3 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/groups.json @@ -0,0 +1 @@ +{"blocks":["3178a166bac0962438e1e2ec6c45b6097fd8621368ca241305d3d41ad072df2a","2f1c46c346a58fc8e91ce1bfe7f30ab1c79fc088078e97748ed311475739fa38","6cf151556e8345d543885f5f60044c92edab71f29afb339edb1a387458a1f047","f771053bc83798d847a8c2511bfc992b0f79f4fa9227f3c26dbf13f2aa0b9959","bf2000dd621ba83c1b96312f2fa3c43031555e4c80d588e81d55a7214c00104d","f35dc7476c9ed9026693ecea3698eab61028efd2f555ea2e7135202b65f6c69d"],"code":"accepted","instr":"f90c52b04a22cbed32b36bac02f7a68304cc7c1c9965825be8207eadb05b91ea","rates":[[25,1,4800,0,24]],"sizes":[[640,360,4416300]],"text_hash":"a8abdb6c365e4d363b36798a3454faf387a0ad42bfb39c79c7db23ee3fcc0fc8","text_record":"eb58245e48162b9f3a55bdce7c640d229d22ca1174d6407c8875a15f717d19a0"} diff --git a/wai/vector-conformance/expected/a11y/intervals.a11y.jsonl b/wai/vector-conformance/expected/a11y/intervals.a11y.jsonl new file mode 100644 index 000000000..99a83ea14 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/intervals.a11y.jsonl @@ -0,0 +1,26 @@ +[{"active":[[0,48000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"active":[[0,60000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"active":[[0,24000],[24001,48000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"active":[[0,96000]],"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"active":[[0,48000],[72000,96000]],"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"touching windows"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"apart windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"overlapping windows"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"nested windows"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"later then earlier"}]} +{"den":1,"g":20,"num":25,"projection":[]} +{"den":1,"g":21,"num":25,"projection":[]} +{"den":1,"g":22,"num":25,"projection":[]} +{"den":1,"g":23,"num":25,"projection":[]} +{"den":1,"g":24,"num":25,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/intervals.json b/wai/vector-conformance/expected/a11y/intervals.json new file mode 100644 index 000000000..45278d455 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/intervals.json @@ -0,0 +1 @@ +{"blocks":["7e4581efd596e36b5e20ac37f54d1b7169fa169bec9d6d8b82f97c02094b400f","91abb1ee408fd4568bf20889ce8d9916b940f63bb461a36755782fadf3cd1e40","fc7cb59cacd92959f7e5169c2d506a413b2cd9ca8b19be988496d0bf96058825","1c4f5fec6b8252da509e726bd2d9c94e287955eb67214b2810e6cf002cae734f","a906c6f214f3d3ba82e034c2fdca7d634cde4ba08cdf0892cbf07d770e49b89b"],"code":"accepted","instr":"4366d09386763911349f4e84cc15a60cef315631c16a394a5326fc2fe294c966","rates":[[25,1,4800,0,24]],"sizes":[[640,360,12359740]],"text_hash":"51fe1ecdb64b542fdb91e9880d7f3b700e47edd01d3590c5e816fb1603fed5e6","text_record":"dd46fc43f39b1e34686b4bbb028decb18dbefba6d95074d4f088175c9ba7101e"} diff --git a/wai/vector-conformance/expected/a11y/modes.a11y.jsonl b/wai/vector-conformance/expected/a11y/modes.a11y.jsonl new file mode 100644 index 000000000..3186462ce --- /dev/null +++ b/wai/vector-conformance/expected/a11y/modes.a11y.jsonl @@ -0,0 +1,6 @@ +[{"active":[[0,24000]],"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"active":[[0,24000]],"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"active":[[0,24000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":1,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":1,"lang":"en","mode":2,"purpose":7,"text":"WAI"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"WAI"}]} diff --git a/wai/vector-conformance/expected/a11y/modes.json b/wai/vector-conformance/expected/a11y/modes.json new file mode 100644 index 000000000..648db55fc --- /dev/null +++ b/wai/vector-conformance/expected/a11y/modes.json @@ -0,0 +1 @@ +{"blocks":["47d74c9ccb8456e3f1171904dda15a84bf74897c9bcc12d9f4fef166730e8c1a","916bd4458608698f35b350172b1827ab26e53094e6d9fd784a62311f4270c7bc","7717339a5b738a00c3f6191d2f43cc78156c65651b8c55d4ec234efa06efc968"],"code":"accepted","instr":"30e09b46e3e68745f9ac635fc20a37b4e16bac29b97739a563138f272e3521bc","rates":[[25,1,4800,0,4]],"sizes":[[640,360,1445598]],"text_hash":"b8dadc4d14f2da3ca1134c42a812c7164b88f89d0c71d3da048de597f8847471","text_record":"e99c7f5224b7d7db1a6aea665fbc94e56048952d8fda357dc5456c7ccd4ef071"} diff --git a/wai/vector-conformance/expected/a11y/nested-groups.a11y.jsonl b/wai/vector-conformance/expected/a11y/nested-groups.a11y.jsonl new file mode 100644 index 000000000..8a74d6b78 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/nested-groups.a11y.jsonl @@ -0,0 +1,51 @@ +[{"active":[[24000,96000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"active":[[48000,60000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"in both groups"},{"active":[[72000,96000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"},{"active":[],"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":7,"text":"in a group that never opens"},{"active":[[100000,130000]],"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}] +{"den":1,"g":0,"num":25,"projection":[]} +{"den":1,"g":1,"num":25,"projection":[]} +{"den":1,"g":2,"num":25,"projection":[]} +{"den":1,"g":3,"num":25,"projection":[]} +{"den":1,"g":4,"num":25,"projection":[]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"in both groups"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"in both groups"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"in both groups"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"in the outer group"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":7,"text":"in both, later"}]} +{"den":1,"g":20,"num":25,"projection":[]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":25,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":26,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":27,"num":25,"projection":[{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":7,"text":"in no group"}]} +{"den":1,"g":28,"num":25,"projection":[]} +{"den":1,"g":29,"num":25,"projection":[]} +{"den":1,"g":30,"num":25,"projection":[]} +{"den":1,"g":31,"num":25,"projection":[]} +{"den":1,"g":32,"num":25,"projection":[]} +{"den":1,"g":33,"num":25,"projection":[]} +{"den":1,"g":34,"num":25,"projection":[]} +{"den":1,"g":35,"num":25,"projection":[]} +{"den":1,"g":36,"num":25,"projection":[]} +{"den":1,"g":37,"num":25,"projection":[]} +{"den":1,"g":38,"num":25,"projection":[]} +{"den":1,"g":39,"num":25,"projection":[]} +{"den":1,"g":40,"num":25,"projection":[]} +{"den":1,"g":41,"num":25,"projection":[]} +{"den":1,"g":42,"num":25,"projection":[]} +{"den":1,"g":43,"num":25,"projection":[]} +{"den":1,"g":44,"num":25,"projection":[]} +{"den":1,"g":45,"num":25,"projection":[]} +{"den":1,"g":46,"num":25,"projection":[]} +{"den":1,"g":47,"num":25,"projection":[]} +{"den":1,"g":48,"num":25,"projection":[]} +{"den":1,"g":49,"num":25,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/nested-groups.json b/wai/vector-conformance/expected/a11y/nested-groups.json new file mode 100644 index 000000000..5adfa9a10 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/nested-groups.json @@ -0,0 +1 @@ +{"blocks":["01b82dd381859ca795e0b61db0fb19df8773f9f5cbc3a65a3c67cc0145d3e1a9","e1392669dad95818d457d1ee8ab65df918934397047f2f42e8f2483cc7d36765","93382a097e389c49c1991864fee8379257cfbc48bc802b1a0e907f99903c7e2c","992de910448c2bc75b0c930481492b070843c7ababf5c75a103dabfa07a8c857","03d264ab7dc26f9561b313b27c2a5e20b1768ee7f0f34b882fbf8cfe16a53d5d"],"code":"accepted","instr":"22351d620fa668a2bd2d8b2d8665797f470d3d306acc0b1bfc67873e2f98e352","rates":[[25,1,4800,0,49]],"sizes":[[640,360,7588831]],"text_hash":"34456e22dcd71bd03e0a47e23530864ba7d4de993037e3a8cb599932a8975407","text_record":"c8d65ab35d3378051eccfd06f52d0f4435367843567db9bfd43fcd61b04f7925"} diff --git a/wai/vector-conformance/expected/a11y/no-text.a11y.jsonl b/wai/vector-conformance/expected/a11y/no-text.a11y.jsonl new file mode 100644 index 000000000..cd4208006 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/no-text.a11y.jsonl @@ -0,0 +1,6 @@ +[] +{"den":1,"g":0,"num":25,"projection":[]} +{"den":1,"g":1,"num":25,"projection":[]} +{"den":1,"g":2,"num":25,"projection":[]} +{"den":1,"g":3,"num":25,"projection":[]} +{"den":1,"g":4,"num":25,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/no-text.json b/wai/vector-conformance/expected/a11y/no-text.json new file mode 100644 index 000000000..2661a937f --- /dev/null +++ b/wai/vector-conformance/expected/a11y/no-text.json @@ -0,0 +1 @@ +{"blocks":[],"code":"accepted","instr":"df8b6dc7bd4dd19892f19db68d78ae66d0353f40ebf8a360f492bdcc34aa655f","rates":[[25,1,4800,0,4]],"sizes":[[640,360,239241]],"text_hash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","text_record":"5bb8213e7e03d4edfb194e18f23bcfec100ed9151a31209c311aec82b6d2a89b"} diff --git a/wai/vector-conformance/expected/a11y/purposes.a11y.jsonl b/wai/vector-conformance/expected/a11y/purposes.a11y.jsonl new file mode 100644 index 000000000..ff3ae267a --- /dev/null +++ b/wai/vector-conformance/expected/a11y/purposes.a11y.jsonl @@ -0,0 +1,6 @@ +[{"active":[[0,24000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"active":[[0,24000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"active":[[0,24000]],"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"active":[[0,24000]],"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"active":[[0,24000]],"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"active":[[0,24000]],"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"active":[[0,24000]],"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"active":[[0,24000]],"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"active":[[0,24000]],"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"active":[[0,24000]],"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"active":[[0,24000]],"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"active":[[0,24000]],"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"active":[[0,24000]],"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"active":[[0,24000]],"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"active":[[0,24000]],"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"title"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"credit"},{"dir":0,"group":0,"index":2,"lang":"en","mode":0,"purpose":3,"text":"lower_third"},{"dir":0,"group":0,"index":3,"lang":"en","mode":0,"purpose":4,"text":"caption"},{"dir":0,"group":0,"index":4,"lang":"en","mode":0,"purpose":5,"text":"subtitle"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":6,"text":"forced_narrative"},{"dir":0,"group":0,"index":6,"lang":"en","mode":0,"purpose":7,"text":"label"},{"dir":0,"group":0,"index":7,"lang":"en","mode":0,"purpose":8,"text":"legal"},{"dir":0,"group":0,"index":8,"lang":"en","mode":0,"purpose":9,"text":"alert"},{"dir":0,"group":0,"index":9,"lang":"en","mode":0,"purpose":10,"text":"logo"},{"dir":0,"group":0,"index":10,"lang":"en","mode":0,"purpose":11,"text":"graphic"},{"dir":0,"group":0,"index":11,"lang":"en","mode":0,"purpose":12,"text":"data"},{"dir":0,"group":0,"index":13,"lang":"en","mode":0,"purpose":14,"text":"p14"},{"dir":0,"group":0,"index":14,"lang":"en","mode":0,"purpose":200,"text":"p200"},{"dir":0,"group":0,"index":15,"lang":"en","mode":0,"purpose":255,"text":"p255"}]} diff --git a/wai/vector-conformance/expected/a11y/purposes.json b/wai/vector-conformance/expected/a11y/purposes.json new file mode 100644 index 000000000..0fd84730c --- /dev/null +++ b/wai/vector-conformance/expected/a11y/purposes.json @@ -0,0 +1 @@ +{"blocks":["7d9e857db708d0b5259fcd4298387b34f93cc6d7062fa0ce6037aa094b6e27dc","f17c0d40188346946440bed75777b14d8bcf2763a1af244d8a9bd9ca818dc28a","45daabd319d36574bc92fd564395e83703e8452d05816a2dcd264c713e87871c","11f2cecc76b477269c751e2a8debea9bdfaf1da8f3d525af2010c6580eddabb3","933575b2576cbbfff7d08dbd8765f15fd445ccea9ace32e4bbc7497dafe7b710","fe0af73ac2e6f43ed88fbc2c7693837996d749cca41a36574545b9b00a32b0f0","995ee4642ae0ba83546b1fab8d649c5cbea163f5d7874ff7b498d38b5062d23b","356e8c59662fccd5d46f60e1d22bb7637dbe09d2f621abdc796b130f40f3447a","75bb28dd70a1da8fd6b43ac92b473ee99a88d8ffbda292e01de100ad68360399","dbc8b9bdbe318420bac1794f3ffa004df06a9265f2d4e1c32ae9b5d6c4835d92","5d459a6f94eaed4e068a5ca4f44f34171b034e913f78c63405f8b016198ca361","712f02b30587742652158953d117673e71dd076ba5ed0ade37ae4770c2b25b44","fb9a1ef96369f402d99a7c9b413aed471697316bb5f41ef0f4862e5ed3bd7ba4","a0bc207b0900868c7ba29f4a7985874a99271ac63c7cd3329e6b2ffc0dc7c8a8","56851537f63a3a5c4345f562aff332148134010c5d806ac955a69f764c0a4b3a","f331e96b11804bea9fdf0bb863180091004b8a65d35292ff61edda7b5ff955bb"],"code":"accepted","instr":"8d72f40b55ce014c463d9c9d877b25f3c29eb8578fb4186124e2e75aa01eaea6","rates":[[25,1,4800,0,4]],"sizes":[[640,360,24150310]],"text_hash":"dfb62a056dfb0d1542231aa7b28ee5f291c62a506755f7fa00701a8d2eac2beb","text_record":"77aab308861a87a1af8a98ae4db67d6f4bc9781f610e9ba3002376e593c52e96"} diff --git a/wai/vector-conformance/expected/a11y/rates.a11y.jsonl b/wai/vector-conformance/expected/a11y/rates.a11y.jsonl new file mode 100644 index 000000000..bf35dfe9a --- /dev/null +++ b/wai/vector-conformance/expected/a11y/rates.a11y.jsonl @@ -0,0 +1,71 @@ +[{"active":[[24000,50000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}] +{"den":1001,"g":0,"num":24000,"projection":[]} +{"den":1001,"g":1,"num":24000,"projection":[]} +{"den":1001,"g":2,"num":24000,"projection":[]} +{"den":1001,"g":3,"num":24000,"projection":[]} +{"den":1001,"g":4,"num":24000,"projection":[]} +{"den":1001,"g":5,"num":24000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":6,"num":24000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":7,"num":24000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":8,"num":24000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":9,"num":24000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":10,"num":24000,"projection":[]} +{"den":1001,"g":11,"num":24000,"projection":[]} +{"den":1,"g":0,"num":25,"projection":[]} +{"den":1,"g":1,"num":25,"projection":[]} +{"den":1,"g":2,"num":25,"projection":[]} +{"den":1,"g":3,"num":25,"projection":[]} +{"den":1,"g":4,"num":25,"projection":[]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":11,"num":25,"projection":[]} +{"den":1,"g":12,"num":25,"projection":[]} +{"den":1001,"g":0,"num":30000,"projection":[]} +{"den":1001,"g":1,"num":30000,"projection":[]} +{"den":1001,"g":2,"num":30000,"projection":[]} +{"den":1001,"g":3,"num":30000,"projection":[]} +{"den":1001,"g":4,"num":30000,"projection":[]} +{"den":1001,"g":5,"num":30000,"projection":[]} +{"den":1001,"g":6,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":7,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":8,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":9,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":10,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":11,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":12,"num":30000,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1001,"g":13,"num":30000,"projection":[]} +{"den":1001,"g":14,"num":30000,"projection":[]} +{"den":1,"g":0,"num":60,"projection":[]} +{"den":1,"g":1,"num":60,"projection":[]} +{"den":1,"g":2,"num":60,"projection":[]} +{"den":1,"g":3,"num":60,"projection":[]} +{"den":1,"g":4,"num":60,"projection":[]} +{"den":1,"g":5,"num":60,"projection":[]} +{"den":1,"g":6,"num":60,"projection":[]} +{"den":1,"g":7,"num":60,"projection":[]} +{"den":1,"g":8,"num":60,"projection":[]} +{"den":1,"g":9,"num":60,"projection":[]} +{"den":1,"g":10,"num":60,"projection":[]} +{"den":1,"g":11,"num":60,"projection":[]} +{"den":1,"g":12,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":13,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":14,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":15,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":16,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":17,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":18,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":19,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":20,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":21,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":22,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":23,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":24,"num":60,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":3,"text":"Four rates"}]} +{"den":1,"g":25,"num":60,"projection":[]} +{"den":1,"g":26,"num":60,"projection":[]} +{"den":1,"g":27,"num":60,"projection":[]} +{"den":1,"g":28,"num":60,"projection":[]} +{"den":1,"g":29,"num":60,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/rates.json b/wai/vector-conformance/expected/a11y/rates.json new file mode 100644 index 000000000..27923c9f1 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/rates.json @@ -0,0 +1 @@ +{"blocks":["0ad56f255b6aa4690808061f233651423d744ce823f923cabbf96310a23b8201"],"code":"accepted","instr":"d4189fec2f822915f98184ffc69379fe2f9b21749fb7f10fcb095d7007c802f2","rates":[[24000,1001,5005,0,11],[25,1,4800,0,12],[30000,1001,4004,0,14],[60,1,2000,0,29]],"sizes":[[640,360,2415278]],"text_hash":"b91c0980a104beb71b72fcdb9759a737a7afd27a9a608e4f9277ea07531fd5ee","text_record":"870f1f9899b38b64a58ac6da51392de453a9c9351b37e4c99f46da23481cda63"} diff --git a/wai/vector-conformance/expected/a11y/ruby.a11y.jsonl b/wai/vector-conformance/expected/a11y/ruby.a11y.jsonl new file mode 100644 index 000000000..af2bc79f5 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/ruby.a11y.jsonl @@ -0,0 +1,26 @@ +[{"active":[[0,96000]],"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"active":[[0,48000]],"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"},{"active":[[48000,120000]],"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":1,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":9,"start":0},"text":"hin"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"hi","mode":0,"purpose":7,"text":"हिंदी"},{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":20,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":21,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":22,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":23,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} +{"den":1,"g":24,"num":25,"projection":[{"dir":0,"group":0,"index":2,"lang":"hi-Latn","mode":0,"purpose":7,"ruby":{"base":0,"len":6,"start":9},"text":"di"}]} diff --git a/wai/vector-conformance/expected/a11y/ruby.json b/wai/vector-conformance/expected/a11y/ruby.json new file mode 100644 index 000000000..4ccb23d7a --- /dev/null +++ b/wai/vector-conformance/expected/a11y/ruby.json @@ -0,0 +1 @@ +{"blocks":["3fa00b9bbe6f4ce44ff3efa35063be19619e6a9d50491980625dab76495506ef","7ea59bb34ed19148010994406826e8aa83fcaa650e09b1a337b0def275d34161","23972f01741155a401f77574f7f5cad70fa0f0a21bdca8afa672ed21d3318922"],"code":"accepted","instr":"d28b7b0cf6ecd4a0b8f5dfe9fb7b17bfd304945ec81d4ec35e8eb3410510b583","rates":[[25,1,4800,0,24]],"sizes":[[640,360,1990795]],"text_hash":"ebbfffe5c0e85c02b4884d4c174419d770100e4796df859dc4c26353ad2c2ae7","text_record":"c197bff9cd3d2f4ca63ae9d8d250ff3ef9883bf2dca2ff972feb2e80655d0d3d"} diff --git a/wai/vector-conformance/expected/a11y/strings.a11y.jsonl b/wai/vector-conformance/expected/a11y/strings.a11y.jsonl new file mode 100644 index 000000000..453c8a3b9 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/strings.a11y.jsonl @@ -0,0 +1,6 @@ +[{"active":[[0,24000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"active":[[0,24000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"active":[[0,24000]],"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"active":[[0,24000]],"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"active":[[0,24000]],"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"active":[[0,24000]],"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}] +{"den":1,"g":0,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}]} +{"den":1,"g":1,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}]} +{"den":1,"g":2,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}]} +{"den":1,"g":3,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}]} +{"den":1,"g":4,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":7,"text":"He said \"A&B\" \\\n2/2"},{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":7,"text":"OK 😀"},{"dir":1,"group":0,"index":2,"lang":"he","mode":0,"purpose":7,"text":"שלום"},{"dir":0,"group":0,"index":3,"lang":"hi","mode":0,"purpose":7,"text":"हिन्दी"},{"dir":0,"group":0,"index":4,"lang":"sr-Latn-RS","mode":0,"purpose":7,"text":"Zdravo"},{"dir":0,"group":0,"index":5,"lang":"en","mode":0,"purpose":7,"text":"soft­hyphen"}]} diff --git a/wai/vector-conformance/expected/a11y/strings.json b/wai/vector-conformance/expected/a11y/strings.json new file mode 100644 index 000000000..8260e868e --- /dev/null +++ b/wai/vector-conformance/expected/a11y/strings.json @@ -0,0 +1 @@ +{"blocks":["1d55c585f948d167f1c192c10a461c93c17de3a381f6df13df3fff1b15103d80","21b751ed648ba10c6594afca8701e14dcc8f19880662f3a571f4da32ae581677","722c8daf25a3241f335fb4286ecdce0ab9363527bdd554d42ac0c07203f52066","7f8c28dc03300c226e65813032980fa890110d4d90c066b0238da0febc0ccdde","173ba88478947083166f18db924bcaf11e0f191428f7989bc84a0b87f4c39f74","908a6204290e5e2814dcd3b30e278c4b75b1b6d2023c71d28f2b9f02ae45c5b3"],"code":"accepted","instr":"fe0b3acd351b5d1c420d0af4552cde490821a65f96d1ad27050c536736b7e00a","rates":[[25,1,4800,0,4]],"sizes":[[640,360,11169106]],"text_hash":"d1d74b9c63272f115c6454dcba48241f02868a52c849f201d64677ba59c4171d","text_record":"145f0cba50b3330f69577daccf8e24682f0403108e26cdd8ce4a31f6e30d24aa"} diff --git a/wai/vector-conformance/expected/a11y/windows.a11y.jsonl b/wai/vector-conformance/expected/a11y/windows.a11y.jsonl new file mode 100644 index 000000000..327d22820 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/windows.a11y.jsonl @@ -0,0 +1,51 @@ +[{"active":[[24000,96000]],"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"},{"active":[[120000,216000]],"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}] +{"den":1,"g":0,"num":25,"projection":[]} +{"den":1,"g":1,"num":25,"projection":[]} +{"den":1,"g":2,"num":25,"projection":[]} +{"den":1,"g":3,"num":25,"projection":[]} +{"den":1,"g":4,"num":25,"projection":[]} +{"den":1,"g":5,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":6,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":7,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":8,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":9,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":10,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":11,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":12,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":13,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":14,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":15,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":16,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":17,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":18,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":19,"num":25,"projection":[{"dir":0,"group":0,"index":0,"lang":"en","mode":0,"purpose":1,"text":"Opening title"}]} +{"den":1,"g":20,"num":25,"projection":[]} +{"den":1,"g":21,"num":25,"projection":[]} +{"den":1,"g":22,"num":25,"projection":[]} +{"den":1,"g":23,"num":25,"projection":[]} +{"den":1,"g":24,"num":25,"projection":[]} +{"den":1,"g":25,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":26,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":27,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":28,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":29,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":30,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":31,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":32,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":33,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":34,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":35,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":36,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":37,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":38,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":39,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":40,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":41,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":42,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":43,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":44,"num":25,"projection":[{"dir":0,"group":0,"index":1,"lang":"en","mode":0,"purpose":2,"text":"Written here"}]} +{"den":1,"g":45,"num":25,"projection":[]} +{"den":1,"g":46,"num":25,"projection":[]} +{"den":1,"g":47,"num":25,"projection":[]} +{"den":1,"g":48,"num":25,"projection":[]} +{"den":1,"g":49,"num":25,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/windows.json b/wai/vector-conformance/expected/a11y/windows.json new file mode 100644 index 000000000..c0d5fb54e --- /dev/null +++ b/wai/vector-conformance/expected/a11y/windows.json @@ -0,0 +1 @@ +{"blocks":["a6ad09349fc54b2e8a83d4798b78b9e57888de7ef5ca28cc751cb8606d09ad6c","d5447338e2cc60b19f5613a91dc55e178867fcd0cbd486a6bb9c3753009ccef2"],"code":"accepted","instr":"9939d05524de498c1dd2bac0cf438f0ba34a44d176a5e8dcf7f1b7be55b02ea1","rates":[[25,1,4800,0,49]],"sizes":[[640,360,2995585]],"text_hash":"355c1d3f1998dd10f25bf8b1bd1ecbf39f190bb4f85c70338d954e57415fd1f7","text_record":"95f151ba7dd7f1ea563c5a05113e6b541ee7a98be08abe783a49f4eb5513680f"} diff --git a/wai/vector-conformance/expected/a11y/worked-example.a11y.jsonl b/wai/vector-conformance/expected/a11y/worked-example.a11y.jsonl new file mode 100644 index 000000000..90c541787 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/worked-example.a11y.jsonl @@ -0,0 +1,76 @@ +[{"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":0,"num":25,"projection":[]} +{"den":1,"g":1,"num":25,"projection":[]} +{"den":1,"g":2,"num":25,"projection":[]} +{"den":1,"g":3,"num":25,"projection":[]} +{"den":1,"g":4,"num":25,"projection":[]} +{"den":1,"g":5,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":6,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":7,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":8,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":9,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":10,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":11,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":12,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":13,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":14,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":15,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":16,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":17,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":18,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":19,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":20,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":21,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":22,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":23,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":24,"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"},{"dir":0,"group":1,"index":2,"lang":"en","mode":0,"purpose":7,"ruby":{"base":1,"len":8,"start":0},"text":"DOP"}]} +{"den":1,"g":25,"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"}]} +{"den":1,"g":26,"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"}]} +{"den":1,"g":27,"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"}]} +{"den":1,"g":28,"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"}]} +{"den":1,"g":29,"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"}]} +{"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"}]} +{"den":1,"g":31,"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"}]} +{"den":1,"g":32,"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"}]} +{"den":1,"g":33,"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"}]} +{"den":1,"g":34,"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"}]} +{"den":1,"g":35,"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"}]} +{"den":1,"g":36,"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"}]} +{"den":1,"g":37,"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"}]} +{"den":1,"g":38,"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"}]} +{"den":1,"g":39,"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"}]} +{"den":1,"g":40,"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"}]} +{"den":1,"g":41,"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"}]} +{"den":1,"g":42,"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"}]} +{"den":1,"g":43,"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"}]} +{"den":1,"g":44,"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"}]} +{"den":1,"g":45,"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"}]} +{"den":1,"g":46,"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"}]} +{"den":1,"g":47,"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"}]} +{"den":1,"g":48,"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"}]} +{"den":1,"g":49,"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"}]} +{"den":1,"g":50,"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"}]} +{"den":1,"g":51,"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"}]} +{"den":1,"g":52,"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"}]} +{"den":1,"g":53,"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"}]} +{"den":1,"g":54,"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"}]} +{"den":1,"g":55,"num":25,"projection":[]} +{"den":1,"g":56,"num":25,"projection":[]} +{"den":1,"g":57,"num":25,"projection":[]} +{"den":1,"g":58,"num":25,"projection":[]} +{"den":1,"g":59,"num":25,"projection":[]} +{"den":1,"g":60,"num":25,"projection":[]} +{"den":1,"g":61,"num":25,"projection":[]} +{"den":1,"g":62,"num":25,"projection":[]} +{"den":1,"g":63,"num":25,"projection":[]} +{"den":1,"g":64,"num":25,"projection":[]} +{"den":1,"g":65,"num":25,"projection":[]} +{"den":1,"g":66,"num":25,"projection":[]} +{"den":1,"g":67,"num":25,"projection":[]} +{"den":1,"g":68,"num":25,"projection":[]} +{"den":1,"g":69,"num":25,"projection":[]} +{"den":1,"g":70,"num":25,"projection":[]} +{"den":1,"g":71,"num":25,"projection":[]} +{"den":1,"g":72,"num":25,"projection":[]} +{"den":1,"g":73,"num":25,"projection":[]} +{"den":1,"g":74,"num":25,"projection":[]} diff --git a/wai/vector-conformance/expected/a11y/worked-example.json b/wai/vector-conformance/expected/a11y/worked-example.json new file mode 100644 index 000000000..70af187c7 --- /dev/null +++ b/wai/vector-conformance/expected/a11y/worked-example.json @@ -0,0 +1 @@ +{"blocks":["50987f1d37f03a0be0501013f423fed3bd083543f4c1ed1de2d37e075c46b752","fe8c37c546452f6f7912e5a5469db18061fd6828f0ed3f4d7f0df412386f13fc","7f0681341d7be2983e71b1441be5f9a1f574640fa298838418b967d1c9610c5c"],"code":"accepted","instr":"c00c5d7926ccfdea223c94e11f71eff3f720dd26fbe9bd44f8b391042981473e","rates":[[25,1,4800,0,74]],"sizes":[[640,360,7393573]],"text_hash":"70d5ce883f06a8aedd33bfa1fbc638e69909a380c19007a252584a7d38e0d432","text_record":"c7d4d5aa901ca7a25aef195f1c186b205dcb0734b6f0b2a8e8115333090b9af2"} diff --git a/wai/vector-conformance/expected/canonical/groups-with-ungrouped.json b/wai/vector-conformance/expected/canonical/groups-with-ungrouped.json new file mode 100644 index 000000000..491fd3b42 --- /dev/null +++ b/wai/vector-conformance/expected/canonical/groups-with-ungrouped.json @@ -0,0 +1 @@ +{"blocks":["621e4d7b8298eccc9a7064872192109d9ccfa13a442235357a3bb05a3861badd","621e4d7b8298eccc9a7064872192109d9ccfa13a442235357a3bb05a3861badd","621e4d7b8298eccc9a7064872192109d9ccfa13a442235357a3bb05a3861badd"],"code":"accepted","instr":"c17c22d2644a5ef2f9f517774014750a92cd237627f3e83ce89c29b69e4e22a2","rates":[[25,1,4800,0,24]],"sizes":[[640,360,245472]],"text_hash":"a1e113ae2bd73a400f060fc215d85ea6afd4a7b7df3084aa53159b3c476ca21f"} diff --git a/wai/vector-conformance/expected/canonical/ruby-ends-mid-character.json b/wai/vector-conformance/expected/canonical/ruby-ends-mid-character.json new file mode 100644 index 000000000..aca015774 --- /dev/null +++ b/wai/vector-conformance/expected/canonical/ruby-ends-mid-character.json @@ -0,0 +1 @@ +{"code":"payload_malformed"} diff --git a/wai/vector-conformance/expected/canonical/ruby-names-later.json b/wai/vector-conformance/expected/canonical/ruby-names-later.json new file mode 100644 index 000000000..aca015774 --- /dev/null +++ b/wai/vector-conformance/expected/canonical/ruby-names-later.json @@ -0,0 +1 @@ +{"code":"payload_malformed"} diff --git a/wai/vector-conformance/expected/canonical/ruby-to-string-end.json b/wai/vector-conformance/expected/canonical/ruby-to-string-end.json new file mode 100644 index 000000000..6fd00b90a --- /dev/null +++ b/wai/vector-conformance/expected/canonical/ruby-to-string-end.json @@ -0,0 +1 @@ +{"blocks":["4945d802da2fbb68f3e726d726375e774435c17a41f19ee263c64849105aa959","06861e0fa3ebbdcf8a61591543447c7e912c42423c37d2abcc035bfdd8ed09d6"],"code":"accepted","instr":"8a50cfa0381f22bd5cbc2069c26f60adf521f42eb9f1723d196030ff43d0907f","rates":[[25,1,4800,0,24]],"sizes":[[640,360,258277]],"text_hash":"489942ecb8278f3462c0ee9a5ee3261a509f3a17d9f638372298514f363d11d4"} diff --git a/wai/vector-conformance/expected/refusals/s4-path-before-text.json b/wai/vector-conformance/expected/refusals/s4-path-before-text.json new file mode 100644 index 000000000..aca015774 --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s4-path-before-text.json @@ -0,0 +1 @@ +{"code":"payload_malformed"} diff --git a/wai/vector-conformance/expected/refusals/s4-text-before-item.json b/wai/vector-conformance/expected/refusals/s4-text-before-item.json new file mode 100644 index 000000000..03af551ba --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s4-text-before-item.json @@ -0,0 +1 @@ +{"code":"field_range"} diff --git a/wai/vector-conformance/expected/refusals/s4-text-ruby-len-no-ruby.json b/wai/vector-conformance/expected/refusals/s4-text-ruby-len-no-ruby.json new file mode 100644 index 000000000..03af551ba --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s4-text-ruby-len-no-ruby.json @@ -0,0 +1 @@ +{"code":"field_range"} diff --git a/wai/vector-conformance/expected/refusals/s5c-typing-before-strings.json b/wai/vector-conformance/expected/refusals/s5c-typing-before-strings.json new file mode 100644 index 000000000..03af551ba --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s5c-typing-before-strings.json @@ -0,0 +1 @@ +{"code":"field_range"} diff --git a/wai/vector-conformance/expected/refusals/s5g-block-before-sets.json b/wai/vector-conformance/expected/refusals/s5g-block-before-sets.json new file mode 100644 index 000000000..aca015774 --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s5g-block-before-sets.json @@ -0,0 +1 @@ +{"code":"payload_malformed"} diff --git a/wai/vector-conformance/expected/refusals/s5h-decorative-before-sets.json b/wai/vector-conformance/expected/refusals/s5h-decorative-before-sets.json new file mode 100644 index 000000000..aca015774 --- /dev/null +++ b/wai/vector-conformance/expected/refusals/s5h-decorative-before-sets.json @@ -0,0 +1 @@ +{"code":"payload_malformed"} diff --git a/wai/vector-conformance/vectors/a11y/alternatives.json b/wai/vector-conformance/vectors/a11y/alternatives.json new file mode 100644 index 000000000..f485e81ca --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/alternatives.json @@ -0,0 +1,86 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":2,"n_texts":3,"n_tracks":4,"n_paths":4,"n_items":5,"n_keys":4,"n_glyphs":4,"n_clusters":4,"path_contours":4,"path_points":22,"strings_len":25,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":1307915} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255], +[220,40,40,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":1} +], +"keys":[ +[0,4096,0,[0,0,0]], +[0,76800,0,[0,0,0]], +[0,102400,0,[0,0,0]], +[0,128000,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":10,"zero@4":0,"xmin":0,"ymin":-60,"xmax":240,"ymax":100}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":64,"ymax":64}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":32,"ymax":32}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":16,"ymax":16} +], +"path_ends":[9,3,3,3], +"path_points":[ +[0,0,0], +[60,100,0], +[120,0,0], +[180,100,0], +[240,0,0], +[200,0,0], +[180,40,0], +[120,-60,0], +[60,40,0], +[40,0,0], +[0,0,0], +[64,0,0], +[64,64,0], +[0,64,0], +[0,0,0], +[32,0,0], +[32,32,0], +[0,32,0], +[0,0,0], +[16,0,0], +[16,16,0], +[0,16,0] +], +"texts":[ +{"purpose":10,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":3}, +{"purpose":11,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":12}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":4} +], +"items":[ +{"kind":2,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":72000,"unit_scale":16777216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":1,"paint":1,"text":1,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":48000,"t_off":120000,"unit_scale":16777216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":1,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":2,"paint":0,"text":2,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":24000,"unit_scale":16777216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":2,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":3,"paint":1,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":16777216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":3,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":2,"zero@10":0,"n_glyphs":4,"n_clusters":4,"t_on":96000,"t_off":120000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[66,0,4096,49152], +[111,0,6596,49152], +[116,0,8836,49152], +[104,0,10276,49152] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1] +], +"strings":"enWAIenA red squareenBoth", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/alternatives.wvg1 b/wai/vector-conformance/vectors/a11y/alternatives.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..88c5f945d5266335974f07970b6bb4b0826dde5e GIT binary patch literal 827 zcmZ`%KTpC?5Wm+#h~PkCVnST1F^0j#m@qLE29d;BOpJpGH4LUGU|`WO_yPO?egsE0 zmkv$P&4JPHAcM~Je(zqJQsX_|-S6(*pWbnu)7F6^qF@0oa%%My6IoU zy}}oC_$;x&h%NnW=(pI4_k8R6=Rb2Qd+o{m<9so1j%)7`(`U6B zS)2oL|4G!KvgX3}<0`B^rSP|4MMdKrMoa2)Zge8#lDncivM6Kc>jHphG_-Dw4L>(=_iHm4v1QgQ|>Ip9D{-Fq=eko&HXImj-LvNT|mfVV- zhQgFVM8e?*f2EH^O<$93Nc>gCA2I1V!G(@Z{H=_?&G}I^CbMmczn$_ov)1{seKb~P zJ0+`bKr_kKguvheqCSfdYsq-T;&DfCkHs$jl|Z(O5r5I&i=JXTz603vJB_yIH=Kdj Qb%wWhmjlo9oBdJm7YDF|)c^nh literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/clipped.json b/wai/vector-conformance/vectors/a11y/clipped.json new file mode 100644 index 000000000..96a1d3f8f --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/clipped.json @@ -0,0 +1,162 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":50000,"dur":94000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":6,"n_tracks":0,"n_paths":0,"n_items":7,"n_keys":0,"n_glyphs":59,"n_clusters":59,"path_contours":0,"path_points":0,"strings_len":71,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":6611664} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":13}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":10}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":8}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":12}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":7}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":9} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":13,"n_clusters":13,"t_on":0,"t_off":72000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":10,"n_clusters":10,"t_on":120000,"t_off":240000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":2,"zero@10":0,"n_glyphs":8,"n_clusters":8,"t_on":0,"t_off":2147483648,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":3,"zero@10":0,"n_glyphs":12,"n_clusters":12,"t_on":0,"t_off":50000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":4,"zero@10":0,"n_glyphs":7,"n_clusters":7,"t_on":144000,"t_off":200000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":5,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":24000,"t_off":60000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":5,"zero@10":0,"n_glyphs":6,"n_clusters":6,"t_on":60000,"t_off":150000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[115,0,4096,9216], +[116,0,5976,9216], +[97,0,7416,9216], +[114,0,9576,9216], +[116,0,11096,9216], +[115,0,12536,9216], +[32,0,14416,9216], +[98,0,15616,9216], +[101,0,17856,9216], +[102,0,20016,9216], +[111,0,21416,9216], +[114,0,23656,9216], +[101,0,25176,9216], +[101,0,4096,15872], +[110,0,6256,15872], +[100,0,8496,15872], +[115,0,10736,15872], +[32,0,12616,15872], +[97,0,13816,15872], +[102,0,15976,15872], +[116,0,17376,15872], +[101,0,18816,15872], +[114,0,20976,15872], +[115,0,4096,22528], +[112,0,5976,22528], +[97,0,8216,22528], +[110,0,10376,22528], +[115,0,12616,22528], +[32,0,14496,22528], +[105,0,15696,22528], +[116,0,16656,22528], +[111,0,4096,29184], +[118,0,6336,29184], +[101,0,8416,29184], +[114,0,10576,29184], +[32,0,12096,29184], +[98,0,13296,29184], +[121,0,15536,29184], +[32,0,17616,29184], +[116,0,18816,29184], +[104,0,20256,29184], +[101,0,22496,29184], +[110,0,24656,29184], +[110,0,4096,35840], +[111,0,6336,35840], +[116,0,8576,35840], +[32,0,10016,35840], +[121,0,11216,35840], +[101,0,13296,35840], +[116,0,15456,35840], +[116,0,4096,42496], +[119,0,5536,42496], +[111,0,8536,42496], +[32,0,10776,42496], +[112,0,11976,42496], +[97,0,14216,42496], +[114,0,16376,42496], +[116,0,17896,42496], +[115,0,19336,42496] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1] +], +"strings":"enstarts beforeenends afterenspans itenover by thenennot yetentwo parts", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/clipped.wvg1 b/wai/vector-conformance/vectors/a11y/clipped.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..fa98d6e37f97caaf6b10377e57878430c0fa0454 GIT binary patch literal 2083 zcmcJPPis>_6vc1SrfF*e-H0i=n1KHUO)b`fRqLOc21O%68&DxBc_9UP3CW`|3nj}g z{0J_(5(O82gn*0aE{lGE;!50@Wp;Yb+`KfDU~rKc-ke|W+_`gR^7=M6k_!P5Y3>Ik z{Z%AU3P>YZ44_FsrZ6KCwxh@mC?CHid$9PP6|`|}`$t~p@uwfF3F`naSy&1$N@c1n% z+*M?Cp6-J}Ux&?3_4AoAi_W`id}5V<=ks*a=)Kk$mCuS1bTCvh{yWnfn&8j%(@v;GQz;H&V*<(0Qudzmp91M9AI=m^EXD0rj(cUYYrJU&J}R zx2*APNSgP6IQN~qqs+70b79jRU^9jROg{%knSRnd7l5JFE6Ti@&Z2T1@?}NYK-%|} zxtBc`))v9$iN-!3cXAiLL>_~!dD z{+IbYIl!R-o*H0&gZzAc34ERzV1DEL_^|*br5&eYw(7z! G<$nRipFgJp9vH>9oK>WnS05Tf^z*113LEOm705X+0^&l4#qdpj9ASkd@7#Na) z)E@~5J0D1$QGl?EfK-nrgq;ecYV;xOVj%U#9KzNDQeboJ7#I|QOb&4%0c3;3cSu9n z3P9?B5`+yBFHncDje!(cZv;f|0gxMjY+WE#APZqD0I39J2-^@y31~pru($)!=ol0R yFfmZL!`LiHY*2{8#DPpW8zu&#i7|&v6~US`g3?wotyeKXU(Se%}Sh{)bP z&#GI|w-F49NVkjWS>!gZcmyMT4wKuO%S+~t6|&L~4x+ptpZ1?G9aWYaYl-yC`<>a$ zz`LR0+-9TsYQOPpBmMmpBPP*S4r_7Ui?Ke$67Ypb4=$o^wxfxk#Nxk*#oB3DQ+K=d zjlrm}ul7)5;OGAOFgY&cXV*QL^giilcIWzBCxJu2GU zB#xURdC16hQ))5+pOKY#}ET z%0_K*3Dpw)?Yvmlo^{^yDfAT6$V0xN^%Is~8`$C+1 zIp9YRUWYXAEAu%VKUDT1Z=NboL0o+#vS+tPF#Fl%9>H9zeoLA0!8^+A=f^$e70C7j z<<@iiAj0S4YJ$0UR};+KI{O53-OQx&9K`kWS^ZxJa1E|NHt~MQ?4ijXzsLaBf!5UNq%9O<#IDo@ z3zl85;16)orHL-;3f-|_;j&BP#{LI-&b#wqBo(@Z*L2Qr?%ez4+&8b&hYyC&gp^X1 zRUSo)P00|33aPM;wd$i%F(|)gJoce~QEGY0+QHHn)eSaLUJnnaZ^VD+CYQaIftzz5 zM+XnT={Y_!yS(ybb@^&x;QJqp7S-g>DAEqu80)MP+g~TvRwveh*e-m{h^UCh$}>un zqKEY%SsU}2Tl~CrK6`J)hhu4;$Ia(wKK*e1v0q!~bEU%m^J&CC^Ev<52K}?|-T3HH zpu5feGvD}cWPO^EPyf+w%-z8JCZB#9>6iJ9_{noZZnR@cWgtt3gr^~6M}@~B)(PPx zq}C^#f|Sn-yAbb^@D#)z61E`UMuiKI**n6lwYl$bU)I*fgeM?nUiAHVSz*@NuZ44v zpYMd}S^K}N)C{oFg#*}uR4rjn5j!cIhD1*bCm^a{*n?>Qs3mLvU|OlsKbSttF=6)T zJrcGck>|qn`Ds#^wc5X5YttZ@J=W%g>A(D5n6=)5FnjD>6xKf5!Ey2Cu}3iDc8@T7 zTw;%i^O-KNN3aF4&k1w>>IGqXD_s|6U-1!Pdh5C?Om9EIRlFz6+UQeZTG2Ph z{H(3KlsG-iz7pnh*FNWE-71|p2h*EWnEBPS!u%~1FAB3Z zdQF%<%eRCrh<9I@ITrVf?_q%t(7%^@A)JQP*bw6Ez04-Sto7KOpI?0|9D;CAH-7of zGcmrO3^xZj9ALifOioLH`7dGOdjh;Szr8`M-T$Z=X1BJ~KU@Ou185A>~eF3OUEk%sAO}!t?$K hCcH#8znfDWySyi literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/modes.json b/wai/vector-conformance/vectors/a11y/modes.json new file mode 100644 index 000000000..bf37bbb71 --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/modes.json @@ -0,0 +1,55 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":24000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":3,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":9,"n_clusters":9,"path_contours":0,"path_points":0,"strings_len":15,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":1445598} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":2,"mode":1,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":3}, +{"purpose":7,"dir":0,"lang_len":2,"mode":2,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":3}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":3} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":2,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[87,0,141660,15360], +[65,0,142080,18760], +[73,0,142840,22160], +[87,0,8540,15360], +[65,0,8960,18760], +[73,0,9720,22160], +[87,0,4096,75776], +[65,0,7496,75776], +[73,0,10056,75776] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1] +], +"strings":"enWAIenWAIenWAI", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/modes.wvg1 b/wai/vector-conformance/vectors/a11y/modes.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..3f6f8d567815b437665f17d660c3c9af05662cec GIT binary patch literal 575 zcmWF!b2nsUU|=|K1w;brSO$g$rVK_PU|?im28%&R1_n+b4}@WWAIdPE1JMiOg7n_w z6JwABvSFeic~x4XK>(;mgAr_whb)8* jGRH$5!iEJJh(^aC_rk=0OgI}R2BJw(lbRRq=!qZz$O~7p literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/nested-groups.json b/wai/vector-conformance/vectors/a11y/nested-groups.json new file mode 100644 index 000000000..8e1be2476 --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/nested-groups.json @@ -0,0 +1,215 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":240000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":5,"n_tracks":0,"n_paths":0,"n_items":11,"n_keys":0,"n_glyphs":84,"n_clusters":84,"path_contours":0,"path_points":0,"strings_len":94,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":7588831} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":18}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":14}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":14}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":27}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":11} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":65535,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":24000,"t_off":96000,"unit_scale":0,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":18,"n_clusters":18,"t_on":0,"t_off":240000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":0,"rule":0,"zero@2":0,"ref":65535,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":48000,"t_off":200000,"unit_scale":0,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":14,"n_clusters":14,"t_on":0,"t_off":60000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":2,"zero@10":0,"n_glyphs":14,"n_clusters":14,"t_on":72000,"t_off":240000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]}, +{"kind":0,"rule":0,"zero@2":0,"ref":65535,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":96000,"t_off":120000,"unit_scale":0,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":3,"zero@10":0,"n_glyphs":27,"n_clusters":27,"t_on":0,"t_off":240000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":4,"zero@10":0,"n_glyphs":11,"n_clusters":11,"t_on":100000,"t_off":130000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[105,0,4096,9216], +[110,0,5056,9216], +[32,0,7296,9216], +[116,0,8496,9216], +[104,0,9936,9216], +[101,0,12176,9216], +[32,0,14336,9216], +[111,0,15536,9216], +[117,0,17776,9216], +[116,0,20016,9216], +[101,0,21456,9216], +[114,0,23616,9216], +[32,0,25136,9216], +[103,0,26336,9216], +[114,0,28536,9216], +[111,0,30056,9216], +[117,0,32296,9216], +[112,0,34536,9216], +[105,0,4096,15872], +[110,0,5056,15872], +[32,0,7296,15872], +[98,0,8496,15872], +[111,0,10736,15872], +[116,0,12976,15872], +[104,0,14416,15872], +[32,0,16656,15872], +[103,0,17856,15872], +[114,0,20056,15872], +[111,0,21576,15872], +[117,0,23816,15872], +[112,0,26056,15872], +[115,0,28296,15872], +[105,0,4096,22528], +[110,0,5056,22528], +[32,0,7296,22528], +[98,0,8496,22528], +[111,0,10736,22528], +[116,0,12976,22528], +[104,0,14416,22528], +[44,0,16656,22528], +[32,0,17536,22528], +[108,0,18736,22528], +[97,0,19696,22528], +[116,0,21856,22528], +[101,0,23296,22528], +[114,0,25456,22528], +[105,0,4096,29184], +[110,0,5056,29184], +[32,0,7296,29184], +[97,0,8496,29184], +[32,0,10656,29184], +[103,0,11856,29184], +[114,0,14056,29184], +[111,0,15576,29184], +[117,0,17816,29184], +[112,0,20056,29184], +[32,0,22296,29184], +[116,0,23496,29184], +[104,0,24936,29184], +[97,0,27176,29184], +[116,0,29336,29184], +[32,0,30776,29184], +[110,0,31976,29184], +[101,0,34216,29184], +[118,0,36376,29184], +[101,0,38456,29184], +[114,0,40616,29184], +[32,0,42136,29184], +[111,0,43336,29184], +[112,0,45576,29184], +[101,0,47816,29184], +[110,0,49976,29184], +[115,0,52216,29184], +[105,0,4096,35840], +[110,0,5056,35840], +[32,0,7296,35840], +[110,0,8496,35840], +[111,0,10736,35840], +[32,0,12976,35840], +[103,0,14176,35840], +[114,0,16376,35840], +[111,0,17896,35840], +[117,0,20136,35840], +[112,0,22376,35840] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,22,1], +[1,0,23,1], +[1,0,24,1], +[1,0,25,1], +[1,0,26,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1] +], +"strings":"enin the outer groupenin both groupsenin both, laterenin a group that never opensenin no group", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/nested-groups.wvg1 b/wai/vector-conformance/vectors/a11y/nested-groups.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..e459ff4a1de9124c29c0600641bdca24f1411a9c GIT binary patch literal 2878 zcmeH}O-NKx7>3XIla^^V{b{y1LWqIXRA94^vZx$EMCeDs3db0|hRh7k__HXPP0}Kw zRk(<7(L$kxw5-iQ3k^53aM7-oLz`R*E$n^IchBu0Qy4_G=niw<=YHp$@4nx6PhoIpW!L!C zfiIbnm2_2mZ1z=8N7IAmmhPFA!r=PKrRnzfTezxJnV(T44A~y5+hMG!#5!9CpS86m z&&c0~PK#5C+fnArMa&phbR<1e@M>3znkh55_qn%9b=FFjQtNGgI;g{k*U)E=f5`HipbQ`_X#)4w!MLm+POsv1j9F{^_d;A6`5|zst|B`nUb5fP)%*7Un{#@ReCg z_j3$0svh6tN+lur1Hu}TJ0_fkM4E*?NTEeI0eN~#SVL4)I0aem5T1mjuLzqwy~5mE zxF$RS>AWSZA(3I>QOKteVXm2BV)&$ZObn*9&I+d?#W~@iuJcY^V2x>07nqMRb%B|4 z;{*mUeJ-C7X1#r^%imjnQJCKHtP62^8(>|2o)~{5!1Pwle_vpFTV>JU4CE1;4Gw$` zI6lD|)5a$_@Cgolf&-u6z$ds3dyP-9$#WS2@HiyWEgXkz^awN0&<$Z)RD;5-D}7fu z@QGk>PQV(|#wVCLO+Wqk0{aPOuli03<5m^hg;~o7o)ZMwt9kYl%wCy(f;G6IU*hz+ zN^gj>UXKq1Q(Er{bIm+rjB7}AMwp(9_k~&G;zQvnNW+uw@#r&QrdWI-tReF+h3PrA zBut;xuZ5X&^^Gu}6@4encfGkL9Q5$9(?hVvwCSNAH$4Og@7iBu&Lo)q96E;qOrM(< zgt@-R9wN?uF7^u3r^g;59)j?qZvXN()yDWcV!6WMN{4qjT;*`J!@C{c<1qgg+5CGQ zu64N1;e8I*JG|dvV35=1rv7Vn{r4&F>2IBruyaoM=gyv$gAN~Z_^`uA9By#f{(q0T zjyiE*kl#M%PoBv{axAH{UP7lPvxx~kI+2=8o7iwF>&a~bM*tG z@4t-=URb@_^`x-9JGHm{Xl3C05mrdmI2px6U}}6?9kg1X)&wo=)9Rr`d|DJ*olj$L z^*)XJMvu`Nj?o%@S{SiSK8>|B`!xEs__Ri7tv;<5+Kx|S?9nkAeccs7_gp+rPLcDi zENYyiES^&x9@SKZs6RVxckXJC`q^nU^O3t0e4ebln*0s$ z598qqQa|rWHS?6ekKp;6k-rfSSCD#I;8*M1rXc>U@UN!dwt)V5_~~nKU8yv1<1Aia z>a+Yn0%u^<6=4&mbWJz`qhlW^WdnE5;RV({mxPlru`XfeKe#Q-{F}YPIzK;DCY8#< zl+FmRz!WbEGiSV8n0tMV3D3Z+4+t}73i~rp3MM)%%zbypgc-Z`OqjJ5CWOnqz`=l| zdjaRbL!Eem^{j3P>shfE=&X6RSJ;B-yeGT}qv!KcsU_ef=L4RD>F5+@&VBX;ojD84 z38o%rPH+Zh|E@6Su+IF@=V5gIFH%E+X9q?}Dt=j*eRbmusWbNQrZ9U?^$Bx^-S>rA z{~_lFf6ifRT$nk#IV&fB$7^BwMA;B@)?9ca%w9S;Jm||X`y2*X*YHl5ed+p3vX?%0 zFw@0&?qK>KuzoQ8XKz(l$Nq5c8}AHw0Y=AyS=$a{!HkVF*3s2qsC>6`!@}GvHY&`1 zH=YTzuTK*de;tc^rtj)Q=wR+!WUQliJg?~5A9s>D*SQzi0Ixk2=9ms&2=m@)bsW}eN2 zF!Oh2gjrkZtuW8GxFpQ{8>_jam5N=o&*KH!+^=3!vj$Mp?5^ZzCGBz1cKMox0C zPNN3zJ^a%>39@exAMfW+HU6fRPvGh(#Pta;}-G0IPG`=yO~Gb8Xp_!T17R_isKie zuN-$OH#_+s2$&g|K+53&EfGBeVQ361$!oZLZq-ID! z*g$77L?}YoMLlHb{Jj351;pq*ho#*d;(p!WqI&1yVaaAnalwr4a~W v!$Jl`qhpY-U}7Nuz}PHEY*r*T8xorxiOqq;=7h3S^W5@Fixi3yOHzvg!DK#w literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/ruby.json b/wai/vector-conformance/vectors/a11y/ruby.json new file mode 100644 index 000000000..c4f9ea8dc --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/ruby.json @@ -0,0 +1,55 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":2,"n_tables":0,"n_paints":1,"n_texts":3,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":10,"n_clusters":7,"path_contours":0,"path_points":0,"strings_len":36,"inline_len":0,"held_len":165402,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":1990795} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"}, +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":15}, +{"purpose":7,"dir":0,"lang_len":7,"mode":0,"group":0,"ruby":1,"ruby_start":0,"ruby_len":9,"str_len":3}, +{"purpose":7,"dir":0,"lang_len":7,"mode":0,"group":0,"ruby":1,"ruby_start":9,"ruby_len":6,"str_len":2} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":5,"n_clusters":2,"t_on":0,"t_off":96000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":1,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":48000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":2,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":48000,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[2367,0,4096,17920], +[2361,0,4896,17920], +[2306,0,6416,17920], +[2342,0,6896,17920], +[2368,0,8776,17920], +[104,0,4096,9216], +[105,0,5216,9216], +[110,0,5696,9216], +[100,0,7680,9216], +[105,0,8800,9216] +], +"clusters":[ +[3,0,0,9], +[2,0,9,6], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,0,1], +[1,0,1,1] +], +"strings":"hiहिंदीhi-Latnhinhi-Latndi", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/ruby.wvg1 b/wai/vector-conformance/vectors/a11y/ruby.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..6b0dc0496d597e2ef049feaa6dece4c3476cf516 GIT binary patch literal 624 zcmWF!b2nsUU|=|K1w=yV2Br)~Mn)zEMh0fE7=&bC-~#g4!6Fbs1AhQtwqynNIBn!gKP~8o?K{AYZ)r0I}0z!~l zNZ4XmZx3>u01&$|FjxZF3c?UJ6Ob(c3Phl|8j$@#3c_{(vOSa_>$piqIqjc;5 literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/strings.json b/wai/vector-conformance/vectors/a11y/strings.json new file mode 100644 index 000000000..fce90704c --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/strings.json @@ -0,0 +1,153 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":24000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":2,"n_tables":0,"n_paints":1,"n_texts":6,"n_tracks":0,"n_paths":0,"n_items":9,"n_keys":0,"n_glyphs":54,"n_clusters":52,"path_contours":0,"path_points":0,"strings_len":97,"inline_len":0,"held_len":165402,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":11169106} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"}, +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":26}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":7}, +{"purpose":7,"dir":1,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":8}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":18}, +{"purpose":7,"dir":0,"lang_len":10,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":12} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":22,"n_clusters":22,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":1,"zero@10":0,"n_glyphs":1,"n_clusters":1,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":2,"zero@10":0,"n_glyphs":4,"n_clusters":4,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":3,"zero@10":0,"n_glyphs":4,"n_clusters":2,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":4,"zero@10":0,"n_glyphs":6,"n_clusters":6,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":5,"zero@10":0,"n_glyphs":5,"n_clusters":5,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":5,"zero@10":0,"n_glyphs":6,"n_clusters":6,"t_on":0,"t_off":24000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[72,0,4096,9216], +[101,0,6696,9216], +[32,0,8856,9216], +[115,0,10056,9216], +[97,0,11936,9216], +[105,0,14096,9216], +[100,0,15056,9216], +[32,0,17296,9216], +[34,0,18496,9216], +[65,0,20016,9216], +[38,0,22576,9216], +[66,0,25176,9216], +[34,0,27676,9216], +[32,0,29196,9216], +[60,0,30396,9216], +[98,0,32596,9216], +[62,0,34836,9216], +[92,0,37036,9216], +[60,0,38636,9216], +[47,0,40836,9216], +[98,0,42436,9216], +[62,0,44676,9216], +[50,0,4096,15872], +[47,0,6336,15872], +[50,0,7936,15872], +[79,0,4096,22528], +[75,0,6936,22528], +[32,0,9336,22528], +[57345,0,10536,22528], +[1501,0,4096,29184], +[1493,0,6216,29184], +[1500,0,7416,29184], +[1513,0,9416,29184], +[2367,0,4096,42496], +[2361,0,4896,42496], +[57344,0,6896,42496], +[2368,0,9496,42496], +[90,0,4096,49152], +[100,0,6396,49152], +[114,0,8636,49152], +[97,0,10156,49152], +[118,0,12316,49152], +[111,0,14396,49152], +[115,0,4096,55808], +[111,0,5976,55808], +[102,0,8216,55808], +[116,0,9616,55808], +[45,0,11056,55808], +[104,0,4096,62464], +[121,0,6336,62464], +[112,0,8416,62464], +[104,0,10656,62464], +[101,0,12896,62464], +[110,0,15056,62464] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,23,1], +[1,0,24,1], +[1,0,25,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,4], +[1,0,6,2], +[1,0,4,2], +[1,0,2,2], +[1,0,0,2], +[2,0,0,6], +[2,0,6,12], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,0], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1] +], +"strings":"enHe said \"A&B\" \\\n2/2enOK 😀heשלוםhiहिन्दीsr-Latn-RSZdravoensoft­hyphen", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/strings.wvg1 b/wai/vector-conformance/vectors/a11y/strings.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..6b5f598c31cd60d4955c567ba6e69012d9800c81 GIT binary patch literal 2101 zcmb`IPe@cz7{$Nwe`aN6W@Y%YvLZ~gFpI&|peED=ixMkn%#jgn6h|!Sp9_mNk(q`TNf}<5hHVpbCc}Ls! zhELI!iH_9Lz^Tz1|B{o-3U(fwc(MQGMAgyKCoJ&(gUIsCo6S?tYtJmLJ&|_*SytY? zg%=XPKfQB2`Aym3cRhXWKQL0|_w~4LF^=h8Dm~nTJ<|S=o9B7ycFeo$>Gd*@NVHPf zlI@+bUxMGm-}QTF(9S2uR>zZP-{Q?PtA}>`Z}pjCSCa7Ih$qkcNQ(9RZ+X^a<&@a7 z6Hh)F`DA=J;?2iXKfr>SI97;+AglSHfE~y{zH$^2Sfw0ARxLwU36Cxwh}NatXxi15D<}b1;EDKTo+1;$`r=tARO7Fo7M&G29>7=c0#e zl&82|>@Du$6*$0};J4tfxym2FkGU59))`RdM?^M*o%zb*ihSn2z^{Oxvr@Sc;^oIh z*#B>=4@M~&Tc*tY8s*-Ni?DsU#memvf0;7(*Q@hdgnq+9OyCwsj-%WK>F0If7eb2H zDu*H7+AktKK(BA$4zTNB0yA@!JpqRxhf0(q5bv3R?*nsCrymKH$G|wD&O%pl^^gIHI=8A6qSS`b=A)2D+9gZ(B$>W!O4r0m&2{@&|P=vfje}~9X{_4 mjkv?T(ayq}U{|EDes^PYXRy0H6p6OCbUnNk?&%1JB0m9kL@#jw literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/windows.json b/wai/vector-conformance/vectors/a11y/windows.json new file mode 100644 index 000000000..180427d78 --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/windows.json @@ -0,0 +1,91 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":240000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":2,"n_tracks":1,"n_paths":0,"n_items":2,"n_keys":3,"n_glyphs":25,"n_clusters":25,"path_contours":0,"path_points":0,"strings_len":29,"inline_len":0,"held_len":105523,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":2995585} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[ +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":3} +], +"keys":[ +[0,255,0,[0,0,0]], +[72000,255,1,[0,0,0]], +[86400,0,0,[0,0,0]] +], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":1,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":13}, +{"purpose":2,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":12} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":13,"n_clusters":13,"t_on":24000,"t_off":96000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":0,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":12,"n_clusters":12,"t_on":120000,"t_off":216000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[79,0,4096,9216], +[112,0,6936,9216], +[101,0,9176,9216], +[110,0,11336,9216], +[105,0,13576,9216], +[110,0,14536,9216], +[103,0,16776,9216], +[32,0,18976,9216], +[116,0,20176,9216], +[105,0,21616,9216], +[116,0,22576,9216], +[108,0,24016,9216], +[101,0,24976,9216], +[87,0,4096,15872], +[114,0,7496,15872], +[105,0,9016,15872], +[116,0,9976,15872], +[116,0,11416,15872], +[101,0,12856,15872], +[110,0,15016,15872], +[32,0,17256,15872], +[104,0,18456,15872], +[101,0,20696,15872], +[114,0,22856,15872], +[101,0,24376,15872] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1] +], +"strings":"enOpening titleenWritten here", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/windows.wvg1 b/wai/vector-conformance/vectors/a11y/windows.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..8183817d60f426f47ba49076a991b7fb2c3ad35c GIT binary patch literal 949 zcmc(dyGjF55QZlkW1=QtnGl2(C6$5~3_(P^fLSjX1u+p3u~^+eSeCdD8zpv@mKHvM zf~|eJXqO^|Z((Z}=RcF3n1) z`N6%kwh{fE-|G)p!l6<2Ng|IWofWoOJ|=`+7Hdkl%92L7w`k-U9*v3#Y=kQyBEG;~Ud4+_vU{5XVA17~*({6CoZ7@o7J33IG5A literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/a11y/worked-example.json b/wai/vector-conformance/vectors/a11y/worked-example.json new file mode 100644 index 000000000..bfa64f500 --- /dev/null +++ b/wai/vector-conformance/vectors/a11y/worked-example.json @@ -0,0 +1,108 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":360000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":2,"n_tables":0,"n_paints":1,"n_texts":3,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":35,"n_clusters":35,"path_contours":0,"path_points":0,"strings_len":49,"inline_len":0,"held_len":165402,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":7393573} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":59879,"sha256":"2310ab64ddcf4d851d4d7c08bbce141fba178f03fdcbb07b05db3863e179846f"}, +{"carriage":0,"zero@1":[0,0,0],"length":105523,"sha256":"05476c4658ef7366c7700736487eac4c3c1b7d21264978c7d05bd4c7427736c9"} +], +"tables":[], +"paints":[ +[255,255,255,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":3,"dir":1,"lang_len":2,"mode":0,"group":1,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":17}, +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":1,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":23}, +{"purpose":7,"dir":0,"lang_len":2,"mode":0,"group":1,"ruby":2,"ruby_start":0,"ruby_len":8,"str_len":3} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":9,"n_clusters":9,"t_on":24000,"t_off":264000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":1,"zero@10":0,"n_glyphs":23,"n_clusters":23,"t_on":24000,"t_off":264000,"unit_scale":131072,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":2,"zero@10":0,"n_glyphs":3,"n_clusters":3,"t_on":24000,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[65174,0,4096,69120], +[65268,0,6016,69120], +[65170,0,7056,69120], +[65247,0,8096,69120], +[65165,0,9136,69120], +[32,0,10096,69120], +[65167,0,11296,69120], +[65166,0,13216,69120], +[65169,0,14176,69120], +[68,0,4096,82432], +[105,0,6696,82432], +[114,0,7656,82432], +[101,0,9176,82432], +[99,0,11336,82432], +[116,0,13336,82432], +[111,0,14776,82432], +[114,0,17016,82432], +[32,0,18536,82432], +[111,0,19736,82432], +[102,0,21976,82432], +[32,0,23376,82432], +[112,0,24576,82432], +[104,0,26816,82432], +[111,0,29056,82432], +[116,0,31296,82432], +[111,0,32736,82432], +[103,0,34976,82432], +[114,0,37176,82432], +[97,0,38696,82432], +[112,0,40856,82432], +[104,0,43096,82432], +[121,0,45336,82432], +[68,0,4096,74752], +[79,0,5396,74752], +[80,0,6816,74752] +], +"clusters":[ +[1,0,15,2], +[1,0,13,2], +[1,0,11,2], +[1,0,9,2], +[1,0,7,2], +[1,0,6,1], +[1,0,4,2], +[1,0,2,2], +[1,0,0,2], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1], +[1,0,3,1], +[1,0,4,1], +[1,0,5,1], +[1,0,6,1], +[1,0,7,1], +[1,0,8,1], +[1,0,9,1], +[1,0,10,1], +[1,0,11,1], +[1,0,12,1], +[1,0,13,1], +[1,0,14,1], +[1,0,15,1], +[1,0,16,1], +[1,0,17,1], +[1,0,18,1], +[1,0,19,1], +[1,0,20,1], +[1,0,21,1], +[1,0,22,1], +[1,0,0,1], +[1,0,1,1], +[1,0,2,1] +], +"strings":"arباب البيتenDirector of photographyenDOP", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/a11y/worked-example.wvg1 b/wai/vector-conformance/vectors/a11y/worked-example.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..6ea15cfef05461eb611c52393126b92435f6c742 GIT binary patch literal 1273 zcma)*O=uHA6vy9WjY<5dRilY5r7M2*;H6Y*1rH?@8^O?A)It#&V$u}EUDtq(A2o+6 zB517#^&psAgLbzjb2!1keMRBXQ-{XCoh>%q$Y6T*@sWewm$vUd zJkxwLvD{3Z8+`E#%?fI3ZAlw}7ID4pmHF&x)!F^W;+1l~Ea3TB8nce_a( z$8%op2|WKB&%0W9I9|?H{WJfap9t(k?@;s|T;;;Sx52l%g+G96y}~!ZOMD54u_5jr z;d%J>Ug4V@KOnpSp4=}y0txm?DVhfMbzuN|kS{&L1&Gi2;pZV~&Iv972m7SjU^HW7)IN^&IEa95&e8m+R3nQG>Z=?XNMI-%I=J4CZ&& zeuu$)KHBHcSu_7PHE%L_v%ztL`S^AGR)c{pU(d7ZGs$^=-Cy+Uw!gSC=hs)R`wt7J bN2a|(zU+9mQ?%WZQ+B4joLj13Vl4X`DDA44 literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/canonical/groups-with-ungrouped.json b/wai/vector-conformance/vectors/canonical/groups-with-ungrouped.json new file mode 100644 index 000000000..585ede480 --- /dev/null +++ b/wai/vector-conformance/vectors/canonical/groups-with-ungrouped.json @@ -0,0 +1,43 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":3,"n_tracks":0,"n_paths":0,"n_items":3,"n_keys":0,"n_glyphs":3,"n_clusters":3,"path_contours":0,"path_points":0,"strings_len":9,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":245472} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[], +"paints":[ +[30,60,200,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":1,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":1}, +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":1}, +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":2,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":1} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":1,"n_clusters":1,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":1,"n_clusters":1,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":2,"zero@10":0,"n_glyphs":1,"n_clusters":1,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1,0,5000,10000], +[1,0,5000,15000], +[1,0,5000,20000] +], +"clusters":[ +[1,0,0,1], +[1,0,0,1], +[1,0,0,1] +], +"strings":"enAenAenA", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/canonical/groups-with-ungrouped.wvg1 b/wai/vector-conformance/vectors/canonical/groups-with-ungrouped.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..b7ef4405494e719649f29b7477a98cb5e0a507f5 GIT binary patch literal 979 zcmb7BzfTlF6n?Y2aI:VegURwU-`q87#&DM(C+$tff%(5}0~-Nw6Fva_eTKtVxq z9nfLz|6wRC2~iUZqex3h<0+o+n_=aM7W#JQ&HM4a@4cB>n4h^Rh-l{>wA#*eDT%S^~tSouOIF=4_7`s zJ^FMxIrw;d_1@c|PoBk>WT-`KRLalBHG)}`a;&A0Tfczgk`u+dG4^puC4QWJ3|h(rd*J6|4t6Wx^QppLU?0rqIuL$%p7hWE^em~Rw+U<6 zGQgZ*QrfLB73L-#b<~vU>7u2IG_FOV$+~&0rkYBtJStQopY`O+D6PFvagpWHZANll zF6CKAE@gSA+m1}wPGhCHa(X$6TdH=eej^N@v{ECpZqZJ)l13HGWJ^z|Q!NW1a payload_malformed", "groups":"groups nested four deep, clips under both rules, translations and opacities on groups: accepted", "groups-in-order":"rule 4: group numbers in order of first use, each group contiguous: accepted", +"groups-with-ungrouped":"rule 4: groups [1, 0, 2], an ungrouped block between two groups: accepted", "held":"a run from a held set, obtained by its SHA-256 and exact length: accepted", "minimal":"one path fill, the smallest payload: accepted", "paint-duplicate":"rule 3: two equal paints -> payload_malformed", @@ -19,9 +20,12 @@ "path-alternative-decorative":"step 5(h): a path whose text alternative is a decorative block -> payload_malformed", "path-duplicate":"rule 3: two paths with equal records, ends and points -> payload_malformed", "ruby":"rule 4: a ruby block annotating the first character of an earlier block: accepted", +"ruby-ends-mid-character":"rule 4: a ruby range ending inside a character -> payload_malformed", "ruby-mid-character":"rule 4: a ruby range starting inside a character -> payload_malformed", "ruby-names-itself":"rule 4: a ruby link to its own block -> payload_malformed", +"ruby-names-later":"rule 4: a ruby link to a later block -> payload_malformed", "ruby-past-string":"rule 4: a ruby range past the base block's string -> payload_malformed", +"ruby-to-string-end":"rule 4: a ruby range that starts inside the base block's string, on a character boundary, and ends where the string ends: accepted", "set-duplicate":"rule 3: two sets with one SHA-256 -> payload_malformed", "shared":"one path, one paint and one track each used by several items, each pool holding an entry once, in first-reference order: accepted", "sizes-rates":"five sizes from 1/8 to 4 times the design width and four rates, each ascending: accepted", diff --git a/wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.json b/wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.json new file mode 100644 index 000000000..29858cd14 --- /dev/null +++ b/wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.json @@ -0,0 +1,43 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":2,"n_tracks":0,"n_paths":0,"n_items":2,"n_keys":0,"n_glyphs":4,"n_clusters":4,"path_contours":0,"path_points":0,"strings_len":16,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":258277} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[], +"paints":[ +[30,60,200,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":5,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6}, +{"purpose":6,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":1,"ruby_start":0,"ruby_len":4,"str_len":6} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[2,0,0,9000], +[3,0,9000,9000], +[5,0,0,3000], +[5,0,4000,3000] +], +"clusters":[ +[1,0,0,3], +[1,0,3,3], +[1,0,0,3], +[1,0,3,3] +], +"strings":"ja漢字jaかん", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.wvg1 b/wai/vector-conformance/vectors/canonical/ruby-ends-mid-character.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..f7163c56395586b557fa9ba6aa07fb87326e6cfe GIT binary patch literal 942 zcmZ`$J%|%Q6n^`YL{~u(ENo=NLXjk>g;;onAczoAQ17l?cjuBllig!yqlB|K<%)oX ziiOyT*7g<_mX?R8U?Fmbv$C+b#^@QpH?so`}WG$1JAy#9((-j<>mGE zkL6D{HoqKShwsk`&Vf)B?f5m%rcGlw#C~| zI)2p5V#``7=f2)!{O9|K*f-l5Ec-l!_mD5qx(XX7ejyDQSP8+;C^YvYO=!5WJ9K*4uF*zPiE#Kzq&e;joovXiT)zb&UdA?l6IW7OfmIgcXZrS0) zgdEeugFJHX{&9u3_phr-HN7rqPq#752yaSjHBb@mfzq%iXVf4qIgEmgW!56 zQ6lRX-6WMl$pTEewWLl`Q7|HxOPNYQKbYohic5t+T~jOU(obWVqelueMYMRRl65gF MbCqPNz}RN+4}^oxyZ`_I literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/canonical/ruby-names-later.json b/wai/vector-conformance/vectors/canonical/ruby-names-later.json new file mode 100644 index 000000000..fd2ca513e --- /dev/null +++ b/wai/vector-conformance/vectors/canonical/ruby-names-later.json @@ -0,0 +1,43 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":2,"n_tracks":0,"n_paths":0,"n_items":2,"n_keys":0,"n_glyphs":4,"n_clusters":4,"path_contours":0,"path_points":0,"strings_len":16,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":258277} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[], +"paints":[ +[30,60,200,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":5,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":2,"ruby_start":0,"ruby_len":3,"str_len":6}, +{"purpose":6,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[2,0,0,9000], +[3,0,9000,9000], +[5,0,0,3000], +[5,0,4000,3000] +], +"clusters":[ +[1,0,0,3], +[1,0,3,3], +[1,0,0,3], +[1,0,3,3] +], +"strings":"ja漢字jaかん", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/canonical/ruby-names-later.wvg1 b/wai/vector-conformance/vectors/canonical/ruby-names-later.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..18916a5ecb460916241609aea1206378b8dcc411 GIT binary patch literal 942 zcmZ`$J%|%Q6n^`YL{~u(ENo=NLXjk>g;;onAczoAQ17l?cjuBllig!yqlB|K<%)oX ziiOyT*7g<_mX?R8U?Fmbv$C+b#^@Q}H?so)Ay?VZ(qzgE%HlE0G~U}5ZxxRp zHCou=l~VNe9&>)aj|gXTa|Vku^x!+?7sKywUTnO2x)_e`Js918GI!;gegoP?_|p+~ z>>t^;GrSDmA*SJYlSS}Nz$0#P!Q=Y>vClDPxAh9gn9s&lj_s_RrrjKwcOH({YT)yz z#y?-*wa0yI5awk#)-)yuaccQCS9i`%sP0_-O{<<>IGo4j0_U{+3tJlO%)4cW6BFk^ z&nLD=&fPz*@b><7HL0f81%1+OfEmN2v{qv(;(MSp?8zB5NQ;gvlBf{|D(mNwoN3Ec z=3yaYadRNbReVj<6ZVj;_W{cfm&ZW76qJI^nLQAajTH%|t^^-iKh z)-SqADut2-GwIfnI!Q$VL@t*yl`#EanzJb`6@u%UTJbFXG?qE{NMTe&3!+Na#jMO# KlBI%Wo54SX?asUa literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/canonical/ruby-to-string-end.json b/wai/vector-conformance/vectors/canonical/ruby-to-string-end.json new file mode 100644 index 000000000..ed927c3aa --- /dev/null +++ b/wai/vector-conformance/vectors/canonical/ruby-to-string-end.json @@ -0,0 +1,43 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":0,"n_paints":1,"n_texts":2,"n_tracks":0,"n_paths":0,"n_items":2,"n_keys":0,"n_glyphs":4,"n_clusters":4,"path_contours":0,"path_points":0,"strings_len":16,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":258277} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[], +"paints":[ +[30,60,200,255] +], +"tracks":[], +"keys":[], +"paths":[], +"path_ends":[], +"path_points":[], +"texts":[ +{"purpose":5,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":6}, +{"purpose":6,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":1,"ruby_start":3,"ruby_len":3,"str_len":6} +], +"items":[ +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":1,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":196608,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[2,0,0,9000], +[3,0,9000,9000], +[5,0,0,3000], +[5,0,4000,3000] +], +"clusters":[ +[1,0,0,3], +[1,0,3,3], +[1,0,0,3], +[1,0,3,3] +], +"strings":"ja漢字jaかん", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/canonical/ruby-to-string-end.wvg1 b/wai/vector-conformance/vectors/canonical/ruby-to-string-end.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..932dac70c3c01f5907a8668804b34defd95def74 GIT binary patch literal 942 zcmZ`$J%|%Q6n?Y2Npuwy!NNvHEEGwCT8M>L2!aR^1@-RQb$2e=Gub_MHcB{)Q?3YD zs91=dXl-v{VQG1Y3Kk+~I4cW_YmA=peKWgY;O-@F-rx7W_jYD(_Tni)L~lRfw06bm z2o@6Ln4CR{JdU8H>}TghSuB$1-T3L!A&$Wa1?JY7hu!7%_wALh2cCUfJ@)w5%ggKS zAIqO^Y<@WyjXrN3KKE`?DdrVaRAr}Xa)Mm;p~%6O;uJl0|0+$U#G*<(cA3Vz+v3Yl zI)2p7f`wL!p|AH?|M@;5`1a;37Cz=dcgimY-`~90c=dEK7~Oj?y8mSE$~E%_w2Sbk zBdo9=S+^6s4BjE4;dqmW;G2L)+~R`E_5Wj!W9(kmD;#6LGOlu5?#gM}&5?cQ;dreE zK9_3z^UPhl+{c2!d<@5i#^x}bT3(r}i?b7|i>tqB)ino)^SE5#M9aIdWx!(IDLO<% zoC7_3ERV$9KaO field_range", "s4-n-sizes-zero":"step 4: n_sizes = 0 -> field_range", "s4-paint-over-alpha":"PAINT r > a -> field_range", +"s4-path-before-text":"pair: a PATH zero field and TEXT.purpose = 0 -> payload_malformed (tables in file order: PATHS before TEXTS)", "s4-path-contours-over-points":"PATH.n_contours above n_points -> field_range", "s4-path-n-contours-zero":"PATH.n_contours = 0 -> field_range", "s4-path-n-points-zero":"PATH.n_points = 0 -> field_range", @@ -107,11 +108,13 @@ "s4-tb-not-divisible":"step 4, the header: tb = 119,999 (a header value in range; the RATE's P is not an integer) -> field_range at RATE.den", "s4-tb-over":"step 4, the header: tb = 2^20 + 1 -> field_range", "s4-tb-zero":"step 4, the header: tb = 0 -> field_range", +"s4-text-before-item":"pair: TEXT.mode = 3 and an ITEM zero field -> field_range (tables in file order: TEXTS before ITEMS)", "s4-text-dir":"TEXT.dir = 2 -> field_range", "s4-text-lang-len-over":"TEXT.lang_len = 64 -> field_range", "s4-text-lang-len-zero":"TEXT.lang_len = 0 -> field_range", "s4-text-mode":"TEXT.mode = 3 -> field_range", "s4-text-purpose-zero":"TEXT.purpose = 0 -> field_range", +"s4-text-ruby-len-no-ruby":"TEXT.ruby_len = 1 with no ruby -> field_range", "s4-text-ruby-len-zero":"TEXT ruby set with ruby_len = 0 -> field_range", "s4-text-ruby-over":"TEXT.ruby above n_texts -> field_range", "s4-text-ruby-start":"TEXT.ruby_start with no ruby -> field_range", @@ -141,6 +144,7 @@ "s5c-slot-kind":"5(c): a matrix slot naming an opacity track -> field_range", "s5c-translation-key":"5(c): a translation key of 2^22 + 1 -> field_range", "s5c-typing-before-groups":"pair: an opacity key of 256 and a group left open -> field_range (5(c) before 5(d))", +"s5c-typing-before-strings":"pair: an opacity key of 256 and a language tag with a leading hyphen -> field_range (5(c) before 5(f))", "s5d-depth-five":"5(d): groups nested five deep -> payload_malformed", "s5d-end-first":"5(d): a group end with no group open -> payload_malformed", "s5d-open":"5(d): a group open after the last item -> payload_malformed", @@ -160,6 +164,8 @@ "s5f-tag-leading-hyphen":"5(f): a language tag with a leading hyphen -> payload_malformed", "s5f-tag-trailing-hyphen":"5(f): a trailing hyphen -> payload_malformed", "s5f-tag-underscore":"5(f): an underscore -> payload_malformed", +"s5g-block-before-sets":"pair: a block no run or path names, and a held set the sink cannot obtain -> payload_malformed (5(g) before 6)", +"s5h-decorative-before-sets":"pair: a path whose text alternative is a decorative block, and a held set the sink cannot obtain -> payload_malformed (5(h) before 6)", "s6-held-malformed":"6: a held set that hashes right and is not a valid WOS1 -> outline_malformed", "s6-held-not-held":"6: a held set the sink cannot obtain -> outline_not_held", "s6-held-one-byte-longer":"6: a held set one byte longer than its length; the sink reads at most length + 1 bytes -> outline_mismatch", diff --git a/wai/vector-conformance/vectors/refusals/s4-path-before-text.json b/wai/vector-conformance/vectors/refusals/s4-path-before-text.json new file mode 100644 index 000000000..fd893a178 --- /dev/null +++ b/wai/vector-conformance/vectors/refusals/s4-path-before-text.json @@ -0,0 +1,64 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":1,"n_paints":2,"n_texts":1,"n_tracks":3,"n_paths":2,"n_items":4,"n_keys":4,"n_glyphs":2,"n_clusters":2,"path_contours":2,"path_points":8,"strings_len":4,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":498549} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[ +[0,100,400,900,1600,2500,3600,4900,6400,8100,10000,12100,14400,16900,19600,40000,65536] +], +"paints":[ +[200,30,30,255], +[30,60,200,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2}, +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":3,"zero@1":0,"zero@2":0,"n_keys":1} +], +"keys":[ +[0,0,2,[0,0,0]], +[120000,2560,0,[0,0,0]], +[0,200,0,[0,0,0]], +[0,65536,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":25600,"ymax":25600}, +{"n_contours":1,"n_points":4,"zero@4":1,"xmin":256,"ymin":256,"xmax":5120,"ymax":5120} +], +"path_ends":[3,3], +"path_points":[ +[0,0,0], +[25600,0,0], +[25600,25600,0], +[0,25600,0], +[256,256,0], +[5120,256,0], +[5120,5120,0], +[256,5120,0] +], +"texts":[ +{"purpose":0,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":2} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":0,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":1,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":2,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1,0,6000,20000], +[3,0,10096,20000] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1] +], +"strings":"enAB", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/refusals/s4-path-before-text.wvg1 b/wai/vector-conformance/vectors/refusals/s4-path-before-text.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..578f5ffa6f739d566b1c840ac0bb6e55098549ef GIT binary patch literal 1178 zcmaJ<&ubGw6n?XtHa3WeM#M`TK`2OCe;lL-3$_$Qh(ADWdyUy?vzYFN-HlS}p@$xN zvmQln`iFS%kYf=QJcv}U9&@o;VV*S)5q zc%sA?PYUpJc|+^Sz;}VpAz^OkWl_$+M@BAU9$kIbTHATQy#DpXi*Fm_&wswYwX^(V z?bCzams7#_&wHc@u(pt};fq`!6?r!%a{QRc`I88M@XcA+<05kx5uZX&v%uG&zhbtM zZl%(%TniVABOk-Wwl|5JbD0~^W}B+ zyZOIHeEYPBzU^wYF&pYR56|IcqM6x+JNN8+6FeZkp>tR~KVlswyh=_Xkj>p5$oL<` z)2<=lBR*_%LL)=*5!k$k96t)1J>@b6i~ItG`|{mLSy*~%bJx;1AB#(^ zko&4?sxMV__ZMET-C!l z5$1%Q%61V{>|3JxikVJVqpWGNurX0DrEw=|nCWE`nWUbXKtEj7PwL^sW7EjuM7tx^ zi0h>!UeQZ&veIeQ(^4yJm4koejG{<7Hlbq5I;a|dtI~BWNES+b~lw!4?XnI zoAoGq(?7(6ha8Kj;6bE%^_Yv*8h>vl)21lC<;}d0-}}8cGrP2S^Qs`CH}82h`-amL zEDi~B$)OzLtj?Go8^tbha7?(i`=UVS5MxGG#;i_1ZLROVYixWu_5ADR)UzM2Ztpg} zuYY_v_%f}WMvW_^Aq@X3e4wH0bfqvmusGs8`D5+?P(el zrGX;R=whR51L^r<8IapBPCOnfk~H+sKZER vj7U@`PD9Z5j&fGztxO`XsZ-3-jRKXRMoH5}*0^b^%5qU9X&6TmZL8iNtK;G1 literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.json b/wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.json new file mode 100644 index 000000000..562f7a4d2 --- /dev/null +++ b/wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.json @@ -0,0 +1,64 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":1,"n_paints":2,"n_texts":1,"n_tracks":3,"n_paths":2,"n_items":4,"n_keys":4,"n_glyphs":2,"n_clusters":2,"path_contours":2,"path_points":8,"strings_len":4,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":498549} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[ +[0,100,400,900,1600,2500,3600,4900,6400,8100,10000,12100,14400,16900,19600,40000,65536] +], +"paints":[ +[200,30,30,255], +[30,60,200,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2}, +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":3,"zero@1":0,"zero@2":0,"n_keys":1} +], +"keys":[ +[0,0,2,[0,0,0]], +[120000,2560,0,[0,0,0]], +[0,200,0,[0,0,0]], +[0,65536,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":25600,"ymax":25600}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":256,"ymin":256,"xmax":5120,"ymax":5120} +], +"path_ends":[3,3], +"path_points":[ +[0,0,0], +[25600,0,0], +[25600,25600,0], +[0,25600,0], +[256,256,0], +[5120,256,0], +[5120,5120,0], +[256,5120,0] +], +"texts":[ +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":1,"str_len":2} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":0,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":1,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":2,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1,0,6000,20000], +[3,0,10096,20000] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1] +], +"strings":"enAB", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.wvg1 b/wai/vector-conformance/vectors/refusals/s4-text-ruby-len-no-ruby.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..df95438c7abc7daec98ec9575193cf4448480e8f GIT binary patch literal 1178 zcmaJ<&1(}u6o0duCUy}KT@f#F1fd{F{5X^zEZ9;IA%1|`_PS=L$~1Qd9(w4Z zH|tUKrhkYB4>=Z5!GlQk>M<9qHU8erZks~!EpO(1{NC@qotdS@+t&pVz5B?k**C0~ zV6jM$O%@dpXSK%kL>9Zk!7<_5-m4N_K#Un#8M8Y5yuH5rp}Fzx%**eaQ!jqKxx3r^ zx&Ha_@asAM$CrJk@S)o%*tCgm6^Y(Y5}iIpbone2Abfif{uI%|Rpe(d(>(BX*sr-- z(4br%m2VD4`J8!w(CIlC#lm_0zm9hGGB`A0aV4+(*O~a_ANT?(ms}gCV}vKi_}PXm z2wbzc%;V~F%)lJiGZ=pa$us#q2;wLU!7|gB8n%zaTL%@UiW$vxrrOFnntLuDLFasV zpTlYXZ!x}mo=Dy^y}Gy?wsRid$5n-M^Go+1>i0%?K>UU-L3#fKJx=&2`4)WI+8ct5 z|AX=D*O9Q1ANLFJC&uy<@cA5ad=Wm+l*=SM<|pv&l$g(^625{yFWX!xH>RQ3+S4>9 z#?7f-$YT{TGop%k@EfmJFL0Onp)pR>3k&-iES9aTLimK34+Jd8ws`i(lJ?2JmL}Eo zI-xCH#xNs1DeaU%#dA~Wb<}Lq57U-PgGSYJlBgFq)NE6QD)v(4%O`#L)C;O>s*y&q zG+QE(mn(79kt9xJYX$K7zqH?b5HCn2Aqjt@49<_o*M!mEhgi0nV1(Pn#sDn_Z vjEGe?N&?XLk8;-Jom3*PsT0i73w;%%MoH5}*12h-DsoZ9Nf3n+ZEMayqLAU? literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.json b/wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.json new file mode 100644 index 000000000..f42071d7f --- /dev/null +++ b/wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.json @@ -0,0 +1,64 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":1,"n_paints":2,"n_texts":1,"n_tracks":3,"n_paths":2,"n_items":4,"n_keys":4,"n_glyphs":2,"n_clusters":2,"path_contours":2,"path_points":8,"strings_len":5,"inline_len":554,"held_len":0,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":498549} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":1,"zero@1":[0,0,0],"length":554,"sha256":"6b368f6c7be1d26780df1299e2842791f9a34de167ec7bd655f4dd1564e3d9fd"} +], +"tables":[ +[0,100,400,900,1600,2500,3600,4900,6400,8100,10000,12100,14400,16900,19600,40000,65536] +], +"paints":[ +[200,30,30,255], +[30,60,200,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2}, +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":3,"zero@1":0,"zero@2":0,"n_keys":1} +], +"keys":[ +[0,0,2,[0,0,0]], +[120000,2560,0,[0,0,0]], +[0,256,0,[0,0,0]], +[0,65536,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":25600,"ymax":25600}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":256,"ymin":256,"xmax":5120,"ymax":5120} +], +"path_ends":[3,3], +"path_points":[ +[0,0,0], +[25600,0,0], +[25600,25600,0], +[0,25600,0], +[256,256,0], +[5120,256,0], +[5120,5120,0], +[256,5120,0] +], +"texts":[ +{"purpose":3,"dir":0,"lang_len":3,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":2} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":0,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":1,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":2,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1,0,6000,20000], +[3,0,10096,20000] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1] +], +"strings":"-enAB", +"inline":"574f53310100000000080000080000002100000008000000d500000000000000000000000000000000000000000000000100010004000000640088faf40100000200020008000000640088fa4c0400000300010008000000640088fa4c0400000400010005000000640088fa4c0400000500010003000000640088fa4c040000060001000100000000000000000000000900010004000000000088fad0070000030003000700070004000200000003006400000000f401000000f40188fa00640088fa0064000000004c040000004c0488fa00640088fa002c0138ff002c0150fb00840350fb00840338ff0058020000004c040000014c0444fd004c0488fa01580288fa00640088fa01640044fd006400000001640000000090017cfc0220030cfe024c0488fa004c0400000064000000004c04000000580288fa0000000000000000000000d007000000d00788fa00000088fa006e616d653d73796e7468657469632d610a736f757263653d67656e657261746564206279207761692d766563746f7220766563746f72732067656e2066726f6d20666f726d756c61730a6c6963656e63653d4170616368652d322e300a0a54686973206f75746c696e6520736574207761732067656e65726174656420696e2074686973207265706f7369746f72792066726f6d20666f726d756c61732e204974206973206c6963656e73656420756e6465722074686520417061636865204c6963656e73652c2056657273696f6e20322e302e0a" +} diff --git a/wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.wvg1 b/wai/vector-conformance/vectors/refusals/s5c-typing-before-strings.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..3b32b4fb0ef34c012ce8beb13aadffd2c9dab42a GIT binary patch literal 1179 zcma)4ziSjh6n?XN$(ch$^h9jp2tq(|@kb#k444oQ;rsz=lJ>lvGe39ZvJ#QEAGo@GEodo9 zAWA|Bq=Yy%7|p(HN@>W;p7;12u_Q45W5FgPwDEE)he+loBk1vvqyJAgrff zMp08ctF7S`BPSs1Z`?gpu1bu{x1ToUXf^4d%zXKf}{S#PHOK%g_vM`o8 z;iR%%1{LR)q_J$K@|C1$nIdl18kIclW=%7-WD=7#3KQvvEBZ+zu01x*BF(hha*e!R z%+h7Om}bk}b|bH}gcc8hkL7@eB}OcvUwjuTz5A~T&d rk3qk3m~%qkEHv_#I>#*CBr+K=M!PP$&ZfB;*K;Pz<22D|J5l)yo#f$2 literal 0 HcmV?d00001 diff --git a/wai/vector-conformance/vectors/refusals/s5g-block-before-sets.json b/wai/vector-conformance/vectors/refusals/s5g-block-before-sets.json new file mode 100644 index 000000000..e57d684df --- /dev/null +++ b/wai/vector-conformance/vectors/refusals/s5g-block-before-sets.json @@ -0,0 +1,65 @@ +{ +"header":{"magic":"WVG1","format":1,"tb":120000,"t0":0,"dur":120000,"design_w":640,"design_h":360,"n_sizes":1,"n_rates":1,"n_sets":1,"n_tables":1,"n_paints":2,"n_texts":2,"n_tracks":3,"n_paths":2,"n_items":4,"n_keys":4,"n_glyphs":2,"n_clusters":2,"path_contours":2,"path_points":8,"strings_len":7,"inline_len":0,"held_len":40,"zero@72":[0,0,0,0,0,0,0,0]}, +"sizes":[ +{"w":640,"h":360,"cost":498549} +], +"rates":[ +{"num":25,"den":1} +], +"sets":[ +{"carriage":0,"zero@1":[0,0,0],"length":40,"sha256":"22b94c8c4fa46c25e33acb82f6071fa66e39381e35c18faf635257340879c8e8"} +], +"tables":[ +[0,100,400,900,1600,2500,3600,4900,6400,8100,10000,12100,14400,16900,19600,40000,65536] +], +"paints":[ +[200,30,30,255], +[30,60,200,255] +], +"tracks":[ +{"kind":2,"zero@1":0,"zero@2":0,"n_keys":2}, +{"kind":1,"zero@1":0,"zero@2":0,"n_keys":1}, +{"kind":3,"zero@1":0,"zero@2":0,"n_keys":1} +], +"keys":[ +[0,0,2,[0,0,0]], +[120000,2560,0,[0,0,0]], +[0,200,0,[0,0,0]], +[0,65536,0,[0,0,0]] +], +"paths":[ +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":0,"ymin":0,"xmax":25600,"ymax":25600}, +{"n_contours":1,"n_points":4,"zero@4":0,"xmin":256,"ymin":256,"xmax":5120,"ymax":5120} +], +"path_ends":[3,3], +"path_points":[ +[0,0,0], +[25600,0,0], +[25600,25600,0], +[0,25600,0], +[256,256,0], +[5120,256,0], +[5120,5120,0], +[256,5120,0] +], +"texts":[ +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":2}, +{"purpose":3,"dir":0,"lang_len":2,"mode":0,"group":0,"ruby":0,"ruby_start":0,"ruby_len":0,"str_len":1} +], +"items":[ +{"kind":0,"rule":0,"zero@2":0,"ref":0,"paint":65535,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":0,"ty":65535,"op":1,"zero@42":[0,0,0,0,0,0]}, +{"kind":2,"rule":0,"zero@2":0,"ref":1,"paint":0,"text":65535,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":120000,"unit_scale":65536,"m00":2,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":3,"rule":0,"zero@2":0,"ref":0,"paint":1,"text":0,"zero@10":0,"n_glyphs":2,"n_clusters":2,"t_on":0,"t_off":120000,"unit_scale":393216,"m00":65535,"m01":65535,"m10":65535,"m11":65535,"tx":65535,"ty":65535,"op":65535,"zero@42":[0,0,0,0,0,0]}, +{"kind":1,"rule":0,"zero@2":0,"ref":0,"paint":0,"text":0,"zero@10":0,"n_glyphs":0,"n_clusters":0,"t_on":0,"t_off":0,"unit_scale":0,"m00":0,"m01":0,"m10":0,"m11":0,"tx":0,"ty":0,"op":0,"zero@42":[0,0,0,0,0,0]} +], +"glyphs":[ +[1,0,6000,20000], +[3,0,10096,20000] +], +"clusters":[ +[1,0,0,1], +[1,0,1,1] +], +"strings":"enABenC", +"inline":"" +} diff --git a/wai/vector-conformance/vectors/refusals/s5g-block-before-sets.wvg1 b/wai/vector-conformance/vectors/refusals/s5g-block-before-sets.wvg1 new file mode 100644 index 0000000000000000000000000000000000000000..e45e9e7c2916743ada3098c3b7c0daa27b1831f0 GIT binary patch literal 647 zcmWF!b2nsUU|=|K1w=yV2Br)~5MW|pVqgYh7N9r?Ga+FPAe$YCK|ljSfk}|w(%I|` zl0Y8BM2Ni7PM;qCB{`~(txh+6W0zl+XK5j4da!?ea!|MlN9BnZAk8U2JOQYwg_(iD zfsKLT2qy!B03QQ`iZD}#k*3PTE<&j>V5gh7NEY%)k5VZ zrGT`a5#b#!2rmOps+;-5L-ba|A7D@1Bn|(uqZ_4|9_ZTkO?3*HuX$! z9SHM4VF3#bQ2c@b#C$ds)iCp5_J9=<=&n2MJvq+(aA&-E}Ok&Ta}0 z0ayQli@(4%p7U;BjFT7c_t&}a^^W(~0{|}{lBO@i(?Ap=L_{8ATpU)oLAy>Ct(bCB zz32CJ%+a%nmS1@8-W(3kmOi)MufFTe$E@8-HeYX_PL6sTjf?zC%_;egP1g~yQv=?{ zf$1q=afS&F95(4M1AA-Cw}`33`Y!DUbqC0kq)4{&!sFC_F~^lyXu3Pxy%X|`a&I_o z$OCuvD3Ky9{ea_Q#F3k [--sets ] every frame's SHA-256 and each clip digest, //! at every listed size and rate +//! wai-vector text [--sets ] text_hash, and each block: its identity (§2.11), its +//! fields and string, when it is active, and what a +//! verifier finds of the cluster rule (§5.3) +//! wai-vector a11y [--sets ] the text record (§5.4): the transcript, then every +//! frame's projection at every listed rate +//! wai-vector a11y transcript [--sets ] the transcript alone +//! wai-vector a11y / [--sets ] frame g's projection //! wai-vector pack the authoring form (§2.12) to bytes //! wai-vector unpack bytes to the authoring form //! wai-vector vectors gen write vector-conformance/ and the fuzz seeds @@ -21,6 +28,7 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; use wai_vector::vectors::{self, CORPUS, SEEDS}; +use wai_vector::text::{self, TextLayer}; use wai_vector::{SetStore, form, frame, place, wvg1}; fn die(msg: &str) -> ExitCode { @@ -98,7 +106,7 @@ fn verify(wai: &Path) -> Result<(), String> { } // 2. Every expected file recomputed from the committed vector and sets alone. let mut store = store_from(Some(&corpus.join("sets")))?; - let mut n = 0usize; + let (mut n, mut records) = (0usize, 0usize); let mut groups: BTreeSet = BTreeSet::new(); walk(&corpus.join("vectors"), &corpus.join("vectors"), &mut groups); for rel in &groups { @@ -115,6 +123,14 @@ fn verify(wai: &Path) -> Result<(), String> { bad += 1; eprintln!("FAIL {rel}: recomputed\n {}", got.trim_end()); } + // The text record beside it (§5.4), from the committed payload alone. + if let Some(record) = vectors::text_record(group, ext, &bytes, &mut store) { + records += 1; + if read(&corpus.join("expected").join(group).join(format!("{name}.a11y.jsonl")))? != record.as_bytes() { + bad += 1; + eprintln!("RECORD {rel}: the recomputed text record differs"); + } + } if ext == "wvg1" { let form = read(&corpus.join("vectors").join(group).join(format!("{name}.json")))?; if vectors::form_of(&bytes).as_bytes() != form { @@ -127,7 +143,7 @@ fn verify(wai: &Path) -> Result<(), String> { } } } - println!("recomputed {n} vectors, checked {} generated files", generated.len()); + println!("recomputed {n} vectors and {records} text records, checked {} generated files", generated.len()); if bad > 0 { return Err(format!("{bad} failures")); } @@ -135,6 +151,58 @@ fn verify(wai: &Path) -> Result<(), String> { Ok(()) } +/// `wai-vector text`: the text digests, and each block as a reviewer reads it. +fn describe_text(d: &wvg1::Doc) -> String { + use std::fmt::Write; + let layer = TextLayer::new(d); + let hex = wai_vector::json::hex; + let mut o = String::new(); + let _ = writeln!(o, "text_hash {}", hex(&text::text_hash(d))); + for (t, b) in d.texts.iter().enumerate() { + let _ = writeln!(o, "block {t} identity {}", hex(&text::block_identity(d, t))); + let purpose = match text::purpose_name(b.purpose) { + Some(n) => n.to_string(), + None => String::from("unregistered, read as label"), + }; + let _ = writeln!( + o, + " purpose {} ({purpose}) dir {} mode {} lang {} group {}", + b.purpose, + ["ltr", "rtl"].get(b.dir as usize).copied().unwrap_or("?"), + ["horizontal-tb", "vertical-rl", "vertical-lr"].get(b.mode as usize).copied().unwrap_or("?"), + String::from_utf8_lossy(d.lang(t)), + b.group, + ); + if b.ruby != 0 { + let _ = writeln!(o, " ruby over block {}, bytes {}..{}", b.ruby - 1, b.ruby_start, u64::from(b.ruby_start) + u64::from(b.ruby_len)); + } + let mut s = String::new(); + wai_vector::json::push_str(&mut s, &String::from_utf8_lossy(d.string(t))); + let _ = writeln!(o, " string {s}"); + let active: Vec = layer.active[t].iter().map(|(a, b)| format!("[{a}, {b})")).collect(); + let _ = writeln!(o, " active {}", if active.is_empty() { String::from("never") } else { active.join(" ") }); + let (mut runs, mut paths) = (0usize, 0usize); + for it in d.items.iter().filter(|it| it.text as usize == t) { + match it.kind { + wvg1::KIND_RUN => runs += 1, + wvg1::KIND_PATH => paths += 1, + _ => {} + } + } + let _ = writeln!(o, " drawn by {runs} run(s) and {paths} path(s)"); + if runs > 0 { + let c = text::coverage(d, t); + let list = |v: &[(u32, u32)]| v.iter().map(|(a, b)| format!("{a}..{b}")).collect::>().join(" "); + if c.complete() { + let _ = writeln!(o, " clusters: every byte in one cluster"); + } else { + let _ = writeln!(o, " clusters: bytes in no cluster [{}], bytes in more than one [{}]", list(&c.uncovered), list(&c.repeated)); + } + } + } + o +} + fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let a: Vec<&str> = args.iter().map(String::as_str).collect(); @@ -180,6 +248,28 @@ fn main() -> ExitCode { } Ok(()) })(), + ["text", p, ..] => (|| { + let mut st = store_from(sets_arg(&args).as_deref())?; + let l = wvg1::load(&read(Path::new(p))?, &mut st).map_err(|e| e.code.name().to_string())?; + print!("{}", describe_text(&l.doc)); + Ok(()) + })(), + ["a11y", p, rest @ ..] => (|| { + let mut st = store_from(sets_arg(&args).as_deref())?; + let l = wvg1::load(&read(Path::new(p))?, &mut st).map_err(|e| e.code.name().to_string())?; + let layer = TextLayer::new(&l.doc); + match rest { + ["transcript", ..] => println!("{}", layer.transcript()), + [rate, g, ..] if !rate.starts_with("--") => { + let (num, den) = rate.split_once('/').ok_or("rate: /")?; + let n = |s: &str| s.parse::().map_err(|_| format!("{s}: not a number")); + let (num, den) = (u32::try_from(n(num)?).map_err(|_| "rate: out of range")?, u32::try_from(n(den)?).map_err(|_| "rate: out of range")?); + println!("{}", layer.projection_request(num, den, n(g)?).map_err(|e| e.code.name().to_string())?); + } + _ => print!("{}", layer.record()), + } + Ok(()) + })(), ["pack", src, dst] => (|| { let text = String::from_utf8(read(Path::new(src))?).map_err(|_| String::from("not UTF-8"))?; std::fs::write(dst, form::to_bytes(&text)?).map_err(|e| e.to_string()) @@ -196,7 +286,7 @@ fn main() -> ExitCode { Ok(()) })(), ["vectors", "verify", wai] => verify(Path::new(wai)), - _ => return die("usage: check|cost|hash [--sets dir] | render x / [--sets dir] | pack
| unpack | vectors gen|verify "), + _ => return die("usage: check|cost|hash|text [--sets dir] | render x / [--sets dir] | a11y [transcript | / ] [--sets dir] | pack | unpack | vectors gen|verify "), }; match r { Ok(()) => ExitCode::SUCCESS, diff --git a/wai/wai-vector/src/lib.rs b/wai/wai-vector/src/lib.rs index 4134ee5f2..1fef1d2e0 100644 --- a/wai/wai-vector/src/lib.rs +++ b/wai/wai-vector/src/lib.rs @@ -11,6 +11,8 @@ //! §3.15): flattening, the row and cell split, accumulation and scan, coverage, the //! glyph-wise union, compositing, groups and clips, and the straight-alpha view. //! - [`frame`]: the frame procedure (§3.10), `RGBA8P` at a listed size, rate and frame. +//! - [`text`]: the text layer (§2.11, §5): `text_hash`, block identities, each block's +//! active set, and the projection and the transcript as JSON. //! - [`arith`]: `R_rat`, `R_half_up`, `div255`, the quantisers and the straight-alpha //! channel (§3.0, §3.8, §3.9, §2.9). //! - [`scene`]: a builder that writes canonical payloads (authoring). @@ -43,6 +45,7 @@ pub mod place; pub mod refuse; pub mod scene; pub mod sha256; +pub mod text; pub mod track; pub mod vectors; pub mod wos1; diff --git a/wai/wai-vector/src/text.rs b/wai/wai-vector/src/text.rs new file mode 100644 index 000000000..30875f2f1 --- /dev/null +++ b/wai/wai-vector/src/text.rs @@ -0,0 +1,431 @@ +//! The text layer (§2.11, §5): the text digests, when each block is active, and the +//! projection and the transcript as the JSON §5.4 defines. +//! +//! - [`text_hash`] and [`block_identity`] are §2.11's digests. +//! - [`effective_windows`] gives each item's effective window, and [`active_sets`] each +//! block's active set: the union of the effective windows of the runs and paths that +//! draw it, as sorted half-open tick intervals with touching intervals merged. +//! - [`TextLayer`] holds a document's active sets and writes the transcript, the +//! projection at a frame, and the text record: the transcript and every frame's +//! projection at every listed rate, one JSON value per line. +//! - [`coverage`] is a verifier's check of §5.3's encoder rule: which bytes of a +//! block's string no cluster covers, and which more than one does. +//! +//! Text-equivalence (§1.3) is byte equality of this JSON. A sink computes it and +//! exposes it; it never shapes, reorders or breaks text. +//! +//! Every function takes a document [`crate::wvg1::parse`] returned, so the tags and +//! strings are valid UTF-8 and every index is in range. + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use crate::bounds::{DECORATIVE, NONE}; +use crate::clock; +use crate::refuse::{Code, Refusal, refuse}; +use crate::sha256::sha256; +use crate::wvg1::{Doc, KIND_BEGIN, KIND_END, KIND_PATH, KIND_RUN}; + +/// `text_hash` (§2.11): the SHA-256 of the `TEXTS` table followed by the `STRINGS` +/// area, exactly as carried. +pub fn text_hash(d: &Doc) -> [u8; 32] { + let mut m = d.texts_bytes(); + m.extend_from_slice(&d.strings); + sha256(&m) +} + +/// A block's identity (§2.11): the SHA-256 of `"wai:text-block"`, `0x01`, `purpose`, +/// `dir`, `mode`, `lang_len`, the tag, `str_len` as `u32` little-endian, and the +/// string. The positional fields, `group` and the ruby link, are not in it. +pub fn block_identity(d: &Doc, t: usize) -> [u8; 32] { + let b = &d.texts[t]; + let mut m = Vec::with_capacity(23 + d.lang(t).len() + d.string(t).len()); + m.extend_from_slice(b"wai:text-block"); + m.extend_from_slice(&[0x01, b.purpose, b.dir, b.mode, b.lang_len]); + m.extend_from_slice(d.lang(t)); + m.extend_from_slice(&b.str_len.to_le_bytes()); + m.extend_from_slice(d.string(t)); + sha256(&m) +} + +/// The purpose vocabulary (§5.2): each registered code and its name. A code above the +/// last is unregistered: it is carried and written as it is, and read as `label`. +pub const PURPOSES: [(u8, &str); 13] = [ + (1, "title"), + (2, "credit"), + (3, "lower_third"), + (4, "caption"), + (5, "subtitle"), + (6, "forced_narrative"), + (7, "label"), + (8, "legal"), + (9, "alert"), + (10, "logo"), + (11, "graphic"), + (12, "data"), + (13, "decorative"), +]; + +/// The name of a registered purpose; `None` for an unregistered code. +pub fn purpose_name(code: u8) -> Option<&'static str> { + PURPOSES.iter().find(|(c, _)| *c == code).map(|&(_, n)| n) +} + +/// A half-open interval of ticks, `[on, off)` with `on < off ≤ 2^31`. +pub type Interval = (u32, u32); + +/// Each item's effective window (§3.4(b) step 4): its `[t_on, t_off)` intersected with +/// the window of every group that encloses it and with the document's window +/// `[t0, t0 + dur)`; `None` when the intersection is empty, and for a group end. +pub fn effective_windows(d: &Doc) -> Vec> { + let doc_win = (u64::from(d.header.t0), u64::from(d.header.t0) + u64::from(d.header.dur)); + // The effective window of each group still open, the innermost last. + let mut open: Vec<(u64, u64)> = Vec::new(); + let mut out = Vec::with_capacity(d.items.len()); + for it in &d.items { + if it.kind == KIND_END { + open.pop(); + out.push(None); + continue; + } + let outer = open.last().copied().unwrap_or(doc_win); + let w = (outer.0.max(u64::from(it.t_on)), outer.1.min(u64::from(it.t_off))); + out.push(if w.0 < w.1 { Some((w.0 as u32, w.1 as u32)) } else { None }); + if it.kind == KIND_BEGIN { + open.push(w); + } + } + out +} + +/// Sort intervals ascending and merge those that overlap or touch, so `[a, b)` and +/// `[b, c)` become `[a, c)`. +pub fn merge(mut v: Vec) -> Vec { + v.sort_unstable(); + let mut out: Vec = Vec::with_capacity(v.len()); + for (on, off) in v { + match out.last_mut() { + Some(last) if on <= last.1 => last.1 = last.1.max(off), + _ => out.push((on, off)), + } + } + out +} + +/// Each block's active set (§5.4): the union of the effective windows of the items +/// that draw it, the runs and the paths whose `text` is the block. Empty for a block +/// that is never active inside the document's window. +pub fn active_sets(d: &Doc) -> Vec> { + let wins = effective_windows(d); + let mut per: Vec> = vec![Vec::new(); d.texts.len()]; + for (i, it) in d.items.iter().enumerate() { + if (it.kind == KIND_RUN || it.kind == KIND_PATH) + && it.text != NONE + && let (Some(w), Some(slot)) = (wins[i], per.get_mut(it.text as usize)) + { + slot.push(w); + } + } + per.into_iter().map(merge).collect() +} + +fn push_int(out: &mut Vec, v: u64) { + let mut buf = [0u8; 20]; + let mut n = 0; + let mut u = v; + loop { + buf[n] = b'0' + (u % 10) as u8; + n += 1; + u /= 10; + if u == 0 { + break; + } + } + for i in (0..n).rev() { + out.push(buf[i]); + } +} + +/// A JSON string in §5.4's encoding: the bytes as carried, with exactly three +/// escapes, `\"`, `\\`, and `\n` for U+000A. +fn push_string(out: &mut Vec, s: &[u8]) { + out.push(b'"'); + for &c in s { + match c { + b'"' => out.extend_from_slice(b"\\\""), + b'\\' => out.extend_from_slice(b"\\\\"), + 0x0A => out.extend_from_slice(b"\\n"), + c => out.push(c), + } + } + out.push(b'"'); +} + +fn key(out: &mut Vec, name: &str) { + out.push(b'"'); + out.extend_from_slice(name.as_bytes()); + out.extend_from_slice(b"\":"); +} + +fn int_member(out: &mut Vec, name: &str, v: u64) { + key(out, name); + push_int(out, v); + out.push(b','); +} + +fn text_of(bytes: Vec) -> String { + String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()) +} + +/// A document's text layer: its blocks and when each is active. +#[derive(Clone, Debug)] +pub struct TextLayer<'a> { + pub doc: &'a Doc, + /// Each block's active set, in block order. + pub active: Vec>, +} + +impl<'a> TextLayer<'a> { + pub fn new(doc: &'a Doc) -> TextLayer<'a> { + TextLayer { doc, active: active_sets(doc) } + } + + /// Whether a block is exposed at all: every purpose but `decorative`. + pub fn exposed(&self, t: usize) -> bool { + self.doc.texts[t].purpose != DECORATIVE + } + + /// Whether a block's active set contains tick `tau`. + pub fn active_at(&self, t: usize, tau: u64) -> bool { + self.active[t].iter().any(|&(on, off)| u64::from(on) <= tau && tau < u64::from(off)) + } + + /// The blocks of the projection at tick `tau`, in block order. + pub fn blocks_at(&self, tau: u64) -> Vec { + (0..self.doc.texts.len()).filter(|&t| self.exposed(t) && self.active_at(t, tau)).collect() + } + + /// One block's object: a node of a projection, or with `active` a block of the + /// transcript. Members in ascending byte order of their keys. + fn node(&self, out: &mut Vec, t: usize, with_active: bool) { + let d = self.doc; + let b = &d.texts[t]; + out.push(b'{'); + if with_active { + key(out, "active"); + out.push(b'['); + for (i, &(on, off)) in self.active[t].iter().enumerate() { + if i > 0 { + out.push(b','); + } + out.push(b'['); + push_int(out, u64::from(on)); + out.push(b','); + push_int(out, u64::from(off)); + out.push(b']'); + } + out.extend_from_slice(b"],"); + } + int_member(out, "dir", u64::from(b.dir)); + int_member(out, "group", u64::from(b.group)); + int_member(out, "index", t as u64); + key(out, "lang"); + push_string(out, d.lang(t)); + out.push(b','); + int_member(out, "mode", u64::from(b.mode)); + int_member(out, "purpose", u64::from(b.purpose)); + if b.ruby != 0 { + key(out, "ruby"); + out.push(b'{'); + int_member(out, "base", u64::from(b.ruby) - 1); + int_member(out, "len", u64::from(b.ruby_len)); + key(out, "start"); + push_int(out, u64::from(b.ruby_start)); + out.extend_from_slice(b"},"); + } + key(out, "text"); + push_string(out, d.string(t)); + out.push(b'}'); + } + + fn array(&self, out: &mut Vec, blocks: impl Iterator, with_active: bool) { + out.push(b'['); + for (i, t) in blocks.enumerate() { + if i > 0 { + out.push(b','); + } + self.node(out, t, with_active); + } + out.push(b']'); + } + + fn transcript_into(&self, out: &mut Vec) { + self.array(out, (0..self.doc.texts.len()).filter(|&t| self.exposed(t)), true); + } + + fn projection_into(&self, out: &mut Vec, tau: u64) { + self.array(out, self.blocks_at(tau).into_iter(), false); + } + + /// The transcript (§5.4): every block whose purpose is not `decorative`, in block + /// order, each with its active set. + pub fn transcript(&self) -> String { + let mut out = Vec::new(); + self.transcript_into(&mut out); + text_of(out) + } + + /// The projection at tick `tau`: every block whose purpose is not `decorative` and + /// whose active set contains `tau`, in block order. + pub fn projection_at(&self, tau: u64) -> String { + let mut out = Vec::new(); + self.projection_into(&mut out, tau); + text_of(out) + } + + /// The projection at frame `g` of listed rate `rate` (an index into `RATES`), where + /// `τ = g·P`. A frame outside the document at that rate is `frame_range`, decided + /// by division (§2.7, *Render*). + pub fn projection(&self, rate: usize, g: u64) -> Result { + let h = &self.doc.header; + let Some(r) = self.doc.rates.get(rate) else { + return refuse(Code::RateUnlisted, "projection", "no RATE lists it", 0); + }; + let p = r.ticks_per_frame(h.tb); + if !clock::in_range(g, h.t0, h.dur, p) { + return refuse(Code::FrameRange, "projection", "frame out of range", g); + } + Ok(self.projection_at(g * p)) + } + + /// The projection for a request by rate and frame: `rate_unlisted` and + /// `frame_range` are request errors, not refusals of the payload. + pub fn projection_request(&self, num: u32, den: u32, g: u64) -> Result { + match self.doc.rates.iter().position(|q| q.num == num && q.den == den) { + Some(r) => self.projection(r, g), + None => refuse(Code::RateUnlisted, "projection", "no RATE lists it", 0), + } + } + + /// The text record (§5.4): the transcript; then, for each listed rate in `RATE` + /// order and each frame `g` of the document at that rate in ascending order, + /// `{"den":D,"g":G,"num":N,"projection":[…]}`. Each line ends in U+000A. + pub fn record(&self) -> String { + let h = &self.doc.header; + let mut out = Vec::new(); + self.transcript_into(&mut out); + out.push(b'\n'); + for r in &self.doc.rates { + let p = r.ticks_per_frame(h.tb); + for g in clock::frames(h.t0, h.dur, p) { + out.push(b'{'); + int_member(&mut out, "den", u64::from(r.den)); + int_member(&mut out, "g", g); + int_member(&mut out, "num", u64::from(r.num)); + key(&mut out, "projection"); + self.projection_into(&mut out, g * p); + out.extend_from_slice(b"}\n"); + } + } + text_of(out) + } +} + +/// The transcript of a parsed document (§5.4). +pub fn transcript(d: &Doc) -> String { + TextLayer::new(d).transcript() +} + +/// The projection of a parsed document at frame `g` of listed rate `rate` (§5.4). +pub fn projection(d: &Doc, rate: usize, g: u64) -> Result { + TextLayer::new(d).projection(rate, g) +} + +/// The text record of a parsed document (§5.4). +pub fn record(d: &Doc) -> String { + TextLayer::new(d).record() +} + +/// What a verifier finds of §5.3's encoder rule in one block: byte ranges of its +/// string that no cluster of any of its runs covers, and ranges that more than one +/// covers. Both are in ascending order, merged, as `[first, end)`. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct Coverage { + /// Bytes no cluster covers, the code points U+000A and U+00AD left out: a line + /// separator, and a soft hyphen at a break, are drawn by no glyph and need no + /// cluster. + pub uncovered: Vec, + /// Bytes more than one cluster covers. + pub repeated: Vec, +} + +impl Coverage { + /// Whether the block meets the rule: every byte in exactly one cluster, but for + /// U+000A and U+00AD, which may be in none. + pub fn complete(&self) -> bool { + self.uncovered.is_empty() && self.repeated.is_empty() + } +} + +/// The byte ranges of `s` where `f` holds, merged. +fn ranges(n: usize, f: impl Fn(usize) -> bool) -> Vec { + let mut out: Vec = Vec::new(); + for i in (0..n).filter(|&i| f(i)) { + match out.last_mut() { + Some(last) if last.1 as usize == i => last.1 += 1, + _ => out.push((i as u32, i as u32 + 1)), + } + } + out +} + +/// §5.3's encoder rule for block `t`, over the clusters of every run that draws it. A +/// sink does not check it; a verifier does. +pub fn coverage(d: &Doc, t: usize) -> Coverage { + let s = d.string(t); + let mut count = vec![0u32; s.len()]; + for (i, it) in d.items.iter().enumerate() { + if it.kind != KIND_RUN || it.text as usize != t { + continue; + } + for c in d.run_clusters(i) { + let (a, n) = (c.byte_first as usize, c.byte_len as usize); + for k in count.iter_mut().skip(a).take(n) { + *k += 1; + } + } + } + // U+000A is one byte, 0A; U+00AD is the two bytes C2 AD. + let exempt = |i: usize| s[i] == 0x0A || (s[i] == 0xC2 && s.get(i + 1) == Some(&0xAD)) || (s[i] == 0xAD && i > 0 && s[i - 1] == 0xC2); + Coverage { uncovered: ranges(s.len(), |i| count[i] == 0 && !exempt(i)), repeated: ranges(s.len(), |i| count[i] > 1) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn merge_sorts_and_joins_touching_intervals() { + assert_eq!(merge(vec![]), vec![]); + assert_eq!(merge(vec![(5, 9), (0, 5)]), vec![(0, 9)], "touching intervals join"); + assert_eq!(merge(vec![(0, 4), (5, 9)]), vec![(0, 4), (5, 9)], "one tick apart stays apart"); + assert_eq!(merge(vec![(0, 10), (2, 3), (9, 12), (20, 21)]), vec![(0, 12), (20, 21)]); + } + + #[test] + fn strings_take_exactly_three_escapes() { + let mut out = Vec::new(); + push_string(&mut out, "a\"b\\c\nd/<&>\u{00AD}\u{2028}\u{1F600}".as_bytes()); + assert_eq!(text_of(out), "\"a\\\"b\\\\c\\nd/<&>\u{00AD}\u{2028}\u{1F600}\""); + } + + #[test] + fn integers_are_decimal() { + for (v, s) in [(0u64, "0"), (7, "7"), (10, "10"), (2_147_483_648, "2147483648"), (u64::MAX, "18446744073709551615")] { + let mut out = Vec::new(); + push_int(&mut out, v); + assert_eq!(text_of(out), s); + } + } +} diff --git a/wai/wai-vector/src/vectors.rs b/wai/wai-vector/src/vectors.rs index 20f4384ad..44f7fdbcc 100644 --- a/wai/wai-vector/src/vectors.rs +++ b/wai/wai-vector/src/vectors.rs @@ -15,7 +15,11 @@ //! accepted payload its SHA-256 (`instr`), `text_hash` and block identities (§2.11), //! each listed size's cost (§3.4(b)) and each listed rate's ticks per frame and //! frame range (§3.1, §2.7); in the groups that render, every frame's SHA-256 at -//! every listed size and rate, and each clip digest (§3.10, §2.9). +//! every listed size and rate, and each clip digest (§3.10, §2.9); in the group +//! `a11y`, `text_record`, the SHA-256 of the payload's text record (§5.4). +//! - `expected/a11y/.a11y.jsonl`: an accepted payload's text record itself: the +//! transcript, then every frame's projection at every listed rate, one JSON value +//! per line. //! - `sha256.json`: the tree-wide digest sidecar (SPEC §7, *Digests*). //! //! [`generate`] writes every file. [`recompute`] gives a vector's expected file from @@ -25,6 +29,8 @@ //! module keeps [`recompute`] and what it needs, and the generator's entry points //! are absent. +#[cfg(feature = "author")] +pub mod a11y; #[cfg_attr(not(feature = "author"), allow(dead_code))] mod arith_vectors; #[cfg_attr(not(feature = "author"), allow(dead_code))] @@ -100,6 +106,7 @@ pub fn all_vectors() -> Vec { v.extend(raster::vectors()); v.extend(motion::vectors()); v.extend(text::vectors()); + v.extend(a11y::vectors()); v } @@ -109,6 +116,10 @@ pub const INPUT_GROUPS: [&str; 2] = ["arith", "flatten"]; /// The groups whose expected files state every frame's digest. pub const FRAME_GROUPS: [&str; 7] = ["fills", "runs", "composite", "motion", "clock", "slicing", "text"]; +/// The groups whose expected files state the SHA-256 of the payload's text record +/// (§5.4), and carry the record beside them as `.a11y.jsonl`. +pub const TEXT_GROUPS: [&str; 1] = ["a11y"]; + /// A resolver holding every set under `sets/`. #[cfg(feature = "author")] pub fn corpus_store() -> SetStore { @@ -143,20 +154,36 @@ fn digest_str(b: &[u8]) -> Value { } /// A payload's expected file: its refusal, or what an accepted payload states; with -/// `frames`, every frame's digest at every listed size and rate. -pub fn expect_wvg1(bytes: &[u8], store: &mut SetStore, frames: bool) -> String { +/// `frames`, every frame's digest at every listed size and rate; with `record`, the +/// SHA-256 of its text record. +pub fn expect_wvg1(bytes: &[u8], store: &mut SetStore, frames: bool, record: bool) -> String { match wvg1::load(bytes, store) { Err(r) => line(&sorted(alloc::vec![("code", string(r.code.name()))])), Ok(l) => { let mut v = accepted(bytes, &l.doc, &l.costs); - if frames && let Value::Obj(m) = &mut v { - m.push((String::from("frames"), frame_digests(&l, false))); + if let Value::Obj(m) = &mut v { + if frames { + m.push((String::from("frames"), frame_digests(&l, false))); + } + if record { + m.push((String::from("text_record"), digest_str(crate::text::record(&l.doc).as_bytes()))); + } } line(&sorted_value(v)) } } } +/// The text record (§5.4) of the vector at `vectors//.`, which the +/// corpus holds as `expected//.a11y.jsonl`: `Some` for an accepted payload +/// of a group in [`TEXT_GROUPS`]. +pub fn text_record(group: &str, ext: &str, bytes: &[u8], store: &mut SetStore) -> Option { + if ext != "wvg1" || !TEXT_GROUPS.contains(&group) { + return None; + } + wvg1::load(bytes, store).ok().map(|l| crate::text::record(&l.doc)) +} + /// Every frame of every listed size and rate: `{w, h, num, den, frames: [[g, SHA-256 of /// RGBA8P], …], clip}`, with `rgba8s` (the SHA-256 of each frame's `RGBA8S`) when /// `straight` is set. @@ -208,8 +235,6 @@ fn sorted_value(v: Value) -> Value { } fn accepted(bytes: &[u8], d: &Doc, costs: &[u64]) -> Value { - let mut th = d.texts_bytes(); - th.extend_from_slice(&d.strings); let blocks: Vec = (0..d.texts.len()).map(|t| string(&hex(&block_identity(d, t)))).collect(); let sizes: Vec = d.sizes.iter().zip(costs).map(|(s, &c)| Value::Arr(alloc::vec![int(s.w), int(s.h), int(c)])).collect(); let rates: Vec = d @@ -224,25 +249,14 @@ fn accepted(bytes: &[u8], d: &Doc, costs: &[u64]) -> Value { sorted(alloc::vec![ ("code", string("accepted")), ("instr", digest_str(bytes)), - ("text_hash", digest_str(&th)), + ("text_hash", string(&hex(&crate::text::text_hash(d)))), ("blocks", Value::Arr(blocks)), ("sizes", Value::Arr(sizes)), ("rates", Value::Arr(rates)), ]) } -/// A block's identity (§2.11): SHA-256 of `"wai:text-block"`, `0x01`, `purpose`, `dir`, -/// `mode`, `lang_len`, the tag, `str_len` as `u32` little-endian, and the string. -pub fn block_identity(d: &Doc, t: usize) -> [u8; 32] { - let b = &d.texts[t]; - let mut m = Vec::new(); - m.extend_from_slice(b"wai:text-block"); - m.extend_from_slice(&[0x01, b.purpose, b.dir, b.mode, b.lang_len]); - m.extend_from_slice(d.lang(t)); - m.extend_from_slice(&b.str_len.to_le_bytes()); - m.extend_from_slice(d.string(t)); - sha256(&m) -} +pub use crate::text::block_identity; /// An outline set's expected file. pub fn expect_wos1(bytes: &[u8]) -> String { @@ -264,7 +278,7 @@ pub fn recompute(group: &str, ext: &str, bytes: &[u8], store: &mut SetStore) -> ("arith", "json") => arith_vectors::recompute(core::str::from_utf8(bytes).map_err(|_| String::from("not UTF-8"))?), ("flatten", "json") => raster::recompute_flatten(core::str::from_utf8(bytes).map_err(|_| String::from("not UTF-8"))?), ("composite", "wvg1") => Ok(expect_composite(bytes, store)), - (g, "wvg1") => Ok(expect_wvg1(bytes, store, FRAME_GROUPS.contains(&g))), + (g, "wvg1") => Ok(expect_wvg1(bytes, store, FRAME_GROUPS.contains(&g), TEXT_GROUPS.contains(&g))), (_, "wos1") => Ok(expect_wos1(bytes)), _ => Err(format!("no recomputation for {group}/*.{ext}")), } @@ -375,6 +389,9 @@ pub fn generate() -> Result)>, String> { } let e = recompute(v.group, v.ext, &v.bytes, &mut store)?; out.push((format!("{CORPUS}/expected/{}/{}.json", v.group, v.name), e.into_bytes())); + if let Some(record) = text_record(v.group, v.ext, &v.bytes, &mut store) { + out.push((format!("{CORPUS}/expected/{}/{}.a11y.jsonl", v.group, v.name), record.into_bytes())); + } } for g in &groups { let members: Vec<&Vector> = vs.iter().filter(|v| v.group == *g).collect(); diff --git a/wai/wai-vector/src/vectors/a11y.rs b/wai/wai-vector/src/vectors/a11y.rs new file mode 100644 index 000000000..a2226197e --- /dev/null +++ b/wai/wai-vector/src/vectors/a11y.rs @@ -0,0 +1,347 @@ +//! The `a11y` group (`graphics-vector.md` §5.4): payloads whose expected files pin the +//! transcript and every frame's projection at every listed rate, as the text record. +//! +//! Each vector isolates one rule: the worked example of §5.4; frames before, during +//! and after each window, with a fade to nothing inside one; a `decorative` block, +//! left out; the text alternatives of paths; reading groups; ruby links, whole and +//! partial, and one that outlasts its base; every registered purpose and three +//! unregistered ones, written raw; intervals that touch, overlap, nest and stay apart; +//! windows clipped by the document's; windows clipped by enclosing groups; the three +//! writing modes; four rates over one document; strings that need each escape and +//! strings that need none; and a payload with no text at all. +//! +//! Every block a run draws meets §5.3's encoder rule: each byte of its string is in +//! exactly one cluster, but for a U+000A or a U+00AD, which no glyph draws. +//! +//! Both sets are held, found by SHA-256 among `sets/`. Every string here is written +//! here. + +use alloc::vec; +use alloc::vec::Vec; + +use super::Vector; +use super::canonical::{TB, held}; +use super::raster::{RED, WHITE}; +use super::repertoire::{self, FACE, lay_out, repertoire_source, shape_arabic, shape_devanagari, shape_hebrew}; +use super::stroke::ascii_source; +use super::text::{PX, block, latin, place, run, us}; +use crate::arith::r_rat; +use crate::scene::*; +use crate::wsf1::Wsf1; + +const GROUP: &str = "a11y"; +/// Design subpixels per outline unit of the lines, Q16. +const U: u32 = us(2); +/// The left edge of every line, and the baseline of line `row`. +const X0: i32 = 16 * PX; +const fn y(row: i32) -> i32 { + (36 + 26 * row) * PX +} + +/// A document over `[t0, t0 + dur)` on a 640 × 360 canvas, listed at that size and +/// 25 fps: 4,800 ticks a frame. +fn doc(t0: u32, dur: u32) -> SceneDef { + let mut s = SceneDef::new(TB, t0, dur, 640, 360); + s.sizes.push((640, 360)); + s.rates.push((25, 1)); + s +} + +/// A run from the stroke set (set index `set`) drawing bytes `[a, b)` of block +/// `text`'s ASCII string `s`, on line `row`, over the window `t`: one cluster a byte. +/// The line begins at byte `line0` of the string, and the run starts where the bytes +/// from `line0` to `a` end. +#[allow(clippy::too_many_arguments)] +fn part(font: &Wsf1, set: usize, text: u16, s: &str, line0: usize, (a, b): (usize, usize), row: i32, t: (u32, u32)) -> ItemDef { + let (_, _, before) = latin(font, &s[line0..a], 0); + let x = X0 + r_rat(i64::from(before) * i64::from(U), 65_536) as i32; + let (g, c, _) = latin(font, &s[a..b], a as u32); + run(set, text, place(&g, (x, y(row)), U), c, U, WHITE, t, NO_SLOTS) +} + +/// A run drawing the whole of block `text`'s ASCII string. +fn line(font: &Wsf1, set: usize, text: u16, s: &str, row: i32, t: (u32, u32)) -> ItemDef { + part(font, set, text, s, 0, (0, s.len()), row, t) +} + +fn grouped(mut t: TextDef, group: u16) -> TextDef { + t.group = group; + t +} + +fn ruby(mut t: TextDef, base: u16, start: u32, len: u32) -> TextDef { + t.ruby = base + 1; + t.ruby_start = start; + t.ruby_len = len; + t +} + +fn moded(mut t: TextDef, mode: u8) -> TextDef { + t.mode = mode; + t +} + +pub fn vectors() -> Vec { + let font = ascii_source(); + let rep = repertoire_source(); + let stroke = held(super::stroke::ascii_set_bytes()); + let tests = held(repertoire::repertoire_set_bytes()); + let mut v = Vec::new(); + let mut add = |name: &str, s: &SceneDef, note: &str| v.push(Vector::wvg1(GROUP, name, s.bytes(), note)); + + // ---- worked-example: §5.4's example. A lower third of two blocks in group 1, in + // two languages and directions, and a ruby annotation over the first word of the + // second, shown for a shorter time. + { + // The Arabic words are common nouns, "the door of the house". + let words = "\u{0628}\u{0627}\u{0628} \u{0627}\u{0644}\u{0628}\u{064A}\u{062A}"; + let role = "Director of photography"; + let mut s = doc(0, 360_000); + s.sets = vec![tests.clone(), stroke.clone()]; + s.texts = vec![grouped(block(3, "ar", 1, words), 1), grouped(block(3, "en", 0, role), 1), grouped(ruby(block(7, "en", 0, "DOP"), 1, 0, 8), 1)]; + let (sh, cl) = shape_arabic(words); + let (g, c) = lay_out(&rep, &sh, &cl, true, 0); + let (rg, rc, _) = latin(&font, "DOP", 0); + s.items = vec![ + run(0, 0, place(&g, (X0, y(9)), U), c, U, WHITE, (24_000, 264_000), NO_SLOTS), + line(&font, 1, 1, role, 11, (24_000, 264_000)), + run(1, 2, place(&rg, (X0, y(10) - 4 * PX), us(1)), rc, us(1), WHITE, (24_000, 120_000), NO_SLOTS), + ]; + add("worked-example", &s, "the worked example of section 5.4: a lower third of an Arabic block and an English block in group 1, both active over [24000, 264000), and a ruby annotation of the first 8 bytes of the second, active over [24000, 120000): accepted"); + } + + // ---- windows: frames before, during and after each window; a fade inside one. + { + let mut s = doc(0, 240_000); + s.sets = vec![stroke.clone()]; + s.texts = vec![block(1, "en", 0, "Opening title"), block(2, "en", 0, "Written here")]; + let fade = TrackDef { + kind: 1, + keys: vec![KeyDef { t: 0, value: 255, ease: Ease::Hold }, KeyDef { t: 72_000, value: 255, ease: Ease::Linear }, KeyDef { t: 86_400, value: 0, ease: Ease::Hold }], + }; + let (g, c, _) = latin(&font, "Opening title", 0); + s.items = vec![ + run(0, 0, place(&g, (X0, y(0)), U), c, U, WHITE, (24_000, 96_000), [None, None, None, None, None, None, Some(fade)]), + line(&font, 0, 1, "Written here", 1, (120_000, 216_000)), + ]; + add("windows", &s, "a title active over [24000, 96000) whose opacity reaches 0 at tick 86400, and a credit active over [120000, 216000): frames before, between and after the windows project nothing, and the title stays in the projection at opacity 0 until its window ends: accepted"); + } + + // ---- decorative: left out of the transcript and of every projection. + { + let mut s = doc(0, 120_000); + s.sets = vec![stroke.clone()]; + s.texts = vec![block(7, "en", 0, "Scores"), block(13, "en", 0, "* * *"), block(12, "en", 0, "3 - 1")]; + s.items = vec![line(&font, 0, 0, "Scores", 0, (0, 120_000)), line(&font, 0, 1, "* * *", 1, (0, 120_000)), line(&font, 0, 2, "3 - 1", 2, (0, 120_000))]; + add("decorative", &s, "three blocks drawn for the whole document, the second of purpose 13 (decorative): the transcript and every projection hold blocks 0 and 2 only, with their own indices: accepted"); + } + + // ---- alternatives: the text alternatives of paths. + { + let mut s = doc(0, 120_000); + s.sets = vec![stroke.clone()]; + s.texts = vec![block(10, "en", 0, "WAI"), block(11, "en", 0, "A red square"), block(7, "en", 0, "Both")]; + let mark = PathDef { contours: vec![poly(&[(0, 0), (60, 100), (120, 0), (180, 100), (240, 0), (200, 0), (180, 40), (120, -60), (60, 40), (40, 0)])] }; + let path = |p: PathDef, paint: [u8; 4], text: Option, t: (u32, u32), x: i32| { + let tx = TrackDef::constant(2, x * PX); + ItemDef::Path { path: p, rule: 0, paint, text, t_on: t.0, t_off: t.1, unit_scale: 1 << 24, slots: [None, None, None, None, Some(tx), None, None] } + }; + s.items = vec![ + path(mark, WHITE, Some(0), (0, 72_000), 16), + path(PathDef::rect(0, 0, 64, 64), RED, Some(1), (48_000, 120_000), 300), + path(PathDef::rect(0, 0, 32, 32), WHITE, Some(2), (0, 24_000), 400), + path(PathDef::rect(0, 0, 16, 16), RED, None, (0, 120_000), 500), + line(&font, 0, 2, "Both", 6, (96_000, 120_000)), + ]; + add("alternatives", &s, "a logo block and a graphic block, each the text alternative of a path and active over that path's window; a block drawn by a path and later by a run, active over both windows; and a path with no alternative: accepted"); + } + + // ---- groups: reading groups, and an ungrouped block between them. + { + let mut s = doc(0, 120_000); + s.sets = vec![stroke.clone()]; + let lines: [(u8, &str, u16); 6] = [(3, "Camera", 1), (3, "Second unit", 1), (7, "Interval", 0), (2, "Editing", 2), (2, "Sound", 2), (2, "Music", 2)]; + s.texts = lines.iter().map(|&(p, t, g)| grouped(block(p, "en", 0, t), g)).collect(); + let win: [(u32, u32); 6] = [(0, 48_000), (0, 48_000), (48_000, 72_000), (72_000, 120_000), (72_000, 120_000), (96_000, 120_000)]; + s.items = lines.iter().enumerate().map(|(i, &(_, t, _))| line(&font, 0, i as u16, t, i as i32, win[i])).collect(); + add("groups", &s, "a lower third of two blocks in group 1, an ungrouped label, and three credits in group 2, the last entering later: each node carries its group number, and reading order is block order: accepted"); + } + + // ---- ruby: partial bases, and an annotation that outlasts its base. + { + let base = "\u{0939}\u{093F}\u{0902}\u{0926}\u{0940}"; + let (sh, cl) = shape_devanagari(base); + let (g, c) = lay_out(&rep, &sh, &cl, false, 0); + let mut s = doc(0, 120_000); + s.sets = vec![tests.clone(), stroke.clone()]; + s.texts = vec![block(7, "hi", 0, base), ruby(block(7, "hi-Latn", 0, "hin"), 0, 0, 9), ruby(block(7, "hi-Latn", 0, "di"), 0, 9, 6)]; + let small = |text: u16, t: &str, x: i32, win: (u32, u32)| { + let (rg, rc, _) = latin(&font, t, 0); + run(1, text, place(&rg, (X0 + x * PX, y(0)), us(1)), rc, us(1), WHITE, win, NO_SLOTS) + }; + s.items = vec![run(0, 0, place(&g, (X0, y(1) + 8 * PX), U), c, U, WHITE, (0, 96_000), NO_SLOTS), small(1, "hin", 0, (0, 48_000)), small(2, "di", 14, (48_000, 120_000))]; + add("ruby", &s, "a Devanagari base block of two syllables and two ruby blocks, one over the base's first 9 bytes and one over the 6 bytes after them: each node's ruby object gives the base's index, the range's length and its start; the second annotation stays active after its base's window ends, and keeps its ruby object: accepted"); + } + + // ---- purposes: every registered purpose, and three unregistered ones. + { + let names = ["title", "credit", "lower_third", "caption", "subtitle", "forced_narrative", "label", "legal", "alert", "logo", "graphic", "data", "decorative"]; + let mut s = doc(0, 24_000); + s.sets = vec![stroke.clone()]; + let mut all: Vec<(u8, &str)> = names.iter().enumerate().map(|(i, &n)| (i as u8 + 1, n)).collect(); + all.extend([(14, "p14"), (200, "p200"), (255, "p255")]); + s.texts = all.iter().map(|&(p, n)| block(p, "en", 0, n)).collect(); + s.items = all.iter().enumerate().map(|(i, &(_, n))| { + let (g, c, _) = latin(&font, n, 0); + run(0, i as u16, place(&g, (X0 + 300 * PX * (i as i32 / 8), y(i as i32 % 8)), U), c, U, WHITE, (0, 24_000), NO_SLOTS) + }).collect(); + add("purposes", &s, "one block of each purpose 1 to 13, then of the unregistered purposes 14, 200 and 255: every node carries the TEXT byte as an integer, 200 as 200; the decorative block (13) is left out: accepted"); + } + + // ---- intervals: touching, overlapping, one tick apart, nested, out of order. + { + let mut s = doc(0, 120_000); + s.sets = vec![stroke.clone()]; + let cases: [(&str, &[(u32, u32)]); 5] = [ + ("touching windows", &[(0, 24_000), (24_000, 48_000)]), + ("overlapping windows", &[(0, 36_000), (24_000, 60_000)]), + ("apart windows", &[(0, 24_000), (24_001, 48_000)]), + ("nested windows", &[(0, 96_000), (24_000, 48_000)]), + ("later then earlier", &[(72_000, 96_000), (0, 24_000), (24_000, 48_000)]), + ]; + s.texts = cases.iter().map(|&(t, _)| block(7, "en", 0, t)).collect(); + for (i, &(t, wins)) in cases.iter().enumerate() { + // One run a word: each word's bytes, and the space before it, in its own run. + let mut cuts: Vec = t.match_indices(' ').map(|(k, _)| k).collect(); + cuts.insert(0, 0); + cuts.push(t.len()); + for (k, &win) in wins.iter().enumerate() { + s.items.push(part(&font, 0, i as u16, t, 0, (cuts[k], cuts[k + 1]), i as i32, win)); + } + } + add("intervals", &s, "five blocks, each drawn by one run a word over that word's own window: windows that touch merge into one interval, as do windows that overlap and a window inside another; windows one tick apart stay two intervals; and the intervals are sorted whatever the items' order: accepted"); + } + + // ---- clipped: windows cut by the document's window [50000, 144000). + { + let mut s = doc(50_000, 94_000); + s.sets = vec![stroke.clone()]; + let cases: [(&str, &[(u32, u32)]); 6] = [ + ("starts before", &[(0, 72_000)]), + ("ends after", &[(120_000, 240_000)]), + ("spans it", &[(0, 1 << 31)]), + ("over by then", &[(0, 50_000)]), + ("not yet", &[(144_000, 200_000)]), + ("two parts", &[(24_000, 60_000), (60_000, 150_000)]), + ]; + s.texts = cases.iter().map(|&(t, _)| block(7, "en", 0, t)).collect(); + for (i, &(t, wins)) in cases.iter().enumerate() { + if wins.len() == 1 { + s.items.push(line(&font, 0, i as u16, t, i as i32, wins[0])); + } else { + let cut = t.find(' ').unwrap_or(t.len()); + s.items.push(part(&font, 0, i as u16, t, 0, (0, cut), i as i32, wins[0])); + s.items.push(part(&font, 0, i as u16, t, 0, (cut, t.len()), i as i32, wins[1])); + } + } + add("clipped", &s, "a document whose window is [50000, 144000), its first frame at tick 52800: windows that begin before it, end after it or span it are cut to it; a window that ends where it begins, and one that begins where it ends, leave their blocks never active, with an empty active set; two windows are cut and then merged: accepted"); + } + + // ---- nested-groups: windows cut by the groups that enclose the items. + { + let mut s = doc(0, 240_000); + s.sets = vec![stroke.clone()]; + let texts = ["in the outer group", "in both groups", "in both, later", "in a group that never opens", "in no group"]; + s.texts = texts.iter().map(|&t| block(7, "en", 0, t)).collect(); + let begin = |t: (u32, u32)| ItemDef::Begin { clip: None, t_on: t.0, t_off: t.1, tx: None, ty: None, op: None }; + s.items = vec![ + begin((24_000, 96_000)), + line(&font, 0, 0, texts[0], 0, (0, 240_000)), + begin((48_000, 200_000)), + line(&font, 0, 1, texts[1], 1, (0, 60_000)), + line(&font, 0, 2, texts[2], 2, (72_000, 240_000)), + ItemDef::End, + begin((96_000, 120_000)), + line(&font, 0, 3, texts[3], 3, (0, 240_000)), + ItemDef::End, + ItemDef::End, + line(&font, 0, 4, texts[4], 4, (100_000, 130_000)), + ]; + add("nested-groups", &s, "runs inside item groups: each run's effective window is its own cut by every enclosing group's, so a run active throughout is active over its group's window only, a run in two groups over the intersection of all three, and a run in a group whose window misses its parent's is never active: accepted"); + } + + // ---- modes: the three writing modes. + { + let mut s = doc(0, 24_000); + s.sets = vec![stroke.clone()]; + s.texts = vec![moded(block(7, "en", 0, "WAI"), 1), moded(block(7, "en", 0, "WAI"), 2), block(7, "en", 0, "WAI")]; + for (i, x) in [(0u16, 560), (1, 40)] { + let mut g = Vec::new(); + let mut c = Vec::new(); + for (k, b) in "WAI".bytes().enumerate() { + let adv = i32::from(font.glyph(u16::from(b)).map_or(0, |g| g.advance)); + g.push((u16::from(b), -adv / 2, 1_700 * k as i32)); + c.push((1, k as u32, 1)); + } + s.items.push(run(0, i, place(&g, (x * PX, 60 * PX), U), c, U, WHITE, (0, 24_000), NO_SLOTS)); + } + s.items.push(line(&font, 0, 2, "WAI", 10, (0, 24_000))); + add("modes", &s, "three blocks equal in everything but the writing mode: vertical-rl (1), vertical-lr (2) and horizontal-tb (0), the vertical ones upright and stacked; each node carries its mode, and the three block identities differ: accepted"); + } + + // ---- rates: one document at four rates. + { + let mut s = doc(0, 60_000); + s.rates = vec![(24_000, 1_001), (25, 1), (30_000, 1_001), (60, 1)]; + s.sets = vec![stroke.clone()]; + s.texts = vec![block(3, "en", 0, "Four rates")]; + s.items = vec![line(&font, 0, 0, "Four rates", 0, (24_000, 50_000))]; + add("rates", &s, "one block active over [24000, 50000) in a document listed at 24000/1001, 25, 30000/1001 and 60 fps: the record gives every frame of each rate in RATE order; at 60 fps frame 12 sits on the window's first tick and is in it, and frame 25 sits on its end and is not: accepted"); + } + + // ---- strings: the three escapes, and bytes that take none. + { + let mut s = doc(0, 24_000); + s.sets = vec![stroke.clone(), tests.clone()]; + let markup = "He said \"A&B\" \\\n2/2"; + let cut = markup.find('\n').unwrap_or(0); + let face = "OK \u{1F600}"; + let hebrew = "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"; + let hindi = "\u{0939}\u{093F}\u{0928}\u{094D}\u{0926}\u{0940}"; + let soft = "soft\u{00AD}hyphen"; + s.texts = vec![block(7, "en", 0, markup), block(7, "en", 0, face), block(7, "he", 1, hebrew), block(7, "hi", 0, hindi), block(7, "sr-Latn-RS", 0, "Zdravo"), block(7, "en", 0, soft)]; + // Block 0: two lines; the U+000A between them is in no cluster. + s.items.push(part(&font, 0, 0, markup, 0, (0, cut), 0, (0, 24_000))); + s.items.push(part(&font, 0, 0, markup, cut + 1, (cut + 1, markup.len()), 1, (0, 24_000))); + // Block 1: three ASCII glyphs, then one glyph for the four bytes of U+1F600. + let (lg, lc, lw) = latin(&font, "OK ", 0); + s.items.push(run(0, 1, place(&lg, (X0, y(2)), U), lc, U, WHITE, (0, 24_000), NO_SLOTS)); + let xf = X0 + r_rat(i64::from(lw) * i64::from(U), 65_536) as i32; + s.items.push(run(1, 1, place(&[(FACE, 0, 0)], (xf, y(2)), U), vec![(1, 3, 4)], U, WHITE, (0, 24_000), NO_SLOTS)); + let (sh, cl) = shape_hebrew(hebrew); + let (g, c) = lay_out(&rep, &sh, &cl, true, 0); + s.items.push(run(1, 2, place(&g, (X0, y(3)), U), c, U, WHITE, (0, 24_000), NO_SLOTS)); + let (sh, cl) = shape_devanagari(hindi); + let (g, c) = lay_out(&rep, &sh, &cl, false, 0); + s.items.push(run(1, 3, place(&g, (X0, y(5)), U), c, U, WHITE, (0, 24_000), NO_SLOTS)); + s.items.push(line(&font, 0, 4, "Zdravo", 6, (0, 24_000))); + // Block 5: "soft" and an inserted hyphen on one line, "hyphen" on the next; the + // U+00AD at the break is in no cluster. + let (mut g, mut c, w) = latin(&font, "soft", 0); + g.push((u16::from(b'-'), w, 0)); + c.push((1, 4, 0)); + s.items.push(run(0, 5, place(&g, (X0, y(7)), U), c, U, WHITE, (0, 24_000), NO_SLOTS)); + let (g, c, _) = latin(&font, "hyphen", 6); + s.items.push(run(0, 5, place(&g, (X0, y(8)), U), c, U, WHITE, (0, 24_000), NO_SLOTS)); + add("strings", &s, "strings as JSON: a quotation mark, a backslash and U+000A take the three escapes, and nothing else is escaped: markup characters, a solidus, a soft hyphen, Hebrew, Devanagari and U+1F600 are written as their UTF-8 bytes; a language tag keeps its letter case: accepted"); + } + + // ---- no-text: a payload with no text blocks. + { + let mut s = doc(0, 24_000); + s.items = vec![ItemDef::Path { path: PathDef::rect(0, 0, 64, 64), rule: 0, paint: RED, text: None, t_on: 0, t_off: 24_000, unit_scale: 1 << 24, slots: NO_SLOTS }]; + add("no-text", &s, "a payload with no text blocks: the transcript and every projection are the empty array: accepted"); + } + + v +} diff --git a/wai/wai-vector/src/vectors/canonical.rs b/wai/wai-vector/src/vectors/canonical.rs index a7501c94b..55b48d147 100644 --- a/wai/wai-vector/src/vectors/canonical.rs +++ b/wai/wai-vector/src/vectors/canonical.rs @@ -331,6 +331,7 @@ pub fn vectors() -> Vec { s }; add("groups-in-order", blocks3([1, 1, 2]).bytes(), "rule 4: group numbers in order of first use, each group contiguous: accepted"); + add("groups-with-ungrouped", blocks3([1, 0, 2]).bytes(), "rule 4: groups [1, 0, 2], an ungrouped block between two groups: accepted"); add("group-number-skips", refuse_doc(&blocks3([2, 2, 0]), |_| {}), "rule 4: the first group is numbered 2 -> payload_malformed"); add("group-not-contiguous", refuse_doc(&blocks3([1, 2, 1]), |_| {}), "rule 4: groups [1, 2, 1] -> payload_malformed"); add("group-split-by-ungrouped", refuse_doc(&blocks3([1, 0, 1]), |_| {}), "rule 4: groups [1, 0, 1] -> payload_malformed"); @@ -354,6 +355,19 @@ pub fn vectors() -> Vec { add("ruby-names-itself", refuse_doc(&ruby_doc(2, 0, 3), |_| {}), "rule 4: a ruby link to its own block -> payload_malformed"); add("ruby-mid-character", refuse_doc(&ruby_doc(1, 1, 2), |_| {}), "rule 4: a ruby range starting inside a character -> payload_malformed"); add("ruby-past-string", refuse_doc(&ruby_doc(1, 3, 6), |_| {}), "rule 4: a ruby range past the base block's string -> payload_malformed"); + add("ruby-to-string-end", ruby_doc(1, 3, 3).bytes(), "rule 4: a ruby range that starts inside the base block's string, on a character boundary, and ends where the string ends: accepted"); + add("ruby-ends-mid-character", refuse_doc(&ruby_doc(1, 0, 4), |_| {}), "rule 4: a ruby range ending inside a character -> payload_malformed"); + add( + "ruby-names-later", + refuse_doc(&ruby_doc(1, 0, 3), |d| { + // Block 0 annotates block 1 instead: a range of one character, on boundaries. + d.texts[0].ruby = 2; + d.texts[0].ruby_len = 3; + d.texts[1].ruby = 0; + d.texts[1].ruby_len = 0; + }), + "rule 4: a ruby link to a later block -> payload_malformed", + ); let cl = |clusters: Vec<(u16, u32, u32)>, n: usize| { let mut s = base(); diff --git a/wai/wai-vector/src/vectors/refusals.rs b/wai/wai-vector/src/vectors/refusals.rs index 43306ad86..39d6a2f90 100644 --- a/wai/wai-vector/src/vectors/refusals.rs +++ b/wai/wai-vector/src/vectors/refusals.rs @@ -277,8 +277,11 @@ pub fn vectors() -> Vec { add("s4-text-ruby-over", patch(&r, |d| { d.texts[0].ruby = 2; d.texts[0].ruby_len = 1; }), "TEXT.ruby above n_texts -> field_range"); add("s4-text-ruby-start", patch(&r, |d| d.texts[0].ruby_start = 1), "TEXT.ruby_start with no ruby -> field_range"); add("s4-text-ruby-len-zero", patch(&r, |d| d.texts[0].ruby = 1), "TEXT ruby set with ruby_len = 0 -> field_range"); + add("s4-text-ruby-len-no-ruby", patch(&r, |d| d.texts[0].ruby_len = 1), "TEXT.ruby_len = 1 with no ruby -> field_range"); add("s4-text-str-len-zero", patch(&r, |d| d.texts[0].str_len = 0), "TEXT.str_len = 0 -> field_range"); add("s4-text-str-len-over", patch(&r, |d| d.texts[0].str_len = 4097), "TEXT.str_len = 4097 -> field_range"); + add("s4-path-before-text", patch(&r, |d| { d.paths[1].zero = 1; d.texts[0].purpose = 0; }), "pair: a PATH zero field and TEXT.purpose = 0 -> payload_malformed (tables in file order: PATHS before TEXTS)"); + add("s4-text-before-item", patch(&r, |d| { d.texts[0].mode = 3; d.items[1].zero2 = 1; }), "pair: TEXT.mode = 3 and an ITEM zero field -> field_range (tables in file order: TEXTS before ITEMS)"); add( "s4-item-kind-four", patch(&r, |d| { @@ -401,6 +404,27 @@ pub fn vectors() -> Vec { add("s5f-string-overlong", text(b"\xc0\x81"), "5(f): an overlong encoding -> payload_malformed"); add("s5f-string-surrogate", text(b"\xed\xa0\x80A"), "5(f): an encoded surrogate -> payload_malformed"); add("s5f-string-newline", text(b"A\nB"), "5(f): U+000A is the one C0 control allowed: accepted"); + add("s5c-typing-before-strings", { let mut s = rich(); s.texts[0].lang = b"-en".to_vec(); let mut d = doc(&s); d.keys[2].value = 256; d.write() }, "pair: an opacity key of 256 and a language tag with a leading hyphen -> field_range (5(c) before 5(f))"); + + // ------------------------------------------------------------ steps 5(g) and 5(h) before the sets + // The run's set made a held set no sink holds: step 6 would report outline_not_held. + let not_held = |s: &SceneDef| { + let mut d = doc(s); + d.sets[0] = SetRec { carriage: 0, zero: [0; 3], length: 40, sha256: sha256(b"no set has this digest") }; + d.inline.clear(); + d.header.held_len = 40; + d.sync_counts(); + d.write() + }; + let mut undrawn = rich(); + undrawn.texts.push(TextDef::new(3, "en", "C")); + add("s5g-block-before-sets", not_held(&undrawn), "pair: a block no run or path names, and a held set the sink cannot obtain -> payload_malformed (5(g) before 6)"); + let mut ornament = rich(); + ornament.texts.push(TextDef::new(13, "en", "*")); + if let ItemDef::Path { text, .. } = &mut ornament.items[1] { + *text = Some(1); + } + add("s5h-decorative-before-sets", not_held(&ornament), "pair: a path whose text alternative is a decorative block, and a held set the sink cannot obtain -> payload_malformed (5(h) before 6)"); // ------------------------------------------------------------ step 6: sets let b_bytes = set_b().write(); diff --git a/wai/wai-vector/src/vectors/text.rs b/wai/wai-vector/src/vectors/text.rs index aa2388795..fd86cbbd4 100644 --- a/wai/wai-vector/src/vectors/text.rs +++ b/wai/wai-vector/src/vectors/text.rs @@ -5,9 +5,9 @@ //! listed sizes each: Latin; Arabic and Hebrew right to left; Devanagari with a //! reordered matra, a conjunct and a mark; a mixed-direction line whose runs come from //! both sets; a block split over three runs and lines, with an inserted hyphen and a -//! zero-glyph cluster; a ruby annotation; a vertical-rl block; and the U+1F600 face. The -//! text layer's projection lands later; these vectors pin runs, clusters, blocks and -//! frames. +//! zero-glyph cluster; a ruby annotation; a vertical-rl block; and the U+1F600 face. +//! These vectors pin runs, clusters, blocks and frames; the `a11y` group +//! (`a11y.rs`) pins the projection and the transcript. //! //! Both sets are held, found by SHA-256 among `sets/`. Every string here is written //! here. @@ -26,22 +26,22 @@ use crate::scene::*; use crate::wsf1::Wsf1; /// Q16 design subpixels per outline unit: `n` subpixels per unit. -const fn us(n: u32) -> u32 { +pub(super) const fn us(n: u32) -> u32 { n * 65_536 } /// A glyph's outline-unit position to design subpixels, from an origin. -fn place(glyphs: &[(u16, i32, i32)], origin: (i32, i32), unit_scale: u32) -> Vec<(u16, i32, i32)> { +pub(super) fn place(glyphs: &[(u16, i32, i32)], origin: (i32, i32), unit_scale: u32) -> Vec<(u16, i32, i32)> { let s = i64::from(unit_scale); glyphs.iter().map(|&(g, x, y)| (g, origin.0 + r_rat(i64::from(x) * s, 65_536) as i32, origin.1 + r_rat(i64::from(y) * s, 65_536) as i32)).collect() } /// A Latin run's glyph records and clusters, and its width in outline units. -type Latin = (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>, i32); +pub(super) type Latin = (Vec<(u16, i32, i32)>, Vec<(u16, u32, u32)>, i32); /// A Latin string in the stroke set: one glyph per byte, its ID the byte, advancing /// left to right; one cluster per glyph from `byte_offset`; and its width. -fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> Latin { +pub(super) fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> Latin { let mut pen = 0i32; let mut g = Vec::new(); let mut c = Vec::new(); @@ -55,11 +55,11 @@ fn latin(set: &Wsf1, s: &str, byte_offset: u32) -> Latin { /// A run item over the whole document. #[allow(clippy::too_many_arguments)] -fn run(set: usize, text: u16, glyphs: Vec<(u16, i32, i32)>, clusters: Vec<(u16, u32, u32)>, unit_scale: u32, paint: [u8; 4], t: (u32, u32), slots: Slots) -> ItemDef { +pub(super) fn run(set: usize, text: u16, glyphs: Vec<(u16, i32, i32)>, clusters: Vec<(u16, u32, u32)>, unit_scale: u32, paint: [u8; 4], t: (u32, u32), slots: Slots) -> ItemDef { ItemDef::Run { set, paint, text, t_on: t.0, t_off: t.1, unit_scale, slots, glyphs, clusters } } -fn block(purpose: u8, lang: &str, dir: u8, s: &str) -> TextDef { +pub(super) fn block(purpose: u8, lang: &str, dir: u8, s: &str) -> TextDef { let mut t = TextDef::new(purpose, lang, s); t.dir = dir; t @@ -72,7 +72,7 @@ fn doc2(w: u16, h: u16) -> SceneDef { s } -const PX: i32 = 256; +pub(super) const PX: i32 = 256; pub fn vectors() -> Vec { let latin_set = ascii_source(); diff --git a/wai/wai-vector/tests/corpus.rs b/wai/wai-vector/tests/corpus.rs index f9edd4424..634c66f78 100644 --- a/wai/wai-vector/tests/corpus.rs +++ b/wai/wai-vector/tests/corpus.rs @@ -1,5 +1,5 @@ //! `vector-conformance/`: the generator reproduces every committed file, and every -//! expected file is recomputed from its vector alone. +//! expected file is recomputed from its vector alone, each text record included. use std::path::PathBuf; @@ -29,7 +29,7 @@ fn every_expected_file_is_recomputed_from_its_vector() { for p in sets { store.add(std::fs::read(p).unwrap()); } - let mut n = 0; + let (mut n, mut records) = (0, 0); let mut groups: Vec = std::fs::read_dir(corpus.join("vectors")).unwrap().flatten().map(|e| e.path()).collect(); groups.sort(); for gdir in groups { @@ -42,11 +42,24 @@ fn every_expected_file_is_recomputed_from_its_vector() { if name == "index" || (ext == "json" && !vectors::INPUT_GROUPS.contains(&group.as_str())) { continue; } - let got = vectors::recompute(&group, &ext, &std::fs::read(&f).unwrap(), &mut store).unwrap(); + let bytes = std::fs::read(&f).unwrap(); + let got = vectors::recompute(&group, &ext, &bytes, &mut store).unwrap(); let exp = std::fs::read_to_string(corpus.join("expected").join(&group).join(format!("{name}.json"))).unwrap(); assert_eq!(got, exp, "{group}/{name}.{ext}"); n += 1; + // The text record beside it (§5.4), recomputed from the payload: the committed + // lines. The expected file above states their SHA-256 (`text_record`). + let path = corpus.join("expected").join(&group).join(format!("{name}.a11y.jsonl")); + match vectors::text_record(&group, &ext, &bytes, &mut store) { + Some(record) => { + let have = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + assert_eq!(record, have, "{group}/{name}: the text record"); + records += 1; + } + None => assert!(!path.exists(), "{}: a record no vector accounts for", path.display()), + } } } assert!(n > 250, "{n} vectors"); + assert!(records > 10, "{records} text records"); } diff --git a/wai/wai-vector/tests/fuzz_smoke.rs b/wai/wai-vector/tests/fuzz_smoke.rs index 3b8e20635..53859b9f6 100644 --- a/wai/wai-vector/tests/fuzz_smoke.rs +++ b/wai/wai-vector/tests/fuzz_smoke.rs @@ -1,15 +1,18 @@ //! A deterministic, corpus-seeded fuzz of the `WVG1` and `WOS1` parsers (§2.7): no -//! input panics, every input accepted writes back to its own bytes, and an accepted -//! payload of small cost renders its first frame. Seeds: every vector of the corpus -//! and the files under `fuzz/seeds/`. +//! input panics, every input accepted writes back to its own bytes, an accepted +//! payload of small cost renders its first frame, and every accepted payload's text +//! layer holds (§5.4). Seeds: every vector of the corpus and the files under +//! `fuzz/seeds/`. //! //! `WAI_VECTOR_FUZZ_ITERS` sets the mutations per seed (default 40). use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; +use wai_vector::json::{self, Value}; +use wai_vector::text::TextLayer; use wai_vector::vectors::{all_vectors, corpus_store}; -use wai_vector::{wos1, wvg1}; +use wai_vector::{clock, wos1, wvg1}; struct Rng(u64); @@ -63,6 +66,77 @@ fn mutate(seed: &[u8], r: &mut Rng) -> Vec { b } +/// The blocks drawn at tick `tau`, by the frame procedure's own rule (§3.10): an item +/// draws when it is active, and an inactive group begin skips everything up to its +/// matching end. This never forms a window. +fn drawn_at(d: &wvg1::Doc, tau: u64) -> Vec { + let active = |it: &wvg1::Item| u64::from(it.t_on) <= tau && tau < u64::from(it.t_off); + let mut drawn = vec![false; d.texts.len()]; + let mut i = 0; + while i < d.items.len() { + let it = &d.items[i]; + match it.kind { + wvg1::KIND_BEGIN if !active(it) => { + let mut depth = 1; + while depth > 0 { + i += 1; + match d.items[i].kind { + wvg1::KIND_BEGIN => depth += 1, + wvg1::KIND_END => depth -= 1, + _ => {} + } + } + } + wvg1::KIND_RUN | wvg1::KIND_PATH if active(it) && it.text != 0xFFFF => drawn[it.text as usize] = true, + _ => {} + } + i += 1; + } + drawn +} + +/// An accepted payload's text layer (§5.4). Each active set is sorted, its intervals +/// neither overlap nor touch, and all lie in the document's window. The transcript is +/// JSON, holds every block that is not decorative, and gives back each block's tag and +/// string. At the first and the last frame of every listed rate, the projection is +/// JSON and lists exactly the blocks the frame procedure draws at that tick. +fn text_layer_holds(d: &wvg1::Doc) { + let layer = TextLayer::new(d); + let (t0, end) = (u64::from(d.header.t0), u64::from(d.header.t0) + u64::from(d.header.dur)); + for set in &layer.active { + let mut last: Option = None; + for &(on, off) in set { + assert!(t0 <= u64::from(on) && on < off && u64::from(off) <= end, "[{on}, {off}) is not an interval of the document's window"); + assert!(last.is_none_or(|l| l < on), "intervals ascend, and neither overlap nor touch"); + last = Some(off); + } + } + let exposed: Vec = (0..d.texts.len()).filter(|&t| d.texts[t].purpose != 13).collect(); + let index = |n: &Value| n.get("index").and_then(Value::as_int).expect("a node has an index") as usize; + let transcript = json::parse(&layer.transcript()).expect("the transcript is JSON"); + let nodes = transcript.as_arr().expect("the transcript is an array"); + assert_eq!(nodes.iter().map(index).collect::>(), exposed, "the transcript holds every block that is not decorative"); + for n in nodes { + let t = index(n); + assert_eq!(n.get("text").and_then(Value::as_str).map(str::as_bytes), Some(d.string(t)), "a string is given back as carried"); + assert_eq!(n.get("lang").and_then(Value::as_str).map(str::as_bytes), Some(d.lang(t))); + assert_eq!(n.get("active").and_then(Value::as_arr).map(<[Value]>::len), Some(layer.active[t].len())); + } + for (r, rate) in d.rates.iter().enumerate() { + let p = rate.ticks_per_frame(d.header.tb); + let (first, last) = clock::frame_range(d.header.t0, d.header.dur, p); + if first > last { + continue; + } + for g in [first as u64, last as u64] { + let projection = json::parse(&layer.projection(r, g).expect("a frame in range")).expect("a projection is JSON"); + let drawn = drawn_at(d, g * p); + let want: Vec = exposed.iter().copied().filter(|&t| drawn[t]).collect(); + assert_eq!(projection.as_arr().expect("an array").iter().map(index).collect::>(), want, "frame {g} at {}/{}", rate.num, rate.den); + } + } +} + #[test] fn fuzz_smoke() { let iters: usize = std::env::var("WAI_VECTOR_FUZZ_ITERS").ok().and_then(|s| s.parse().ok()).unwrap_or(40); @@ -79,6 +153,15 @@ fn fuzz_smoke() { seeds.push((p.extension().is_some_and(|x| x == "wos1"), std::fs::read(&p).unwrap())); } let mut store = corpus_store(); + // The text layer of every seed that loads, before any is mutated. + let mut layers = 0usize; + for (is_wos1, seed) in &seeds { + if !*is_wos1 && let Ok(l) = wvg1::load(seed, &mut store) { + text_layer_holds(&l.doc); + layers += 1; + } + } + assert!(layers > 60, "{layers} seeds load"); let mut r = Rng(0x5EED_0FD7_0002); let (mut runs, mut accepted, mut rendered) = (0usize, 0usize, 0usize); for (is_wos1, seed) in &seeds { @@ -100,11 +183,12 @@ fn fuzz_smoke() { assert_eq!(px.len(), 4 * usize::from(d.sizes[0].w) * usize::from(d.sizes[0].h)); rendered += 1; } + text_layer_holds(d); l.doc.write() }) } })); - let out = ok.unwrap_or_else(|_| panic!("a parser panicked on input {}", wai_vector::json::hex(&input))); + let out = ok.unwrap_or_else(|_| panic!("the parser, the renderer or the text layer panicked on input {}", json::hex(&input))); if let Some(written) = out { assert_eq!(written, input, "an accepted input writes back to its own bytes"); accepted += 1; @@ -112,6 +196,6 @@ fn fuzz_smoke() { runs += 1; } } - eprintln!("fuzz smoke: {runs} inputs from {} seeds, {accepted} accepted, {rendered} rendered", seeds.len()); + eprintln!("fuzz smoke: {runs} inputs from {} seeds, {accepted} accepted, {rendered} rendered; the text layer of {layers} seeds and of every accepted input", seeds.len()); assert!(runs >= seeds.len() * iters); } diff --git a/wai/wai-vector/tests/text_layer.rs b/wai/wai-vector/tests/text_layer.rs new file mode 100644 index 000000000..ea436546c --- /dev/null +++ b/wai/wai-vector/tests/text_layer.rs @@ -0,0 +1,489 @@ +//! The text layer (`graphics-vector.md` §2.11, §5): the text digests, each block's +//! active set, and the projection and the transcript as JSON. +//! +//! Each test reads committed payloads of `vector-conformance/` and holds what §5 says +//! of them, written out here by hand: the JSON is never taken from the code it checks. +//! The worked example is read from the extension's own text. +//! +//! Every leg runs these: natively, and under WASI with the `wai` directory preopened. + +use std::path::PathBuf; + +use wai_vector::json::hex; +use wai_vector::refuse::Code; +use wai_vector::sha256::sha256; +use wai_vector::text::{self, TextLayer}; +use wai_vector::wvg1::{self, Loaded, SetStore}; + +fn wai() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn read(rel: &str) -> Vec { + std::fs::read(wai().join(rel)).unwrap_or_else(|e| panic!("{rel}: {e}")) +} + +/// A resolver holding every set under `sets/`. +fn store() -> SetStore { + let mut s = SetStore::new(); + let mut sets: Vec = std::fs::read_dir(wai().join("vector-conformance/sets")).unwrap().flatten().map(|e| e.path()).collect(); + sets.sort(); + for p in sets { + s.add(std::fs::read(p).unwrap()); + } + s +} + +fn payload(group: &str, name: &str) -> Vec { + read(&format!("vector-conformance/vectors/{group}/{name}.wvg1")) +} + +fn load(group: &str, name: &str) -> Loaded { + wvg1::load(&payload(group, name), &mut store()).unwrap_or_else(|e| panic!("{group}/{name}: {}", e.code.name())) +} + +/// The committed text record of an `a11y` vector, by line. +fn committed(name: &str) -> Vec { + let text = String::from_utf8(read(&format!("vector-conformance/expected/a11y/{name}.a11y.jsonl"))).unwrap(); + assert!(text.ends_with('\n'), "{name}: every line of a record ends in U+000A"); + text.lines().map(String::from).collect() +} + +/// A node as §5.4 writes it: `active` (a transcript's) first when given, then `dir`, +/// `group`, `index`, `lang`, `mode`, `purpose`, `ruby` when given, `text`. +#[allow(clippy::too_many_arguments)] +fn node(active: Option<&str>, dir: u8, group: u16, index: usize, lang: &str, mode: u8, purpose: u8, ruby: Option<&str>, text: &str) -> String { + let active = active.map(|a| format!("\"active\":{a},")).unwrap_or_default(); + let ruby = ruby.map(|r| format!("\"ruby\":{r},")).unwrap_or_default(); + format!("{{{active}\"dir\":{dir},\"group\":{group},\"index\":{index},\"lang\":\"{lang}\",\"mode\":{mode},\"purpose\":{purpose},{ruby}\"text\":\"{text}\"}}") +} + +/// A plain English label: left to right, ungrouped, horizontal, purpose 7. +fn label(active: Option<&str>, index: usize, text: &str) -> String { + node(active, 0, 0, index, "en", 0, 7, None, text) +} + +fn array(nodes: &[String]) -> String { + format!("[{}]", nodes.join(",")) +} + +/// A record's line for one frame. +fn frame(num: u32, den: u32, g: u64, nodes: &[String]) -> String { + format!("{{\"den\":{den},\"g\":{g},\"num\":{num},\"projection\":{}}}", array(nodes)) +} + +/// The fenced block that follows `marker` in the extension's §5.4, by line. +fn fenced_after(marker: &str) -> Vec { + let doc = String::from_utf8(read("extensions/graphics-vector.md")).unwrap(); + let at = doc.find(marker).unwrap_or_else(|| panic!("the extension has no {marker}")); + let rest = &doc[at..]; + let open = rest.find("```").expect("a fenced block"); + let body = &rest[open..]; + let body = &body[body.find('\n').unwrap() + 1..]; + body[..body.find("```").expect("the fence closes")].lines().map(String::from).collect() +} + +#[test] +fn the_worked_example_of_section_5_4_byte_for_byte() { + let example = fenced_after("**A worked example.**"); + assert_eq!(example.len(), 2, "the example is a transcript and one frame's line"); + let l = load("a11y", "worked-example"); + let layer = TextLayer::new(&l.doc); + // The transcript, and frame 30 at 25 fps: tick 144000, after the annotation's window. + assert_eq!(layer.transcript(), example[0], "the transcript"); + assert_eq!(l.doc.rates[0].ticks_per_frame(l.doc.header.tb), 4800); + assert_eq!(format!("{{\"den\":1,\"g\":30,\"num\":25,\"projection\":{}}}", layer.projection(0, 30).unwrap()), example[1], "frame 30"); + // The committed record holds both lines: the transcript first, then frames 0 to 74. + let lines = committed("worked-example"); + assert_eq!(lines.len(), 1 + 75); + assert_eq!(lines[0], example[0]); + assert_eq!(lines[1 + 30], example[1]); + assert_eq!(text::record(&l.doc), lines.iter().map(|l| format!("{l}\n")).collect::()); + // What the example says of itself, written out again here. + // The Arabic block's words: common nouns, "the door of the house". + let words = "\u{0628}\u{0627}\u{0628} \u{0627}\u{0644}\u{0628}\u{064A}\u{062A}"; + let both = "[[24000,264000]]"; + let want = array(&[ + node(Some(both), 1, 1, 0, "ar", 0, 3, None, words), + node(Some(both), 0, 1, 1, "en", 0, 3, None, "Director of photography"), + node(Some("[[24000,120000]]"), 0, 1, 2, "en", 0, 7, Some("{\"base\":1,\"len\":8,\"start\":0}"), "DOP"), + ]); + assert_eq!(example[0], want); + assert_eq!(example[1], frame(25, 1, 30, &[node(None, 1, 1, 0, "ar", 0, 3, None, words), node(None, 0, 1, 1, "en", 0, 3, None, "Director of photography")])); + // Before the windows open nothing is projected; inside the annotation's, all three. + assert_eq!(lines[1 + 4], frame(25, 1, 4, &[])); + assert!(lines[1 + 5].contains("\"text\":\"DOP\"") && lines[1 + 24].contains("\"text\":\"DOP\"") && !lines[1 + 25].contains("DOP")); + assert_eq!(lines[1 + 55], frame(25, 1, 55, &[]), "tick 264000 is past both windows"); +} + +#[test] +fn purpose_200_is_written_raw() { + let l = load("a11y", "purposes"); + let names = ["title", "credit", "lower_third", "caption", "subtitle", "forced_narrative", "label", "legal", "alert", "logo", "graphic", "data", "decorative", "p14", "p200", "p255"]; + let purposes: Vec = (1..=13).chain([14, 200, 255]).collect(); + assert_eq!(l.doc.texts.iter().map(|t| t.purpose).collect::>(), purposes, "the TEXT bytes"); + // Every node carries the TEXT byte itself; block 12, decorative, is in neither. + let nodes = |active: Option<&str>| -> Vec { (0..16).filter(|&i| i != 12).map(|i| node(active, 0, 0, i, "en", 0, purposes[i], None, names[i])).collect() }; + let layer = TextLayer::new(&l.doc); + assert_eq!(layer.transcript(), array(&nodes(Some("[[0,24000]]")))); + assert_eq!(layer.projection(0, 0).unwrap(), array(&nodes(None))); + let t = layer.transcript(); + assert!(t.contains("\"index\":14,\"lang\":\"en\",\"mode\":0,\"purpose\":200,\"text\":\"p200\""), "200 is written as 200"); + assert!(t.contains("\"purpose\":14,") && t.contains("\"purpose\":255,")); + assert_eq!(t.matches("\"purpose\":7,").count(), 1, "only the label block reads 7"); + assert_eq!(committed("purposes")[0], t); +} + +#[test] +fn touching_intervals_merge() { + let l = load("a11y", "intervals"); + let want = array(&[ + label(Some("[[0,48000]]"), 0, "touching windows"), + label(Some("[[0,60000]]"), 1, "overlapping windows"), + label(Some("[[0,24000],[24001,48000]]"), 2, "apart windows"), + label(Some("[[0,96000]]"), 3, "nested windows"), + label(Some("[[0,48000],[72000,96000]]"), 4, "later then earlier"), + ]); + assert_eq!(text::transcript(&l.doc), want); + assert_eq!(committed("intervals")[0], want); + // The windows as carried: [0, 24000) and [24000, 48000) for block 0. + let wins: Vec<(u32, u32)> = l.doc.items.iter().filter(|it| it.text == 0).map(|it| (it.t_on, it.t_off)).collect(); + assert_eq!(wins, [(0, 24_000), (24_000, 48_000)]); + assert_eq!(text::active_sets(&l.doc)[0], [(0, 48_000)]); + // Tick 24000 is in neither interval of block 2, which stays two intervals. + assert_eq!(text::active_sets(&l.doc)[2], [(0, 24_000), (24_001, 48_000)]); +} + +#[test] +fn windows_are_clipped_to_the_document() { + let l = load("a11y", "clipped"); + assert_eq!((l.doc.header.t0, l.doc.header.dur), (50_000, 94_000), "the document's window is [50000, 144000)"); + let want = array(&[ + label(Some("[[50000,72000]]"), 0, "starts before"), + label(Some("[[120000,144000]]"), 1, "ends after"), + label(Some("[[50000,144000]]"), 2, "spans it"), + label(Some("[]"), 3, "over by then"), + label(Some("[]"), 4, "not yet"), + label(Some("[[50000,144000]]"), 5, "two parts"), + ]); + assert_eq!(text::transcript(&l.doc), want); + // The windows as carried run past the document on both sides. + assert_eq!((l.doc.items[0].t_on, l.doc.items[0].t_off), (0, 72_000)); + assert_eq!((l.doc.items[2].t_on, l.doc.items[2].t_off), (0, 1 << 31)); + // Frames 11 to 29: the first at tick 52800, the last at 139200. + let lines = committed("clipped"); + assert_eq!(lines.len(), 1 + 19); + assert_eq!(lines[0], want); + assert_eq!(lines[1], frame(25, 1, 11, &[label(None, 0, "starts before"), label(None, 2, "spans it"), label(None, 5, "two parts")])); + assert_eq!(lines[19], frame(25, 1, 29, &[label(None, 1, "ends after"), label(None, 2, "spans it"), label(None, 5, "two parts")])); +} + +#[test] +fn the_ruby_object_is_base_len_start() { + let l = load("a11y", "ruby"); + let base = "\u{0939}\u{093F}\u{0902}\u{0926}\u{0940}"; + assert_eq!(base.len(), 15); + // TEXT carries r = 1 for block 0; the JSON carries the block's index, r − 1 = 0. + assert_eq!(l.doc.texts.iter().map(|t| (t.ruby, t.ruby_start, t.ruby_len)).collect::>(), [(0, 0, 0), (1, 0, 9), (1, 9, 6)]); + let first = "{\"base\":0,\"len\":9,\"start\":0}"; + let second = "{\"base\":0,\"len\":6,\"start\":9}"; + let want = array(&[ + node(Some("[[0,96000]]"), 0, 0, 0, "hi", 0, 7, None, base), + node(Some("[[0,48000]]"), 0, 0, 1, "hi-Latn", 0, 7, Some(first), "hin"), + node(Some("[[48000,120000]]"), 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di"), + ]); + let layer = TextLayer::new(&l.doc); + assert_eq!(layer.transcript(), want); + // Frame 21, tick 100800: the base's window has ended, and the annotation keeps its + // ruby object. + assert_eq!(layer.projection(0, 21).unwrap(), array(&[node(None, 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di")])); + assert_eq!(committed("ruby")[1 + 21], frame(25, 1, 21, &[node(None, 0, 0, 2, "hi-Latn", 0, 7, Some(second), "di")])); + // The range rebuilds the annotated part of the base exactly. + assert_eq!(&base[0..9], "\u{0939}\u{093F}\u{0902}"); + assert_eq!(&base[9..15], "\u{0926}\u{0940}"); +} + +#[test] +fn mode_is_in_the_block_identity() { + let l = load("a11y", "modes"); + let d = &l.doc; + assert_eq!(d.texts.iter().map(|t| t.mode).collect::>(), [1, 2, 0]); + // The three blocks are equal in every other field. + for t in 1..3 { + let (a, b) = (d.texts[0], d.texts[t]); + assert_eq!((a.purpose, a.dir, a.lang_len, a.group, a.ruby, a.str_len), (b.purpose, b.dir, b.lang_len, b.group, b.ruby, b.str_len)); + assert_eq!((d.lang(0), d.string(0)), (d.lang(t), d.string(t))); + } + // "wai:text-block", 0x01, purpose 7, dir 0, the mode, lang_len 2, "en", str_len 3 as + // u32 little-endian, "WAI": built here byte by byte. + let identity = |mode: u8| { + let mut m = b"wai:text-block".to_vec(); + m.extend_from_slice(&[0x01, 7, 0, mode, 2, b'e', b'n', 3, 0, 0, 0, b'W', b'A', b'I']); + sha256(&m) + }; + for (t, mode) in [(0usize, 1u8), (1, 2), (2, 0)] { + assert_eq!(text::block_identity(d, t), identity(mode), "block {t}, mode {mode}"); + } + assert!(identity(0) != identity(1) && identity(1) != identity(2) && identity(0) != identity(2)); + // The same digests from another SHA-256 (Python's hashlib), over the same bytes. + assert_eq!(hex(&text::block_identity(d, 2)), IDENTITY_MODE_0); + assert_eq!(hex(&text::block_identity(d, 0)), IDENTITY_MODE_1); + assert_eq!(hex(&text::block_identity(d, 1)), IDENTITY_MODE_2); + // The expected file states them in block order, and each node carries its mode. + let expected = String::from_utf8(read("vector-conformance/expected/a11y/modes.json")).unwrap(); + assert!(expected.contains(&format!("\"blocks\":[\"{IDENTITY_MODE_1}\",\"{IDENTITY_MODE_2}\",\"{IDENTITY_MODE_0}\"]"))); + let nodes: Vec = [1u8, 2, 0].iter().enumerate().map(|(i, &m)| node(Some("[[0,24000]]"), 0, 0, i, "en", m, 7, None, "WAI")).collect(); + assert_eq!(text::transcript(d), array(&nodes)); +} + +// hashlib.sha256(b"wai:text-block\x01\x07\x00" + bytes([mode]) + b"\x02en\x03\x00\x00\x00WAI") +const IDENTITY_MODE_0: &str = "7717339a5b738a00c3f6191d2f43cc78156c65651b8c55d4ec234efa06efc968"; +const IDENTITY_MODE_1: &str = "47d74c9ccb8456e3f1171904dda15a84bf74897c9bcc12d9f4fef166730e8c1a"; +const IDENTITY_MODE_2: &str = "916bd4458608698f35b350172b1827ab26e53094e6d9fd784a62311f4270c7bc"; + +#[test] +fn group_contiguity_1_2_1_is_refused() { + let groups = |name: &str| -> Vec { wvg1::split(&payload("canonical", name)).unwrap().texts.iter().map(|t| t.group).collect() }; + let refusal = |name: &str| wvg1::load(&payload("canonical", name), &mut store()).err().unwrap_or_else(|| panic!("canonical/{name} was accepted")); + // [1, 2, 1]: group 1 resumes after group 2. + assert_eq!(groups("group-not-contiguous"), [1, 2, 1]); + let r = refusal("group-not-contiguous"); + assert_eq!((r.code, r.step, r.index), (Code::PayloadMalformed, "5g", 2), "refused at block 2, by the canonical rules"); + assert!(r.rule.contains("contiguous"), "{}", r.rule); + // [1, 0, 1]: an ungrouped block splits it too. + assert_eq!(groups("group-split-by-ungrouped"), [1, 0, 1]); + let r = refusal("group-split-by-ungrouped"); + assert_eq!((r.code, r.step), (Code::PayloadMalformed, "5g")); + assert!(r.rule.contains("contiguous"), "{}", r.rule); + // Contiguous groups load, with or without an ungrouped block between them. + for (name, want) in [("groups-in-order", [1, 1, 2]), ("groups-with-ungrouped", [1, 0, 2])] { + assert_eq!(groups(name), want); + assert!(wvg1::load(&payload("canonical", name), &mut store()).is_ok(), "{name}"); + } +} + +#[test] +fn text_hash_is_texts_then_strings() { + for (group, name) in [("a11y", "worked-example"), ("a11y", "strings"), ("canonical", "every-table")] { + let bytes = payload(group, name); + let l = wvg1::load(&bytes, &mut store()).unwrap(); + let h = &l.doc.header; + // The two areas cut out of the payload by the header's counts (§2.2, §2.3): + // STRINGS is the last table before INLINE, and TEXTS ends where ITEMS, + // GLYPHS, CLUSTERS, STRINGS and INLINE begin. + let strings_end = bytes.len() - h.inline_len as usize; + let strings = &bytes[strings_end - h.strings_len as usize..strings_end]; + let texts_end = strings_end - h.strings_len as usize - 12 * h.n_clusters as usize - 12 * h.n_glyphs as usize - 48 * h.n_items as usize; + let texts = &bytes[texts_end - 20 * h.n_texts as usize..texts_end]; + assert!(h.n_texts >= 3 && !strings.is_empty()); + let want = sha256(&[texts, strings].concat()); + assert_eq!(text::text_hash(&l.doc), want, "{group}/{name}: TEXTS, then STRINGS"); + assert_ne!(sha256(&[strings, texts].concat()), want); + let expected = String::from_utf8(read(&format!("vector-conformance/expected/{group}/{name}.json"))).unwrap(); + assert!(expected.contains(&format!("\"text_hash\":\"{}\"", hex(&want))), "{group}/{name}: the expected file states it"); + } + // The worked example's, from another SHA-256 (Python's hashlib) over the same bytes. + assert_eq!(hex(&text::text_hash(&load("a11y", "worked-example").doc)), TEXT_HASH_WORKED_EXAMPLE); +} + +// hashlib.sha256(TEXTS + STRINGS) of a11y/worked-example.wvg1, the areas cut by offset. +const TEXT_HASH_WORKED_EXAMPLE: &str = "70d5ce883f06a8aedd33bfa1fbc638e69909a380c19007a252584a7d38e0d432"; + +#[test] +fn decorative_blocks_are_excluded() { + let l = load("a11y", "decorative"); + assert_eq!(l.doc.texts.iter().map(|t| t.purpose).collect::>(), [7, 13, 12]); + // Block 1 is drawn for the whole document, like the others. + assert_eq!(text::active_sets(&l.doc), [[(0, 120_000)], [(0, 120_000)], [(0, 120_000)]]); + let whole = Some("[[0,120000]]"); + let lines = committed("decorative"); + assert_eq!(lines.len(), 1 + 25); + assert_eq!(lines[0], array(&[label(whole, 0, "Scores"), node(whole, 0, 0, 2, "en", 0, 12, None, "3 - 1")])); + for g in 0..25u64 { + assert_eq!(lines[1 + g as usize], frame(25, 1, g, &[label(None, 0, "Scores"), node(None, 0, 0, 2, "en", 0, 12, None, "3 - 1")])); + } + let layer = TextLayer::new(&l.doc); + assert_eq!(layer.transcript(), lines[0]); + assert_eq!(layer.blocks_at(0), [0, 2]); + assert!(!text::record(&l.doc).contains("* * *"), "the ornament's string is nowhere in the record"); +} + +#[test] +fn windows_are_cut_by_enclosing_groups() { + let l = load("a11y", "nested-groups"); + let want = array(&[ + label(Some("[[24000,96000]]"), 0, "in the outer group"), + label(Some("[[48000,60000]]"), 1, "in both groups"), + label(Some("[[72000,96000]]"), 2, "in both, later"), + label(Some("[]"), 3, "in a group that never opens"), + label(Some("[[100000,130000]]"), 4, "in no group"), + ]); + assert_eq!(text::transcript(&l.doc), want); + assert_eq!(committed("nested-groups")[0], want); + // The runs as carried are active far longer than their groups. + let runs: Vec<(u32, u32)> = l.doc.items.iter().filter(|it| it.kind == wvg1::KIND_RUN).map(|it| (it.t_on, it.t_off)).collect(); + assert_eq!(runs, [(0, 240_000), (0, 60_000), (72_000, 240_000), (0, 240_000), (100_000, 130_000)]); + // What a frame draws agrees: at tick 96000 the outer group has closed. + let layer = TextLayer::new(&l.doc); + assert_eq!(layer.blocks_at(95_999), [0, 2]); + assert_eq!(layer.blocks_at(96_000), Vec::::new()); +} + +#[test] +fn effective_windows_are_the_costs() { + // §5.4 uses §3.4(b)'s effective windows. Over every accepted payload of the corpus, + // the text layer's are the ones the cost was computed from. + let mut n = 0; + let root = wai().join("vector-conformance/vectors"); + let mut groups: Vec = std::fs::read_dir(&root).unwrap().flatten().map(|e| e.path()).collect(); + groups.sort(); + for g in groups { + let mut files: Vec = std::fs::read_dir(&g).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "wvg1")).collect(); + files.sort(); + for f in files { + let Ok(d) = wvg1::parse(&std::fs::read(&f).unwrap()) else { continue }; + let Ok(sets) = wvg1::load_sets(&d, &mut store()) else { continue }; + let cost: Vec> = wai_vector::place::Scene::new(&d, &sets).item_win; + let mine: Vec> = text::effective_windows(&d).into_iter().map(|w| w.map(|(a, b)| (i64::from(a), i64::from(b)))).collect(); + assert_eq!(mine, cost, "{}", f.display()); + n += 1; + } + } + assert!(n > 60, "{n} payloads"); +} + +#[test] +fn strings_take_three_escapes_and_no_others() { + let l = load("a11y", "strings"); + let whole = Some("[[0,24000]]"); + // The string of block 0 holds a quotation mark, a backslash and U+000A; in JSON + // those three are escaped, and `<`, `&`, `>` and `/` are not. + assert_eq!(l.doc.string(0), b"He said \"A&B\" \\\n2/2"); + let want = array(&[ + label(whole, 0, "He said \\\"A&B\\\" \\\\\\n2/2"), + label(whole, 1, "OK \u{1F600}"), + node(whole, 1, 0, 2, "he", 0, 7, None, "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"), + node(whole, 0, 0, 3, "hi", 0, 7, None, "\u{0939}\u{093F}\u{0928}\u{094D}\u{0926}\u{0940}"), + node(whole, 0, 0, 4, "sr-Latn-RS", 0, 7, None, "Zdravo"), + label(whole, 5, "soft\u{00AD}hyphen"), + ]); + let t = text::transcript(&l.doc); + assert_eq!(t, want); + assert_eq!(committed("strings")[0], want); + // Nothing is written as \u, and no byte below U+0020 is in a line. + assert!(!t.contains("\\u") && !t.contains("\\/")); + assert!(t.bytes().all(|b| b >= 0x20)); + assert!(t.contains("\u{1F600}") && t.as_bytes().windows(2).any(|w| w == [0xC2, 0xAD])); +} + +#[test] +fn a_window_holds_its_first_tick_and_not_its_last() { + // One block active over [24000, 50000), at four rates in RATE order. + let l = load("a11y", "rates"); + let n = node(None, 0, 0, 0, "en", 0, 3, None, "Four rates"); + let mut want = vec![array(&[node(Some("[[24000,50000]]"), 0, 0, 0, "en", 0, 3, None, "Four rates")])]; + // (num, den, frames, the frames whose tick is in the window) + for (num, den, count, inside) in [(24_000u32, 1_001u32, 12u64, 5..=9u64), (25, 1, 13, 5..=10), (30_000, 1_001, 15, 6..=12), (60, 1, 30, 12..=24)] { + for g in 0..count { + want.push(frame(num, den, g, if inside.contains(&g) { std::slice::from_ref(&n) } else { &[] })); + } + } + assert_eq!(committed("rates"), want); + assert_eq!(text::record(&l.doc).lines().map(String::from).collect::>(), want); + // At 60 fps a frame is 2,000 ticks: frame 12 sits on the first tick, frame 25 on the + // tick the window ends at. + let layer = TextLayer::new(&l.doc); + assert_eq!(layer.projection_request(60, 1, 12).unwrap(), array(std::slice::from_ref(&n))); + assert_eq!(layer.projection_request(60, 1, 25).unwrap(), "[]"); + // A title that fades to nothing stays in the projection until its window ends. + let w = committed("windows"); + let title = node(None, 0, 0, 0, "en", 0, 1, None, "Opening title"); + let credit = node(None, 0, 0, 1, "en", 0, 2, None, "Written here"); + assert_eq!(w.len(), 1 + 50); + for (g, nodes) in [(4u64, vec![]), (5, vec![title.clone()]), (18, vec![title.clone()]), (19, vec![title]), (20, vec![]), (24, vec![]), (25, vec![credit.clone()]), (44, vec![credit]), (45, vec![]), (49, vec![])] { + assert_eq!(w[1 + g as usize], frame(25, 1, g, &nodes), "frame {g}"); + } +} + +#[test] +fn a_paths_alternative_is_active_over_the_paths_window() { + let l = load("a11y", "alternatives"); + let want = array(&[ + node(Some("[[0,72000]]"), 0, 0, 0, "en", 0, 10, None, "WAI"), + node(Some("[[48000,120000]]"), 0, 0, 1, "en", 0, 11, None, "A red square"), + label(Some("[[0,24000],[96000,120000]]"), 2, "Both"), + ]); + assert_eq!(text::transcript(&l.doc), want); + assert_eq!(committed("alternatives")[0], want); + // Blocks 0 and 1 are drawn by paths alone. + for t in 0..2u16 { + assert!(l.doc.items.iter().filter(|it| it.text == t).all(|it| it.kind == wvg1::KIND_PATH)); + } + // And the groups vector: each node carries its group number, in block order. + let g = [ + node(Some("[[0,48000]]"), 0, 1, 0, "en", 0, 3, None, "Camera"), + node(Some("[[0,48000]]"), 0, 1, 1, "en", 0, 3, None, "Second unit"), + label(Some("[[48000,72000]]"), 2, "Interval"), + node(Some("[[72000,120000]]"), 0, 2, 3, "en", 0, 2, None, "Editing"), + node(Some("[[72000,120000]]"), 0, 2, 4, "en", 0, 2, None, "Sound"), + node(Some("[[96000,120000]]"), 0, 2, 5, "en", 0, 2, None, "Music"), + ]; + assert_eq!(committed("groups")[0], array(&g)); +} + +#[test] +fn a_frame_outside_the_document_is_a_request_error() { + let l = load("a11y", "clipped"); + let layer = TextLayer::new(&l.doc); + // The document's frames at 25 fps are 11 to 29. + assert!(layer.projection(0, 11).is_ok() && layer.projection(0, 29).is_ok()); + for g in [0u64, 10, 30, 1 << 40, u64::MAX] { + assert_eq!(layer.projection(0, g).map_err(|e| e.code), Err(Code::FrameRange), "frame {g}"); + } + assert_eq!(layer.projection_request(30, 1, 11).map_err(|e| e.code), Err(Code::RateUnlisted)); + assert_eq!(layer.projection(1, 11).map_err(|e| e.code), Err(Code::RateUnlisted)); + assert_eq!(text::projection(&l.doc, 0, 11).unwrap(), layer.projection_request(25, 1, 11).unwrap()); + // A payload with no text: the empty array, at every frame. + let none = committed("no-text"); + assert_eq!(none[0], "[]"); + assert_eq!(none[1..], (0..5).map(|g| frame(25, 1, g, &[])).collect::>()); +} + +#[test] +fn a_verifier_finds_bytes_no_cluster_covers() { + // §5.3's encoder rule holds for every block a run draws in the text and a11y + // groups: each byte in exactly one cluster, but for U+000A and U+00AD. + let mut n = 0; + for group in ["text", "a11y"] { + let dir = wai().join("vector-conformance/vectors").join(group); + let mut files: Vec = std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "wvg1")).collect(); + files.sort(); + for f in files { + let d = wvg1::parse(&std::fs::read(&f).unwrap()).unwrap(); + for t in 0..d.texts.len() { + if d.items.iter().any(|it| it.kind == wvg1::KIND_RUN && it.text as usize == t) { + let c = text::coverage(&d, t); + assert!(c.complete(), "{} block {t}: {c:?}", f.display()); + n += 1; + } + } + } + } + assert!(n > 50, "{n} blocks"); + // The line separator and the soft hyphen of a11y/strings are in no cluster. + let d = load("a11y", "strings").doc; + let covered = |t: usize, at: usize| d.items.iter().enumerate().filter(|(_, it)| it.kind == wvg1::KIND_RUN && it.text as usize == t).any(|(i, _)| d.run_clusters(i).iter().any(|c| (c.byte_first as usize..(c.byte_first + c.byte_len) as usize).contains(&at))); + assert_eq!(d.string(0)[22], 0x0A); + assert!(!covered(0, 22) && covered(0, 21) && covered(0, 23)); + assert_eq!(&d.string(5)[4..6], [0xC2, 0xAD]); + assert!(!covered(5, 4) && !covered(5, 5)); + // A payload that breaks the rule still loads, and a verifier reports where: with + // one cluster dropped its bytes are uncovered, and with one repeated they are + // covered twice. + let mut d = load("a11y", "decorative").doc; + let first = d.ix.item_cluster0[0] as usize; + d.clusters[first + 1].byte_first = 0; + assert_eq!(text::coverage(&d, 0), text::Coverage { uncovered: vec![(1, 2)], repeated: vec![(0, 1)] }); + assert!(!text::coverage(&d, 0).complete()); + assert!(wvg1::parse(&d.write()).is_ok(), "a sink does not check the rule"); +} -- 2.54.0 (Apple Git-157) From 2ed3aff4eb266a4834ce126e99bad48136354037 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:43:02 -0400 Subject: [PATCH 2/4] wai_vector_check.py: section 5, from the extension text alone The independent checker recomputes, from graphics-vector.md section 5, each a11y payload's text record byte for byte and its digest, by two derivations that share neither the interval code nor the JSON writer, and compares every file under expected/, the text records among them. Canonical vectors that name a rule must be refused under it. It agrees with every expected file of the corpus; twenty deliberate changes to it each made it disagree. Co-Authored-By: Claude Opus 5.5 --- wai/tools/wai_vector_check.py | 483 +++++++++++++++++++++++++++++++++- 1 file changed, 476 insertions(+), 7 deletions(-) diff --git a/wai/tools/wai_vector_check.py b/wai/tools/wai_vector_check.py index 737ead53d..ccbb01e87 100644 --- a/wai/tools/wai_vector_check.py +++ b/wai/tools/wai_vector_check.py @@ -2,9 +2,9 @@ """Independent conformance checker for `wai.graphics.vector`. Written from the extension text alone: wai/extensions/graphics-vector.md, sections -1-3 (the normative revision), with the notation of integer-operators section 2 and -the digests of SPEC section 7, which that text cites. It shares no code with the -reference crate, and was written without reading it. +1-3 and section 5 (the normative revision), with the notation of integer-operators +section 2 and the digests of SPEC section 7, which that text cites. It shares no code +with the reference crate, and was written without reading it. Python 3.11 or later, standard library only. Run it with `python3 -I`. @@ -23,18 +23,27 @@ What it computes, each from the section that defines it: - the digest sidecar (2.10): its canonical form and checks, the probe digests, the clip digests, and the authoring tool's probe set; - the text digests (2.11); + - the text layer (5.4): each block's active set from the effective windows of the + items that draw it, the projection at a frame of a listed rate and the transcript, + their JSON byte for byte, and the text record with its SHA-256; and the same + record a second way, from activity tick by tick and JSON with sorted keys; - section 3.12's bounds, asserted on every value the procedure forms. python3 -I wai_vector_check.py load PAYLOAD [--sets DIR] python3 -I wai_vector_check.py render PAYLOAD WxH NUM/DEN G [--sets DIR] [--out F] + python3 -I wai_vector_check.py text PAYLOAD [NUM/DEN G] [--sets DIR] [--out F] python3 -I wai_vector_check.py sidecar PAYLOAD SIDECAR [--sets DIR] [--verify] python3 -I wai_vector_check.py selftest python3 -I wai_vector_check.py corpus vector-conformance [--groups a,b] [--quiet] +`text` prints the SHA-256 of a payload's text record, and writes the record with +`--out`; with a rate and a frame it prints that frame's projection. + `corpus` recomputes every expected file under vector-conformance/ from its vector and -sets/ alone and requires the committed bytes; rewrites every payload and set to its -own bytes; checks each refusal's step where the vector's name or description pins -one; and recomputes every digest of sha256.json. +sets/ alone and requires the committed bytes, the text records among them, and +requires that it recomputed every file under expected/; rewrites every payload and +set to its own bytes; checks each refusal's step, and its canonical rule, where the +vector's name or description pins one; and recomputes every digest of sha256.json. Exit status 1 on any failure. """ @@ -1969,6 +1978,254 @@ def probe_set(P, pairs): return sorted(held) +# ------------------------------------------------------------------ section 5.4 +# +# The text layer: each block's active set, the projection at a frame, the transcript, +# their JSON, and the text record. Section 5.4 defines all of it over the loaded +# payload; nothing here reads a set, a glyph or a pixel. + +DECORATIVE = 13 # section 2.3: text used as ornament, never exposed + + +def text_fault(P, what): + """A statement of section 5.4 that a computed value breaks. It is recorded, not + raised, so the record is still written and compared with the corpus.""" + if getattr(P, "text_faults", None) is None: + P.text_faults = [] + P.text_faults.append(what) + + +def drawing_items(P, k): + """Section 5.4, "the items that draw a block": the run items whose `text` is the + block's index, and the path items whose `text` is the block's index (section 2.4). + A group begin carries no text, and its clip draws no block.""" + return [i for i, it in enumerate(P.items) if it.kind in (2, 3) and it.text == k] + + +def active_set(P, k): + """Section 5.4, "a block's active set": the union of the effective windows + (section 3.4(b) step 4) of the items that draw block k, as half-open tick + intervals [on, off): ascending by `on`, with intervals that overlap or touch + merged; empty when the block is never active inside the document's window.""" + windows = [] + for i in drawing_items(P, k): + a, b = effective_window(P, i) + if a < b: # "It may be empty": an empty window adds nothing to the union + windows.append((a, b)) + windows.sort() # ascending order of `on` + out = [] + for a, b in windows: + if out and a <= out[-1][1]: # overlaps the last interval, or touches it: merge + if b > out[-1][1]: + out[-1][1] = b + else: + out.append([a, b]) + t0, end = P.H["t0"], P.H["t0"] + P.H["dur"] + for j, (a, b) in enumerate(out): + # "Every interval has t0 <= on < off <= t0 + dur", and "no two intervals of + # the list share a tick or an end". + if not t0 <= a < b <= end: + text_fault(P, f"block {k}: interval [{a}, {b}) outside [t0, t0 + dur) or empty") + if j > 0 and not out[j - 1][1] < a: + text_fault(P, f"block {k}: intervals {out[j - 1]} and {[a, b]} share a tick or an " + "end, or are out of order") + return out + + +def active_sets(P): + """Every block's active set, computed once for a payload.""" + if getattr(P, "active", None) is None: + P.active = [active_set(P, k) for k in range(len(P.texts))] + return P.active + + +def holds(active, tau): + """Section 5.4: an active set holds a tick when some interval has on <= tau < off.""" + return any(a <= tau < b for a, b in active) + + +def projection_blocks(P, num, den, g): + """Section 5.4, "the projection at frame g of listed rate r": the indices, in block + order, of every block whose purpose is not 13 and whose active set holds the + frame's tick tau = g*P_r (section 3.1). A request at an unlisted rate is + rate_unlisted and one at a frame outside the document at that rate frame_range, + the request errors of section 2.7, decided by its divisions.""" + if not any(r.num == num and r.den == den for r in P.rates): + raise RequestError("rate_unlisted") + lo, hi = frame_range(P, num, den) + if not (isinstance(g, int) and g >= 0 and lo <= g <= hi): + raise RequestError("frame_range") + tau = g * ticks_per_frame(P, num, den) + act = active_sets(P) + return [k for k, t in enumerate(P.texts) if t.purpose != DECORATIVE and holds(act[k], tau)] + + +def transcript_blocks(P): + """Section 5.4, "the transcript": the indices, in block order, of every block whose + purpose is not 13. A block that is never active is in it.""" + return [k for k, t in enumerate(P.texts) if t.purpose != DECORATIVE] + + +def json_string(b): + """Section 5.4, "the encoding": a quotation mark, the bytes carried, a quotation + mark, with exactly three replacements among those bytes: 0x22 as \\", 0x5C as \\\\ + and 0x0A as \\n. Nothing else is escaped.""" + out = bytearray(b'"') + for c in b: + if c == 0x22: + out += b'\\"' + elif c == 0x5C: + out += b"\\\\" + elif c == 0x0A: + out += b"\\n" + else: + out.append(c) + out += b'"' + return bytes(out) + + +def json_uint(v): + """Section 5.4, "the encoding": decimal, no sign, no leading zero; zero is 0.""" + assert type(v) is int and v >= 0 + return b"%d" % v + + +def node_members(P, k): + """Section 5.4's table: a node object's members for block k, in the stated order + (the ascending byte order of their keys): dir, group, index, lang, mode, purpose, + ruby (only when the block's `ruby` is not 0) and text.""" + t = P.texts[k] + if t.purpose == 13: # "1 ..= 255 and never 13" + text_fault(P, f"block {k}: a node of purpose 13") + m = [b'"dir":' + json_uint(t.dir), + b'"group":' + json_uint(t.group), + b'"index":' + json_uint(k), + b'"lang":' + json_string(P.text_lang[k]), + b'"mode":' + json_uint(t.mode), + b'"purpose":' + json_uint(t.purpose)] + if t.ruby != 0: + # {"base":B,"len":L,"start":S}: B = ruby - 1, S = ruby_start, L = ruby_len; + # "written whether or not block B is itself in the list". + m.append(b'"ruby":{"base":' + json_uint(t.ruby - 1) + b',"len":' + json_uint(t.ruby_len) + + b',"start":' + json_uint(t.ruby_start) + b"}") + m.append(b'"text":' + json_string(P.text_str[k])) + return m + + +def node_json(P, k): + return b"{" + b",".join(node_members(P, k)) + b"}" + + +def block_json(P, k): + """Section 5.4: a block object has the member `active` first, then the members of a + node object. `active` is an array of two-element arrays [on,off], in ticks.""" + act = b"[" + b",".join(b"[" + json_uint(a) + b"," + json_uint(b) + b"]" + for a, b in active_sets(P)[k]) + b"]" + return b"{" + b",".join([b'"active":' + act] + node_members(P, k)) + b"}" + + +def projection_json(P, num, den, g): + """Section 5.4: a projection is one JSON array of node objects, in block order.""" + return b"[" + b",".join(node_json(P, k) for k in projection_blocks(P, num, den, g)) + b"]" + + +def transcript_json(P): + """Section 5.4: a transcript is one JSON array of block objects, in block order.""" + return b"[" + b",".join(block_json(P, k) for k in transcript_blocks(P)) + b"]" + + +def text_record_lines(P): + """Section 5.4, "the text record", as its lines without their line ends: the + transcript; then, for each listed rate in RATE order and each frame g of the + document at that rate in ascending order, {"den":D,"g":G,"num":N,"projection":J}. + A rate at which the document has no frame adds no line.""" + yield transcript_json(P) + for r in P.rates: + lo, hi = frame_range(P, r.num, r.den) + for g in range(lo, hi + 1): + yield (b'{"den":' + json_uint(r.den) + b',"g":' + json_uint(g) + b',"num":' + + json_uint(r.num) + b',"projection":' + projection_json(P, r.num, r.den, g) + + b"}") + + +def text_record(P): + """Section 5.4: a payload's text record, each line ended by one U+000A.""" + return b"".join(line + b"\n" for line in text_record_lines(P)) + + +def blocks_drawn_at(P, tau): + """The blocks an active item draws at tick tau, by the walk of section 3.10 over + section 2.4's activity rule: an item is active when t_on <= tau < t_off, and an + inactive group begin skips everything up to its matching end. No tick outside the + document's window [t0, t0 + dur) draws anything (section 5.4).""" + out = set() + if not P.H["t0"] <= tau < P.H["t0"] + P.H["dur"]: + return out + i = 0 + while i < len(P.items): + it = P.items[i] + if it.kind == 0 and not it.t_on <= tau < it.t_off: + i = P.match[i] + 1 + continue + if it.kind in (2, 3) and it.t_on <= tau < it.t_off and it.text != ABSENT: + out.add(it.text) + i += 1 + return out + + +def text_record_recheck(P, record): + """A second derivation of the text record, which shares neither the interval + arithmetic nor the writer of the first. Returns a list of failures. + + - Active sets, by ticks: between two consecutive ticks at which some window opens + or closes nothing changes, so a block's active set is the maximal stretches of + such ticks at which section 3.10's walk finds an active item that draws it. + - Frames, by section 3.1's definition: the g with t0 <= g*P < t0 + dur. + - The JSON: section 5.4 states that each object's member order is the ascending + byte order of its keys, and that after its three replacements no other control + character occurs (section 2.5 rule 6). So each line is the value written with + sorted keys, no whitespace, and characters outside ASCII as their UTF-8 bytes.""" + t0, end = P.H["t0"], P.H["t0"] + P.H["dur"] + edges = {t0, end} + for it in P.items: + if it.kind != 1: + edges.update(t for t in (it.t_on, it.t_off) if t0 < t < end) + edges = sorted(edges) + act = [[] for _ in P.texts] + for p, q in zip(edges, edges[1:]): + for k in blocks_drawn_at(P, p): + if act[k] and act[k][-1][1] == p: + act[k][-1][1] = q + else: + act[k].append([p, q]) + + def dump(v): + return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + + def node(k): + t = P.texts[k] + d = {"dir": t.dir, "group": t.group, "index": k, "lang": P.text_lang[k].decode("utf-8"), + "mode": t.mode, "purpose": t.purpose, "text": P.text_str[k].decode("utf-8")} + if t.ruby: + d["ruby"] = {"base": t.ruby - 1, "len": t.ruby_len, "start": t.ruby_start} + return d + + shown = [k for k, t in enumerate(P.texts) if t.purpose != DECORATIVE] + lines = [dump([dict(node(k), active=act[k]) for k in shown])] + for r in P.rates: + Pf = ticks_per_frame(P, r.num, r.den) + g = cdiv(t0, Pf) + while g * Pf < end: + here = blocks_drawn_at(P, g * Pf) + lines.append(dump({"den": r.den, "g": g, "num": r.num, + "projection": [node(k) for k in shown if k in here]})) + g += 1 + if "".join(line + "\n" for line in lines).encode("utf-8") != record: + return ["the text record differs from its second derivation (activity by ticks, " + "frames by section 3.1, JSON by sorted keys)"] + return [] + + # ------------------------------------------------------------------ set store @@ -1986,14 +2243,111 @@ def load_store(dirpath): # ------------------------------------------------------------------ self-test from the text +def spec_worked_example(): + """Section 5.4's worked example as the extension text prints it: the two lines of + its fenced block (the transcript, and the text record's line for frame 30), read + from the text beside this tool. None when the text is not there.""" + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "extensions", + "graphics-vector.md") + try: + with open(path, "rb") as f: + text = f.read() + except OSError: + return None + at = text.find(b"**A worked example.**") + start = text.find(b"```text\n", at) + stop = text.find(b"\n```", start) + if at < 0 or start < 0 or stop < 0: + return None + lines = text[start + len(b"```text\n"):stop].split(b"\n") + return lines if len(lines) == 2 else None + + +def text_only_payload(tb, t0, dur, rates, blocks, runs): + """A payload's text layer alone, as section 5.4 reads it. `blocks` are + (purpose, dir, mode, group, ruby, ruby_start, ruby_len, tag, string), and `runs` + are (block index, t_on, t_off) with no enclosing group.""" + P = Payload() + P.H = {"tb": tb, "t0": t0, "dur": dur} + P.rates = [Rate(*r) for r in rates] + P.texts = [Text(b[0], b[1], len(b[7]), b[2], b[3], b[4], b[5], b[6], len(b[8])) for b in blocks] + P.text_lang = [b[7] for b in blocks] + P.text_str = [b[8] for b in blocks] + P.items = [Item(3, 0, 0, 0, 0, k, 0, 1, 1, on, off, 65536, (ABSENT,) * 7, b"\0" * 6) + for k, on, off in runs] + P.parents = [() for _ in runs] + P.match = {} + return P + + +def selftest_text(eq, notes): + """Section 5.4's statements, recomputed.""" + # "The encoding": exactly three replacements, and a solidus takes none. + eq("5.4 string escapes", json_string(b'a"b\\c\nd/<&'), b'"a\\"b\\\\c\\nd/<&"') + eq("5.4 zero", json_uint(0), b"0") + # "A block's active set": [a, b) and [b, c) become [a, c); windows one tick apart + # stay two; the list ascends whatever the items' order; a window is cut to the + # document's, and a block never active inside it has an empty active set. + lab = (7, 0, 0, 0, 0, 0, 0, b"en", b"x") + P = text_only_payload(1000, 100, 900, [(10, 1)], [lab, lab, lab, lab], + [(0, 300, 400), (0, 200, 300), (1, 520, 600), (1, 500, 519), + (2, 0, 150), (2, 950, 5000), (2, 140, 160), (3, 1000, 2000)]) + eq("5.4 active set, touching", active_set(P, 0), [[200, 400]]) + eq("5.4 active set, one tick apart", active_set(P, 1), [[500, 519], [520, 600]]) + eq("5.4 active set, cut and merged", active_set(P, 2), [[100, 160], [950, 1000]]) + eq("5.4 active set, never active", active_set(P, 3), []) + eq("5.4 never active, in the transcript", transcript_blocks(P), [0, 1, 2, 3]) + # "on <= tau < off": frame 2 sits at tick 200, block 0's first; frame 4 at its end. + eq("5.4 projection at a first tick", projection_blocks(P, 10, 1, 2), [0]) + eq("5.4 projection at an end", projection_blocks(P, 10, 1, 4), []) + for bad, code in (((25, 1, 2), "rate_unlisted"), ((10, 1, 0), "frame_range"), + ((10, 1, 10), "frame_range")): + try: + projection_blocks(P, *bad) + eq(f"5.4 request {bad}", "answered", code) + except RequestError as e: + eq(f"5.4 request {bad}", e.code, code) + eq("5.4 second derivation", text_record_recheck(P, text_record(P)), []) + eq("5.4 stated invariants", getattr(P, "text_faults", None) or [], []) + # "A worked example": tb = 120000, t0 = 0, dur = 360000, 25 fps, so P = 4800; three + # blocks in group 1, the third a ruby annotation of the first 8 bytes of block 1. + # THE TEXT DOES NOT SETTLE the example's inputs in full: its list of blocks states + # no string (block 0 only by its meaning) and no writing mode. So the strings are + # read from the two lines it prints, and the mode is taken as 0 (horizontal-tb); + # every other byte of those lines is recomputed from the stated fields. + ex = spec_worked_example() + if ex is None: + notes.append("section 5.4's worked example was not recomputed: the extension text " + "is not beside this tool") + return + s = [e["text"].encode("utf-8") for e in json.loads(ex[0].decode("utf-8"))] + eq("5.4 worked example, blocks", len(s), 3) + if len(s) != 3: + return + P = text_only_payload(120000, 0, 360000, [(25, 1)], + [(3, 1, 0, 1, 0, 0, 0, b"ar", s[0]), (3, 0, 0, 1, 0, 0, 0, b"en", s[1]), + (7, 0, 0, 1, 2, 0, 8, b"en", s[2])], + [(0, 24000, 264000), (1, 24000, 264000), (2, 24000, 120000)]) + eq("5.4 worked example, P", ticks_per_frame(P, 25, 1), 4800) + eq("5.4 worked example, frames", frame_range(P, 25, 1), (0, 74)) + lines = list(text_record_lines(P)) + eq("5.4 worked example, transcript", lines[0], ex[0]) + eq("5.4 worked example, frame 30", lines[1 + 30], ex[1]) + eq("5.4 worked example, second derivation", text_record_recheck(P, text_record(P)), []) + eq("5.4 worked example, stated invariants", getattr(P, "text_faults", None) or [], []) + + def selftest(): """Worked examples and claims the text states, recomputed.""" fails = [] + notes = [] def eq(what, got, want): if got != want: fails.append(f"{what}: {got} != {want}") + selftest_text(eq, notes) + # 3.8: the triangle (0,0)-(512,3)-(0,3) gives |v| = 1280 and 512 in its two pixels. vals = fill_values([[(0, 0), (512, 3), (0, 3), (0, 0)]], 4, 4) got = sorted(abs(v) for spans in vals.values() for c0, c1, v in spans for _ in range(c0, c1)) @@ -2024,6 +2378,8 @@ def selftest(): a = fill_values([[(10, 7), (900, 700), (10, 700), (10, 7)]], 8, 4) b = fill_values([[(10, 7), (10, 700), (900, 700), (10, 7)]], 8, 4) eq("3.11 P1 reversal", {r: [(c0, c1, -v) for c0, c1, v in s] for r, s in a.items()}, b) + for n in notes: + print("NOTE", n) if fails: for f in fails: print("FAIL", f) @@ -2043,6 +2399,10 @@ def selftest(): # The groups the corpus README names as rendering: their expected files list frames. RENDER_GROUPS = ("clock", "composite", "fills", "motion", "runs", "slicing", "text") RGBA8S_GROUPS = ("composite",) +# The group whose expected files state `text_record`, each with its text record beside +# it as .a11y.jsonl. Its frames are not rendered. +TEXT_GROUPS = ("a11y",) +TEXT_RECORD_SUFFIX = ".a11y.jsonl" def canon(obj): @@ -2089,6 +2449,9 @@ def payload_result(data, store, group): "text_hash": P.text_hash.hex()} if group in RENDER_GROUPS: d["frames"] = frames_of(P, group) + if group in TEXT_GROUPS: + P.record = text_record(P) # section 5.4; the file beside the expected file + d["text_record"] = sha256(P.record).hex() return d, write_wvg1(P) == data, None, P @@ -2168,9 +2531,22 @@ def pinned_step(name, desc): m = re.match(r"(?:step|check) (\d+[a-h]?):", desc) if m: return m.group(1) + m = re.match(r"step (\d+)\(([a-h])\):", desc) + if m: + return m.group(1) + m.group(2) + if pinned_rule(desc) is not None: + return "5g" # section 2.7: rules 2-5 of section 2.5 are checked at step 5(g) return None +def pinned_rule(desc): + """The canonical rule of section 2.5 a vector's description pins ("rule 4: ..."), + or None.""" + import re + m = re.match(r"rule (\d):", desc) + return m.group(1) if m else None + + def json_diff(a, b, path=""): """Field paths where two JSON values differ.""" if isinstance(a, dict) and isinstance(b, dict): @@ -2191,6 +2567,24 @@ def json_diff(a, b, path=""): return [] if a == b else [f"{path}: checker {a!r}, expected {b!r}"] +def text_record_diff(mine, want): + """Where a recomputed text record first differs from the committed one.""" + if want is None: + return ["no text record beside the expected file"] + a, b = mine.split(b"\n"), want.split(b"\n") + out = [] + if len(a) != len(b): + out.append(f"{len(a) - 1} lines, expected {len(b) - 1}") + for n, (x, y) in enumerate(zip(a, b)): + if x != y: + at = next((i for i, (p, q) in enumerate(zip(x, y)) if p != q), min(len(x), len(y))) + lo = max(0, at - 60) + out.append(f"line {n + 1}, byte {at}: checker ...{x[lo:at + 60]!r}") + out.append(f"line {n + 1}, byte {at}: expected ...{y[lo:at + 60]!r}") + break + return out or ["byte form differs"] + + def sidecar_exercise(P, frames): """The digest sidecar (section 2.10) the authoring tool writes for an accepted payload, built from its rendered frames: the probe set over every listed pair and @@ -2247,6 +2641,9 @@ def corpus(root, only_groups=None, verbose=True): step_disagreements = [] rendered = {} sidecars = 0 + records = 0 + compared = set() # the files under expected/ this run recomputed + example = spec_worked_example() vroot = os.path.join(root, "vectors") for group in sorted(os.listdir(vroot)): if only_groups and group not in only_groups: @@ -2259,6 +2656,7 @@ def corpus(root, only_groups=None, verbose=True): exp_path = os.path.join(root, "expected", group, name + ".json") with open(exp_path, "rb") as f: exp_bytes = f.read() + compared.add((group, name + ".json")) refusal = None rw = None P = None @@ -2284,6 +2682,11 @@ def corpus(root, only_groups=None, verbose=True): except BoundError as e: mine = None got = {"bound": str(e)} + except AssertionError as e: + import traceback + at = traceback.extract_tb(e.__traceback__)[-1] + mine = None + got = {"assertion": f"line {at.lineno}: {at.line}"} total += 1 ok = mine == exp_bytes if ok: @@ -2295,6 +2698,34 @@ def corpus(root, only_groups=None, verbose=True): except ValueError: diff = ["expected file is not JSON"] disagreements.append((f"{group}/{name}", diff[:6] or ["byte form differs"])) + if P is not None and group in TEXT_GROUPS: + # The text record (section 5.4) is an expected file of its own, the + # file beside the one that states its SHA-256. + records += 1 + total += 1 + rec_name = name + TEXT_RECORD_SUFFIX + rec_path = os.path.join(root, "expected", group, rec_name) + want_rec = None + if os.path.exists(rec_path): + with open(rec_path, "rb") as f: + want_rec = f.read() + compared.add((group, rec_name)) + if P.record == want_rec: + agree += 1 + else: + disagreements.append((f"{group}/{rec_name}", + text_record_diff(P.record, want_rec))) + for fl in text_record_recheck(P, P.record): + disagreements.append((f"{group}/{rec_name}", [fl])) + for fl in getattr(P, "text_faults", None) or []: + disagreements.append((f"{group}/{rec_name}", [f"section 5.4: {fl}"])) + if name == "worked-example" and example is not None: + # The vector its index calls the worked example of section 5.4: + # the text prints its transcript and its line for frame 30. + lines = P.record.split(b"\n") + if lines[0] != example[0] or example[1] not in lines[1:]: + disagreements.append((f"{group}/{rec_name}", [ + "the record is not the worked example section 5.4 prints"])) if rw is not None: rewrite_total += 1 rewrite_ok += bool(rw) @@ -2305,6 +2736,12 @@ def corpus(root, only_groups=None, verbose=True): mystep = refusal.step if group == "refusals" and name.startswith("wos1"): mystep = refusal.where.split(":")[0].replace("wos1 step ", "") + rule = pinned_rule(index[name]) + if rule is not None: + # A canonical rule of section 2.5: the checker names the rule it + # refused under at the start of `where`. + want += f" rule {rule}" + mystep += " " + refusal.where.split(":")[0] if want is not None: step_total += 1 if want == mystep: @@ -2349,8 +2786,22 @@ def corpus(root, only_groups=None, verbose=True): sha_ok += good if not good: disagreements.append((f"sha256.json {key}", ["digest differs"])) + # Every file under expected/, in the groups this run covers, must be one it + # recomputed: a file no index names and no rule above reaches is a disagreement. + eroot = os.path.join(root, "expected") + present = 0 + for group in sorted(os.listdir(eroot)): + if only_groups and group not in only_groups: + continue + for fname in sorted(os.listdir(os.path.join(eroot, group))): + present += 1 + if (group, fname) not in compared: + disagreements.append((f"{group}/{fname}", + ["a file under expected/ that this run did not recompute"])) print() print(f"expected files recomputed byte for byte: {agree} of {total}") + print(f"files under expected/ in the groups covered: {present}, compared: {len(compared)}") + print(f"text records recomputed, and recomputed a second way: {records}") print(f"payloads and sets rewritten to their own bytes: {rewrite_ok} of {rewrite_total}") print(f"refusal steps the vectors pin, reported at that step: {step_ok} of {step_total}") if sha_total: @@ -2393,7 +2844,8 @@ def main(argv): args.remove("--quiet") groups = _opt(args, "--groups") rc = selftest() - return rc or corpus(args[0], groups.split(",") if groups else None, not quiet) + rc2 = corpus(args[0], groups.split(",") if groups else None, not quiet) + return rc or rc2 sets_dir = _opt(args, "--sets") out = _opt(args, "--out") verify = "--verify" in args @@ -2422,6 +2874,23 @@ def main(argv): open(out, "wb").write(buf) print(sha256(buf).hex()) return 0 + if cmd == "text": + data = open(args[0], "rb").read() + P = load(data, store) + if len(args) >= 3: + num, den = (int(x) for x in args[1].split("/")) + try: + line = projection_json(P, num, den, int(args[2])) + except RequestError as e: + print(json.dumps({"request": e.code})) + return 1 + sys.stdout.buffer.write(line + b"\n") + return 0 + rec = text_record(P) + if out: + open(out, "wb").write(rec) + print(sha256(rec).hex()) + return 0 if cmd == "sidecar": data = open(args[0], "rb").read() sc_bytes = open(args[1], "rb").read() -- 2.54.0 (Apple Git-157) From 5189e5f50be4b6e8af2bf29c5119fa9fd9dde9bf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:44:44 -0400 Subject: [PATCH 3/4] wai-vector: the wasm exports and the three-engine browser leg (D7-8) - wai/wai-vector/wasm: the sink's exports (wv_alloc, wv_free, wv_open, wv_close, wv_render, wv_out_ptr, wv_out_len, wv_sha256, wv_cost, wv_transcript, wv_projection), no_std with its own allocator, built for wasm32v1-none so the module is WebAssembly 1.0 only; check-mvp.mjs refuses any post-1.0 feature, and fails on the wasm32-unknown-unknown build. - build-pkg.sh: the package a page serves. - browser/: a harness that renders every listed frame of every vector in Chromium, WebKit and Firefox, hashes it in the page twice, re-hashes sampled frames in Node, recomputes the arithmetic vectors and authoring forms, and compares each a11y payload's transcript and projections with its text record. A missing engine fails the run. - wai-vector.yml: the browser job, in a Playwright container pinned by digest, on the linux runner. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-vector.yml | 92 +++++ wai/CHANGELOG.md | 40 +++ wai/vector-conformance/README.md | 19 + wai/wai-vector/browser/package-lock.json | 60 ++++ wai/wai-vector/browser/package.json | 14 + wai/wai-vector/browser/page.html | 230 ++++++++++++ wai/wai-vector/browser/run.mjs | 439 +++++++++++++++++++++++ wai/wai-vector/wasm/Cargo.toml | 22 +- wai/wai-vector/wasm/build-pkg.sh | 71 ++++ wai/wai-vector/wasm/check-mvp.mjs | 385 ++++++++++++++++++++ wai/wai-vector/wasm/src/heap.rs | 119 ++++++ wai/wai-vector/wasm/src/lib.rs | 321 ++++++++++++++--- 12 files changed, 1763 insertions(+), 49 deletions(-) create mode 100644 wai/wai-vector/browser/package-lock.json create mode 100644 wai/wai-vector/browser/package.json create mode 100644 wai/wai-vector/browser/page.html create mode 100644 wai/wai-vector/browser/run.mjs create mode 100755 wai/wai-vector/wasm/build-pkg.sh create mode 100644 wai/wai-vector/wasm/check-mvp.mjs create mode 100644 wai/wai-vector/wasm/src/heap.rs diff --git a/.github/workflows/wai-vector.yml b/.github/workflows/wai-vector.yml index c4e66defd..e160b6535 100644 --- a/.github/workflows/wai-vector.yml +++ b/.github/workflows/wai-vector.yml @@ -23,6 +23,17 @@ name: wai-vector vector graphics # payload's expected file states the SHA-256 of its text record, so the projection # and the transcript are recomputed here too), and the default stroke set's # SHA-256 against Annex B; +# - three browser engines (chromium, webkit and firefox, as Playwright 1.60.0 names +# the engines it drives), in that version's container image, pinned by digest. The +# package a site serves (the sink's exports alone, built for wasm32v1-none and read +# back as WebAssembly 1.0 by wasm/check-mvp.mjs) loads every payload to its code, +# states each listed size's cost, and renders the frames through the page's hook: +# each is hashed by the module and by the platform, and the sampled ones are sent +# to Node and hashed there. For each a11y payload the sink's transcript and its +# projection at every frame of every listed rate, written out as the text +# record's lines, must be the committed record byte for byte. The recomputation +# module then recomputes every expected file and authoring form in each engine. +# A missing engine fails the job; # - the fuzz leg: the corpus-seeded fuzz of the WOS1 and WVG1 parsers at depth, each # accepted payload of small cost rendered and each accepted payload's text layer # held (its transcript is JSON, and its projections list the blocks the frame @@ -206,6 +217,87 @@ jobs: shell: bash run: node verify.mjs target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm ../../vector-conformance + browser: + name: wasm32 in three browser engines + runs-on: linux + timeout-minutes: 45 + # Playwright 1.60.0's own image: Node and the three engines that version drives, + # already installed. Pinned by the digest of its linux/amd64 manifest (the linux + # runner is x86_64), so the engines' builds cannot move under the tag. + container: + image: mcr.microsoft.com/playwright:v1.60.0-noble@sha256:83192064c7510f7ee73dd63dc5f22a5e01a92c81a2e6a9c715d9e3fe55471fd9 + env: + # The image runs as root, and an engine that finds HOME owned by another user + # refuses to start. + HOME: /root + steps: + - uses: actions/checkout@v4 + + # The image ships no Rust. wasm32v1-none is the package's target: its + # precompiled core and alloc are WebAssembly 1.0, which those of + # wasm32-unknown-unknown are not (wasm/build-pkg.sh says why that decides it). + - name: Install Rust 1.98.0 with the wasm32v1-none and wasm32-unknown-unknown targets + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add wasm32v1-none wasm32-unknown-unknown + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + + # The sink's exports alone, built and then read back: every section, type, + # segment and instruction WebAssembly 1.0's, no imports, no 64-bit integer at an + # exported function, no floating point, and the exported functions the script + # lists. The build fails otherwise. + - name: The package, built for wasm32v1-none and read back as WebAssembly 1.0 + shell: bash + run: bash wai/wai-vector/wasm/build-pkg.sh + + - name: Build the recomputation for wasm32-unknown-unknown + working-directory: wai/wai-vector/wasm + shell: bash + run: cargo build --locked --release --target wasm32-unknown-unknown + + # The check must be able to fail. The recomputation module is the default + # target's build, whose precompiled core and alloc use bulk memory: the reader + # must refuse it, and for that reason. + - name: The reader refuses a module that is not WebAssembly 1.0 + working-directory: wai/wai-vector/wasm + shell: bash + run: | + set -euo pipefail + if node check-mvp.mjs target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm > "$RUNNER_TEMP/check-default.log" 2>&1; then + cat "$RUNNER_TEMP/check-default.log" + echo "::error::check-mvp.mjs passed the default target's build" + exit 1 + fi + cat "$RUNNER_TEMP/check-default.log" + grep -q 'NOT 1.0: bulk-memory (memory.copy)' "$RUNNER_TEMP/check-default.log" \ + || { echo "::error::check-mvp.mjs did not report bulk memory in the default target's build"; exit 1; } + + # The image's engines are the ones this Playwright drives; the package comes + # from the lockfile, with its integrity hash. + - name: Playwright 1.60.0, from the lockfile + working-directory: wai/wai-vector/browser + shell: bash + run: | + set -euo pipefail + node --version + npm ci --no-audit --no-fund + + # A missing engine is a failure: run.mjs passes only when all three ran. + - name: Every payload, frame, text record and vector in chromium, webkit and firefox + working-directory: wai/wai-vector/browser + shell: bash + run: | + set -euo pipefail + node run.mjs --engines chromium,webkit,firefox \ + --pkg ../wasm/dist/pkg-vector \ + --recompute ../wasm/target/wasm32-unknown-unknown/release/wai_vector_wasm.wasm \ + ../../vector-conformance + fuzz: name: fuzz, peak allocation, comparison count runs-on: linux diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index fce771448..50cc45d0c 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,46 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Vector graphics (Draft): the sink as a WebAssembly 1.0 module, in three browser engines + +Additive. No payload, outline set, vector or digest changes. + +- **The sink's exports.** `wai-vector/wasm` exports the sink of + [graphics-vector](extensions/graphics-vector.md) behind a C ABI, with no bindings + generator and no imports. `wv_add_set` holds an outline set a payload may name; + `wv_open` loads a payload to a handle, or to its refusal's code; `wv_render` gives a + frame's `RGBA8P` at a listed size and rate, or the request error's code; `wv_cost` + gives a listed size's cost; `wv_transcript` gives the transcript, and `wv_projection` + the projection at a frame of a listed rate, each as the JSON of §5.4, or the request + error's code; `wv_sha256` gives a digest; `wv_close` releases a handle. A result is + read at `wv_out_ptr`, `wv_out_len` bytes of it. No export takes or returns a 64-bit + integer: a frame number goes in as two halves. +- **The package.** `wai-vector/wasm/build-pkg.sh` builds the module a site serves: the + sink's exports alone, with no glue script. It is WebAssembly 1.0 and nothing later: + no SIMD, no threads, no bulk memory, no sign-extension operators, no reference types. + The target is `wasm32v1-none`, whose precompiled `core` and `alloc` are 1.0 + themselves, so the crate is now `no_std` and carries its own allocator. The build + then reads the module back: `wasm/check-mvp.mjs` decodes every section, type, segment + and instruction, and refuses anything later than 1.0, any import, any 64-bit integer + at an exported function, any floating point, and any exported function the script + does not list. +- **Three browser engines.** `wai-vector/browser/run.mjs` serves a page, the package and + the corpus, and in each engine loads every payload of `vector-conformance` to its + expected code, checks its SHA-256 and each listed size's cost, and renders its frames + through the page's hook, `window.__wai_overlay.frame(size, rate, g)`, which returns + the renderer's own buffer: every listed frame at sizes up to 1280 × 720, and sampled + frames above. The page hashes each frame with the module's SHA-256 and with the + platform's digest. The sampled frames are also sent to Node as bytes, and Node hashes + them itself. Where a payload's expected file states a text record, the page's hook + also gives the sink's transcript and its projection at every frame of every listed + rate, `transcript()` and `projection(rate, g)`; written out as the record's lines, + they must be the committed `.a11y.jsonl` byte for byte. The recomputation + module then recomputes every expected file and authoring form in each engine. + Nothing is read from a canvas. A run passes only when all three engines ran: a + missing engine is a failure. +- **CI.** `wai-vector.yml` gains the `browser` job: the package built and read back, + then the three engines, in a container image pinned by digest. + ### Vector graphics (Draft): text and accessibility, §5 of `wai.graphics.vector` - **[graphics-vector](extensions/graphics-vector.md) §5 is written.** A payload carries diff --git a/wai/vector-conformance/README.md b/wai/vector-conformance/README.md index a39821e49..7795adee1 100644 --- a/wai/vector-conformance/README.md +++ b/wai/vector-conformance/README.md @@ -156,3 +156,22 @@ cargo run --release --features std --bin wai-vector -- text ../vector-conformanc `a11y` prints a payload's text record (or, with `transcript`, or a rate and a frame, one value of it), and `text` its `text_hash` and each block's identity, fields, string and active set. + +`wai-vector/browser/run.mjs` runs the corpus in three browser engines. It puts every +payload through the sink's package, the module `wai-vector/wasm/build-pkg.sh` builds: +the payload's code, each listed size's cost, and its frames, each hashed in the page by +the module and by the platform, the sampled ones again in Node from the bytes the page +sent; and, for each `a11y` payload, the sink's transcript and its projection at every +frame of every listed rate, which written out as lines must be the committed +`.a11y.jsonl` byte for byte. It then runs the recomputation of +`wai-vector/wasm/verify.mjs` in each engine. A run passes only when all three engines +ran. + +```sh +bash wai/wai-vector/wasm/build-pkg.sh +(cd wai/wai-vector/wasm && cargo build --release --target wasm32-unknown-unknown) +cd wai/wai-vector/browser +npm ci +npx playwright install chromium webkit firefox +node run.mjs --engines chromium,webkit,firefox ../../vector-conformance +``` diff --git a/wai/wai-vector/browser/package-lock.json b/wai/wai-vector/browser/package-lock.json new file mode 100644 index 000000000..e0a7e1655 --- /dev/null +++ b/wai/wai-vector/browser/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "wai-vector-browser", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wai-vector-browser", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/wai/wai-vector/browser/package.json b/wai/wai-vector/browser/package.json new file mode 100644 index 000000000..11d8ddc36 --- /dev/null +++ b/wai/wai-vector/browser/package.json @@ -0,0 +1,14 @@ +{ + "name": "wai-vector-browser", + "version": "0.1.0", + "private": true, + "description": "The sink of wai.graphics.vector in three browser engines, against wai/vector-conformance", + "license": "Apache-2.0", + "type": "module", + "scripts": { + "test": "node run.mjs --engines chromium,webkit,firefox ../../vector-conformance" + }, + "dependencies": { + "playwright": "1.60.0" + } +} diff --git a/wai/wai-vector/browser/page.html b/wai/wai-vector/browser/page.html new file mode 100644 index 000000000..a8d65221a --- /dev/null +++ b/wai/wai-vector/browser/page.html @@ -0,0 +1,230 @@ + + + + +wai-vector in this engine + + +

loading

+ + + diff --git a/wai/wai-vector/browser/run.mjs b/wai/wai-vector/browser/run.mjs new file mode 100644 index 000000000..67f347db2 --- /dev/null +++ b/wai/wai-vector/browser/run.mjs @@ -0,0 +1,439 @@ +// The sink of wai.graphics.vector in three browser engines, against +// wai/vector-conformance. +// +// node run.mjs --engines chromium,webkit,firefox [--pkg ] [--recompute ] +// +// +// It serves page.html, the two modules and the corpus from a local server, launches +// each engine named, and in each one: +// +// 1. Through the package (/wai_vector.wasm, which wasm/build-pkg.sh builds), +// every payload of the corpus: +// - it loads to the expected file's code: accepted, or the first refusal; +// - an accepted payload's SHA-256 is the expected `instr`, and each listed +// size's cost is the expected one; +// - in the groups that render, frames come through the page's hook, +// window.__wai_overlay.frame(size, rate, g), which returns the renderer's own +// buffer: every listed frame at sizes up to 1280 x 720, and at larger sizes the +// sampled frames. The page hashes each frame twice, with the module's SHA-256 +// and with the platform's digest, and both must be the expected digest; +// - the sampled frames (the first, the last, the middle one, and the frames on +// each side of every key tick and of every item's window edges) are also sent +// here as bytes, and Node hashes them itself: the run does not rest on a hash +// the page computed; +// - a frame outside the range, a frame number above 2^32, an unlisted size and +// an unlisted rate are each refused with their code; +// - where the expected file states a text record, the sink's transcript and its +// projection at every frame of every listed rate, written out as the record's +// lines, are the committed .a11y.jsonl byte for byte; and a projection +// outside the range or at an unlisted rate is refused with its code. +// 2. Through the recomputation module (the build verify.mjs runs under Node), every +// vector's expected file and every payload's authoring form, byte for byte. +// +// A missing engine is a failure, never a skip: the run passes only when chromium, +// webkit and firefox each ran and passed. --engines says which to launch; any of the +// three left out, or that cannot launch, fails the run. +// +// Nothing is read from a canvas, and nothing waits for a length of time. +// +// --lying-page is for testing this script. The page then changes one byte of every +// frame and reports the expected digest as both of its hashes; Node's own hash of the +// bytes sent must report every frame it was sent. A run with it never passes. +// +// Exit status 0: all three engines pass; 1: a failure; 2: usage. +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { dirname, join, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REQUIRED = ['chromium', 'webkit', 'firefox']; +// The groups whose vectors are arithmetic inputs, not payloads or outline sets. +const INPUT_GROUPS = ['arith', 'flatten']; +// Every listed frame is rendered at sizes up to this many pixels; sampled frames above. +const FULL = 1280 * 720; + +function usage(why) { + if (why) console.error(`run.mjs: ${why}`); + console.error('usage: node run.mjs --engines chromium,webkit,firefox [--pkg ] [--recompute ] '); + process.exit(2); +} + +let engines = null, pkgDir = join(HERE, '..', 'wasm', 'dist', 'pkg-vector'); +let recomputePath = join(HERE, '..', 'wasm', 'target', 'wasm32-unknown-unknown', 'release', 'wai_vector_wasm.wasm'); +let root = null, lying = false; +for (let a = process.argv.slice(2), i = 0; i < a.length; i++) { + if (a[i] === '--engines' && i + 1 < a.length) engines = [...new Set(a[++i].split(',').filter(Boolean))]; + else if (a[i] === '--pkg' && i + 1 < a.length) pkgDir = a[++i]; + else if (a[i] === '--recompute' && i + 1 < a.length) recomputePath = a[++i]; + else if (a[i] === '--lying-page') lying = true; + else if (!a[i].startsWith('--') && root === null) root = a[i]; + else usage(`unexpected argument ${a[i]}`); +} +if (engines === null || engines.length === 0) usage('--engines is required'); +if (root === null) usage('the vector-conformance directory is required'); +for (const e of engines) if (!REQUIRED.includes(e)) usage(`no engine named ${e}`); +root = resolve(root); +const pkgPath = resolve(pkgDir, 'wai_vector.wasm'); +recomputePath = resolve(recomputePath); +const pagePath = join(HERE, 'page.html'); + +const sha256 = (b) => createHash('sha256').update(b).digest('hex'); +const firstLine = (s) => String(s).split('\n')[0]; +const failuresOf = (n) => `${n} failure${n === 1 ? '' : 's'}`; + +for (const [what, p] of [['the package', pkgPath], ['the recomputation module', recomputePath], ['the corpus', join(root, 'vectors')]]) { + if (!existsSync(p)) { + console.error(`run.mjs: ${what} is not at ${p}`); + process.exit(1); + } +} +console.log(`the package: ${pkgPath}, ${statSync(pkgPath).size} bytes, SHA-256 ${sha256(readFileSync(pkgPath))}`); +console.log(`the recomputation module: ${recomputePath}, ${statSync(recomputePath).size} bytes, SHA-256 ${sha256(readFileSync(recomputePath))}`); + +// ---- the server --------------------------------------------------------------------- + +// The frames the page sent: id -> what Node read, hashed here. +const received = new Map(); + +function fileFor(path) { + if (path === '/' || path === '/page.html') return [pagePath, 'text/html; charset=utf-8']; + if (path === '/pkg/wai_vector.wasm') return [pkgPath, 'application/wasm']; + if (path === '/recompute.wasm') return [recomputePath, 'application/wasm']; + if (path.startsWith('/corpus/')) { + const f = resolve(root, path.slice('/corpus/'.length)); + if (f.startsWith(root + sep)) return [f, 'application/octet-stream']; + } + return null; +} + +const server = createServer((req, res) => { + let path; + try { + path = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); + } catch { + res.writeHead(400).end(); + return; + } + if (req.method === 'POST' && path.startsWith('/frame/')) { + const h = createHash('sha256'); + let bytes = 0; + req.on('data', (d) => { h.update(d); bytes += d.length; }); + req.on('end', () => { + received.set(path.slice('/frame/'.length), { sha256: h.digest('hex'), bytes }); + res.writeHead(204).end(); + }); + return; + } + const f = req.method === 'GET' ? fileFor(path) : null; + if (f === null || !existsSync(f[0]) || !statSync(f[0]).isFile()) { + res.writeHead(404).end(); + return; + } + const body = readFileSync(f[0]); + res.writeHead(200, { 'content-type': f[1], 'content-length': body.length, 'cache-control': 'no-store' }).end(body); +}); +await new Promise((ok) => server.listen(0, '127.0.0.1', ok)); +const origin = `http://127.0.0.1:${server.address().port}`; + +// ---- the corpus --------------------------------------------------------------------- + +// Every vector, in the order verify.mjs reads them: an index file is not a vector, and +// a .json file is one only in the input groups (elsewhere it is an authoring form). +const vectors = []; +for (const group of readdirSync(join(root, 'vectors')).sort()) { + for (const f of readdirSync(join(root, 'vectors', group)).sort()) { + const dot = f.lastIndexOf('.'); + const name = f.slice(0, dot), ext = f.slice(dot + 1); + if (name === 'index' || (ext === 'json' && !INPUT_GROUPS.includes(group))) continue; + vectors.push({ group, name, ext, id: `${group}/${name}` }); + } +} +const setNames = readdirSync(join(root, 'sets')).filter((n) => n.endsWith('.wos1')).sort(); +const expectedBytes = (v) => readFileSync(join(root, 'expected', v.group, `${v.name}.json`)); +const key = (f) => `${f.size[0]}x${f.size[1]}@${f.rate[0]}/${f.rate[1]}#${f.g}`; + +// The sampled frames of a list: the first, the last, the middle one, and the frame at +// or after each tick with the frame before it. +function sampled(gs, ticks, ticksPerFrame) { + const first = gs[0], last = gs[gs.length - 1]; + const s = new Set([first, last, gs[gs.length >> 1]]); + for (const t of ticks) { + const g = Math.ceil(t / ticksPerFrame); + for (const c of [g - 1, g]) if (c >= first && c <= last) s.add(c); + } + return s; +} + +// The first pair [n, 1] that `listed` does not hold. +function unlisted(listed) { + for (let n = 1; ; n++) if (!listed.some((p) => p[0] === n && p[1] === 1)) return [n, 1]; +} + +// What to ask of a payload, from its expected file: the sizes whose cost is asked, the +// frames to render (with each one's expected digest, and an id when it is also sent), +// the requests that must be refused (with each one's code), and, where the expected +// file states a text record, every projection the record holds and the projections +// that must be refused. +let nextId = 0; +function planOf(v, expected) { + const plan = { sizes: [], frames: [], requests: [], text: null }; + if (expected.code !== 'accepted') return plan; + plan.sizes = expected.sizes.map((s) => [s[0], s[1]]); + if (expected.frames) { + const form = JSON.parse(readFileSync(join(root, 'vectors', v.group, `${v.name}.json`), 'utf8')); + const ticks = [...form.keys.map((k) => k[0]), ...form.items.flatMap((i) => [i.t_on, i.t_off])]; + for (const s of expected.frames) { + if (s.frames.length === 0) continue; + const rate = expected.rates.find((r) => r[0] === s.num && r[1] === s.den); + const sample = sampled(s.frames.map((f) => f[0]), ticks, rate[2]); + const full = s.w * s.h <= FULL; + for (const [g, digest] of s.frames) { + if (!full && !sample.has(g)) continue; + plan.frames.push({ size: [s.w, s.h], rate: [s.num, s.den], g, send: sample.has(g) ? nextId++ : null, digest }); + } + } + } + if (plan.sizes.length > 0 && expected.rates.length > 0) { + const size = plan.sizes[0]; + for (const [num, den, , first, last] of expected.rates) { + plan.requests.push({ size, rate: [num, den], g: last + 1, want: 'frame_range' }); + if (first > 0) plan.requests.push({ size, rate: [num, den], g: first - 1, want: 'frame_range' }); + } + const [num, den, , first] = expected.rates[0]; + // A frame number whose low half alone would be in range. + plan.requests.push({ size, rate: [num, den], g: 4294967296 + Math.max(first, 0), want: 'frame_range' }); + plan.requests.push({ size: unlisted(plan.sizes), rate: [num, den], g: Math.max(first, 0), want: 'size_unlisted' }); + plan.requests.push({ size, rate: unlisted(expected.rates), g: Math.max(first, 0), want: 'rate_unlisted' }); + } + if (expected.text_record !== undefined) { + // The record's order: each listed rate, and within it each frame of the document. + plan.text = { projections: [], requests: [] }; + for (const [num, den, , first, last] of expected.rates) { + for (let g = first; g <= last; g++) plan.text.projections.push({ rate: [num, den], g }); + plan.text.requests.push({ rate: [num, den], g: last + 1, want: 'frame_range' }); + } + if (expected.rates.length > 0) { + const [num, den, , first] = expected.rates[0]; + plan.text.requests.push({ rate: [num, den], g: 4294967296 + Math.max(first, 0), want: 'frame_range' }); + plan.text.requests.push({ rate: unlisted(expected.rates), g: Math.max(first, 0), want: 'rate_unlisted' }); + } + } + return plan; +} + +// A text record's line for a projection. +const recordLine = (q, projection) => `{"den":${q.rate[1]},"g":${q.g},"num":${q.rate[0]},"projection":${projection}}`; +// Two lines that differ, each shown around the first character at which they do. +function differing(got, want) { + if (got === undefined || want === undefined) return `the sink gives ${got === undefined ? 'no such line' : 'a line'}; the committed record has ${want === undefined ? 'no such line' : 'one'}`; + let at = 0; + while (at < got.length && at < want.length && got[at] === want[at]) at++; + const from = Math.max(0, at - 60); + const part = (s) => `${from > 0 ? '…' : ''}${s.slice(from, at + 60)}${s.length > at + 60 ? '…' : ''}`; + return `at character ${at} the sink gives ${part(got)}; expected ${part(want)}`; +} + +// ---- one engine --------------------------------------------------------------------- + +async function runEngine(playwright, name) { + const failures = []; + const fail = (where, what) => { + failures.push(`${where}: ${what}`); + console.error(`FAIL ${name} ${where}: ${what}`); + }; + const n = { payloads: 0, accepted: 0, frames: 0, sent: 0, sentBytes: 0, records: 0, lines: 0, recomputed: 0, forms: 0 }; + let browser; + try { + browser = await playwright[name].launch(); + } catch (e) { + fail('launch', firstLine(e.message)); + return { failures, version: null }; + } + const version = browser.version(); + try { + const page = await browser.newPage(); + let dead = false; + page.on('pageerror', (e) => fail('the page', `uncaught: ${firstLine(e.message)}`)); + page.on('crash', () => { dead = true; fail('the page', 'crashed'); }); + await page.goto(`${origin}/page.html`); + await page.waitForFunction(() => window.__wai_ready !== undefined); + const ready = await page.evaluate(() => window.__wai_ready); + if (!ready.platformDigest) fail('the page', 'has no platform digest (crypto.subtle)'); + await page.evaluate((names) => window.__wai_harness.sets(names), setNames); + if (lying) { + await page.evaluate(() => { + const h = window.__wai_harness; + const honest = h.check.bind(h); + h.check = async (size, rate, g) => { + const r = await honest(size, rate, g); + const claim = window.__wai_claims[`${size[0]}x${size[1]}@${rate[0]}/${rate[1]}#${g}`]; + r.bytes[r.bytes.length >> 1] ^= 0x01; + return { bytes: r.bytes, inModule: claim, platform: claim }; + }; + }); + } + + // 1. Every payload through the package. + for (const v of vectors.filter((x) => x.ext === 'wvg1')) { + if (dead) break; + const expected = JSON.parse(expectedBytes(v).toString('utf8')); + const plan = planOf(v, expected); + const asked = { + sizes: plan.sizes, + frames: plan.frames.map((f) => ({ size: f.size, rate: f.rate, g: f.g, send: f.send })), + requests: plan.requests.map((q) => ({ size: q.size, rate: q.rate, g: q.g })), + text: plan.text === null ? null : { + projections: plan.text.projections, + requests: plan.text.requests.map((q) => ({ rate: q.rate, g: q.g })), + }, + }; + const claims = lying ? Object.fromEntries(plan.frames.map((f) => [key(f), f.digest])) : null; + let got; + try { + got = await page.evaluate(([path, p, c]) => { + if (c !== null) window.__wai_claims = c; + return window.__wai_harness.payload(path, p); + }, [`vectors/${v.group}/${v.name}.wvg1`, asked, claims]); + } catch (e) { + fail(v.id, firstLine(e.message)); + continue; + } + n.payloads += 1; + if (got.code !== expected.code) { + fail(v.id, `loads as ${got.code}; expected ${expected.code}`); + continue; + } + if (expected.code !== 'accepted') continue; + n.accepted += 1; + if (got.instr.inModule !== expected.instr) fail(v.id, `the module's SHA-256 of the payload is ${got.instr.inModule}; expected ${expected.instr}`); + if (got.instr.platform !== expected.instr) fail(v.id, `the platform's digest of the payload is ${got.instr.platform}; expected ${expected.instr}`); + expected.sizes.forEach(([w, h, cost], i) => { + if (got.costs[i] !== cost) fail(v.id, `the cost at ${w}x${h} is ${got.costs[i]}; expected ${cost}`); + }); + plan.frames.forEach((f, i) => { + const r = got.frames[i]; + const where = `${v.id} frame ${key(f)}`; + n.frames += 1; + if (r === undefined) { fail(where, 'not rendered'); return; } + if (r.bytes !== 4 * f.size[0] * f.size[1]) fail(where, `${r.bytes} bytes; expected ${4 * f.size[0] * f.size[1]}`); + if (r.inModule !== f.digest) fail(where, `the module's SHA-256 is ${r.inModule}; expected ${f.digest}`); + if (r.platform !== f.digest) fail(where, `the platform's digest is ${r.platform}; expected ${f.digest}`); + if (f.send === null) return; + const sent = received.get(String(f.send)); + received.delete(String(f.send)); + if (sent === undefined) { fail(where, 'its bytes did not arrive'); return; } + n.sent += 1; + n.sentBytes += sent.bytes; + if (sent.sha256 !== f.digest) fail(where, `Node's SHA-256 of the bytes sent is ${sent.sha256}; expected ${f.digest}`); + }); + plan.requests.forEach((q, i) => { + if (got.requests[i] !== q.want) fail(`${v.id} request ${key(q)}`, `${got.requests[i]}; expected ${q.want}`); + }); + if (plan.text === null) continue; + + // The text record: the transcript and every projection, as the record's lines, + // against the committed file's bytes. + const recordPath = join(root, 'expected', v.group, `${v.name}.a11y.jsonl`); + if (!existsSync(recordPath)) { + fail(v.id, `its expected file states a text record, and expected/${v.group}/${v.name}.a11y.jsonl is missing`); + continue; + } + const want = readFileSync(recordPath); + if (sha256(want) !== expected.text_record) fail(v.id, `the committed text record's SHA-256 is ${sha256(want)}; its expected file states ${expected.text_record}`); + if (got.text === null) { + fail(v.id, 'the page returned no transcript and no projection'); + continue; + } + const lines = [got.text.transcript, ...plan.text.projections.map((q, i) => recordLine(q, got.text.projections[i]))]; + n.records += 1; + n.lines += lines.length; + if (Buffer.compare(Buffer.from(lines.map((l) => `${l}\n`).join(''), 'utf8'), want) !== 0) { + const wantLines = want.toString('utf8').split('\n'); + if (wantLines[wantLines.length - 1] === '') wantLines.pop(); + const differ = []; + for (let i = 0; i < Math.max(lines.length, wantLines.length); i++) if (lines[i] !== wantLines[i]) differ.push(i); + if (differ.length === 0) { + fail(`${v.id} text record`, 'every line is the committed one, and the bytes differ: the committed file does not end each line with one U+000A'); + } else { + const i = differ[0]; + const q = plan.text.projections[i - 1]; + const which = i === 0 ? 'the transcript' : q === undefined ? `line ${i + 1}, which the sink was not asked for` : `the projection at ${q.rate.join('/')}#${q.g}`; + fail( + `${v.id} text record`, + `${differ.length} of ${wantLines.length} lines differ (${differ.includes(0) ? 'the transcript and ' : ''}${differ.filter((d) => d > 0).length} projections); ` + + `the first is ${which}: ${differing(lines[i], wantLines[i])}`, + ); + } + } + plan.text.requests.forEach((q, i) => { + if (got.text.requests[i] !== q.want) fail(`${v.id} projection request ${q.rate.join('/')}#${q.g}`, `${got.text.requests[i]}; expected ${q.want}`); + }); + } + + // 2. Every vector through the recomputation. + for (const v of vectors) { + if (dead) break; + let got; + try { + got = await page.evaluate(([g, name, ext]) => window.__wai_harness.recompute(g, name, ext), [v.group, v.name, v.ext]); + } catch (e) { + fail(v.id, `recomputation: ${firstLine(e.message)}`); + continue; + } + n.recomputed += 1; + if (got.status !== 0) fail(`${v.id}.${v.ext}`, `recomputation: ${got.expected}`); + else if (Buffer.compare(Buffer.from(got.expected, 'utf8'), expectedBytes(v)) !== 0) fail(`${v.id}.${v.ext}`, 'the recomputed expected file differs'); + if (v.ext === 'wvg1') { + n.forms += 1; + const form = readFileSync(join(root, 'vectors', v.group, `${v.name}.json`)); + if (Buffer.compare(Buffer.from(got.form, 'utf8'), form) !== 0) fail(`${v.id}.${v.ext}`, 'the authoring form differs'); + } + } + } catch (e) { + fail('the run', firstLine(e.message)); + } finally { + await browser.close(); + } + console.log( + `${name} ${version}: ${n.payloads} payloads through the package, ${n.accepted} accepted; ` + + `${n.frames} frames rendered and hashed twice in the page, ${n.sent} of them hashed again in Node (${n.sentBytes} bytes); ` + + `${n.records} text records, ${n.lines} lines; ` + + `${n.recomputed} vectors and ${n.forms} authoring forms recomputed; ${failuresOf(failures.length)}`, + ); + return { failures, version, n }; +} + +// ---- the run ------------------------------------------------------------------------ + +let playwright; +try { + playwright = await import('playwright'); +} catch (e) { + console.error(`run.mjs: playwright is not installed here (npm ci): ${firstLine(e.message)}`); + process.exit(1); +} +if (lying) console.log('--lying-page: the page changes one byte of every frame and reports the expected digest; this run cannot pass'); + +const results = new Map(); +for (const name of engines) results.set(name, await runEngine(playwright, name)); +server.close(); + +let bad = 0; +for (const name of REQUIRED) { + const r = results.get(name); + if (r === undefined) { + bad += 1; + console.error(`FAIL ${name}: not run. A missing engine is a failure: chromium, webkit and firefox must each run.`); + } else if (r.failures.length > 0) { + bad += 1; + console.error(`FAIL ${name}: ${failuresOf(r.failures.length)}, the first: ${r.failures[0]}`); + } +} +if (lying && bad === 0) { + console.error('FAIL --lying-page: no engine reported the changed frames'); + bad += 1; +} +if (bad > 0) process.exit(1); +console.log('vector-conformance: every payload, frame, text record and vector verifies in chromium, webkit and firefox'); diff --git a/wai/wai-vector/wasm/Cargo.toml b/wai/wai-vector/wasm/Cargo.toml index 6370e8a21..4020a197a 100644 --- a/wai/wai-vector/wasm/Cargo.toml +++ b/wai/wai-vector/wasm/Cargo.toml @@ -1,6 +1,14 @@ -# The vector recomputation of `wai-vector`, as a WebAssembly module for a browser's -# engine (`wasm32-unknown-unknown`), with a C ABI and no bindings generator, so that -# `verify.mjs` recomputes every vector file under Node with no dependency. +# The sink of `wai-vector`, and the recomputation of its vectors, as a WebAssembly +# module for a browser's engine, with a C ABI and no bindings generator. +# +# Two builds come from it. With the default feature, for `wasm32-unknown-unknown`: the +# module `verify.mjs` recomputes every vector file with, under Node with no +# dependency. Without it, for `wasm32v1-none`: the package a site serves, the sink's +# exports alone, which `build-pkg.sh` builds and `check-mvp.mjs` holds to +# WebAssembly 1.0. +# +# `no_std` with `alloc`, and no dependency but `wai-vector`: it builds only for a +# wasm32 target. [package] name = "wai-vector-wasm" version = "0.1.0" @@ -13,8 +21,14 @@ publish = false crate-type = ["cdylib"] path = "src/lib.rs" +[features] +default = ["recompute"] +# The exports `wv_recompute` and `wv_form`: a vector's expected file and a payload's +# authoring form. A sink needs neither, and the package is built without them. +recompute = [] + [dependencies] -# The recomputation needs no authoring code: a sink's build, with no stroker. +# Neither build needs authoring code: a sink's build, with no stroker. wai-vector = { path = "..", default-features = false } [profile.release] diff --git a/wai/wai-vector/wasm/build-pkg.sh b/wai/wai-vector/wasm/build-pkg.sh new file mode 100755 index 000000000..8fdc75a67 --- /dev/null +++ b/wai/wai-vector/wasm/build-pkg.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# The package a site serves under /wai/pkg-vector/: the sink of wai.graphics.vector as +# one WebAssembly module, wai_vector.wasm, which a page instantiates with no imports +# and no glue script. +# +# bash wai/wai-vector/wasm/build-pkg.sh [] +# +# Writes /wai_vector.wasm (default wai/wai-vector/wasm/dist/pkg-vector, which +# git ignores) and prints its size and SHA-256. +# +# The module is WebAssembly 1.0 and nothing later: no SIMD, no threads, no bulk +# memory, no sign-extension operators, no saturating conversions, no reference types, +# no multi-value. That is enforced twice. +# +# 1. By the target. The module is built for wasm32v1-none, whose precompiled core +# and alloc are themselves WebAssembly 1.0. Those of wasm32-unknown-unknown are +# not: they use bulk memory, sign extension and reference types, and the linker +# carries what they use into any module linked against them, whatever +# -C target-cpu or -C target-feature the build passes. That target has no std, so +# the crate is no_std with its own allocator (src/heap.rs). +# 2. By reading the result. check-mvp.mjs decodes the module that was built: every +# section, type, segment and instruction must be 1.0's; and, as a package, it +# must have no imports, no exported function with a 64-bit parameter or result, +# no floating point, and exactly the exported functions listed below. The build +# fails otherwise, and leaves no module in . +# +# The crate is built without its default feature, so the module has the sink's +# exports and not the recomputation of the conformance vectors. +# +# The same source gives different module bytes on different hosts, so the SHA-256 +# printed here names this build and not the source. What every host's build has in +# common is what it computes: the frames of wai/vector-conformance, checked in three +# browser engines by wai/wai-vector/browser/run.mjs. +# +# Needs bash, cargo with the wasm32v1-none target (rustup target add wasm32v1-none) +# and node >= 18. +set -euo pipefail + +if [ $# -gt 1 ]; then + echo "usage: bash build-pkg.sh []" >&2 + exit 2 +fi +CRATE="$(cd "$(dirname "$0")" && pwd -P)" +OUT="${1:-$CRATE/dist/pkg-vector}" +# The package's exported functions. A function added to the sink is added here. +FUNCTIONS=wv_add_set,wv_alloc,wv_close,wv_cost,wv_free,wv_open,wv_out_len,wv_out_ptr,wv_projection,wv_render,wv_sha256,wv_transcript + +for t in cargo node; do + command -v "$t" >/dev/null || { echo "pkg-vector: needs $t" >&2; exit 1; } +done +mkdir -p "$OUT" +OUT="$(cd "$OUT" && pwd -P)" +rm -f "$OUT/wai_vector.wasm" + +cd "$CRATE" +# Asked of the toolchain this crate builds with, before anything is built. +LIBDIR="$(rustc --print target-libdir --target wasm32v1-none 2>/dev/null || true)" +if [ -z "$LIBDIR" ] || [ ! -d "$LIBDIR" ]; then + echo "pkg-vector: this toolchain has no wasm32v1-none target (rustup target add wasm32v1-none)" >&2 + exit 1 +fi +cargo build --locked --release --target wasm32v1-none --no-default-features +TARGET_DIR="$(cargo metadata --locked --format-version 1 --no-deps \ + | node -e 'let s = ""; process.stdin.on("data", (d) => { s += d; }).on("end", () => console.log(JSON.parse(s).target_directory));')" +BUILT="$TARGET_DIR/wasm32v1-none/release/wai_vector_wasm.wasm" +[ -f "$BUILT" ] || { echo "pkg-vector: the build left no $BUILT" >&2; exit 1; } + +node "$CRATE/check-mvp.mjs" "$BUILT" --package --functions "$FUNCTIONS" + +cp "$BUILT" "$OUT/wai_vector.wasm" +echo "pkg-vector: $OUT/wai_vector.wasm" diff --git a/wai/wai-vector/wasm/check-mvp.mjs b/wai/wai-vector/wasm/check-mvp.mjs new file mode 100644 index 000000000..87e6f077a --- /dev/null +++ b/wai/wai-vector/wasm/check-mvp.mjs @@ -0,0 +1,385 @@ +// Read a WebAssembly module and refuse it unless it is WebAssembly 1.0 and nothing +// later: every section, type, import, export, segment and instruction is read, and +// anything a later proposal added is a finding. No dependency: the binary format is +// decoded here. +// +// node check-mvp.mjs [--package] [--functions ] +// +// Always checked: +// - sections: only the eleven of 1.0 and custom sections, in order (no DataCount +// section, no tag section); +// - types: function types over i32, i64, f32 and f64, with at most one result; +// - one memory and one table at most, neither shared nor 64-bit; a table of +// function references; +// - no mutable global imported or exported; +// - element and data segments of the 1.0 kind only (active, memory or table 0); +// - constant expressions of one constant or one global.get; +// - every instruction of every function is one of 1.0's, with 1.0's immediates: +// no SIMD, no atomics, no bulk memory, no sign-extension operators, no +// saturating conversions, no reference types, no multi-value blocks, no tail +// calls, no exceptions, and a call_indirect that names table 0 in one byte; +// - a `target_features` section, if present, declares none of them in use. +// +// With --package, the rules of a module a page instantiates with no glue script: +// - no imports; +// - no exported function takes or returns i64, which a caller could pass only +// through the engine's later BigInt interface; +// - no floating-point type, constant or instruction anywhere. +// +// With --functions, the exported functions are exactly the names given. +// +// Prints what it found. Exit status 0: the module passes; 1: it does not; 2: usage, +// or a module this reader cannot decode. +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; + +let file = null, pkg = false, functions = null, usage = false; +for (let a = process.argv.slice(2), i = 0; i < a.length; i++) { + if (a[i] === '--package') pkg = true; + else if (a[i] === '--functions' && i + 1 < a.length) functions = a[++i].split(',').filter(Boolean); + else if (!a[i].startsWith('--') && file === null) file = a[i]; + else usage = true; +} +if (usage || file === null) { + console.error('usage: node check-mvp.mjs [--package] [--functions ]'); + process.exit(2); +} + +const I32 = 0x7f, I64 = 0x7e, F32 = 0x7d, F64 = 0x7c; +const NUM = new Set([I32, I64, F32, F64]); + +class Reader { + constructor(b, at = 0, end = b.length) { this.b = b; this.at = at; this.end = end; } + eof() { return this.at >= this.end; } + byte() { + if (this.at >= this.end) throw new Error(`truncated at byte ${this.at}`); + return this.b[this.at++]; + } + // An unsigned LEB128 of at most `max` bytes. + u(max = 5) { + let r = 0, s = 1; + for (let i = 0; i < max; i++) { + const c = this.byte(); + r += (c & 0x7f) * s; + s *= 128; + if (!(c & 0x80)) return r; + } + throw new Error(`a LEB128 longer than ${max} bytes before byte ${this.at}`); + } + skip(n) { + if (this.at + n > this.end) throw new Error(`truncated at byte ${this.at}`); + this.at += n; + } + sub(n) { + const r = new Reader(this.b, this.at, this.at + n); + this.skip(n); + return r; + } + name() { + const n = this.u(); + const s = new TextDecoder().decode(this.b.subarray(this.at, this.at + n)); + this.skip(n); + return s; + } +} + +// What was found, by feature: a count and the first place. +const found = new Map(); +function finding(feature, where) { + const f = found.get(feature); + if (f) f.count += 1; + else found.set(feature, { count: 1, where }); +} + +const SECTION = ['custom', 'type', 'import', 'function', 'table', 'memory', 'global', 'export', 'start', 'element', 'code', 'data']; +const types = []; // { params: [valtype], results: [valtype] } +const funcTypes = []; // function index -> type index, imports first +const globalsMutable = []; // global index -> mutable, imports first +const exports = []; // { name, kind, index } +const imports = []; // "module.field" +let tables = 0, memories = 0, floats = 0, bodies = 0, instructions = 0; +let declared = null; // the target_features section's entries + +function valtype(t, where) { + if (NUM.has(t)) { + if (t === F32 || t === F64) floats += 1; + return; + } + if (t === 0x7b) finding('simd128 (the type v128)', where); + else if (t === 0x70 || t === 0x6f) finding('reference-types (a reference as a value type)', where); + else finding(`a value type 0x${t.toString(16)} that 1.0 does not have`, where); +} + +function limits(r, what, where) { + const flag = r.u(); + if (flag & 0x02) finding(`threads (a shared ${what})`, where); + if (flag & 0x04) finding(`memory64 (a 64-bit ${what})`, where); + if (flag > 0x07) finding(`${what} limits with flags 0x${flag.toString(16)}`, where); + r.u(10); + if (flag & 0x01) r.u(10); +} + +// A constant expression: one constant or one global.get, then end. +function constExpr(r, where) { + const op = r.byte(); + if (op === 0x41) r.u(5); + else if (op === 0x42) r.u(10); + else if (op === 0x43) { r.skip(4); floats += 1; } + else if (op === 0x44) { r.skip(8); floats += 1; } + else if (op === 0x23) r.u(); + else { + finding('a constant expression beyond one constant or global.get (extended-const, or a reference)', where); + throw new Error('stop'); + } + if (r.byte() !== 0x0b) { + finding('a constant expression beyond one constant or global.get (extended-const)', where); + throw new Error('stop'); + } +} + +const FLOAT_OP = (op) => op === 0x2a || op === 0x2b || op === 0x38 || op === 0x39 + || (op >= 0x5b && op <= 0x66) || (op >= 0x8b && op <= 0xa6) + || (op >= 0xa8 && op <= 0xab) || (op >= 0xae && op <= 0xbf); + +function blockType(r, where) { + const t = r.byte(); + if (t === 0x40) return; + if (NUM.has(t)) { valtype(t, where); return; } + if (t === 0x7b || t === 0x70 || t === 0x6f) { valtype(t, where); return; } + // Anything else is a type index, as a signed LEB128. + finding('multivalue (a block whose type is a type index)', where); + if (t & 0x80) { r.at -= 1; r.u(5); } +} + +// One function body's instructions. Returns false when it met an instruction whose +// immediates this reader does not know, and stopped. +function body(r, where) { + while (!r.eof()) { + const at = r.at; + const op = r.byte(); + const here = `${where}, byte 0x${at.toString(16)}`; + instructions += 1; + if (op === 0x02 || op === 0x03 || op === 0x04) blockType(r, here); + else if (op === 0x0c || op === 0x0d || op === 0x10 || (op >= 0x20 && op <= 0x24)) r.u(); + else if (op === 0x0e) { for (let n = r.u(); n > 0; n--) r.u(); r.u(); } + else if (op === 0x11) { + r.u(); + const t = r.byte(); + if (t !== 0x00) { + finding('reference-types (a call_indirect that names a table, or encodes table 0 in more than one byte)', here); + if (t & 0x80) { r.at -= 1; r.u(5); } + } + } else if (op >= 0x28 && op <= 0x3e) { + const align = r.u(); + if (align >= 64) { finding('multi-memory (a load or store that names a memory)', here); r.u(); } + r.u(10); + if (FLOAT_OP(op)) floats += 1; + } else if (op === 0x3f || op === 0x40) { + const m = r.byte(); + if (m !== 0x00) { + finding('multi-memory (memory.size or memory.grow that names a memory, or encodes memory 0 in more than one byte)', here); + if (m & 0x80) { r.at -= 1; r.u(5); } + } + } else if (op === 0x41) r.u(5); + else if (op === 0x42) r.u(10); + else if (op === 0x43) { r.skip(4); floats += 1; } + else if (op === 0x44) { r.skip(8); floats += 1; } + else if (op === 0x00 || op === 0x01 || op === 0x05 || op === 0x0b || op === 0x0f || op === 0x1a || op === 0x1b) { /* no immediate */ } + else if (op >= 0x45 && op <= 0xbf) { if (FLOAT_OP(op)) floats += 1; } + else if (op >= 0xc0 && op <= 0xc4) finding('sign-ext (i32.extend8_s and its kin)', here); + else if (op === 0xfc) { + const sub = r.u(); + if (sub <= 7) finding('nontrapping-fptoint (a saturating conversion)', here); + else if (sub === 8) { finding('bulk-memory (memory.init)', here); r.u(); r.byte(); } + else if (sub === 9) { finding('bulk-memory (data.drop)', here); r.u(); } + else if (sub === 10) { finding('bulk-memory (memory.copy)', here); r.byte(); r.byte(); } + else if (sub === 11) { finding('bulk-memory (memory.fill)', here); r.byte(); } + else if (sub === 12) { finding('bulk-memory (table.init)', here); r.u(); r.u(); } + else if (sub === 13) { finding('bulk-memory (elem.drop)', here); r.u(); } + else if (sub === 14) { finding('bulk-memory (table.copy)', here); r.u(); r.u(); } + else if (sub <= 17) { finding('reference-types (table.grow, table.size or table.fill)', here); r.u(); } + else { finding(`an instruction 0xfc ${sub} that 1.0 does not have`, here); return false; } + } else { + const feature = op === 0xfd ? 'simd128 (a vector instruction)' + : op === 0xfe ? 'threads (an atomic instruction)' + : op === 0xfb ? 'gc (a struct, array or cast instruction)' + : (op >= 0x06 && op <= 0x0a) || op === 0x18 || op === 0x19 || op === 0x1f ? 'exception-handling' + : op === 0x12 || op === 0x13 ? 'tail-call' + : op === 0x14 || op === 0x15 ? 'typed function references (call_ref)' + : op === 0x1c || op === 0x25 || op === 0x26 || (op >= 0xd0 && op <= 0xd6) ? 'reference-types (a typed select, a table access or a reference instruction)' + : `an instruction 0x${op.toString(16)} that 1.0 does not have`; + finding(feature, here); + return false; + } + } + if (r.b[r.end - 1] !== 0x0b) finding('a function body that does not end with end', where); + return true; +} + +function section(id, r) { + const where = `the ${SECTION[id]} section`; + if (id === 1) { + for (let n = r.u(), i = 0; i < n; i++) { + const form = r.byte(); + if (form !== 0x60) { finding(`a type of form 0x${form.toString(16)} (1.0 has function types only)`, `${where}, type ${i}`); throw new Error('stop'); } + const t = { params: [], results: [] }; + for (let k = r.u(); k > 0; k--) { const v = r.byte(); valtype(v, `${where}, type ${i}`); t.params.push(v); } + for (let k = r.u(); k > 0; k--) { const v = r.byte(); valtype(v, `${where}, type ${i}`); t.results.push(v); } + if (t.results.length > 1) finding('multivalue (a function type with more than one result)', `${where}, type ${i}`); + types.push(t); + } + } else if (id === 2) { + for (let n = r.u(), i = 0; i < n; i++) { + const name = `${r.name()}.${r.name()}`; + imports.push(name); + const kind = r.byte(); + if (kind === 0) funcTypes.push(r.u()); + else if (kind === 1) { + const t = r.byte(); + if (t !== 0x70) finding('reference-types (a table that is not of function references)', `${where}, ${name}`); + limits(r, 'table', `${where}, ${name}`); + tables += 1; + } else if (kind === 2) { limits(r, 'memory', `${where}, ${name}`); memories += 1; } + else if (kind === 3) { + valtype(r.byte(), `${where}, ${name}`); + const mutable = r.byte() === 1; + globalsMutable.push(mutable); + if (mutable) finding('mutable-globals (a mutable global imported)', `${where}, ${name}`); + } else { finding(`an import of kind ${kind} (1.0 has functions, tables, memories and globals)`, `${where}, ${name}`); throw new Error('stop'); } + } + } else if (id === 3) { + for (let n = r.u(); n > 0; n--) funcTypes.push(r.u()); + } else if (id === 4) { + for (let n = r.u(), i = 0; i < n; i++) { + const t = r.byte(); + if (t !== 0x70) finding('reference-types (a table that is not of function references)', `${where}, table ${i}`); + limits(r, 'table', `${where}, table ${i}`); + tables += 1; + } + } else if (id === 5) { + for (let n = r.u(), i = 0; i < n; i++) { limits(r, 'memory', `${where}, memory ${i}`); memories += 1; } + } else if (id === 6) { + for (let n = r.u(), i = 0; i < n; i++) { + valtype(r.byte(), `${where}, global ${i}`); + globalsMutable.push(r.byte() === 1); + constExpr(r, `${where}, global ${i}`); + } + } else if (id === 7) { + for (let n = r.u(); n > 0; n--) { + const name = r.name(); + const kind = r.byte(); + const index = r.u(); + exports.push({ name, kind, index }); + if (kind > 3) finding(`an export of kind ${kind} (1.0 has functions, tables, memories and globals)`, `${where}, ${name}`); + } + } else if (id === 8) { + r.u(); + } else if (id === 9) { + for (let n = r.u(), i = 0; i < n; i++) { + const flag = r.u(); + if (flag !== 0) { finding(`bulk-memory or reference-types (an element segment of kind ${flag}; 1.0 has kind 0)`, `${where}, segment ${i}`); throw new Error('stop'); } + constExpr(r, `${where}, segment ${i}`); + for (let k = r.u(); k > 0; k--) r.u(); + } + } else if (id === 10) { + for (let n = r.u(), i = 0; i < n; i++) { + const f = r.sub(r.u()); + const w = `function ${funcTypes.length - n + i}`; + for (let k = f.u(); k > 0; k--) { f.u(); valtype(f.byte(), `${w}, its locals`); } + bodies += 1; + body(f, w); + } + } else if (id === 11) { + for (let n = r.u(), i = 0; i < n; i++) { + const flag = r.u(); + if (flag !== 0) { finding(`bulk-memory (a data segment of kind ${flag}; 1.0 has kind 0)`, `${where}, segment ${i}`); throw new Error('stop'); } + constExpr(r, `${where}, segment ${i}`); + r.skip(r.u()); + } + } +} + +const b = readFileSync(file); +let decoded = true; +try { + if (b.length < 8 || b.readUInt32LE(0) !== 0x6d736100) throw new Error('not a WebAssembly module (no \\0asm)'); + if (b.readUInt32LE(4) !== 1) throw new Error(`binary format version ${b.readUInt32LE(4)}, not 1`); + const r = new Reader(b, 8); + let last = 0; + while (!r.eof()) { + const id = r.byte(); + const s = r.sub(r.u()); + if (id === 0) { + const name = s.name(); + if (name === 'target_features') { + declared = []; + for (let n = s.u(); n > 0; n--) { const prefix = String.fromCharCode(s.byte()); declared.push(prefix + s.name()); } + } + continue; + } + if (id === 12) { finding('bulk-memory (a DataCount section)', 'section 12'); continue; } + if (id === 13) { finding('exception-handling (a tag section)', 'section 13'); continue; } + if (id > 13) throw new Error(`section id ${id}`); + if (id <= last) throw new Error(`section ${id} after section ${last}`); + last = id; + try { + section(id, s); + if (!s.eof()) throw new Error(`${s.end - s.at} bytes left over in the ${SECTION[id]} section`); + } catch (e) { + // A finding that ends a section's reading says so with 'stop'. + if (e.message !== 'stop') throw e; + } + } +} catch (e) { + decoded = false; + console.error(`check-mvp: ${file}: cannot decode: ${e.message}`); +} +if (!decoded) process.exit(2); + +if (tables > 1) finding('reference-types (more than one table)', 'the module'); +if (memories > 1) finding('multi-memory (more than one memory)', 'the module'); +for (const e of exports) { + if (e.kind === 3 && globalsMutable[e.index]) finding('mutable-globals (a mutable global exported)', `the export ${e.name}`); +} +// The linker's own account of what the module uses: nothing later than 1.0 may be in +// use ('+') or required ('='). mutable-globals is declared by every module with a +// stack pointer; the module's imports and exports were checked for it above. +for (const d of declared || []) { + if ((d[0] === '+' || d[0] === '=') && d.slice(1) !== 'mutable-globals') finding(`${d.slice(1)} (declared in use by the target_features section)`, 'the target_features section'); +} + +const exportedFunctions = exports.filter((e) => e.kind === 0).map((e) => e.name).sort(); +const policy = []; +if (pkg) { + if (imports.length > 0) policy.push(`it imports ${imports.join(', ')}; a package imports nothing`); + for (const e of exports.filter((x) => x.kind === 0)) { + const t = types[funcTypes[e.index]]; + if (t && [...t.params, ...t.results].includes(I64)) policy.push(`the export ${e.name} takes or returns i64`); + } + if (floats > 0) policy.push(`floating point: ${floats} types, constants or instructions; a package has none`); +} +if (functions) { + const want = [...functions].sort(); + const extra = exportedFunctions.filter((n) => !want.includes(n)); + const missing = want.filter((n) => !exportedFunctions.includes(n)); + if (extra.length > 0) policy.push(`it exports ${extra.join(', ')}, which the list does not name`); + if (missing.length > 0) policy.push(`it does not export ${missing.join(', ')}`); +} + +console.log(`${file}: ${b.length} bytes, SHA-256 ${createHash('sha256').update(b).digest('hex')}`); +console.log(` functions: ${bodies} bodies, ${instructions} instructions read`); +console.log(` imports: ${imports.length === 0 ? 'none' : imports.join(', ')}`); +console.log(` exported functions: ${exportedFunctions.join(', ') || 'none'}`); +console.log(` other exports: ${exports.filter((e) => e.kind !== 0).map((e) => e.name).join(', ') || 'none'}`); +console.log(` floating point: ${floats === 0 ? 'none' : `${floats} types, constants or instructions`}`); +console.log(` target_features: ${declared === null ? 'no such section' : declared.join(' ') || 'empty'}`); +if (found.size === 0 && policy.length === 0) { + console.log(' WebAssembly 1.0: every section, type, segment and instruction is 1.0\'s'); + if (pkg) console.log(' a package: no imports, no i64 at an exported function, no floating point'); + if (functions) console.log(' the exported functions are the list\'s'); + process.exit(0); +} +for (const [feature, f] of found) console.error(` NOT 1.0: ${feature}: ${f.count} time${f.count === 1 ? '' : 's'}, first in ${f.where}`); +for (const p of policy) console.error(` NOT A PACKAGE: ${p}`); +console.error(`check-mvp: ${file} fails`); +process.exit(1); diff --git a/wai/wai-vector/wasm/src/heap.rs b/wai/wai-vector/wasm/src/heap.rs new file mode 100644 index 000000000..a7d548956 --- /dev/null +++ b/wai/wai-vector/wasm/src/heap.rs @@ -0,0 +1,119 @@ +//! The module's allocator. A `no_std` module has none unless it brings one, and this +//! crate takes no dependency. +//! +//! Blocks come in size classes: 16, 32, 48 and 64 bytes, then four classes to each +//! doubling (80, 96, 112, 128, 160, …), so a block is less than a quarter larger than +//! what was asked for once past 64 bytes. A freed block goes on its class's list, and +//! the next request of that class takes it. A class with no free block takes fresh +//! memory from the end of the linear memory, which grows when it runs out. Blocks are +//! never split or merged: the memory held is at most the sum, over the classes, of +//! each class's peak. Every block is aligned to 16 bytes; a request for a larger +//! alignment is refused. +//! +//! One thread: WebAssembly 1.0 has no other, and the module shares its memory with +//! none. + +use core::alloc::{GlobalAlloc, Layout}; +use core::arch::wasm32::{memory_grow, memory_size}; +use core::cell::UnsafeCell; +use core::ptr::null_mut; + +/// A page of linear memory. +const PAGE: u64 = 65_536; +/// Every block's alignment, and the smallest block. +const ALIGN: usize = 16; +/// The classes: four below 64 bytes, then four to each doubling up to 2^31. +const CLASSES: usize = 104; +/// Memory is asked of the engine at least this many pages at a time. +const GROW_PAGES: u64 = 16; + +struct Inner { + /// Each class's free blocks, as a list through the blocks' first words; 0 ends it. + free: [usize; CLASSES], + /// Fresh memory this allocator grew and has not handed out: `[top, end)`. + top: u64, + end: u64, +} + +pub struct Heap(UnsafeCell); + +// One thread (see the module's text). +unsafe impl Sync for Heap {} + +impl Heap { + pub const fn new() -> Heap { + Heap(UnsafeCell::new(Inner { free: [0; CLASSES], top: 0, end: 0 })) + } +} + +/// The class of a request of `n` bytes: its index and its block size, a multiple of 16. +fn class(n: usize) -> (usize, usize) { + let n = if n < ALIGN { ALIGN } else { n }; + // ⌊log2 n⌋, at least 4. + let k = (usize::BITS - 1 - n.leading_zeros()) as usize; + if k < 6 { + let c = n.div_ceil(16) * 16; + return (c / 16 - 1, c); + } + // Quarters of 2^k: j = 4 is 2^k itself and j = 8 is 2^(k+1), the next doubling's + // first class, which the same expression gives the same index. + let step = 1usize << (k - 2); + let j = n.div_ceil(step); + (3 + 4 * (k - 6) + (j - 4), j * step) +} + +unsafe impl GlobalAlloc for Heap { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + if l.align() > ALIGN { + return null_mut(); + } + let h = unsafe { &mut *self.0.get() }; + let (i, c) = class(l.size()); + let head = h.free[i]; + if head != 0 { + h.free[i] = unsafe { *(head as *const usize) }; + return head as *mut u8; + } + let c = c as u64; + if h.end - h.top < c { + // Memory this allocator did not grow is not its to hand out: before its + // first block, and after anything else grew the memory, it starts again at + // the memory's end. + let now = memory_size::<0>() as u64 * PAGE; + if now != h.end { + h.top = now; + h.end = now; + } + let need = (c - (h.end - h.top)).div_ceil(PAGE); + let Some(pages) = [need.max(GROW_PAGES), need].into_iter().find(|&p| memory_grow::<0>(p as usize) != usize::MAX) else { + return null_mut(); + }; + h.end += pages * PAGE; + } + let p = h.top; + h.top += c; + p as usize as *mut u8 + } + + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + let h = unsafe { &mut *self.0.get() }; + let (i, _) = class(l.size()); + unsafe { *(p as *mut usize) = h.free[i] }; + h.free[i] = p as usize; + } + + unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 { + // The block already holds any size of its class. + if class(n).0 == class(l.size()).0 { + return p; + } + let q = unsafe { self.alloc(Layout::from_size_align_unchecked(n, l.align())) }; + if !q.is_null() { + unsafe { + core::ptr::copy_nonoverlapping(p, q, if l.size() < n { l.size() } else { n }); + self.dealloc(p, l); + } + } + q + } +} diff --git a/wai/wai-vector/wasm/src/lib.rs b/wai/wai-vector/wasm/src/lib.rs index 44fd7250a..6c86dae7e 100644 --- a/wai/wai-vector/wasm/src/lib.rs +++ b/wai/wai-vector/wasm/src/lib.rs @@ -1,16 +1,107 @@ -//! `wai_vector::vectors::recompute` behind a C ABI. The caller hands every set under -//! `sets/` to [`wv_add_set`], then for each vector writes its group, extension and -//! bytes into memory from [`wv_alloc`], calls [`wv_recompute`] (or [`wv_form`] for -//! the authoring form), and reads the result from [`wv_out_ptr`] and [`wv_out_len`]. +//! The sink of `wai.graphics.vector` (`extensions/graphics-vector.md`) behind a C ABI, +//! for a browser's engine: no bindings generator, no imports, and no dependency but +//! `wai-vector`. +//! +//! A caller hands every outline set a held `SET` record may name to [`wv_add_set`], +//! opens a payload with [`wv_open`], and asks for frames with [`wv_render`]. Bytes go +//! in through a buffer from [`wv_alloc`]. A result comes back as *the output*: the +//! bytes at [`wv_out_ptr`], [`wv_out_len`] of them, which stay until the next call +//! that writes an output. +//! +//! | export | returns | the output | +//! |---|---|---| +//! | [`wv_alloc`]`(n)` | a buffer of `n` bytes | unchanged | +//! | [`wv_free`]`(p, n)` | | unchanged | +//! | [`wv_add_set`]`(p, n)` | | unchanged | +//! | [`wv_open`]`(p, n)` | a handle, or 0 | empty, or the refusal's code (§2.7) | +//! | [`wv_close`]`(handle)` | | unchanged | +//! | [`wv_render`]`(handle, w, h, num, den, g_lo, g_hi)` | a status | the frame's `RGBA8P` (§2.9), or the request error's code (§2.7) | +//! | [`wv_cost`]`(handle, w, h)` | a status | the size's cost (§3.4(b)) in decimal, or `size_unlisted` | +//! | [`wv_transcript`]`(handle)` | a status | the transcript (§5.4), JSON | +//! | [`wv_projection`]`(handle, num, den, g_lo, g_hi)` | a status | the frame's projection (§5.4), JSON, or the request error's code (§2.7) | +//! | [`wv_sha256`]`(p, n)` | | the SHA-256 of the `n` bytes at `p`, 32 bytes | +//! +//! A status is [`WV_OK`], [`WV_REFUSED`] or [`WV_NO_HANDLE`]. +//! +//! No export takes or returns a 64-bit integer, so a caller needs only the engine's +//! 32-bit numbers: a frame number goes in as two halves. +//! +//! With the feature `recompute`, on by default, the module also exports +//! [`wv_recompute`] and [`wv_form`]: the recomputation of the conformance vectors, +//! which `verify.mjs` runs. The package a site serves is built without it +//! (`build-pkg.sh`). +//! +//! The crate is `no_std`, so that it builds for `wasm32v1-none`, the target whose +//! precompiled `core` and `alloc` are WebAssembly 1.0 themselves. It therefore +//! brings its own allocator ([`heap`]), and a panic is a trap. The module has one +//! thread, and its state is one static. +#![no_std] -use std::cell::RefCell; +extern crate alloc; -use wai_vector::SetStore; -use wai_vector::vectors; +mod heap; -thread_local! { - static OUT: RefCell> = const { RefCell::new(Vec::new()) }; - static STORE: RefCell> = const { RefCell::new(None) }; +use alloc::string::ToString; +use alloc::vec; +use alloc::vec::Vec; +use core::cell::UnsafeCell; + +use wai_vector::sha256::sha256; +use wai_vector::{Code, Loaded, SetStore, frame, text}; + +#[global_allocator] +static HEAP: heap::Heap = heap::Heap::new(); + +/// A panic is a trap: the engine reports it to the caller, and nothing is unwound. +#[panic_handler] +fn panic(_: &core::panic::PanicInfo<'_>) -> ! { + core::arch::wasm32::unreachable() +} + +/// The call succeeded; the output is its result. +pub const WV_OK: u32 = 0; +/// A refusal of the payload or a render request error (§2.7); the output is the +/// code's name. +pub const WV_REFUSED: u32 = 1; +/// The handle names no open payload; the output is empty. +pub const WV_NO_HANDLE: u32 = 2; + +struct State { + out: Vec, + store: SetStore, + /// The open payloads; a handle is an index here, plus one. + docs: Vec>, +} + +struct Module(UnsafeCell>); + +// One thread (see the crate's text). +unsafe impl Sync for Module {} + +static MODULE: Module = Module(UnsafeCell::new(None)); + +/// The module's state. Each export takes it once, and no export calls another. +fn state() -> &'static mut State { + let s = unsafe { &mut *MODULE.0.get() }; + s.get_or_insert_with(|| State { out: Vec::new(), store: SetStore::new(), docs: Vec::new() }) +} + +/// The `n` bytes at `p`. +/// +/// # Safety +/// `p` points to `n` readable bytes, or `n` is 0. +unsafe fn bytes<'a>(p: *const u8, n: usize) -> &'a [u8] { + if n == 0 { &[] } else { unsafe { core::slice::from_raw_parts(p, n) } } +} + +fn open_doc(s: &State, handle: u32) -> Option<&Loaded> { + s.docs.get((handle as usize).wrapping_sub(1)).and_then(Option::as_ref) +} + +/// Answer with a code of §2.7: its name as the output, and [`WV_REFUSED`]. +fn refused(out: &mut Vec, code: Code) -> u32 { + *out = code.name().as_bytes().to_vec(); + WV_REFUSED } /// A buffer of `n` bytes for the caller to fill; freed by [`wv_free`]. @@ -18,7 +109,7 @@ thread_local! { pub extern "C" fn wv_alloc(n: usize) -> *mut u8 { let mut v = vec![0u8; n]; let p = v.as_mut_ptr(); - std::mem::forget(v); + core::mem::forget(v); p } @@ -31,57 +122,197 @@ pub unsafe extern "C" fn wv_free(p: *mut u8, n: usize) { drop(unsafe { Vec::from_raw_parts(p, n, n) }); } -/// Hold the `n` bytes at `p` as a set a held `SET` record may name. +/// Hold the `n` bytes at `p` as a set a held `SET` record may name (§2.7 step 6). The +/// set is found by its SHA-256, and is checked when a payload that names it loads. /// /// # Safety /// `p` points to `n` readable bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn wv_add_set(p: *const u8, n: usize) { - let b = unsafe { std::slice::from_raw_parts(p, n) }.to_vec(); - STORE.with(|s| s.borrow_mut().get_or_insert_with(SetStore::new).add(b)); + let b = unsafe { bytes(p, n) }.to_vec(); + state().store.add(b); } -fn set_out(status: u32, out: Vec) -> u32 { - OUT.with(|o| *o.borrow_mut() = out); - status -} - -/// Recompute the expected file of the vector of group `g`, extension `e` and bytes -/// `d`. Returns 0 with the expected file as the output, or 1 with the error. -/// -/// # Safety -/// Each pointer points to its length of readable bytes. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn wv_recompute(g: *const u8, gn: usize, e: *const u8, en: usize, d: *const u8, dn: usize) -> u32 { - let group = unsafe { std::slice::from_raw_parts(g, gn) }; - let ext = unsafe { std::slice::from_raw_parts(e, en) }; - let data = unsafe { std::slice::from_raw_parts(d, dn) }; - let (Ok(group), Ok(ext)) = (std::str::from_utf8(group), std::str::from_utf8(ext)) else { - return set_out(1, b"group or extension not UTF-8".to_vec()); - }; - let r = STORE.with(|s| vectors::recompute(group, ext, data, s.borrow_mut().get_or_insert_with(SetStore::new))); - match r { - Ok(s) => set_out(0, s.into_bytes()), - Err(e) => set_out(1, e.into_bytes()), - } -} - -/// The authoring form of the payload of `n` bytes at `p`. +/// Load the payload of `n` bytes at `p` (§2.7), with the sets held so far. Returns a +/// handle, which is not 0, and an empty output; or 0, and the refusal's code as the +/// output. The payload's bytes are not kept: the caller may free them. /// /// # Safety /// `p` points to `n` readable bytes. #[unsafe(no_mangle)] -pub unsafe extern "C" fn wv_form(p: *const u8, n: usize) -> u32 { - let data = unsafe { std::slice::from_raw_parts(p, n) }; - set_out(0, vectors::form_of(data).into_bytes()) +pub unsafe extern "C" fn wv_open(p: *const u8, n: usize) -> u32 { + let b = unsafe { bytes(p, n) }; + let s = state(); + match wai_vector::load(b, &mut s.store) { + Ok(l) => { + s.out = Vec::new(); + let slot = match s.docs.iter().position(Option::is_none) { + Some(i) => i, + None => { + s.docs.push(None); + s.docs.len() - 1 + } + }; + s.docs[slot] = Some(l); + slot as u32 + 1 + } + Err(r) => { + refused(&mut s.out, r.code); + 0 + } + } } +/// Close a handle from [`wv_open`]. A handle that is not open is ignored. +#[unsafe(no_mangle)] +pub extern "C" fn wv_close(handle: u32) { + if let Some(d) = state().docs.get_mut((handle as usize).wrapping_sub(1)) { + *d = None; + } +} + +/// Frame `g = g_hi·2^32 + g_lo` of the payload at the size `w × h` and the rate +/// `num/den` (§3.10). [`WV_OK`]: the output is the frame's `RGBA8P`, `4·w·h` bytes +/// (§2.9). [`WV_REFUSED`]: the output is `size_unlisted`, `rate_unlisted` or +/// `frame_range` (§2.7, *Render*). +#[unsafe(no_mangle)] +pub extern "C" fn wv_render(handle: u32, w: u32, h: u32, num: u32, den: u32, g_lo: u32, g_hi: u32) -> u32 { + let s = state(); + let Some(l) = open_doc(s, handle) else { + s.out = Vec::new(); + return WV_NO_HANDLE; + }; + let g = u64::from(g_hi) << 32 | u64::from(g_lo); + // No SIZE lists a dimension above 65,535. + let r = match (u16::try_from(w), u16::try_from(h)) { + (Ok(w), Ok(h)) => frame::render_request(l, w, h, num, den, g).map_err(|e| e.code), + _ => Err(Code::SizeUnlisted), + }; + match r { + Ok(px) => { + s.out = px; + WV_OK + } + Err(code) => refused(&mut s.out, code), + } +} + +/// The per-frame cost the payload states for the size `w × h`, which loading verified +/// (§3.4(b)). [`WV_OK`]: the output is the cost in decimal. [`WV_REFUSED`]: the output +/// is `size_unlisted`. +#[unsafe(no_mangle)] +pub extern "C" fn wv_cost(handle: u32, w: u32, h: u32) -> u32 { + let s = state(); + let Some(l) = open_doc(s, handle) else { + s.out = Vec::new(); + return WV_NO_HANDLE; + }; + match l.doc.sizes.iter().position(|z| u32::from(z.w) == w && u32::from(z.h) == h) { + Some(z) => { + s.out = l.costs[z].to_string().into_bytes(); + WV_OK + } + None => refused(&mut s.out, Code::SizeUnlisted), + } +} + +/// The transcript of the payload (§5.4): every block that is not decorative, in block +/// order, each with the ticks over which it is active. [`WV_OK`]: the output is the +/// transcript, as the JSON §5.4 defines. +#[unsafe(no_mangle)] +pub extern "C" fn wv_transcript(handle: u32) -> u32 { + let s = state(); + let Some(l) = open_doc(s, handle) else { + s.out = Vec::new(); + return WV_NO_HANDLE; + }; + s.out = text::transcript(&l.doc).into_bytes(); + WV_OK +} + +/// The projection of the payload at frame `g = g_hi·2^32 + g_lo` of the rate `num/den` +/// (§5.4): the blocks active at that frame. [`WV_OK`]: the output is the projection, +/// as the JSON §5.4 defines. [`WV_REFUSED`]: the output is `rate_unlisted` or +/// `frame_range` (§2.7, *Render*). +#[unsafe(no_mangle)] +pub extern "C" fn wv_projection(handle: u32, num: u32, den: u32, g_lo: u32, g_hi: u32) -> u32 { + let s = state(); + let Some(l) = open_doc(s, handle) else { + s.out = Vec::new(); + return WV_NO_HANDLE; + }; + let g = u64::from(g_hi) << 32 | u64::from(g_lo); + let r = match l.doc.rates.iter().position(|q| q.num == num && q.den == den) { + Some(rate) => text::projection(&l.doc, rate, g).map_err(|e| e.code), + None => Err(Code::RateUnlisted), + }; + match r { + Ok(t) => { + s.out = t.into_bytes(); + WV_OK + } + Err(code) => refused(&mut s.out, code), + } +} + +/// The SHA-256 of the `n` bytes at `p`, as the output: 32 bytes. The bytes may be the +/// output itself, which is read before it is replaced. +/// +/// # Safety +/// `p` points to `n` readable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wv_sha256(p: *const u8, n: usize) { + let d = sha256(unsafe { bytes(p, n) }); + state().out = d.to_vec(); +} + +/// The output's first byte. #[unsafe(no_mangle)] pub extern "C" fn wv_out_ptr() -> *const u8 { - OUT.with(|o| o.borrow().as_ptr()) + state().out.as_ptr() } +/// The output's length in bytes. #[unsafe(no_mangle)] pub extern "C" fn wv_out_len() -> usize { - OUT.with(|o| o.borrow().len()) + state().out.len() +} + +/// Recompute the expected file of the vector of group `g`, extension `e` and bytes +/// `d`, with the sets held so far. Returns 0 with the expected file as the output, or +/// 1 with the error. +/// +/// # Safety +/// Each pointer points to its length of readable bytes. +#[cfg(feature = "recompute")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wv_recompute(g: *const u8, gn: usize, e: *const u8, en: usize, d: *const u8, dn: usize) -> u32 { + let (group, ext, data) = unsafe { (bytes(g, gn), bytes(e, en), bytes(d, dn)) }; + let s = state(); + let (Ok(group), Ok(ext)) = (core::str::from_utf8(group), core::str::from_utf8(ext)) else { + s.out = b"group or extension not UTF-8".to_vec(); + return 1; + }; + match wai_vector::vectors::recompute(group, ext, data, &mut s.store) { + Ok(t) => { + s.out = t.into_bytes(); + 0 + } + Err(e) => { + s.out = e.into_bytes(); + 1 + } + } +} + +/// The authoring form (§2.12) of the payload of `n` bytes at `p`, as the output. +/// +/// # Safety +/// `p` points to `n` readable bytes. +#[cfg(feature = "recompute")] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn wv_form(p: *const u8, n: usize) -> u32 { + let data = unsafe { bytes(p, n) }; + state().out = wai_vector::vectors::form_of(data).into_bytes(); + 0 } -- 2.54.0 (Apple Git-157) From 4084a31bc67c9330152755fb5119a4ccac6dc538 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 13 Oct 2026 02:46:34 -0400 Subject: [PATCH 4/4] 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 --- wai/extensions/graphics-vector.md | 6 +++--- wai/tools/wai_vector_check.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/wai/extensions/graphics-vector.md b/wai/extensions/graphics-vector.md index 507fb4e79..27d11a980 100644 --- a/wai/extensions/graphics-vector.md +++ b/wai/extensions/graphics-vector.md @@ -1009,9 +1009,9 @@ A block object has the member `active` first, then the members of a node object. 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, the words "the door of the house", drawn by a run active over `[24000, 264000)`; -- block 1: purpose 3, `en`, left-to-right, drawn by a run active over the same window; -- block 2: purpose 7, `en`, 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)`. +- 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: diff --git a/wai/tools/wai_vector_check.py b/wai/tools/wai_vector_check.py index ccbb01e87..093cba219 100644 --- a/wai/tools/wai_vector_check.py +++ b/wai/tools/wai_vector_check.py @@ -2311,10 +2311,9 @@ def selftest_text(eq, notes): eq("5.4 stated invariants", getattr(P, "text_faults", None) or [], []) # "A worked example": tb = 120000, t0 = 0, dur = 360000, 25 fps, so P = 4800; three # blocks in group 1, the third a ruby annotation of the first 8 bytes of block 1. - # THE TEXT DOES NOT SETTLE the example's inputs in full: its list of blocks states - # no string (block 0 only by its meaning) and no writing mode. So the strings are - # read from the two lines it prints, and the mode is taken as 0 (horizontal-tb); - # every other byte of those lines is recomputed from the stated fields. + # Its list of blocks states each block's string and writing mode (0). The strings + # are read from the two lines it prints, which hold the same ones; every other byte + # of those lines is recomputed from the stated fields. ex = spec_worked_example() if ex is None: notes.append("section 5.4's worked example was not recomputed: the extension text " -- 2.54.0 (Apple Git-157)