wai-vector: the independent checker, written from the extension text alone, and a text fix (D7-4) #219

Merged
dcharlot merged 5 commits from wai/d7-4-vector-checker into main 2026-10-11 16:59:32 -04:00
3 changed files with 2484 additions and 7 deletions

View file

@ -16,7 +16,12 @@ name: wai-vector vector graphics
# every expected file and every authoring form;
# - 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.
# 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
# crossings in big integers), rewrites every payload and set to its own bytes, and
# recomputes every digest of sha256.json.
#
# There is no Windows leg: the Windows runner has been offline since 2026-08-28.
@ -215,3 +220,25 @@ jobs:
- name: Every workflow declares a concurrency group
shell: bash
run: bash scripts/check-workflow-concurrency.sh
checker:
name: independent checker (Python)
runs-on: linux
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# Standard library only, so it needs nothing installed. Every frame of every
# rendering vector is rendered, the clock vector's 3840 x 2160 frames included.
- name: wai_vector_check.py recomputes every expected file of the corpus
working-directory: wai
shell: bash
run: python3 -I tools/wai_vector_check.py corpus vector-conformance --quiet
- name: Every workflow declares a concurrency group
shell: bash
run: bash scripts/check-workflow-concurrency.sh

View file

@ -164,8 +164,8 @@ Anything else is malformed. The walk never needs an index past `m`. A contour of
**Notes.** UTF-8 text inside the set's digest: `key=value` lines, a blank line, then the parent font's full licence text. §4 defines the keys. A sink checks only that the notes are valid UTF-8.
**Load checks, in order**
1. The magic, `format`, the `zero` field and the reserved bits of `flags`: `outline_malformed`.
2. The header's counts `n_glyphs`, `points_total`, `contours_total` and `notes_len`, in field order: a count above its cap is `too_large`; `n_glyphs = 0` or `notes_len = 0` is `outline_malformed`.
1. Fewer than the header's 32 bytes; the magic, `format`, the `zero` field and the reserved bits of `flags`: `outline_malformed`.
2. The header's counts `n_glyphs`, `points_total`, `contours_total` and `notes_len`, one at a time in field order, each against its whole range before the next: a count above its cap is `too_large`, and `n_glyphs = 0` or `notes_len = 0` is `outline_malformed`. So `n_glyphs = 0` with `points_total` past its cap is `outline_malformed`.
3. The exact length differs from the number of bytes in the set: `outline_malformed`.
4. Each glyph record, in order: `n_points ≤ 16,383` (`points_limit`); then `n_contours ≤ n_points`, `n_contours ≥ 1` when `n_points ≥ 1`, and the `zero` field (`outline_malformed`).
5. The `gid`s strictly increase.
@ -229,7 +229,7 @@ Each table holds the number of records its header count states: `n_sizes` `SIZE`
| `PAINT` | 4 | `r, g, b, a: u8`, premultiplied and sRGB-encoded: `r, g, b ≤ a` |
| `TRACK` | 8 | `kind: u8`: 1 opacity, 2 translation, 3 matrix entry. `zero: u8`. `zero: u16`. `n_keys: u32`, 1 ..= 4096. The track's keys are the next `n_keys` records of `KEYS`, and the tracks' `n_keys` sum to the header's `n_keys` |
| `KEY` | 12 | `t: u32`: a tick, < 2^31. `value: i32`. `interp: u8`: 0 hold, 1 linear, or `2 + j` for easing table `j`, with `j < n_tables`. `zero: [u8; 3]`. Within a track, keys strictly ascend in `t`, and the last key's `interp` is 0. `value` by the track's `kind`: opacity 0 ..= 255; translation −2^22 ..= 2^22, in design subpixels; matrix entry −2^20 ..= 2^20, in Q16 |
| `PATH` | 16 | `n_contours: u16`, 1 ..= `n_points`. `n_points: u16`, 1 ..= **16,383**. `zero: u32`. `xmin, ymin, xmax, ymax: i16`, exact over the path's points as in `WOS1`. Its contour ends are the next `n_contours` records of `PATH_ENDS`, and its points the next `n_points` records of `PATH_POINTS`. The paths' `n_contours` sum to `path_contours`, and their `n_points` to `path_points` |
| `PATH` | 16 | `n_contours: u16`, at least 1. `n_points: u16`, 1 ..= **16,383**; then `n_contours ≤ n_points`, checked at `n_points`, the later field (§2.0). `zero: u32`. `xmin, ymin, xmax, ymax: i16`, exact over the path's points as in `WOS1`. Its contour ends are the next `n_contours` records of `PATH_ENDS`, and its points the next `n_points` records of `PATH_POINTS`. The paths' `n_contours` sum to `path_contours`, and their `n_points` to `path_points` |
| `PATH_END` | 2 | `u16`: as a `WOS1` contour end, within its path |
| `PATH_POINT` | 5 | `x: i16`, `y: i16`, `tag: u8`: as a `WOS1` point, in path units. A path's contours meet §2.1's contour grammar |
| `TEXT` | 20 | a **text block**, in reading order. `purpose: u8`, 1 ..= 255 (§5; 13 is `decorative`). `dir: u8`: 0 left-to-right, 1 right-to-left. `lang_len: u8`, 1 ..= 63. `mode: u8`, the writing mode: 0 horizontal-tb, 1 vertical-rl, 2 vertical-lr. `group: u16`: 0 ungrouped. `ruby: u16`: 0 none; `ruby = r ≥ 1` annotates block `r − 1`, so `r ≤ n_texts`. `ruby_start: u32` and `ruby_len: u32`: a byte range of the base block's string; both 0 when `ruby = 0`, and `ruby_len ≥ 1` otherwise. `str_len: u32`, 1 ..= 4096. The blocks' `lang_len + str_len` sum to `strings_len` |
@ -248,7 +248,7 @@ Items are listed in **paint order**: the first is drawn first, at the bottom. An
| off | size | field | group begin (0) | group end (1) | path fill (2) | run fill (3) |
|---|---|---|---|---|---|---|
| 0 | 1 | `kind` | 0 | 1 | 2 | 3 |
| 1 | 1 | `rule`: 0 non-zero, 1 even-odd | the clip's rule; 0 with no clip | 0 | the path's rule | 0 (glyphs are non-zero) |
| 1 | 1 | `rule`: 0 non-zero, 1 even-odd | the clip's rule; 0 with no clip, checked at `ref`, the later field (§2.0) | 0 | the path's rule | 0 (glyphs are non-zero) |
| 2 | 2 | `zero` | | | | |
| 4 | 2 | `ref` | a clip path index (< `n_paths`), or `0xFFFF` for no clip | 0 | a path index (< `n_paths`) | a set index (< `n_sets`) |
| 6 | 2 | `paint` | `0xFFFF` | 0 | a paint index (< `n_paints`) | a paint index (< `n_paints`) |
@ -326,7 +326,7 @@ A sink refuses a payload at load, before the first frame, and reports the first
**Load, once per payload**
1. `payload_malformed`: the magic, `format`, or the header's `zero` field.
1. `payload_malformed`: a payload shorter than the header's 80 bytes; the magic, `format`, or the header's `zero` field.
2. `too_large`, in field order: a count or length field of the header above its cap (`n_sizes`, `n_rates`, `n_sets`, `n_tables`, `n_paints`, `n_paths`, `n_items`, `n_keys`, `n_glyphs`, `n_clusters`, `path_contours`, `path_points`, `strings_len`, `inline_len`, `held_len`); then `inline_len + held_len > 2^26`; then the computed exact length above 2^26.
3. `payload_malformed`: the payload's length differs from the computed exact length.
4. **The record pass.** One pass, in file order: first the header's remaining fields, then every record of every table, each record's fields in offset order.
@ -425,10 +425,11 @@ On either code the sink ignores the sidecar, keeps presenting the overlay, and r
**The authoring tool's probe set.** The tool builds the probe set deterministically, over the (size, rate) pairs it is asked to cover (by default every listed pair):
- **Candidates, in priority order,** for each pair:
1. the document's first frame `⌈t0/P⌉` and last frame `⌊(t0 + dur − 1)/P⌋`;
1. the document's first frame `⌈t0/P⌉` and last frame `⌊(t0 + dur − 1)/P⌋`, when the document has a frame at that rate, that is when the first is at most the last;
2. for each item, in item order, whose effective window (§3.4(b)), the ticks `a ≤ τ < b`, holds at least one frame, that is `⌈a/P⌉ ≤ ⌈b/P⌉ − 1`: the first frame inside it, `⌈a/P⌉`, and the last, `⌈b/P⌉ − 1`;
3. for each key of each track, the first frame at or after the key's tick `t`, `⌈t/P⌉`, if that frame is in range.
- The tool takes level 1, then level 2, then level 3. Within a level it takes candidates in ascending order of `(w, h, num, den, g)`, drops any it already holds, and stops at 1,024. The sidecar then lists the probes in its canonical order.
- A document with no frame at any pair it covers has no candidate. `probes` holds at least one entry, so the tool writes no sidecar for it.
A probe set is detection, not proof.

File diff suppressed because it is too large Load diff