open-standards/wai/live-conformance/tonemap_verify.py
Transaction Science 2b54ac2054 tonemap: digests of L2, E2 and tan over whole ranges in the corpus
A second falsification: L2's squarings rounded instead of truncated still
regenerated the corpus (the variant differs on about 3 in 100000 mantissas).
vectors.json now states the SHA-256 of L2 at 2^18 spread values, of E2's
mantissa for every fraction and of tan for every angle; both implementations
check them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:43:06 -04:00

1365 lines
51 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Second implementation of extensions/exact-tonemap.md (`wai.media.tonemap`, `wai.meta.gainmap`,
`wai.meta.st2094_50`), over the corpus `live-conformance/tonemap/`.
Written from the extension's text. It shares no code with wai-rs: it computes both derivations in
integer arithmetic of its own (the fixed-point log2 and exp2 of §3, the sRGB table, the gain map
of §4, the Annex C reader, slopes, gain curves and weights of §5), reads PNG with the standard
library's zlib and its own unfiltering, and reads records as §2 has them read. It verifies every
signature with WAI's strict Ed25519 rule (claim-conformance/_ed25519.py) and every link hash and
content hash with BLAKE3 written from its specification (claim-conformance/_blake3.py), and reads
envelopes and SPEC §7.1's component rules with the staged-delivery corpus's second implementation
(staged-conformance/staged_verify.py). It reads §3.3's table of K, §5.4's constant P and §9's codes
from the extension.
python3 -I tonemap_verify.py <live-conformance/tonemap>
It checks that frames.rgb8 holds the frames whose digests the int_video sidecar publishes, the
arithmetic at the corpus's stated points (L2, E2, the sRGB table, tan, the video's slopes), every
envelope's content hash and SPEC §7.1 role, and gives every case the outcome, the reason, the place
and the statements the corpus states; it checks that every reason of §9 refuses a case, and
recomputes §6's measurement in binary64 (within one code value of the largest difference and 0.01
of the mean). Then it mutates copies of the corpus and requires every copy to fail, and drops each
of its own checks in turn and requires the corpus to catch the weakened verifier. Exit status 0
only if all of that holds. Python 3.8 or later; standard library only.
"""
import copy
import hashlib
import json
import math
import os
import re
import struct
import sys
import zlib
from fractions import Fraction
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
sys.path.insert(0, os.path.join(HERE, "..", "staged-conformance"))
import _ed25519 as ED # noqa: E402
from _blake3 import blake3 # noqa: E402
import staged_verify as SV # noqa: E402
WAI = os.path.join(HERE, "..")
EXTENSION = os.path.join(WAI, "extensions", "exact-tonemap.md")
SIGN_DOMAIN = b"wai:tonemap\x01" # §2
LINK_DOMAIN = b"wai:tonemap-link\x01" # §2
KIND = "wai-tonemap" # §7.1
GAINMAP, ST2094 = "wai-tonemap:gainmap", "wai-tonemap:st2094-50"
CAP_GAINMAP, CAP_ST2094 = "wai.meta.gainmap", "wai.meta.st2094_50"
CAP_PNG = "wai.image.png"
CLASSES = ["captured", "reconstructed", "assistive", "synthesized"]
MAX_INT = (1 << 53) - 1
MAX_HEADROOM, MAX_SIDE, MAX_PICTURES, MAX_INPUTS = 60000, 4096, 256, 16
MAX_METADATA, MAX_SETS = 4096, 256
# A check named here is left out: the verifier weakened on purpose, to show the corpus catches it.
SKIP = set()
class Refuse(Exception):
def __init__(self, reason, at=None):
super().__init__(reason)
self.reason = reason
self.at = at
def on(check):
return check not in SKIP
# ------------------------------------------------------------------ the extension's text
def extension_text():
with open(EXTENSION, encoding="utf-8") as f:
return f.read()
def section(text, heading):
at = text.index(heading)
end = text.find("\n## ", at + 1)
return text[at:end if end >= 0 else len(text)]
def read_k_table(text):
"""§3.3's table: K_1..K_16."""
s = section(text, "### 3.3 ")
k = {}
for line in s.splitlines():
cells = [c.strip().strip("*") for c in line.strip().strip("|").split("|")]
if len(cells) == 8 and cells[0].isdigit():
for i in range(0, 8, 2):
k[int(cells[i])] = int(cells[i + 1])
return [k[i] for i in range(1, 17)]
def read_pi(text):
"""§5.4's constant P = ⌊π · 2^62⌋."""
m = re.search(r"2\^62⌋ =\s*([0-9]+)", section(text, "### 5.4 "))
return int(m.group(1))
def read_reasons(text):
return [l.split("`")[1] for l in section(text, "\n## 9. Reasons").splitlines() if l.startswith("| `")]
TEXT = extension_text()
K = read_k_table(TEXT)
PI62 = read_pi(TEXT)
REASONS = read_reasons(TEXT)
# ------------------------------------------------------------------ §3 integer arithmetic
def rnd(a, b):
"""rnd(a, b) = ⌊(a + ⌊b/2⌋) / b⌋, b > 0 (§3.1)."""
return (a + b // 2) // b
def L2(v):
"""§3.2: log2(v) in Q16, from below."""
n = v.bit_length() - 1
m = v << (30 - n) if n <= 30 else v >> (n - 30)
f = 0
for i in range(1, 17):
m = (m * m) >> 30
if m >= 1 << 31:
m >>= 1
f += 1 << (16 - i)
return (n << 16) + f
def k_by_rule():
"""§3.3: R_0 = 2^61, R_i = ⌊√(R_{i−1} · 2^60)⌋, K_i = rnd(R_i, 2^30)."""
r, out = 1 << 61, []
for _ in range(16):
r = math.isqrt(r << 60)
out.append(rnd(r, 1 << 30))
return out
_E2 = {}
def E2(y):
"""§3.3: (p, e) with 2^(y / 2^16) ≈ p · 2^(e − 30)."""
e = y >> 16
f = y - (e << 16)
p = _E2.get(f)
if p is None:
p = 1 << 30
for i in range(1, 17):
if (f >> (16 - i)) & 1:
p = rnd(p * K[i - 1], 1 << 30)
_E2[f] = p
return p, e
def scale(x, y, f):
"""§3.3: X · 2^(y / 2^16) / 2^F as an integer."""
p, e = E2(y)
t = e - 30 - f
return (x * p) << t if t >= 0 else rnd(x * p, 1 << -t)
def srgb_table():
"""§3.4."""
base = L2(269025)
s = []
for k in range(256):
if k <= 10:
s.append(rnd(k * 100 * (1 << 30), 255 * 1292))
else:
lam = L2(1000 * k + 14025) - base
s.append(scale(1 << 30, rnd(12 * lam, 5), 0))
return s
S = srgb_table()
def hq16(h):
"""§3.5: a headroom in Q16."""
return rnd(h * (1 << 16), 10000)
def code(v):
return min(max(v, 0), 65535)
def rgb16(samples):
return struct.pack("<%dH" % len(samples), *samples)
def sha(b):
return hashlib.sha256(b).digest()
# ------------------------------------------------------------------ PNG (8-bit grey or RGB)
PNG_SIG = b"\x89PNG\r\n\x1a\n"
def png_decode(b, grey_ok):
"""A PNG of 8-bit samples, colour type 2 (and 0 where grey_ok), no interlace: (w, h, channels, samples)."""
if b[:8] != PNG_SIG:
raise ValueError("signature")
at, ihdr, z, ended, palette, idat = 8, None, b"", False, False, 0
while at < len(b):
if ended:
raise ValueError("after IEND")
if at + 8 > len(b):
raise ValueError("truncated")
ln = struct.unpack_from(">I", b, at)[0]
kind = b[at + 4:at + 8]
if at + 12 + ln > len(b):
raise ValueError("truncated")
data = b[at + 8:at + 8 + ln]
if not all(65 <= c <= 90 or 97 <= c <= 122 for c in kind):
raise ValueError("type")
if struct.unpack_from(">I", b, at + 8 + ln)[0] != zlib.crc32(kind + data) & 0xFFFFFFFF:
raise ValueError("crc")
if idat == 1 and kind != b"IDAT":
idat = 2
if kind == b"IHDR":
if ihdr is not None or at != 8 or ln != 13:
raise ValueError("IHDR")
w, h = struct.unpack_from(">II", data, 0)
tail = data[8:]
if tail == bytes([8, 2, 0, 0, 0]):
ch = 3
elif grey_ok and tail == bytes([8, 0, 0, 0, 0]):
ch = 1
else:
raise ValueError("format")
if w == 0 or h == 0 or w * h * ch > 1 << 26:
raise ValueError("size")
ihdr = (w, h, ch)
elif ihdr is None:
raise ValueError("before IHDR")
elif kind == b"PLTE":
if ihdr[2] == 1 or palette or idat != 0 or ln == 0 or ln % 3 or ln > 768:
raise ValueError("PLTE")
palette = True
elif kind == b"IDAT":
if idat == 2:
raise ValueError("IDAT")
idat = 1
z += data
elif kind == b"IEND":
if ln:
raise ValueError("IEND")
ended = True
elif not kind[0] & 0x20:
raise ValueError("critical")
at += 12 + ln
if ihdr is None or not ended:
raise ValueError("incomplete")
w, h, ch = ihdr
d = zlib.decompressobj()
raw = d.decompress(z)
if not d.eof or d.unused_data:
raise ValueError("zlib")
stride = ch * w
if len(raw) != (stride + 1) * h:
raise ValueError("rows")
out = bytearray(stride * h)
for y in range(h):
f = raw[y * (stride + 1)]
row = raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)]
for i in range(stride):
a = out[y * stride + i - ch] if i >= ch else 0
up = out[(y - 1) * stride + i] if y else 0
c = out[(y - 1) * stride + i - ch] if y and i >= ch else 0
if f == 0:
p = 0
elif f == 1:
p = a
elif f == 2:
p = up
elif f == 3:
p = (a + up) // 2
elif f == 4:
q = a + up - c
pa, pb, pc = abs(q - a), abs(q - up), abs(q - c)
p = a if pa <= pb and pa <= pc else (up if pb <= pc else c)
else:
raise ValueError("filter")
out[y * stride + i] = (row[i] + p) & 0xFF
return w, h, ch, bytes(out)
# ------------------------------------------------------------------ §4 gain maps
def read_iso(b):
"""§4.2 steps 1 and 2: the fields, big-endian."""
if len(b) < 2:
raise Refuse("metadata_malformed")
minimum = struct.unpack_from(">H", b, 0)[0]
if minimum != 0 and on("iso_version"):
raise Refuse("metadata_unsupported")
if len(b) < 5:
raise Refuse("metadata_malformed")
writer = struct.unpack_from(">H", b, 2)[0]
if writer < minimum:
raise Refuse("metadata_malformed")
flags = b[4]
multi, base_space = bool(flags & 0x80), bool(flags & 0x40)
at = [5]
def pair(signed):
if at[0] + 8 > len(b):
raise Refuse("metadata_malformed")
n = struct.unpack_from(">i" if signed else ">I", b, at[0])[0]
d = struct.unpack_from(">I", b, at[0] + 4)[0]
at[0] += 8
return n, d
bh, ah = pair(False), pair(False)
chans = [[pair(True), pair(True), pair(False), pair(True), pair(True)] for _ in range(3 if multi else 1)]
if writer == 0 and at[0] != len(b) and on("iso_length"):
raise Refuse("metadata_malformed")
return {"multi": multi, "base_space": base_space, "bh": bh, "ah": ah, "chans": chans}
def iso_params(m):
"""§4.2 steps 3 and 4, and the channels' Q16 / Q30 values."""
def le(nd, limit):
return abs(nd[0]) <= limit * nd[1]
ok = m["bh"][1] != 0 and m["ah"][1] != 0
ok = ok and le(m["bh"], 6) and le(m["ah"], 6)
ok = ok and m["ah"][0] * m["bh"][1] != m["bh"][0] * m["ah"][1]
for mn, mx, g, bo, ao in m["chans"]:
ok = ok and all(x[1] != 0 for x in (mn, mx, g, bo, ao))
if not ok:
break
ok = ok and le(mn, 16) and le(mx, 16) and Fraction(*mn) <= Fraction(*mx)
ok = ok and g[0] >= 1 and Fraction(1, 64) <= Fraction(*g) <= 64
ok = ok and le(bo, 1) and le(ao, 1)
if not ok and on("iso_range"):
raise Refuse("metadata_out_of_range")
if not m["base_space"] and on("gain_space"):
raise Refuse("gain_space_unsupported")
return [{"min": rnd(mn[0] << 16, mn[1]), "max": rnd(mx[0] << 16, mx[1]), "g": g,
"bo": rnd(bo[0] << 30, bo[1]), "ao": rnd(ao[0] << 30, ao[1])} for mn, mx, g, bo, ao in m["chans"]]
def read_gainmap(p):
"""§4.1 to §4.3."""
if (len(p) < 8 or p[:4] != b"WGM1" or p[4] != 1 or p[5] != 0) and on("wgm1_frame"):
raise Refuse("companion_malformed")
md_len = struct.unpack_from("<H", p, 6)[0]
if (md_len == 0 or md_len > MAX_METADATA or len(p) < 8 + md_len) and on("wgm1_frame"):
raise Refuse("companion_malformed")
try:
w, h, ch, samples = png_decode(p[8 + md_len:], True)
except (ValueError, zlib.error, struct.error, IndexError):
raise Refuse("companion_malformed")
m = read_iso(p[8:8 + md_len])
chans = iso_params(m)
if ch != (3 if m["multi"] else 1) and on("channels"):
raise Refuse("map_channels_mismatch")
return {"multi": m["multi"], "bh": m["bh"], "ah": m["ah"], "chans": chans, "w": w, "h": h, "ch": ch, "samples": samples}
def gain_weight(g, hd):
"""§4.4 step 1."""
(bn, bd), (an, ad) = g["bh"], g["ah"]
a = hd * bd * ad - 10000 * bn * ad
d = 10000 * (an * bd - bn * ad)
if d > 0:
return min(max(rnd(a << 16, d), 0), 1 << 16)
return -min(max(rnd((-a) << 16, -d), 0), 1 << 16)
def gain_factor(g, w, h):
for s in (1, 2, 4, 8):
if g["w"] * s == w and g["h"] * s == h:
return s
if on("map_dims"):
raise Refuse("map_dimensions_mismatch")
return 1
def render_gainmap(g, base, w, h, hd):
"""§4.4."""
s = gain_factor(g, w, h)
wt, ht = gain_weight(g, hd), hq16(hd)
gw, gh, ch, smp = g["w"], g["h"], g["ch"], g["samples"]
q = 255 * 4 * s * s
lq = L2(q)
out = []
for y in range(h):
yy = max(0, 2 * y + 1 - s)
y0, ay = divmod(yy, 2 * s)
y1 = min(y0 + 1, gh - 1)
for x in range(w):
xx = max(0, 2 * x + 1 - s)
x0, ax = divmod(xx, 2 * s)
x1 = min(x0 + 1, gw - 1)
for c in range(3):
j = c if g["multi"] else 0
p = g["chans"][j]
def at(xi, yi):
return smp[(yi * gw + xi) * ch + j]
n = (at(x0, y0) * (2 * s - ax) * (2 * s - ay) + at(x1, y0) * ax * (2 * s - ay)
+ at(x0, y1) * (2 * s - ax) * ay + at(x1, y1) * ax * ay)
gn, gd = p["g"]
if gn == gd:
v = rnd(n << 30, q)
elif n == 0:
v = 0
else:
v = scale(1 << 30, rnd((L2(n) - lq) * gd, gn), 0)
lg = rnd(p["min"] * ((1 << 30) - v) + p["max"] * v, 1 << 30)
gg = rnd(lg * wt, 1 << 16)
t = scale((S[base[(y * w + x) * 3 + c]] + p["bo"]) * 65535, gg - ht, 0) - scale(p["ao"] * 65535, -ht, 0)
out.append(code(rnd(t, 1 << 30)))
return out
def float_gainmap(g, base, w, h, hd):
"""§6's binary64 evaluation of §4.4's formulas."""
s = gain_factor(g, w, h)
def srgb(k):
e = k / 255.0
return e / 12.92 if e <= 0.04045 else ((e + 0.055) / 1.055) ** 2.4
hb = g["bh"][0] / g["bh"][1]
ha = g["ah"][0] / g["ah"][1]
ht = hd / 10000.0
wt = min(max((ht - hb) / (ha - hb), 0.0), 1.0)
if ha < hb:
wt = -wt
gw, gh, ch, smp = g["w"], g["h"], g["ch"], g["samples"]
out = []
for y in range(h):
fy = max(0.0, (y + 0.5) / s - 0.5)
y0 = math.floor(fy)
ay = fy - y0
y1 = min(y0 + 1, gh - 1)
for x in range(w):
fx = max(0.0, (x + 0.5) / s - 0.5)
x0 = math.floor(fx)
ax = fx - x0
x1 = min(x0 + 1, gw - 1)
for c in range(3):
j = c if g["multi"] else 0
p = g["chans"][j]
def at(xi, yi):
return smp[(yi * gw + xi) * ch + j] / 255.0
v = (at(x0, y0) * (1 - ax) * (1 - ay) + at(x1, y0) * ax * (1 - ay)
+ at(x0, y1) * (1 - ax) * ay + at(x1, y1) * ax * ay)
mn, mx = p["min"] / 65536.0, p["max"] / 65536.0
gam = p["g"][0] / p["g"][1]
l = mn + (mx - mn) * v ** (1.0 / gam)
t = (srgb(base[(y * w + x) * 3 + c]) + p["bo"] / 2.0 ** 30) * 2.0 ** (l * wt) - p["ao"] / 2.0 ** 30
out.append(int(min(max(round_half_away(t / 2.0 ** ht * 65535.0), 0), 65535)))
return out
def round_half_away(x):
"""Rounding to nearest, halves away from zero (binary64's `round`)."""
return math.floor(x + 0.5) if x >= 0 else -math.floor(-x + 0.5)
# ------------------------------------------------------------------ §5 ST 2094-50
class Bits:
def __init__(self, b):
self.b, self.at = b, 0
def u(self, n):
v = 0
for _ in range(n):
if self.at // 8 >= len(self.b):
raise Refuse("metadata_malformed")
v = (v << 1) | ((self.b[self.at // 8] >> (7 - self.at % 8)) & 1)
self.at += 1
return v
def zero(self, n):
if self.u(n) != 0 and on("annexc_reserved"):
raise Refuse("metadata_malformed")
def tan_q30(theta):
"""§5.4: tan of the angle, through Q62 series."""
d = min(max(theta, 1), 35999) - 18000
a = rnd(d * PI62, 36000)
q = rnd(a * a, 1 << 62)
t = sig = a
for k in range(1, 16):
t = rnd(-t * q, (1 << 62) * (2 * k) * (2 * k + 1))
sig += t
t = kap = 1 << 62
for k in range(1, 16):
t = rnd(-t * q, (1 << 62) * (2 * k - 1) * (2 * k))
kap += t
return rnd(sig << 30, kap)
def sign(fr):
return (fr > 0) - (fr < 0)
def pchip(x, y):
"""C.3.9 on exact rationals (§5.4); slopes in stops per unit of x."""
n = len(x)
h = [x[i + 1] - x[i] for i in range(n - 1)]
s = [Fraction(0) if h[i] == 0 else Fraction(y[i + 1] - y[i], 10 * h[i]) for i in range(n - 1)]
out = []
for i in range(n):
prev = i >= 1 and x[i - 1] < x[i]
nxt = i + 1 < n and x[i] < x[i + 1]
if prev and nxt:
s0, s1 = s[i - 1], s[i]
if sign(s0) != sign(s1) or (s0 == 0 and s1 == 0):
m = Fraction(0)
else:
h0, h1 = h[i - 1], h[i]
m = 3 * (h0 + h1) * s0 * s1 / ((2 * h0 + h1) * s0 + (h0 + 2 * h1) * s1)
elif nxt:
if i + 2 < n and x[i + 1] < x[i + 2]:
m = ((2 * h[i] + h[i + 1]) * s[i] - h[i] * s[i + 1]) / (h[i] + h[i + 1])
if sign(m) != sign(s[i]):
m = Fraction(0)
if sign(m) != sign(s[i + 1]) and abs(m) > 3 * abs(s[i]):
m = 3 * s[i]
else:
m = s[i]
elif prev:
if i >= 2 and x[i - 2] < x[i - 1]:
m = ((2 * h[i - 1] + h[i - 2]) * s[i - 1] - h[i - 1] * s[i - 2]) / (h[i - 1] + h[i - 2])
if sign(m) != sign(s[i - 1]):
m = Fraction(0)
if sign(m) != sign(s[i - 2]) and abs(m) > 3 * abs(s[i - 1]):
m = 3 * s[i - 1]
else:
m = s[i - 1]
else:
m = Fraction(0)
out.append(m)
return out
def triangle_ok(c):
p = [(c[2 * i], c[2 * i + 1]) for i in range(4)]
def cross(a, b, q):
return (b[0] - a[0]) * (q[1] - a[1]) - (b[1] - a[1]) * (q[0] - a[0])
r, g, b, w = p
area = cross(r, g, b)
if area == 0:
return False
sgn = sign(area)
return sign(cross(r, g, w)) == sgn and sign(cross(g, b, w)) == sgn and sign(cross(b, r, w)) == sgn
MIX_TYPES = {0: [0, 0, 0, 150000, 0, 0], 1: [0, 0, 0, 0, 0, 150000], 2: [25000, 25000, 25000, 75000, 0, 0]}
def read_set(b, first, count):
"""§5.2 over one Annex C metadata set."""
if not b:
raise Refuse("metadata_malformed")
if (b[0] >> 2) & 7 and on("annexc_version"):
raise Refuse("metadata_unsupported")
r = Bits(b)
r.u(3)
r.u(3)
r.zero(2)
custom, atm = r.u(1), r.u(1)
r.zero(6)
if custom:
r.u(16)
if not atm:
return {"first": first, "count": count, "tm": None}
baseline_raw = r.u(16)
if r.u(1):
r.zero(7)
if on("annexc_gain_space"):
raise Refuse("gain_space_unsupported")
return {"first": first, "count": count, "tm": (min(baseline_raw, 60000), [])}
num, mode, common_mix, common_curve = r.u(3), r.u(2), r.u(1), r.u(1)
chroma = [min(r.u(16), 50000) for _ in range(8)] if mode == 3 else None
raw = []
for a in range(min(num, 4)):
hr = r.u(16)
if a == 0 or not common_mix:
t = r.u(2)
if t != 3:
r.zero(6)
k = list(MIX_TYPES[t])
else:
flags = [r.u(1) for _ in range(6)]
k = [3 * min(r.u(16), 50000) if f else 0 for f in flags]
else:
k = list(raw[0][1])
if a == 0 or not common_curve:
ncp = r.u(5) + 1
pch = r.u(1)
r.zero(2)
xs = [r.u(16) for _ in range(ncp)]
else:
pch, xs = raw[0][2], list(raw[0][3])
ys = [r.u(16) for _ in range(len(xs))]
th = [r.u(16) for _ in range(len(xs))] if not pch else None
raw.append((hr, k, pch, xs, ys, th))
baseline = min(baseline_raw, 60000)
alts, bad = [], False
for hr, k, pch, xs, ys, th in raw:
sg = 1 if baseline_raw < hr else -1
x = [min(v, 64000) for v in xs]
y = [sg * min(v, 60000) for v in ys]
for i in range(len(x) - 1):
if x[i] > x[i + 1] or (x[i] == x[i + 1] and y[i] != y[i + 1]):
bad = True
if x[-1] == 0 or sum(k) == 0:
bad = True
alts.append({"h": min(hr, 60000), "k": k, "x": x, "y": y, "pchip": pch, "theta": th})
for i, a in enumerate(alts):
if a["h"] == baseline or (i + 1 < len(alts) and a["h"] >= alts[i + 1]["h"]):
bad = True
if mode == 3 and not triangle_ok(chroma):
bad = True
if bad and on("annexc_range"):
raise Refuse("metadata_out_of_range")
if mode != 0 and on("annexc_gain_space"):
raise Refuse("gain_space_unsupported")
for a in alts:
if a["pchip"]:
fr = pchip(a["x"], a["y"])
a["m"] = [rnd(f.numerator << 30, f.denominator) for f in fr]
a["mf"] = [float(f) for f in fr]
else:
a["m"] = [tan_q30(t) for t in a["theta"]]
a["mf"] = [math.tan((min(max(t, 1), 35999) - 18000.0) * math.pi / 36000.0) for t in a["theta"]]
a["cache"] = {}
return {"first": first, "count": count, "tm": (baseline, alts)}
def read_wd50(p):
"""§5.1, then §5.2 for each set."""
if len(p) < 8 or p[:4] != b"WD50" or p[4] != 1 or p[5] != 0:
raise Refuse("companion_malformed")
n = struct.unpack_from("<H", p, 6)[0]
if not 1 <= n <= MAX_SETS:
raise Refuse("companion_malformed")
at, end, raw = 8, 0, []
for _ in range(n):
if at + 10 > len(p):
raise Refuse("companion_malformed")
first, count, ln = struct.unpack_from("<IIH", p, at)
if (count == 0 or first < end or first + count > 0xFFFFFFFF or not 1 <= ln <= MAX_METADATA) and on("wd50_frame"):
raise Refuse("companion_malformed")
if at + 10 + ln > len(p):
raise Refuse("companion_malformed")
raw.append((first, count, p[at + 10:at + 10 + ln]))
end = first + count
at += 10 + ln
if at != len(p):
raise Refuse("companion_malformed")
return [read_set(b, f, c) for f, c, b in raw]
def curve(a, m):
"""§5.5: the gain curve at M (Q30), in Q16."""
x, y, ms = a["x"], a["y"], a["m"]
n = len(x)
xm = 1000 * m
if xm < x[0] << 30:
return rnd(y[0] << 16, 10000)
for i in range(n):
if xm == x[i] << 30:
return rnd(y[i] << 16, 10000)
for i in range(n - 1):
if x[i] << 30 < xm < x[i + 1] << 30:
h = x[i + 1] - x[i]
t = rnd(xm - (x[i] << 30), h)
y0, y1 = rnd(y[i] << 30, 10000), rnd(y[i + 1] << 30, 10000)
m0, m1 = rnd(h * ms[i], 1000), rnd(h * ms[i + 1], 1000)
c3 = 2 * y0 + m0 - 2 * y1 + m1
c2 = -3 * y0 + 3 * y1 - 2 * m0 - m1
u = rnd(c3 * t, 1 << 30) + c2
u = rnd(u * t, 1 << 30) + m0
u = rnd(u * t, 1 << 30) + y0
return rnd(u, 1 << 14)
return rnd(y[n - 1] << 16, 10000) + L2(x[n - 1] << 30) - L2(xm)
def gain(a, c):
"""Formulas (7), (9), (10)."""
hit = a["cache"].get(c)
if hit is None:
k = a["k"]
ksum = sum(k)
common = k[0] * c[0] + k[1] * c[1] + k[2] * c[2] + k[3] * max(c) + k[4] * min(c)
hit = tuple(curve(a, rnd(cj * k[5] + common, ksum)) for cj in c)
a["cache"][c] = hit
return hit
def plan(st, hd):
baseline, alts = st["tm"]
hs = sorted([a["h"] for a in alts] + [baseline])
entries = []
for hv in hs:
entries.append(None if hv == baseline else next(a for a in alts if a["h"] == hv))
hc = min(max(hd, hs[0]), hs[-1])
return hs, entries, hc
def render_st(st, frame, hd):
"""§5.6."""
ht = hq16(hd)
out = []
if st["tm"] is not None:
hs, entries, hc = plan(st, hd)
eq = hs.index(hc) if hc in hs else None
i = eq if eq is not None else next(i for i in range(len(hs) - 1) if hs[i] < hc < hs[i + 1])
def g_at(idx, c):
return (0, 0, 0) if entries[idx] is None else gain(entries[idx], c)
for px in range(0, len(frame), 3):
c = (S[frame[px]], S[frame[px + 1]], S[frame[px + 2]])
if st["tm"] is None:
g = (0, 0, 0)
elif eq is not None:
g = g_at(i, c)
else:
g0, g1 = g_at(i, c), g_at(i + 1, c)
h0, h1 = hs[i], hs[i + 1]
g = tuple(rnd(g0[k] * (h1 - hc) + g1[k] * (hc - h0), h1 - h0) for k in range(3))
for k in range(3):
out.append(code(scale(c[k] * 65535, g[k] - ht, 30)))
return out
def float_st(st, frame, hd):
"""§6's binary64 evaluation of §5.6's formulas."""
def srgb(k):
e = k / 255.0
return e / 12.92 if e <= 0.04045 else ((e + 0.055) / 1.055) ** 2.4
def fcurve(a, xv):
xs = [v / 1000.0 for v in a["x"]]
ys = [v / 10000.0 for v in a["y"]]
n = len(xs)
if xv < xs[0]:
return ys[0]
for i in range(n):
if xv == xs[i]:
return ys[i]
for i in range(n - 1):
if xs[i] < xv < xs[i + 1]:
h = xs[i + 1] - xs[i]
t = (xv - xs[i]) / h
m0, m1 = h * a["mf"][i], h * a["mf"][i + 1]
c3 = 2 * ys[i] + m0 - 2 * ys[i + 1] + m1
c2 = -3 * ys[i] + 3 * ys[i + 1] - 2 * m0 - m1
return ((c3 * t + c2) * t + m0) * t + ys[i]
return ys[n - 1] + math.log2(xs[n - 1] / xv)
def fgain(a, c):
k = [v / 150000.0 for v in a["k"]]
ksum = sum(k)
common = k[0] * c[0] + k[1] * c[1] + k[2] * c[2] + k[3] * max(c) + k[4] * min(c)
return [fcurve(a, (cj * k[5] + common) / ksum) for cj in c]
ht = hd / 10000.0
out = []
for px in range(0, len(frame), 3):
c = [srgb(frame[px]), srgb(frame[px + 1]), srgb(frame[px + 2])]
if st["tm"] is None:
g = [0.0] * 3
else:
baseline, alts = st["tm"]
lst = sorted([(a["h"] / 10000.0, a) for a in alts] + [(baseline / 10000.0, None)], key=lambda e: e[0])
hc = min(max(ht, lst[0][0]), lst[-1][0])
def fg(e):
return [0.0] * 3 if e[1] is None else fgain(e[1], c)
hit = [e for e in lst if e[0] == hc]
if hit:
g = fg(hit[0])
else:
i = next(i for i in range(len(lst) - 1) if lst[i][0] < hc < lst[i + 1][0])
w0 = (hc - lst[i + 1][0]) / (lst[i][0] - lst[i + 1][0])
g0, g1 = fg(lst[i]), fg(lst[i + 1])
g = [w0 * g0[k] + (1 - w0) * g1[k] for k in range(3)]
for k in range(3):
t = c[k] * 2.0 ** g[k]
out.append(int(min(max(round_half_away(t / 2.0 ** ht * 65535.0), 0), 65535)))
return out
# ------------------------------------------------------------------ §2 and §7 records
class Malformed(ValueError):
pass
def strict_json(text):
def pairs(kv):
d = {}
for k, v in kv:
if k in d:
raise Malformed("a member named twice")
d[k] = v
return d
def bad(_):
raise Malformed("a number outside the subset")
def an_int(s):
if s.startswith("-"):
raise Malformed("a signed integer")
return int(s)
v = json.loads(text, object_pairs_hook=pairs, parse_float=bad, parse_constant=bad, parse_int=an_int)
def depth(x, d):
if d > 32:
raise Malformed("nesting")
if isinstance(x, dict):
for y in x.values():
depth(y, d + 1)
elif isinstance(x, list):
for y in x:
depth(y, d + 1)
depth(v, 1)
return v
def subset_ok(v):
if v is None or isinstance(v, str):
return True
if isinstance(v, bool):
return False
if isinstance(v, int):
return 0 <= v <= MAX_INT
if isinstance(v, list):
return all(subset_ok(x) for x in v)
if isinstance(v, dict):
return all(subset_ok(x) for x in v.values())
return False
def jcs(v):
return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def is_hex(s, n):
return isinstance(s, str) and len(s) == 2 * n and all(c in "0123456789abcdef" for c in s)
def is_int(v, lo=0, hi=MAX_INT):
return isinstance(v, int) and not isinstance(v, bool) and lo <= v <= hi
def exact(v, members):
return isinstance(v, dict) and set(v) == set(members)
WS = set("\t\n\x0b\x0c\r \x85\xa0            "
"

   ")
def term_ok(s):
return (isinstance(s, str) and 0 < len(s.encode("utf-8")) <= 2048
and not any(c in WS or ord(c) < 32 or 127 <= ord(c) < 160 for c in s))
DATE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-](\d{2}):(\d{2}))$")
def date_ok(s):
m = DATE.match(s) if isinstance(s, str) else None
if not m:
return False
y, mo, d, hh, mi, ss = (int(m.group(i)) for i in range(1, 7))
if not 1 <= mo <= 12 or hh > 23 or mi > 59 or ss > 60:
return False
leap = y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo - 1]
if not 1 <= d <= days:
return False
if m.group(9) is not None and (int(m.group(9)) > 23 or int(m.group(10)) > 59):
return False
return True
SIG_MEMO = {}
def read_record(text):
"""§2, §7.1 and §8.2 step 1."""
try:
v = strict_json(text)
except (ValueError, RecursionError):
raise Refuse("record_malformed")
members = ["kind", "derivation", "base", "companion", "pictures", "class", "inputs", "issued_at", "signer_pubkey", "sig"]
ok = exact(v, members) and on("members") or (isinstance(v, dict) and not on("members"))
if not ok or v.get("kind") != KIND or not subset_ok(v):
raise Refuse("record_malformed")
d = v.get("derivation")
if not term_ok(d) or d not in (GAINMAP, ST2094):
raise Refuse("record_malformed")
b = v.get("base")
if not exact(b, ["object", "rendition", "width", "height"]) or not is_hex(b["object"], 32) or not is_int(b["rendition"]):
raise Refuse("record_malformed")
if not is_int(b["width"], 1, MAX_SIDE) or not is_int(b["height"], 1, MAX_SIDE):
raise Refuse("record_malformed")
c = v.get("companion")
if not exact(c, ["index", "sha256"]) or not is_int(c["index"]) or not is_hex(c["sha256"], 32):
raise Refuse("record_malformed")
pics = v.get("pictures")
if not isinstance(pics, list) or not 1 <= len(pics) <= MAX_PICTURES:
raise Refuse("record_malformed")
prev, high = None, False
for p in pics:
if not exact(p, ["frame", "headroom", "input", "output"]) or not is_int(p["frame"]) or not is_int(p["headroom"]):
raise Refuse("record_malformed")
if not is_hex(p["input"], 32) or not is_hex(p["output"], 32):
raise Refuse("record_malformed")
if d == GAINMAP and p["frame"] != 0:
raise Refuse("record_malformed")
key = (p["frame"], p["headroom"])
if prev is not None and key <= prev:
raise Refuse("record_malformed")
prev = key
high = high or p["headroom"] > MAX_HEADROOM
if v.get("class") not in CLASSES:
raise Refuse("record_malformed")
ins = v.get("inputs")
if not isinstance(ins, list) or not 1 <= len(ins) <= MAX_INPUTS:
raise Refuse("record_malformed")
seen = set()
for i in ins:
if not exact(i, ["content_hash", "origin", "class"]) or not is_hex(i["content_hash"], 32):
raise Refuse("record_malformed")
if i["origin"] is not None and not is_hex(i["origin"], 32):
raise Refuse("record_malformed")
if i["content_hash"] in seen or i["class"] not in CLASSES:
raise Refuse("record_malformed")
seen.add(i["content_hash"])
if b["object"] not in seen:
raise Refuse("record_malformed")
if not date_ok(v.get("issued_at")) or not is_hex(v.get("signer_pubkey"), 32) or not is_hex(v.get("sig"), 64):
raise Refuse("record_malformed")
body = {k: x for k, x in v.items() if k != "sig"}
key = (bytes.fromhex(v["signer_pubkey"]), SIGN_DOMAIN + jcs(body).encode("utf-8"), bytes.fromhex(v["sig"]))
if key not in SIG_MEMO:
SIG_MEMO[key] = ED.verify(*key)
why = SIG_MEMO[key]
if why is not None and on("signature"):
raise Refuse(why)
if high and on("headroom"):
raise Refuse("headroom_out_of_range")
v["_link"] = blake3(LINK_DOMAIN + jcs({k: x for k, x in v.items() if k != "_link"}).encode("utf-8")).hex()
return v
# ------------------------------------------------------------------ §8 verification
RENDER_MEMO = {}
def wai1_role(b):
"""SPEC §5 / §7.1 for a WAI1: a companion-only capability is neither its capability nor its fallback."""
m, _, _ = SV.read_wai1(b)
mr = m["model_requirement"]
if SV._companion_only(mr["capability"]):
return "companion-only-as-capability"
if mr.get("fallback") is not None and SV._companion_only(mr["fallback"]):
return "companion-only-as-fallback"
return None
def envelope_role(b):
if b[:4] == b"WAI2":
_, entries, _ = SV.read_wai2(b)
return SV.component_roles(entries)
return wai1_role(b)
def selectable(e):
return e.get("role") not in ("companion", "refinement") and SV._reg_selectable(e)
def bound(entries, base_i, comp_i):
"""SPEC §7.1: the companion at comp_i binds to the entry at base_i."""
e, base = entries[comp_i], entries[base_i]
if e.get("companion_of_id") is not None:
return base.get("id") == e["companion_of_id"]
return e.get("companion_of") == base["capability"]
def envelope_step(r, held):
"""§8.2 step 3. None where the envelope is not held, else (companion, still frame or None)."""
env = held["envelopes"].get(r["base"]["object"])
if env is None:
return None
want = CAP_GAINMAP if r["derivation"] == GAINMAP else CAP_ST2094
if env[:4] == b"WAI2":
try:
_, entries, payloads = SV.read_wai2(env)
except Exception:
raise Refuse("envelope_nonconforming")
if SV.component_roles(entries) is not None and on("roles"):
raise Refuse("envelope_nonconforming")
bi, ci = r["base"]["rendition"], r["companion"]["index"]
if on("binding"):
if bi >= len(entries) or not selectable(entries[bi]):
raise Refuse("companion_mismatch")
if ci >= len(entries) or entries[ci].get("role") != "companion" or entries[ci]["capability"] != want:
raise Refuse("companion_mismatch")
if not bound(entries, bi, ci):
raise Refuse("companion_mismatch")
if ci >= len(payloads):
raise Refuse("companion_mismatch")
payload = payloads[ci]
if sha(payload).hex() != r["companion"]["sha256"] and on("companion_digest"):
raise Refuse("companion_mismatch")
comp = read_gainmap(payload) if want == CAP_GAINMAP else read_wd50(payload)
still = None
if bi < len(entries) and entries[bi]["capability"] == CAP_PNG:
try:
still = png_decode(payloads[bi], False)[3]
except (ValueError, zlib.error, struct.error, IndexError):
still = None
return comp, still, sha(payload)
try:
role = wai1_role(env)
except Exception:
raise Refuse("envelope_nonconforming")
if role is not None:
raise Refuse("envelope_nonconforming")
raise Refuse("companion_mismatch")
def verify_record(i, text, held, policy):
r = read_record(text)
want = max([1] + [CLASSES.index(x["class"]) for x in r["inputs"]])
if CLASSES.index(r["class"]) != want and on("class"):
raise Refuse("class_mismatch", ["record", i])
step = envelope_step(r, held)
statuses = []
w, h = r["base"]["width"], r["base"]["height"]
for j, p in enumerate(r["pictures"]):
at = ["record", i, "picture", j]
if step is None:
statuses.append("not-recomputed")
continue
comp, still, comp_digest = step
if r["derivation"] == ST2094 and on("coverage"):
if not any(s["first"] <= p["frame"] < s["first"] + s["count"] for s in comp):
raise Refuse("frame_uncovered", at)
frame = still if (still is not None and p["frame"] == 0) else held["frames"].get((r["base"]["object"], p["frame"]))
if frame is None:
statuses.append("not-recomputed")
continue
if (len(frame) != w * h * 3 or sha(frame).hex() != p["input"]) and on("input_digest"):
raise Refuse("input_samples_mismatch", at)
key = (comp_digest, sha(frame), w, h, p["frame"], p["headroom"])
out = RENDER_MEMO.get(key)
if out is None:
try:
if r["derivation"] == GAINMAP:
out = sha(rgb16(render_gainmap(comp, frame, w, h, p["headroom"])))
else:
st = next(s for s in comp if s["first"] <= p["frame"] < s["first"] + s["count"])
out = sha(rgb16(render_st(st, frame, p["headroom"])))
except Refuse as e:
raise Refuse(e.reason, at)
if not SKIP:
RENDER_MEMO[key] = out
if out.hex() != p["output"] and on("recompute"):
raise Refuse("rendition_mismatch", at)
statuses.append("proved")
if policy == "require-proof" and on("require_proof"):
for j, s in enumerate(statuses):
if s != "proved":
raise Refuse("rendition_not_proved", ["record", i, "picture", j])
return {"record": r["_link"], "class": r["class"], "pictures": statuses}
def run_case(case, held):
out = []
for i, t in enumerate(case["records"]):
try:
out.append(verify_record(i, t, held, case["policy"]))
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at or ["record", i]}
except (IndexError, KeyError, ValueError, ZeroDivisionError, StopIteration) as e:
# Only a weakened verifier gets here (a check dropped in the self-test).
return {"outcome": "error", "error": "%s: %s" % (type(e).__name__, e)}
return {"outcome": "accept", "statements": out}
# ------------------------------------------------------------------ the corpus
def load(d):
with open(os.path.join(d, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
files = {}
for e in doc["envelopes"]:
with open(os.path.join(d, e["file"]), "rb") as f:
files[e["name"]] = f.read()
with open(os.path.join(d, doc["frames"]["file"]), "rb") as f:
frames = f.read()
return doc, files, frames
def held_for(case, doc, files, frames):
held = {"envelopes": {}, "frames": {}}
for n in case["held"]["envelopes"]:
b = files[n]
held["envelopes"][blake3(b"moq-jwp:object\x01" + b).hex()] = b
if case["held"]["frames"]:
fr = doc["frames"]
size = fr["width"] * fr["height"] * 3
for k in range(fr["count"]):
held["frames"][(fr["object"], k)] = frames[k * size:(k + 1) * size]
return held
def check_corpus(doc, files, frames, measure=False):
"""Every check of the corpus; a list of failures."""
fails = []
# The frames are the int_video decode whose digests the sidecar publishes.
fr = doc["frames"]
with open(os.path.join(WAI, fr["sidecar"]), encoding="utf-8") as f:
side = json.load(f)["int_video"]["frames_sha256"]
size = fr["width"] * fr["height"] * 3
got = [sha(frames[k * size:(k + 1) * size]).hex() for k in range(fr["count"])]
if len(frames) != size * fr["count"] or got != side:
fails.append("frames.rgb8: not the int_video frames the sidecar names")
with open(os.path.join(WAI, fr["source"]), "rb") as f:
wiv1 = f.read()
video = files.get("video", b"")
try:
_, entries, payloads = SV.read_wai2(video)
if payloads[0] != wiv1 or entries[0]["capability"] != "wai.video.int_motion":
fails.append("the video envelope does not carry the int_video clip")
except Exception:
fails.append("the video envelope does not read")
if blake3(b"moq-jwp:object\x01" + video).hex() != fr["object"]:
fails.append("frames: object is not the video envelope's content hash")
for e in doc["envelopes"]:
if blake3(b"moq-jwp:object\x01" + files[e["name"]]).hex() != e["content_hash"]:
fails.append("envelope %s: content hash" % e["name"])
for ec in doc["envelope_cases"]:
try:
role = envelope_role(files[ec["envelope"]])
except Exception as x:
role = "unreadable: %s" % x
if role != ec["role"]:
fails.append("%s: role %r where the corpus states %r" % (ec["name"], role, ec["role"]))
refused = set()
for case in doc["cases"]:
got = run_case(case, held_for(case, doc, files, frames))
if got != case["expect"]:
fails.append("case %r: %s where the corpus states %s" % (case["name"], json.dumps(got), json.dumps(case["expect"])))
if got["outcome"] == "refuse":
refused.add(got["reason"])
missing = [r for r in REASONS if r not in refused]
if missing:
fails.append("no case refused with %s" % missing)
unlisted = sorted(refused - set(REASONS))
if unlisted:
fails.append("refused with %s, which §9 does not list" % unlisted)
fails += check_arithmetic(doc, files)
if measure:
fails += check_measurement(doc, files, frames)
return fails
RANGES = {}
def check_arithmetic(doc, files):
"""§3 and §5.4 at the corpus's stated points: L2, E2, S, tan, and the video's slopes."""
a, fails = doc["arithmetic"], []
if not RANGES:
RANGES["l2"] = sha(b"".join(struct.pack("<q", L2(1 + (k * 2654435761) % (1 << 40))) for k in range(1 << 18))).hex()
RANGES["e2"] = sha(b"".join(struct.pack("<q", E2(f)[0]) for f in range(1 << 16))).hex()
RANGES["tan"] = sha(b"".join(struct.pack("<q", tan_q30(t)) for t in range(36001))).hex()
for k in ("l2", "e2", "tan"):
if RANGES[k] != a[k + "_sha256"]:
fails.append("%s over its stated range does not give the corpus's digest" % k)
for v, want in a["l2"]:
if L2(int(v)) != want:
fails.append("L2(%s) is %d, the corpus states %d" % (v, L2(int(v)), want))
for y, p, e in a["e2"]:
if E2(int(y)) != (int(p), int(e)):
fails.append("E2(%s) is %s, the corpus states %s" % (y, E2(int(y)), (p, e)))
if [str(x) for x in S] != a["srgb"]:
fails.append("the sRGB table is not the corpus's")
for t, want in a["tan_q30"]:
if tan_q30(t) != int(want):
fails.append("tan at %d is %d, the corpus states %s" % (t, tan_q30(t), want))
try:
_, _, payloads = SV.read_wai2(files["video"])
alts = [x for st in read_wd50(payloads[1]) for x in (st["tm"][1] if st["tm"] else [])]
except Exception as x:
return fails + ["the video's companion does not read: %s" % x]
got = [{"x": x["x"], "y": x["y"], "m": [str(m) for m in x["m"]]} for x in alts]
if got != a["slopes"]:
fails.append("the video's slopes are not the corpus's")
return fails
def check_measurement(doc, files, frames):
"""§6: the binary64 evaluation against the integer renditions, every distinct rendition of
an accepted record."""
seen, stats = set(), {}
for case in doc["cases"]:
if case["expect"]["outcome"] != "accept":
continue
held = held_for(case, doc, files, frames)
for t in case["records"]:
r = read_record(t)
step = envelope_step(r, held)
if step is None:
continue
comp, still, _ = step
w, h = r["base"]["width"], r["base"]["height"]
for p in r["pictures"]:
k = (r["base"]["object"], p["frame"], p["headroom"])
frame = still if (still is not None and p["frame"] == 0) else held["frames"].get((r["base"]["object"], p["frame"]))
if k in seen or frame is None:
continue
seen.add(k)
if r["derivation"] == GAINMAP:
name, a, b = "gainmap", render_gainmap(comp, frame, w, h, p["headroom"]), float_gainmap(comp, frame, w, h, p["headroom"])
else:
st = next(s for s in comp if s["first"] <= p["frame"] < s["first"] + s["count"])
name, a, b = "st2094-50", render_st(st, frame, p["headroom"]), float_st(st, frame, p["headroom"])
e = stats.setdefault(name, [0, 0, 0, 0])
e[0] += 1
for x, y in zip(a, b):
dd = abs(x - y)
e[1] += 1
e[2] = max(e[2], dd)
e[3] += dd
fails = []
for name, (n, samples, largest, total) in stats.items():
st = doc["measurement"].get(name)
if st is None:
fails.append("measurement: no %s" % name)
continue
if (st["renditions"], st["samples"]) != (n, samples):
fails.append("measurement %s: %d renditions, %d samples where the corpus states %d, %d" % (name, n, samples, st["renditions"], st["samples"]))
if abs(largest - st["largest"]) > 1 or abs(total / samples - float(st["mean"])) > 0.01:
fails.append("measurement %s: largest %d, mean %.4f where the corpus states %d, %s" % (name, largest, total / samples, st["largest"], st["mean"]))
# §6's table states the same figures.
six = section(TEXT, "\n## 6. ")
for name, st in doc["measurement"].items():
row = "| `wai-tonemap:%s` | %d | %d | %d | %s |" % (name, st["renditions"], st["samples"], st["largest"], st["mean"])
if row not in six:
fails.append("§6 does not state the corpus's figures for %s" % name)
return fails
# ------------------------------------------------------------------ self-tests
CHECKS = ["members", "signature", "headroom", "class", "roles", "binding", "companion_digest", "wgm1_frame",
"iso_version", "iso_length", "iso_range", "gain_space", "channels", "wd50_frame", "annexc_version",
"annexc_reserved", "annexc_range", "annexc_gain_space", "coverage", "input_digest", "map_dims",
"recompute", "require_proof"]
def mutations(doc, files, frames):
"""Copies of the corpus, each changed in one place; every one must fail."""
out = []
def case_named(d, name):
return next(c for c in d["cases"] if c["name"] == name)
# A rendition digest changed in an accepted record (and re-signed is impossible here: the
# signature then fails; the stated statements no longer match either way).
d = copy.deepcopy(doc)
c = case_named(d, "video: two metadata sets, proved at five headrooms a frame")
c["expect"]["statements"][0]["pictures"][3] = "not-recomputed"
out.append(("a stated status changed", d, files, frames))
# One byte of a frame.
fr = bytearray(frames)
fr[1000] ^= 1
out.append(("a frame's byte flipped", doc, files, bytes(fr)))
# One byte of the gain map picture inside an envelope: its content hash changes, so the
# accepted still is no longer held and require-proof refuses it.
f2 = dict(files)
b = bytearray(f2["still-luma-quarter"])
b[-30] ^= 1
f2["still-luma-quarter"] = bytes(b)
out.append(("an envelope's byte flipped", doc, f2, frames))
# A stated reason changed.
d = copy.deepcopy(doc)
c = case_named(d, "a map whose size does not divide the base's")
c["expect"]["reason"] = "rendition_mismatch"
out.append(("a stated reason changed", d, files, frames))
# A stated place changed.
d = copy.deepcopy(doc)
c = case_named(d, "a rendition one sample of which was changed")
c["expect"]["at"] = ["record", 0, "picture", 2]
out.append(("a stated place changed", d, files, frames))
# A stated role changed.
d = copy.deepcopy(doc)
next(e for e in d["envelope_cases"] if e["envelope"] == "gainmap-on-video")["role"] = None
out.append(("a stated role changed", d, files, frames))
# The policy relaxed.
d = copy.deepcopy(doc)
case_named(d, "require-proof without the frames")["policy"] = "accept-unrecomputed"
out.append(("a policy relaxed", d, files, frames))
# A stated tan value changed by one.
d = copy.deepcopy(doc)
d["arithmetic"]["tan_q30"][-1][1] = str(int(d["arithmetic"]["tan_q30"][-1][1]) + 1)
out.append(("a stated tan changed", d, files, frames))
# A measurement figure changed.
d = copy.deepcopy(doc)
d["measurement"]["gainmap"]["largest"] += 3
out.append(("a measurement figure changed", d, files, frames))
return out
def self_test(doc, files, frames):
fails = []
for name, d, f, fr in mutations(doc, files, frames):
if not check_corpus(d, f, fr, measure=name.startswith("a measurement")):
fails.append("mutation %r: the corpus still passes" % name)
for check in CHECKS:
SKIP.clear()
SKIP.add(check)
if not check_corpus(doc, files, frames):
fails.append("check %r dropped: the corpus does not catch it" % check)
SKIP.clear()
# The table of K is its rule, and a K one unit off changes a rendition.
if K != k_by_rule():
fails.append("§3.3's table of K is not its rule")
return fails
def main(d):
fails = []
if K != k_by_rule():
fails.append("§3.3's table of K is not its rule")
if L2(3) != 103872 or E2(1 << 15)[0] != K[0] or S[255] != 1 << 30 or tan_q30(27000) - (1 << 30) not in (-1, 0, 1):
fails.append("the arithmetic does not reproduce its known points")
doc, files, frames = load(d)
fails += check_corpus(doc, files, frames, measure=True)
if not fails:
fails += self_test(doc, files, frames)
for f in fails:
print("FAIL", f)
n = len(doc["cases"])
print("%d cases, %d envelope cases, %d mutations, %d checks dropped: %d failure(s)"
% (n, len(doc["envelope_cases"]), len(mutations(doc, files, frames)), len(CHECKS), len(fails)))
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "tonemap")))