diff --git a/Cargo.lock b/Cargo.lock index cc5c4fe..48b18f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1703,6 +1703,13 @@ dependencies = [ "webpkit", ] +[[package]] +name = "webpkit-libwebp-timing" +version = "0.1.0" +dependencies = [ + "libwebp-sys", +] + [[package]] name = "webpkit-lossless-conformance" version = "0.1.0" @@ -1820,6 +1827,8 @@ dependencies = [ "anyhow", "cargo_metadata", "clap", + "image", + "png", "serde", "serde_json", "tempfile", @@ -1827,6 +1836,7 @@ dependencies = [ "webpkit", "webpkit-alloc-count", "webpkit-ci-core", + "webpkit-libwebp-timing", "webpkit-lossless-conformance", "webpkit-samples", "yaml-rust2", diff --git a/Cargo.toml b/Cargo.toml index 667fc3c..3b4319a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/webpkit-cli", "crates/webpkit-conformance", "crates/webpkit-fuzz", + "crates/webpkit-libwebp-timing", "crates/webpkit-lossless-conformance", "crates/webpkit-lossless-fuzz", "crates/webpkit-lossy-conformance", diff --git a/Justfile b/Justfile index 8887031..82d1875 100644 --- a/Justfile +++ b/Justfile @@ -103,6 +103,11 @@ fmt-check: clippy: cargo clippy --workspace --all-targets --features webpkit/image,webpkit/rayon -- -D warnings +# Fail on narrative / history / measurement-boast phrasing in library + CLI comments. +# A comment states the constraint in one line; the rationale record lives in Linear/ADR. +lint-comments: + cargo run --quiet -p xtask -- lint-comments + deny: cargo deny check advisories bans licenses sources @@ -203,6 +208,19 @@ metrics-lossy: metrics-lossy-bless: cargo +1.96 run --release --quiet -p xtask -- metrics --lossy --bless +# Compare our encoder against libwebp over the synthetic corpus and gate +# corpus/vs-libwebp.json (our l9 lossless + AUTO lossy sizes + centi-dB PSNR vs +# cwebp, both decoded by our own decoder). --release only for runtime budget: the +# rows are integer sizes + centi-dB PSNR, deterministic and profile-independent +# (no peak memory), so NO +1.96 pin is needed. NEEDS cwebp 1.6.0; a missing or +# version-mismatched reference fails closed. +vs-libwebp: + cargo run --release --quiet -p xtask -- vs-libwebp + +# (Re)author the cwebp/dwebp comparison ledger after an intended size/fidelity change. +vs-libwebp-bless: + cargo run --release --quiet -p xtask -- vs-libwebp --bless + # Measure deterministic algorithmic-work counters and gate corpus/work.json. # --release for runtime budget only; unlike `metrics`, the counts are integer # event tallies that are toolchain- AND profile-independent, so NO +1.96 pin is @@ -251,7 +269,8 @@ bench-kernels filter="": # In-process comparison vs the libwebp C reference -> self-contained HTML # dashboard (size, lossy quality, and wall-clock speed). Links libwebp via -# `libwebp-sys` and measures both codecs in the same process (no CLI/IO skew). +# `libwebp-sys`, measures both codecs in the same process (no CLI/IO skew), and +# compares lossy `VP8 ` payload bytes on both sides. # Local-only (timing is machine-dependent); the report is written under target/. # just report-vs-libwebp # -> target/vs-libwebp.html # just report-vs-libwebp out.html @@ -369,7 +388,7 @@ _hook-yamlfmt +files: yamlfmt {{ files }} _hook-typos-fix +files: - typos --write-changes {{ files }} + typos --force-exclude --write-changes {{ files }} _hook-actionlint +files: actionlint {{ files }} diff --git a/corpus/baseline.json b/corpus/baseline.json index b5b974a..61d6bc4 100644 --- a/corpus/baseline.json +++ b/corpus/baseline.json @@ -85,8 +85,8 @@ "height": 32, "decoded_hash": 14606322490691510853, "reencode_ok": true, - "reencode_len": 84, - "reencode_hash": 16276054770118723875, + "reencode_len": 80, + "reencode_hash": 5036386195150727325, "self_roundtrip": true }, { @@ -228,8 +228,8 @@ "height": 32, "decoded_hash": 16666787168084109780, "reencode_ok": true, - "reencode_len": 2880, - "reencode_hash": 15581092113230976398, + "reencode_len": 2710, + "reencode_hash": 18282578891653707315, "self_roundtrip": true }, { diff --git a/corpus/metrics-lossy.json b/corpus/metrics-lossy.json index 18ff7d5..1d7b45f 100644 --- a/corpus/metrics-lossy.json +++ b/corpus/metrics-lossy.json @@ -134,10 +134,10 @@ "method": "auto", "quality": 50, "raw_len": 196608, - "encoded_len": 2689, - "ratio_permille": 13, - "encoded_hash": 18395043139578960763, - "sse": 21857875, + "encoded_len": 2170, + "ratio_permille": 11, + "encoded_hash": 780805984462654190, + "sse": 24745826, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -148,10 +148,10 @@ "method": "auto", "quality": 75, "raw_len": 196608, - "encoded_len": 3937, - "ratio_permille": 20, - "encoded_hash": 7346441501526423700, - "sse": 19998979, + "encoded_len": 2785, + "ratio_permille": 14, + "encoded_hash": 6222769344177949444, + "sse": 21558537, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -162,10 +162,10 @@ "method": "auto", "quality": 90, "raw_len": 196608, - "encoded_len": 7865, - "ratio_permille": 40, - "encoded_hash": 6341795753763510762, - "sse": 18112455, + "encoded_len": 4546, + "ratio_permille": 23, + "encoded_hash": 8616409656144393841, + "sse": 19258017, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -218,10 +218,10 @@ "method": "l9", "quality": 50, "raw_len": 196608, - "encoded_len": 2689, - "ratio_permille": 13, - "encoded_hash": 18395043139578960763, - "sse": 21857875, + "encoded_len": 2170, + "ratio_permille": 11, + "encoded_hash": 780805984462654190, + "sse": 24745826, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -232,10 +232,10 @@ "method": "l9", "quality": 75, "raw_len": 196608, - "encoded_len": 3937, - "ratio_permille": 20, - "encoded_hash": 7346441501526423700, - "sse": 19998979, + "encoded_len": 2785, + "ratio_permille": 14, + "encoded_hash": 6222769344177949444, + "sse": 21558537, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -246,10 +246,10 @@ "method": "l9", "quality": 90, "raw_len": 196608, - "encoded_len": 7865, - "ratio_permille": 40, - "encoded_hash": 6341795753763510762, - "sse": 18112455, + "encoded_len": 4546, + "ratio_permille": 23, + "encoded_hash": 8616409656144393841, + "sse": 19258017, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -260,10 +260,10 @@ "method": "auto", "quality": 50, "raw_len": 786432, - "encoded_len": 9592, - "ratio_permille": 12, - "encoded_hash": 13446987475819365190, - "sse": 93633648, + "encoded_len": 7832, + "ratio_permille": 9, + "encoded_hash": 8567123004001117837, + "sse": 104995454, "encode_peak_bytes": 3838983, "decode_peak_bytes": 1445895 }, @@ -274,10 +274,10 @@ "method": "auto", "quality": 75, "raw_len": 786432, - "encoded_len": 14087, - "ratio_permille": 17, - "encoded_hash": 11920288249041959513, - "sse": 86066768, + "encoded_len": 9895, + "ratio_permille": 12, + "encoded_hash": 7007469542973574931, + "sse": 92236896, "encode_peak_bytes": 3838983, "decode_peak_bytes": 1445895 }, @@ -288,10 +288,10 @@ "method": "auto", "quality": 90, "raw_len": 786432, - "encoded_len": 29070, - "ratio_permille": 36, - "encoded_hash": 456515033765631150, - "sse": 78680018, + "encoded_len": 16200, + "ratio_permille": 20, + "encoded_hash": 8230433147962038997, + "sse": 83277930, "encode_peak_bytes": 3838983, "decode_peak_bytes": 1445895 }, @@ -1016,11 +1016,11 @@ "method": "auto", "quality": 50, "raw_len": 12288, - "encoded_len": 832, - "ratio_permille": 67, - "encoded_hash": 15417366243449656060, - "sse": 3567119, - "encode_peak_bytes": 74327, + "encoded_len": 699, + "ratio_permille": 56, + "encoded_hash": 6104115688345785904, + "sse": 3969623, + "encode_peak_bytes": 73431, "decode_peak_bytes": 23047 }, { @@ -1030,11 +1030,11 @@ "method": "auto", "quality": 75, "raw_len": 12288, - "encoded_len": 1336, - "ratio_permille": 108, - "encoded_hash": 6342631136435914276, - "sse": 2647515, - "encode_peak_bytes": 76887, + "encoded_len": 1049, + "ratio_permille": 85, + "encoded_hash": 7907629803167330486, + "sse": 3068725, + "encode_peak_bytes": 74327, "decode_peak_bytes": 23047 }, { @@ -1044,11 +1044,11 @@ "method": "auto", "quality": 90, "raw_len": 12288, - "encoded_len": 3334, - "ratio_permille": 271, - "encoded_hash": 14011554288828430552, - "sse": 1862031, - "encode_peak_bytes": 86359, + "encoded_len": 2254, + "ratio_permille": 183, + "encoded_hash": 12415993993468198666, + "sse": 2092867, + "encode_peak_bytes": 78935, "decode_peak_bytes": 23047 }, { @@ -1100,11 +1100,11 @@ "method": "l9", "quality": 50, "raw_len": 12288, - "encoded_len": 832, - "ratio_permille": 67, - "encoded_hash": 15417366243449656060, - "sse": 3567119, - "encode_peak_bytes": 74327, + "encoded_len": 699, + "ratio_permille": 56, + "encoded_hash": 6104115688345785904, + "sse": 3969623, + "encode_peak_bytes": 73431, "decode_peak_bytes": 23047 }, { @@ -1114,11 +1114,11 @@ "method": "l9", "quality": 75, "raw_len": 12288, - "encoded_len": 1336, - "ratio_permille": 108, - "encoded_hash": 6342631136435914276, - "sse": 2647515, - "encode_peak_bytes": 76887, + "encoded_len": 1049, + "ratio_permille": 85, + "encoded_hash": 7907629803167330486, + "sse": 3068725, + "encode_peak_bytes": 74327, "decode_peak_bytes": 23047 }, { @@ -1128,11 +1128,11 @@ "method": "l9", "quality": 90, "raw_len": 12288, - "encoded_len": 3334, - "ratio_permille": 271, - "encoded_hash": 14011554288828430552, - "sse": 1862031, - "encode_peak_bytes": 86359, + "encoded_len": 2254, + "ratio_permille": 183, + "encoded_hash": 12415993993468198666, + "sse": 2092867, + "encode_peak_bytes": 78935, "decode_peak_bytes": 23047 }, { @@ -1142,10 +1142,10 @@ "method": "auto", "quality": 50, "raw_len": 196608, - "encoded_len": 11789, - "ratio_permille": 59, - "encoded_hash": 9416887795081962230, - "sse": 56873185, + "encoded_len": 9732, + "ratio_permille": 49, + "encoded_hash": 7450145025211966608, + "sse": 64112049, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -1156,11 +1156,11 @@ "method": "auto", "quality": 75, "raw_len": 196608, - "encoded_len": 19158, - "ratio_permille": 97, - "encoded_hash": 15219110384449797214, - "sse": 42578148, - "encode_peak_bytes": 969543, + "encoded_len": 15181, + "ratio_permille": 77, + "encoded_hash": 15952262635040045909, + "sse": 48822807, + "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, { @@ -1170,11 +1170,11 @@ "method": "auto", "quality": 90, "raw_len": 196608, - "encoded_len": 50844, - "ratio_permille": 258, - "encoded_hash": 12138087888936769483, - "sse": 29227104, - "encode_peak_bytes": 1114951, + "encoded_len": 32944, + "ratio_permille": 167, + "encoded_hash": 4572887221349278660, + "sse": 33026023, + "encode_peak_bytes": 1000263, "decode_peak_bytes": 362503 }, { @@ -1226,10 +1226,10 @@ "method": "l9", "quality": 50, "raw_len": 196608, - "encoded_len": 11789, - "ratio_permille": 59, - "encoded_hash": 9416887795081962230, - "sse": 56873185, + "encoded_len": 9732, + "ratio_permille": 49, + "encoded_hash": 7450145025211966608, + "sse": 64112049, "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, @@ -1240,11 +1240,11 @@ "method": "l9", "quality": 75, "raw_len": 196608, - "encoded_len": 19158, - "ratio_permille": 97, - "encoded_hash": 15219110384449797214, - "sse": 42578148, - "encode_peak_bytes": 969543, + "encoded_len": 15181, + "ratio_permille": 77, + "encoded_hash": 15952262635040045909, + "sse": 48822807, + "encode_peak_bytes": 960775, "decode_peak_bytes": 362503 }, { @@ -1254,11 +1254,11 @@ "method": "l9", "quality": 90, "raw_len": 196608, - "encoded_len": 50844, - "ratio_permille": 258, - "encoded_hash": 12138087888936769483, - "sse": 29227104, - "encode_peak_bytes": 1114951, + "encoded_len": 32944, + "ratio_permille": 167, + "encoded_hash": 4572887221349278660, + "sse": 33026023, + "encode_peak_bytes": 1000263, "decode_peak_bytes": 362503 }, { @@ -1268,10 +1268,10 @@ "method": "auto", "quality": 50, "raw_len": 786432, - "encoded_len": 46452, - "ratio_permille": 59, - "encoded_hash": 479887631306265077, - "sse": 227016444, + "encoded_len": 38196, + "ratio_permille": 48, + "encoded_hash": 4230894108751328973, + "sse": 256248899, "encode_peak_bytes": 3838983, "decode_peak_bytes": 1445895 }, @@ -1282,10 +1282,10 @@ "method": "auto", "quality": 75, "raw_len": 786432, - "encoded_len": 76007, - "ratio_permille": 96, - "encoded_hash": 15238015160159323056, - "sse": 168495787, + "encoded_len": 59860, + "ratio_permille": 76, + "encoded_hash": 2816649319331489248, + "sse": 193808701, "encode_peak_bytes": 3838983, "decode_peak_bytes": 1445895 }, @@ -1296,11 +1296,11 @@ "method": "auto", "quality": 90, "raw_len": 786432, - "encoded_len": 202514, - "ratio_permille": 257, - "encoded_hash": 2570099992867881369, - "sse": 115839716, - "encode_peak_bytes": 4404359, + "encoded_len": 130481, + "ratio_permille": 165, + "encoded_hash": 16531065402061667687, + "sse": 131021979, + "encode_peak_bytes": 3945607, "decode_peak_bytes": 1445895 }, { diff --git a/corpus/metrics.json b/corpus/metrics.json index 2505095..c5cbaee 100644 --- a/corpus/metrics.json +++ b/corpus/metrics.json @@ -10,7 +10,7 @@ "encoded_len": 48, "ratio_permille": 2, "encoded_hash": 14606360518551429122, - "encode_peak_bytes": 4450944, + "encode_peak_bytes": 4391456, "decode_peak_bytes": 32768 }, { @@ -22,7 +22,7 @@ "encoded_len": 48, "ratio_permille": 2, "encoded_hash": 10074923605720275346, - "encode_peak_bytes": 255104, + "encode_peak_bytes": 221240, "decode_peak_bytes": 32768 }, { @@ -34,7 +34,7 @@ "encoded_len": 48, "ratio_permille": 2, "encoded_hash": 14606360518551429122, - "encode_peak_bytes": 4450944, + "encode_peak_bytes": 4391456, "decode_peak_bytes": 32768 }, { @@ -43,10 +43,10 @@ "edge": 256, "method": "auto", "raw_len": 262144, - "encoded_len": 72, + "encoded_len": 70, "ratio_permille": 0, - "encoded_hash": 7437409027669007438, - "encode_peak_bytes": 4319616, + "encoded_hash": 2381832034210974153, + "encode_peak_bytes": 6812304, "decode_peak_bytes": 524288 }, { @@ -58,7 +58,7 @@ "encoded_len": 72, "ratio_permille": 0, "encoded_hash": 11478166848529266526, - "encode_peak_bytes": 2135168, + "encode_peak_bytes": 1836928, "decode_peak_bytes": 524288 }, { @@ -67,10 +67,10 @@ "edge": 256, "method": "l9", "raw_len": 262144, - "encoded_len": 72, + "encoded_len": 70, "ratio_permille": 0, - "encoded_hash": 7437409027669007438, - "encode_peak_bytes": 4319616, + "encoded_hash": 2381832034210974153, + "encode_peak_bytes": 6812304, "decode_peak_bytes": 524288 }, { @@ -79,10 +79,10 @@ "edge": 512, "method": "auto", "raw_len": 1048576, - "encoded_len": 146, + "encoded_len": 142, "ratio_permille": 0, - "encoded_hash": 14243579108915922498, - "encode_peak_bytes": 7243840, + "encoded_hash": 4337772350110927355, + "encode_peak_bytes": 10756816, "decode_peak_bytes": 2097152 }, { @@ -103,10 +103,10 @@ "edge": 512, "method": "l9", "raw_len": 1048576, - "encoded_len": 146, + "encoded_len": 142, "ratio_permille": 0, - "encoded_hash": 14243579108915922498, - "encode_peak_bytes": 7243840, + "encoded_hash": 4337772350110927355, + "encode_peak_bytes": 10756816, "decode_peak_bytes": 2097152 }, { @@ -118,7 +118,7 @@ "encoded_len": 12416, "ratio_permille": 757, "encoded_hash": 4397543384991770754, - "encode_peak_bytes": 273584, + "encode_peak_bytes": 238696, "decode_peak_bytes": 32768 }, { @@ -130,7 +130,7 @@ "encoded_len": 12416, "ratio_permille": 757, "encoded_hash": 4397543384991770754, - "encode_peak_bytes": 257152, + "encode_peak_bytes": 221240, "decode_peak_bytes": 32768 }, { @@ -142,7 +142,7 @@ "encoded_len": 12416, "ratio_permille": 757, "encoded_hash": 4397543384991770754, - "encode_peak_bytes": 6715184, + "encode_peak_bytes": 6614720, "decode_peak_bytes": 32768 }, { @@ -154,7 +154,7 @@ "encoded_len": 196688, "ratio_permille": 750, "encoded_hash": 10614248086587253036, - "encode_peak_bytes": 4351152, + "encode_peak_bytes": 3817576, "decode_peak_bytes": 524288 }, { @@ -166,7 +166,7 @@ "encoded_len": 196688, "ratio_permille": 750, "encoded_hash": 10614248086587253036, - "encode_peak_bytes": 4072576, + "encode_peak_bytes": 3539000, "decode_peak_bytes": 524288 }, { @@ -178,7 +178,7 @@ "encoded_len": 196688, "ratio_permille": 750, "encoded_hash": 10614248086587253036, - "encode_peak_bytes": 8243744, + "encode_peak_bytes": 7438784, "decode_peak_bytes": 524288 }, { @@ -190,7 +190,7 @@ "encoded_len": 786512, "ratio_permille": 750, "encoded_hash": 9214316020504461142, - "encode_peak_bytes": 17376432, + "encode_peak_bytes": 15269992, "decode_peak_bytes": 2097152 }, { @@ -202,7 +202,7 @@ "encoded_len": 786512, "ratio_permille": 750, "encoded_hash": 9214316020504461142, - "encode_peak_bytes": 16262272, + "encode_peak_bytes": 14155832, "decode_peak_bytes": 2097152 }, { @@ -214,7 +214,7 @@ "encoded_len": 786512, "ratio_permille": 750, "encoded_hash": 9214316020504461142, - "encode_peak_bytes": 17376448, + "encode_peak_bytes": 21561464, "decode_peak_bytes": 2097152 }, { @@ -226,7 +226,7 @@ "encoded_len": 2162, "ratio_permille": 131, "encoded_hash": 9110948604003149871, - "encode_peak_bytes": 5496864, + "encode_peak_bytes": 5621704, "decode_peak_bytes": 32768 }, { @@ -238,7 +238,7 @@ "encoded_len": 2162, "ratio_permille": 131, "encoded_hash": 9110948604003149871, - "encode_peak_bytes": 233088, + "encode_peak_bytes": 196664, "decode_peak_bytes": 32768 }, { @@ -250,7 +250,7 @@ "encoded_len": 2162, "ratio_permille": 131, "encoded_hash": 9110948604003149871, - "encode_peak_bytes": 5496864, + "encode_peak_bytes": 5621704, "decode_peak_bytes": 32768 }, { @@ -262,7 +262,7 @@ "encoded_len": 32864, "ratio_permille": 125, "encoded_hash": 2068593557044878499, - "encode_peak_bytes": 11822944, + "encode_peak_bytes": 14566280, "decode_peak_bytes": 524288 }, { @@ -274,7 +274,7 @@ "encoded_len": 32864, "ratio_permille": 125, "encoded_hash": 2068593557044878499, - "encode_peak_bytes": 3802752, + "encode_peak_bytes": 3145784, "decode_peak_bytes": 524288 }, { @@ -286,7 +286,7 @@ "encoded_len": 32864, "ratio_permille": 125, "encoded_hash": 2068593557044878499, - "encode_peak_bytes": 11822944, + "encode_peak_bytes": 14566280, "decode_peak_bytes": 524288 }, { @@ -298,7 +298,7 @@ "encoded_len": 131168, "ratio_permille": 125, "encoded_hash": 7835227823585378801, - "encode_peak_bytes": 22994400, + "encode_peak_bytes": 32865416, "decode_peak_bytes": 2097152 }, { @@ -310,7 +310,7 @@ "encoded_len": 131168, "ratio_permille": 125, "encoded_hash": 7835227823585378801, - "encode_peak_bytes": 15730304, + "encode_peak_bytes": 12582968, "decode_peak_bytes": 2097152 }, { @@ -322,7 +322,7 @@ "encoded_len": 131168, "ratio_permille": 125, "encoded_hash": 7835227823585378801, - "encode_peak_bytes": 22994400, + "encode_peak_bytes": 32865416, "decode_peak_bytes": 2097152 }, { @@ -334,7 +334,7 @@ "encoded_len": 9242, "ratio_permille": 564, "encoded_hash": 857663790811136786, - "encode_peak_bytes": 280752, + "encode_peak_bytes": 238696, "decode_peak_bytes": 32768 }, { @@ -346,7 +346,7 @@ "encoded_len": 9242, "ratio_permille": 564, "encoded_hash": 857663790811136786, - "encode_peak_bytes": 263296, + "encode_peak_bytes": 221240, "decode_peak_bytes": 32768 }, { @@ -358,7 +358,7 @@ "encoded_len": 9242, "ratio_permille": 564, "encoded_hash": 857663790811136786, - "encode_peak_bytes": 5525024, + "encode_peak_bytes": 5457344, "decode_peak_bytes": 32768 }, { @@ -369,8 +369,8 @@ "raw_len": 262144, "encoded_len": 144990, "ratio_permille": 553, - "encoded_hash": 7256391066050103155, - "encode_peak_bytes": 4343472, + "encoded_hash": 8089291678391903291, + "encode_peak_bytes": 3817576, "decode_peak_bytes": 524288 }, { @@ -382,7 +382,7 @@ "encoded_len": 144990, "ratio_permille": 553, "encoded_hash": 7256391066050103155, - "encode_peak_bytes": 4064392, + "encode_peak_bytes": 3539000, "decode_peak_bytes": 524288 }, { @@ -393,8 +393,8 @@ "raw_len": 262144, "encoded_len": 144990, "ratio_permille": 553, - "encoded_hash": 7256391066050103155, - "encode_peak_bytes": 7170848, + "encoded_hash": 8089291678391903291, + "encode_peak_bytes": 6374080, "decode_peak_bytes": 524288 }, { @@ -403,10 +403,10 @@ "edge": 512, "method": "auto", "raw_len": 1048576, - "encoded_len": 579118, + "encoded_len": 578974, "ratio_permille": 552, - "encoded_hash": 9958802527480970027, - "encode_peak_bytes": 17368256, + "encoded_hash": 11626991128039079400, + "encode_peak_bytes": 15269992, "decode_peak_bytes": 2097152 }, { @@ -415,10 +415,10 @@ "edge": 512, "method": "l0", "raw_len": 1048576, - "encoded_len": 579118, + "encoded_len": 578974, "ratio_permille": 552, - "encoded_hash": 9958802527480970027, - "encode_peak_bytes": 16254088, + "encoded_hash": 11626991128039079400, + "encode_peak_bytes": 14155832, "decode_peak_bytes": 2097152 }, { @@ -429,8 +429,8 @@ "raw_len": 1048576, "encoded_len": 578974, "ratio_permille": 552, - "encoded_hash": 13944555446728732688, - "encode_peak_bytes": 17368272, + "encoded_hash": 11626991128039079400, + "encode_peak_bytes": 21561464, "decode_peak_bytes": 2097152 }, { @@ -442,7 +442,7 @@ "encoded_len": 32, "ratio_permille": 1, "encoded_hash": 12564815948471088500, - "encode_peak_bytes": 1175472, + "encode_peak_bytes": 1173104, "decode_peak_bytes": 32768 }, { @@ -454,7 +454,7 @@ "encoded_len": 32, "ratio_permille": 1, "encoded_hash": 12564815948471088500, - "encode_peak_bytes": 135832, + "encode_peak_bytes": 101904, "decode_peak_bytes": 32768 }, { @@ -466,7 +466,7 @@ "encoded_len": 32, "ratio_permille": 1, "encoded_hash": 12564815948471088500, - "encode_peak_bytes": 1175472, + "encode_peak_bytes": 1173104, "decode_peak_bytes": 32768 }, { @@ -478,7 +478,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 15258434529653969540, - "encode_peak_bytes": 1699216, + "encode_peak_bytes": 1696112, "decode_peak_bytes": 524288 }, { @@ -490,7 +490,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 15258434529653969540, - "encode_peak_bytes": 1610920, + "encode_peak_bytes": 1384496, "decode_peak_bytes": 524288 }, { @@ -502,7 +502,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 15258434529653969540, - "encode_peak_bytes": 1699216, + "encode_peak_bytes": 1696112, "decode_peak_bytes": 524288 }, { @@ -514,7 +514,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 790988056304228601, - "encode_peak_bytes": 6464952, + "encode_peak_bytes": 5717080, "decode_peak_bytes": 2097152 }, { @@ -526,7 +526,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 790988056304228601, - "encode_peak_bytes": 6331432, + "encode_peak_bytes": 5494832, "decode_peak_bytes": 2097152 }, { @@ -538,7 +538,7 @@ "encoded_len": 32, "ratio_permille": 0, "encoded_hash": 790988056304228601, - "encode_peak_bytes": 6464952, + "encode_peak_bytes": 5717080, "decode_peak_bytes": 2097152 }, { @@ -547,10 +547,10 @@ "edge": 64, "method": "auto", "raw_len": 16384, - "encoded_len": 88, + "encoded_len": 86, "ratio_permille": 5, - "encoded_hash": 17849160317125822639, - "encode_peak_bytes": 1230112, + "encoded_hash": 4448925606828131889, + "encode_peak_bytes": 1458800, "decode_peak_bytes": 32768 }, { @@ -562,7 +562,7 @@ "encoded_len": 88, "ratio_permille": 5, "encoded_hash": 17849160317125822639, - "encode_peak_bytes": 138536, + "encode_peak_bytes": 105056, "decode_peak_bytes": 32768 }, { @@ -571,10 +571,10 @@ "edge": 64, "method": "l9", "raw_len": 16384, - "encoded_len": 88, + "encoded_len": 86, "ratio_permille": 5, - "encoded_hash": 17849160317125822639, - "encode_peak_bytes": 1230112, + "encoded_hash": 4448925606828131889, + "encode_peak_bytes": 1458800, "decode_peak_bytes": 32768 }, { @@ -583,10 +583,10 @@ "edge": 256, "method": "auto", "raw_len": 262144, - "encoded_len": 134, + "encoded_len": 132, "ratio_permille": 0, - "encoded_hash": 17315346349709203316, - "encode_peak_bytes": 2159120, + "encoded_hash": 234905882766249058, + "encode_peak_bytes": 4014944, "decode_peak_bytes": 524288 }, { @@ -598,7 +598,7 @@ "encoded_len": 134, "ratio_permille": 0, "encoded_hash": 17315346349709203316, - "encode_peak_bytes": 1615528, + "encode_peak_bytes": 1424488, "decode_peak_bytes": 524288 }, { @@ -607,10 +607,10 @@ "edge": 256, "method": "l9", "raw_len": 262144, - "encoded_len": 134, + "encoded_len": 132, "ratio_permille": 0, - "encoded_hash": 17315346349709203316, - "encode_peak_bytes": 2159120, + "encoded_hash": 234905882766249058, + "encode_peak_bytes": 4014944, "decode_peak_bytes": 524288 }, { @@ -619,10 +619,10 @@ "edge": 512, "method": "auto", "raw_len": 1048576, - "encoded_len": 210, + "encoded_len": 208, "ratio_permille": 0, - "encoded_hash": 8209894277142791847, - "encode_peak_bytes": 6527080, + "encoded_hash": 3714207409566099409, + "encode_peak_bytes": 5998560, "decode_peak_bytes": 2097152 }, { @@ -634,7 +634,7 @@ "encoded_len": 290, "ratio_permille": 0, "encoded_hash": 14517539101935122167, - "encode_peak_bytes": 6392104, + "encode_peak_bytes": 5651944, "decode_peak_bytes": 2097152 }, { @@ -643,10 +643,10 @@ "edge": 512, "method": "l9", "raw_len": 1048576, - "encoded_len": 210, + "encoded_len": 208, "ratio_permille": 0, - "encoded_hash": 8209894277142791847, - "encode_peak_bytes": 6527080, + "encoded_hash": 3714207409566099409, + "encode_peak_bytes": 5998560, "decode_peak_bytes": 2097152 } ] diff --git a/corpus/vs-libwebp.json b/corpus/vs-libwebp.json new file mode 100644 index 0000000..33d7632 --- /dev/null +++ b/corpus/vs-libwebp.json @@ -0,0 +1,798 @@ +{ + "version": 1, + "libwebp_version": "1.6.0", + "cases": [ + { + "id": "gradient_064/lossless", + "content": "gradient", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 48, + "cwebp_bytes": 60, + "size_permille": 800, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "gradient_064/lossy/q050", + "content": "gradient", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 130, + "cwebp_bytes": 204, + "size_permille": 637, + "our_psnr_centidb": 3526, + "cwebp_psnr_centidb": 3921 + }, + { + "id": "gradient_064/lossy/q075", + "content": "gradient", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 173, + "cwebp_bytes": 220, + "size_permille": 786, + "our_psnr_centidb": 3949, + "cwebp_psnr_centidb": 4016 + }, + { + "id": "gradient_064/lossy/q090", + "content": "gradient", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 261, + "cwebp_bytes": 310, + "size_permille": 841, + "our_psnr_centidb": 4280, + "cwebp_psnr_centidb": 4397 + }, + { + "id": "gradient_256/lossless", + "content": "gradient", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 70, + "cwebp_bytes": 92, + "size_permille": 760, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "gradient_256/lossy/q050", + "content": "gradient", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 2170, + "cwebp_bytes": 2574, + "size_permille": 843, + "our_psnr_centidb": 2713, + "cwebp_psnr_centidb": 2537 + }, + { + "id": "gradient_256/lossy/q075", + "content": "gradient", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 2785, + "cwebp_bytes": 2886, + "size_permille": 965, + "our_psnr_centidb": 2773, + "cwebp_psnr_centidb": 2545 + }, + { + "id": "gradient_256/lossy/q090", + "content": "gradient", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 4546, + "cwebp_bytes": 4126, + "size_permille": 1101, + "our_psnr_centidb": 2822, + "cwebp_psnr_centidb": 2837 + }, + { + "id": "gradient_512/lossy/q050", + "content": "gradient", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 7832, + "cwebp_bytes": 9074, + "size_permille": 863, + "our_psnr_centidb": 2688, + "cwebp_psnr_centidb": 2487 + }, + { + "id": "gradient_512/lossy/q075", + "content": "gradient", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 9895, + "cwebp_bytes": 10166, + "size_permille": 973, + "our_psnr_centidb": 2744, + "cwebp_psnr_centidb": 2498 + }, + { + "id": "gradient_512/lossy/q090", + "content": "gradient", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 16200, + "cwebp_bytes": 14374, + "size_permille": 1127, + "our_psnr_centidb": 2788, + "cwebp_psnr_centidb": 2802 + }, + { + "id": "noise_064/lossless", + "content": "noise", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 12416, + "cwebp_bytes": 12404, + "size_permille": 1000, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "noise_064/lossy/q050", + "content": "noise", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 2270, + "cwebp_bytes": 2402, + "size_permille": 945, + "our_psnr_centidb": 1260, + "cwebp_psnr_centidb": 1272 + }, + { + "id": "noise_064/lossy/q075", + "content": "noise", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 2853, + "cwebp_bytes": 2824, + "size_permille": 1010, + "our_psnr_centidb": 1271, + "cwebp_psnr_centidb": 1278 + }, + { + "id": "noise_064/lossy/q090", + "content": "noise", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 4930, + "cwebp_bytes": 3840, + "size_permille": 1283, + "our_psnr_centidb": 1281, + "cwebp_psnr_centidb": 1282 + }, + { + "id": "noise_256/lossless", + "content": "noise", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 196688, + "cwebp_bytes": 196688, + "size_permille": 1000, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "noise_256/lossy/q050", + "content": "noise", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 33669, + "cwebp_bytes": 35150, + "size_permille": 957, + "our_psnr_centidb": 1249, + "cwebp_psnr_centidb": 1262 + }, + { + "id": "noise_256/lossy/q075", + "content": "noise", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 42743, + "cwebp_bytes": 40994, + "size_permille": 1042, + "our_psnr_centidb": 1261, + "cwebp_psnr_centidb": 1269 + }, + { + "id": "noise_256/lossy/q090", + "content": "noise", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 75034, + "cwebp_bytes": 55442, + "size_permille": 1353, + "our_psnr_centidb": 1271, + "cwebp_psnr_centidb": 1272 + }, + { + "id": "noise_512/lossy/q050", + "content": "noise", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 133766, + "cwebp_bytes": 138984, + "size_permille": 962, + "our_psnr_centidb": 1248, + "cwebp_psnr_centidb": 1262 + }, + { + "id": "noise_512/lossy/q075", + "content": "noise", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 169916, + "cwebp_bytes": 162014, + "size_permille": 1048, + "our_psnr_centidb": 1260, + "cwebp_psnr_centidb": 1268 + }, + { + "id": "noise_512/lossy/q090", + "content": "noise", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 299088, + "cwebp_bytes": 219656, + "size_permille": 1361, + "our_psnr_centidb": 1270, + "cwebp_psnr_centidb": 1272 + }, + { + "id": "palette_064/lossless", + "content": "palette", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 2162, + "cwebp_bytes": 2156, + "size_permille": 1002, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "palette_064/lossy/q050", + "content": "palette", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 2832, + "cwebp_bytes": 2846, + "size_permille": 995, + "our_psnr_centidb": 986, + "cwebp_psnr_centidb": 994 + }, + { + "id": "palette_064/lossy/q075", + "content": "palette", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 3547, + "cwebp_bytes": 3272, + "size_permille": 1084, + "our_psnr_centidb": 991, + "cwebp_psnr_centidb": 997 + }, + { + "id": "palette_064/lossy/q090", + "content": "palette", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 6051, + "cwebp_bytes": 4252, + "size_permille": 1423, + "our_psnr_centidb": 998, + "cwebp_psnr_centidb": 998 + }, + { + "id": "palette_256/lossless", + "content": "palette", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 32864, + "cwebp_bytes": 32868, + "size_permille": 999, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "palette_256/lossy/q050", + "content": "palette", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 42159, + "cwebp_bytes": 41302, + "size_permille": 1020, + "our_psnr_centidb": 979, + "cwebp_psnr_centidb": 987 + }, + { + "id": "palette_256/lossy/q075", + "content": "palette", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 53766, + "cwebp_bytes": 47214, + "size_permille": 1138, + "our_psnr_centidb": 985, + "cwebp_psnr_centidb": 990 + }, + { + "id": "palette_256/lossy/q090", + "content": "palette", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 93922, + "cwebp_bytes": 61732, + "size_permille": 1521, + "our_psnr_centidb": 992, + "cwebp_psnr_centidb": 992 + }, + { + "id": "palette_512/lossy/q050", + "content": "palette", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 167878, + "cwebp_bytes": 163400, + "size_permille": 1027, + "our_psnr_centidb": 981, + "cwebp_psnr_centidb": 989 + }, + { + "id": "palette_512/lossy/q075", + "content": "palette", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 214008, + "cwebp_bytes": 186928, + "size_permille": 1144, + "our_psnr_centidb": 988, + "cwebp_psnr_centidb": 993 + }, + { + "id": "palette_512/lossy/q090", + "content": "palette", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 374901, + "cwebp_bytes": 244748, + "size_permille": 1531, + "our_psnr_centidb": 994, + "cwebp_psnr_centidb": 994 + }, + { + "id": "photo_064/lossless", + "content": "photo", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 9242, + "cwebp_bytes": 9564, + "size_permille": 966, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "photo_064/lossy/q050", + "content": "photo", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 699, + "cwebp_bytes": 1026, + "size_permille": 681, + "our_psnr_centidb": 2304, + "cwebp_psnr_centidb": 2419 + }, + { + "id": "photo_064/lossy/q075", + "content": "photo", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 1049, + "cwebp_bytes": 1392, + "size_permille": 753, + "our_psnr_centidb": 2416, + "cwebp_psnr_centidb": 2515 + }, + { + "id": "photo_064/lossy/q090", + "content": "photo", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 2254, + "cwebp_bytes": 2336, + "size_permille": 964, + "our_psnr_centidb": 2582, + "cwebp_psnr_centidb": 2615 + }, + { + "id": "photo_256/lossless", + "content": "photo", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 144990, + "cwebp_bytes": 146212, + "size_permille": 991, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "photo_256/lossy/q050", + "content": "photo", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 9732, + "cwebp_bytes": 14282, + "size_permille": 681, + "our_psnr_centidb": 2300, + "cwebp_psnr_centidb": 2429 + }, + { + "id": "photo_256/lossy/q075", + "content": "photo", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 15181, + "cwebp_bytes": 19726, + "size_permille": 769, + "our_psnr_centidb": 2418, + "cwebp_psnr_centidb": 2520 + }, + { + "id": "photo_256/lossy/q090", + "content": "photo", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 32944, + "cwebp_bytes": 33430, + "size_permille": 985, + "our_psnr_centidb": 2588, + "cwebp_psnr_centidb": 2627 + }, + { + "id": "photo_512/lossy/q050", + "content": "photo", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 38196, + "cwebp_bytes": 55444, + "size_permille": 688, + "our_psnr_centidb": 2300, + "cwebp_psnr_centidb": 2432 + }, + { + "id": "photo_512/lossy/q075", + "content": "photo", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 59860, + "cwebp_bytes": 78122, + "size_permille": 766, + "our_psnr_centidb": 2421, + "cwebp_psnr_centidb": 2529 + }, + { + "id": "photo_512/lossy/q090", + "content": "photo", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 130481, + "cwebp_bytes": 132648, + "size_permille": 983, + "our_psnr_centidb": 2591, + "cwebp_psnr_centidb": 2631 + }, + { + "id": "solid_064/lossless", + "content": "solid", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 32, + "cwebp_bytes": 32, + "size_permille": 1000, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "solid_064/lossy/q050", + "content": "solid", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 37, + "cwebp_bytes": 60, + "size_permille": 616, + "our_psnr_centidb": 3464, + "cwebp_psnr_centidb": 4571 + }, + { + "id": "solid_064/lossy/q075", + "content": "solid", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 41, + "cwebp_bytes": 68, + "size_permille": 602, + "our_psnr_centidb": 4813, + "cwebp_psnr_centidb": 4867 + }, + { + "id": "solid_064/lossy/q090", + "content": "solid", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 46, + "cwebp_bytes": 60, + "size_permille": 766, + "our_psnr_centidb": 4512, + "cwebp_psnr_centidb": 5290 + }, + { + "id": "solid_256/lossless", + "content": "solid", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 32, + "cwebp_bytes": 38, + "size_permille": 842, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "solid_256/lossy/q050", + "content": "solid", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 141, + "cwebp_bytes": 202, + "size_permille": 698, + "our_psnr_centidb": 3464, + "cwebp_psnr_centidb": 5033 + }, + { + "id": "solid_256/lossy/q075", + "content": "solid", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 145, + "cwebp_bytes": 206, + "size_permille": 703, + "our_psnr_centidb": 4813, + "cwebp_psnr_centidb": 5181 + }, + { + "id": "solid_256/lossy/q090", + "content": "solid", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 149, + "cwebp_bytes": 174, + "size_permille": 856, + "our_psnr_centidb": 4512, + "cwebp_psnr_centidb": 5290 + }, + { + "id": "solid_512/lossy/q050", + "content": "solid", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 470, + "cwebp_bytes": 590, + "size_permille": 796, + "our_psnr_centidb": 3464, + "cwebp_psnr_centidb": 5155 + }, + { + "id": "solid_512/lossy/q075", + "content": "solid", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 474, + "cwebp_bytes": 590, + "size_permille": 803, + "our_psnr_centidb": 4813, + "cwebp_psnr_centidb": 5239 + }, + { + "id": "solid_512/lossy/q090", + "content": "solid", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 478, + "cwebp_bytes": 522, + "size_permille": 915, + "our_psnr_centidb": 4512, + "cwebp_psnr_centidb": 5290 + }, + { + "id": "tiled_064/lossless", + "content": "tiled", + "edge": 64, + "mode": "lossless", + "quality": 0, + "our_bytes": 86, + "cwebp_bytes": 90, + "size_permille": 955, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "tiled_064/lossy/q050", + "content": "tiled", + "edge": 64, + "mode": "lossy", + "quality": 50, + "our_bytes": 377, + "cwebp_bytes": 264, + "size_permille": 1428, + "our_psnr_centidb": 1735, + "cwebp_psnr_centidb": 1734 + }, + { + "id": "tiled_064/lossy/q075", + "content": "tiled", + "edge": 64, + "mode": "lossy", + "quality": 75, + "our_bytes": 426, + "cwebp_bytes": 278, + "size_permille": 1532, + "our_psnr_centidb": 1732, + "cwebp_psnr_centidb": 1728 + }, + { + "id": "tiled_064/lossy/q090", + "content": "tiled", + "edge": 64, + "mode": "lossy", + "quality": 90, + "our_bytes": 514, + "cwebp_bytes": 308, + "size_permille": 1668, + "our_psnr_centidb": 1734, + "cwebp_psnr_centidb": 1734 + }, + { + "id": "tiled_256/lossless", + "content": "tiled", + "edge": 256, + "mode": "lossless", + "quality": 0, + "our_bytes": 132, + "cwebp_bytes": 134, + "size_permille": 985, + "our_psnr_centidb": 9900, + "cwebp_psnr_centidb": 9900 + }, + { + "id": "tiled_256/lossy/q050", + "content": "tiled", + "edge": 256, + "mode": "lossy", + "quality": 50, + "our_bytes": 5346, + "cwebp_bytes": 2640, + "size_permille": 2025, + "our_psnr_centidb": 1683, + "cwebp_psnr_centidb": 1679 + }, + { + "id": "tiled_256/lossy/q075", + "content": "tiled", + "edge": 256, + "mode": "lossy", + "quality": 75, + "our_bytes": 5939, + "cwebp_bytes": 2846, + "size_permille": 2086, + "our_psnr_centidb": 1685, + "cwebp_psnr_centidb": 1680 + }, + { + "id": "tiled_256/lossy/q090", + "content": "tiled", + "edge": 256, + "mode": "lossy", + "quality": 90, + "our_bytes": 7297, + "cwebp_bytes": 3310, + "size_permille": 2204, + "our_psnr_centidb": 1683, + "cwebp_psnr_centidb": 1684 + }, + { + "id": "tiled_512/lossy/q050", + "content": "tiled", + "edge": 512, + "mode": "lossy", + "quality": 50, + "our_bytes": 21283, + "cwebp_bytes": 10090, + "size_permille": 2109, + "our_psnr_centidb": 1675, + "cwebp_psnr_centidb": 1671 + }, + { + "id": "tiled_512/lossy/q075", + "content": "tiled", + "edge": 512, + "mode": "lossy", + "quality": 75, + "our_bytes": 23440, + "cwebp_bytes": 10934, + "size_permille": 2143, + "our_psnr_centidb": 1678, + "cwebp_psnr_centidb": 1672 + }, + { + "id": "tiled_512/lossy/q090", + "content": "tiled", + "edge": 512, + "mode": "lossy", + "quality": 90, + "our_bytes": 28781, + "cwebp_bytes": 12758, + "size_permille": 2255, + "our_psnr_centidb": 1676, + "cwebp_psnr_centidb": 1676 + } + ] +} diff --git a/corpus/work.json b/corpus/work.json index 82a03d2..6cfaa00 100644 --- a/corpus/work.json +++ b/corpus/work.json @@ -1,6 +1,942 @@ { - "version": 2, + "version": 3, "cases": [ + { + "id": "lossless-decode/gradient_064/decode", + "codec": "lossless-decode", + "content": "gradient", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 4093, + "vp8l_backref_unit": 2, + "vp8l_bit_refill": 36, + "vp8l_cache_insert": 4096, + "vp8l_cache_lookup": 1, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 1047, + "vp8l_literal_pixel": 258, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 4096, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/gradient_256/decode", + "codec": "lossless-decode", + "content": "gradient", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 65533, + "vp8l_backref_unit": 17, + "vp8l_bit_refill": 59, + "vp8l_cache_insert": 65536, + "vp8l_cache_lookup": 1, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 16437, + "vp8l_literal_pixel": 4098, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 65536, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/gradient_512/decode", + "codec": "lossless-decode", + "content": "gradient", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 262141, + "vp8l_backref_unit": 65, + "vp8l_bit_refill": 131, + "vp8l_cache_insert": 262144, + "vp8l_cache_lookup": 1, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 65685, + "vp8l_literal_pixel": 16386, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 262144, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/noise_064/decode", + "codec": "lossless-decode", + "content": "noise", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 12405, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 633, + "vp8l_huffman_symbol": 16715, + "vp8l_literal_pixel": 4096, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/noise_256/decode", + "codec": "lossless-decode", + "content": "noise", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 196676, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 262274, + "vp8l_literal_pixel": 65536, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/noise_512/decode", + "codec": "lossless-decode", + "content": "noise", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 786500, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 1048706, + "vp8l_literal_pixel": 262144, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/palette_064/decode", + "codec": "lossless-decode", + "content": "palette", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 2150, + "vp8l_cache_insert": 4096, + "vp8l_cache_lookup": 4080, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 4197, + "vp8l_literal_pixel": 16, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/palette_256/decode", + "codec": "lossless-decode", + "content": "palette", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 32852, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 131212, + "vp8l_literal_pixel": 32784, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 65536, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/palette_512/decode", + "codec": "lossless-decode", + "content": "palette", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 131156, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 524428, + "vp8l_literal_pixel": 131088, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 262144, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/photo_064/decode", + "codec": "lossless-decode", + "content": "photo", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 9231, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 365, + "vp8l_huffman_symbol": 16653, + "vp8l_literal_pixel": 4112, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 4096, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/photo_256/decode", + "codec": "lossless-decode", + "content": "photo", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 144978, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 5799, + "vp8l_huffman_symbol": 266466, + "vp8l_literal_pixel": 66560, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 65536, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/photo_512/decode", + "codec": "lossless-decode", + "content": "photo", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 578963, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 24104, + "vp8l_huffman_symbol": 1052916, + "vp8l_literal_pixel": 263168, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 262144, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/solid_064/decode", + "codec": "lossless-decode", + "content": "solid", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 21, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 16384, + "vp8l_literal_pixel": 4096, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/solid_256/decode", + "codec": "lossless-decode", + "content": "solid", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 21, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 262144, + "vp8l_literal_pixel": 65536, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/solid_512/decode", + "codec": "lossless-decode", + "content": "solid", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 21, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 1048576, + "vp8l_literal_pixel": 262144, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/tiled_064/decode", + "codec": "lossless-decode", + "content": "tiled", + "edge": 64, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 4083, + "vp8l_backref_unit": 27, + "vp8l_bit_refill": 74, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 126, + "vp8l_literal_pixel": 13, + "vp8l_pack_pixel": 4096, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/tiled_256/decode", + "codec": "lossless-decode", + "content": "tiled", + "edge": 256, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 65525, + "vp8l_backref_unit": 38, + "vp8l_bit_refill": 121, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 141, + "vp8l_literal_pixel": 11, + "vp8l_pack_pixel": 65536, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, + { + "id": "lossless-decode/tiled_512/decode", + "codec": "lossless-decode", + "content": "tiled", + "edge": 512, + "method": "decode", + "counts": { + "bool_read": 0, + "bool_renorm": 0, + "cache_bits_build": 0, + "cluster_estimate": 0, + "cluster_scan": 0, + "coeff_token": 0, + "color_cache_alloc": 0, + "cross_color_eval": 0, + "fdct_call": 0, + "histogram_alloc": 0, + "histogram_pass": 0, + "idct_call": 0, + "kmeans_compare": 0, + "loop_filter_edge": 0, + "match_compare": 0, + "match_hop": 0, + "mb_complexity_fdct": 0, + "optimal_dp_step": 0, + "predict_chroma": 0, + "predict_luma": 0, + "predictor_mode_eval": 0, + "proba_opt_node": 0, + "quantize_call": 0, + "resolve_walk": 0, + "sse_block": 0, + "token_cost_walk": 0, + "token_partition_walk": 0, + "trellis_eval": 0, + "upsample_row": 0, + "vp8l_backref_pixel": 131025, + "vp8l_backref_unit": 42, + "vp8l_bit_refill": 196, + "vp8l_cache_insert": 131072, + "vp8l_cache_lookup": 36, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 227, + "vp8l_literal_pixel": 17, + "vp8l_pack_pixel": 262144, + "vp8l_palette_pixel": 262144, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 + } + }, { "id": "lossless/gradient_064/auto", "codec": "lossless", @@ -10,33 +946,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 1272362, - "cluster_scan": 429656, + "cache_bits_build": 528, + "cluster_estimate": 895039, + "cluster_scan": 346171, "coeff_token": 0, - "color_cache_alloc": 96, + "color_cache_alloc": 68, "cross_color_eval": 493056, "fdct_call": 0, - "histogram_alloc": 140, - "histogram_pass": 860, + "histogram_alloc": 154, + "histogram_pass": 682, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 1133738, - "match_hop": 1975078, + "match_compare": 2248704, + "match_hop": 4560691, "mb_complexity_fdct": 0, - "optimal_dp_step": 81920, + "optimal_dp_step": 229376, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 417516, + "resolve_walk": 335302, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -48,7 +998,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -56,7 +1006,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -69,12 +1019,26 @@ "predictor_mode_eval": 224, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 41066, + "resolve_walk": 32874, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -86,33 +1050,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 1272362, - "cluster_scan": 429656, + "cache_bits_build": 528, + "cluster_estimate": 895039, + "cluster_scan": 346171, "coeff_token": 0, - "color_cache_alloc": 96, + "color_cache_alloc": 68, "cross_color_eval": 493056, "fdct_call": 0, - "histogram_alloc": 140, - "histogram_pass": 860, + "histogram_alloc": 154, + "histogram_pass": 682, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 1133738, - "match_hop": 1975078, + "match_compare": 2248704, + "match_hop": 4560691, "mb_complexity_fdct": 0, - "optimal_dp_step": 81920, + "optimal_dp_step": 229376, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 417516, + "resolve_walk": 335302, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -125,32 +1103,46 @@ "bool_read": 0, "bool_renorm": 0, "cache_bits_build": 720, - "cluster_estimate": 42002, - "cluster_scan": 2048, + "cluster_estimate": 660265, + "cluster_scan": 30408, "coeff_token": 0, - "color_cache_alloc": 116, + "color_cache_alloc": 128, "cross_color_eval": 7884800, "fdct_call": 0, - "histogram_alloc": 172, - "histogram_pass": 892, + "histogram_alloc": 214, + "histogram_pass": 934, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 17307488, - "match_hop": 35958913, + "match_compare": 32997288, + "match_hop": 81821545, "mb_complexity_fdct": 0, - "optimal_dp_step": 1310720, + "optimal_dp_step": 3670016, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3288740, + "resolve_walk": 5055612, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -162,7 +1154,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -170,7 +1162,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -183,12 +1175,26 @@ "predictor_mode_eval": 3584, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 559436, + "resolve_walk": 542652, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -201,32 +1207,46 @@ "bool_read": 0, "bool_renorm": 0, "cache_bits_build": 720, - "cluster_estimate": 42002, - "cluster_scan": 2048, + "cluster_estimate": 660265, + "cluster_scan": 30408, "coeff_token": 0, - "color_cache_alloc": 116, + "color_cache_alloc": 128, "cross_color_eval": 7884800, "fdct_call": 0, - "histogram_alloc": 172, - "histogram_pass": 892, + "histogram_alloc": 214, + "histogram_pass": 934, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 17307488, - "match_hop": 35958913, + "match_compare": 32997288, + "match_hop": 81821545, "mb_complexity_fdct": 0, - "optimal_dp_step": 1310720, + "optimal_dp_step": 3670016, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3288740, + "resolve_walk": 5055612, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -239,32 +1259,46 @@ "bool_read": 0, "bool_renorm": 0, "cache_bits_build": 720, - "cluster_estimate": 10360, - "cluster_scan": 254, + "cluster_estimate": 656229, + "cluster_scan": 487093, "coeff_token": 0, - "color_cache_alloc": 116, + "color_cache_alloc": 128, "cross_color_eval": 31539200, "fdct_call": 0, - "histogram_alloc": 172, - "histogram_pass": 892, + "histogram_alloc": 214, + "histogram_pass": 934, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 54427630, - "match_hop": 157182863, + "match_compare": 99127560, + "match_hop": 352528258, "mb_complexity_fdct": 0, - "optimal_dp_step": 5242880, + "optimal_dp_step": 14680064, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 304640, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 11549604, + "resolve_walk": 19262828, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -276,7 +1310,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -284,7 +1318,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -297,12 +1331,26 @@ "predictor_mode_eval": 14336, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 2137164, + "resolve_walk": 2120300, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -314,20 +1362,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, "color_cache_alloc": 12, "cross_color_eval": 548352, "fdct_call": 0, - "histogram_alloc": 66, - "histogram_pass": 570, + "histogram_alloc": 56, + "histogram_pass": 392, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 57142, + "match_hop": 57302, "mb_complexity_fdct": 0, "optimal_dp_step": 57344, "predict_chroma": 0, @@ -335,12 +1383,26 @@ "predictor_mode_eval": 4704, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 192960, + "resolve_walk": 142976, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -352,20 +1414,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 6, + "color_cache_alloc": 4, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 10, - "histogram_pass": 130, + "histogram_pass": 106, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 12255, + "match_hop": 12170, "mb_complexity_fdct": 0, "optimal_dp_step": 8192, "predict_chroma": 0, @@ -373,12 +1435,26 @@ "predictor_mode_eval": 224, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 49216, + "resolve_walk": 41024, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -390,33 +1466,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 2045950, - "cluster_scan": 207156, + "cache_bits_build": 480, + "cluster_estimate": 1023745, + "cluster_scan": 113867, "coeff_token": 0, - "color_cache_alloc": 48, + "color_cache_alloc": 28, "cross_color_eval": 555008, "fdct_call": 0, - "histogram_alloc": 160, - "histogram_pass": 880, + "histogram_alloc": 146, + "histogram_pass": 626, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 81312, + "match_hop": 1269391, "mb_complexity_fdct": 0, - "optimal_dp_step": 81920, + "optimal_dp_step": 229376, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 589976, + "resolve_walk": 353776, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -428,20 +1518,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 42, + "color_cache_alloc": 28, "cross_color_eval": 8771584, "fdct_call": 0, - "histogram_alloc": 76, - "histogram_pass": 580, + "histogram_alloc": 64, + "histogram_pass": 400, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 919820, + "match_hop": 918432, "mb_complexity_fdct": 0, "optimal_dp_step": 917504, "predict_chroma": 0, @@ -449,12 +1539,26 @@ "predictor_mode_eval": 75264, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3756032, + "resolve_walk": 2817024, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -466,20 +1570,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 10, + "color_cache_alloc": 8, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 198479, + "match_hop": 198310, "mb_complexity_fdct": 0, "optimal_dp_step": 131072, "predict_chroma": 0, @@ -487,12 +1591,26 @@ "predictor_mode_eval": 3584, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 919040, + "resolve_walk": 787968, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -504,33 +1622,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 2340900, - "cluster_scan": 2340900, + "cache_bits_build": 480, + "cluster_estimate": 1170450, + "cluster_scan": 1170450, "coeff_token": 0, - "color_cache_alloc": 96, + "color_cache_alloc": 58, "cross_color_eval": 8877568, "fdct_call": 0, - "histogram_alloc": 104, - "histogram_pass": 824, + "histogram_alloc": 124, + "histogram_pass": 604, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 0, - "match_hop": 1314506, + "match_compare": 2, + "match_hop": 20666657, "mb_complexity_fdct": 0, - "optimal_dp_step": 1310720, + "optimal_dp_step": 3670016, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 7689216, + "resolve_walk": 5177088, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -542,20 +1674,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 42, + "color_cache_alloc": 28, "cross_color_eval": 35066368, "fdct_call": 0, - "histogram_alloc": 76, - "histogram_pass": 580, + "histogram_alloc": 64, + "histogram_pass": 400, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 3670062, + "match_hop": 3670554, "mb_complexity_fdct": 0, "optimal_dp_step": 3670016, "predict_chroma": 0, @@ -563,12 +1695,26 @@ "predictor_mode_eval": 301056, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 15024128, + "resolve_walk": 11268096, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -580,20 +1726,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 10, + "color_cache_alloc": 8, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 785040, + "match_hop": 785979, "mb_complexity_fdct": 0, "optimal_dp_step": 524288, "predict_chroma": 0, @@ -601,12 +1747,26 @@ "predictor_mode_eval": 14336, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3676160, + "resolve_walk": 3151872, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -618,33 +1778,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 2363628, - "cluster_scan": 1873064, + "cache_bits_build": 1212, + "cluster_estimate": 4881227, + "cluster_scan": 4093703, "coeff_token": 0, - "color_cache_alloc": 132, + "color_cache_alloc": 249, "cross_color_eval": 478720, "fdct_call": 0, - "histogram_alloc": 181, - "histogram_pass": 937, + "histogram_alloc": 321, + "histogram_pass": 1533, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 358654, - "match_hop": 916786, + "match_compare": 561725, + "match_hop": 2503234, "mb_complexity_fdct": 0, - "optimal_dp_step": 83968, + "optimal_dp_step": 235520, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 635537, + "resolve_walk": 1111066, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -682,7 +1856,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -694,33 +1882,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 2363628, - "cluster_scan": 1873064, + "cache_bits_build": 1212, + "cluster_estimate": 4881227, + "cluster_scan": 4093703, "coeff_token": 0, - "color_cache_alloc": 132, + "color_cache_alloc": 249, "cross_color_eval": 478720, "fdct_call": 0, - "histogram_alloc": 181, - "histogram_pass": 937, + "histogram_alloc": 321, + "histogram_pass": 1533, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 358654, - "match_hop": 916786, + "match_compare": 561725, + "match_hop": 2503234, "mb_complexity_fdct": 0, - "optimal_dp_step": 83968, + "optimal_dp_step": 235520, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 635537, + "resolve_walk": 1111066, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -732,33 +1934,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 2299822, - "cluster_scan": 1569681, + "cache_bits_build": 1380, + "cluster_estimate": 5616486, + "cluster_scan": 3823519, "coeff_token": 0, - "color_cache_alloc": 132, + "color_cache_alloc": 291, "cross_color_eval": 7781376, "fdct_call": 0, - "histogram_alloc": 177, - "histogram_pass": 933, + "histogram_alloc": 361, + "histogram_pass": 1741, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 17098064, - "match_hop": 97161374, + "match_compare": 26070245, + "match_hop": 163961362, "mb_complexity_fdct": 0, - "optimal_dp_step": 1343488, + "optimal_dp_step": 3768320, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 8524307, + "resolve_walk": 17936394, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -796,7 +2012,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -808,33 +2038,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 2299822, - "cluster_scan": 1569681, + "cache_bits_build": 1380, + "cluster_estimate": 5616486, + "cluster_scan": 3823519, "coeff_token": 0, - "color_cache_alloc": 132, + "color_cache_alloc": 291, "cross_color_eval": 7781376, "fdct_call": 0, - "histogram_alloc": 177, - "histogram_pass": 933, + "histogram_alloc": 361, + "histogram_pass": 1741, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 17098064, - "match_hop": 97161374, + "match_compare": 26070245, + "match_hop": 163961362, "mb_complexity_fdct": 0, - "optimal_dp_step": 1343488, + "optimal_dp_step": 3768320, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 8524307, + "resolve_walk": 17936394, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -846,33 +2090,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 2137136, - "cluster_scan": 1649287, + "cache_bits_build": 1404, + "cluster_estimate": 5286693, + "cluster_scan": 4066641, "coeff_token": 0, - "color_cache_alloc": 129, + "color_cache_alloc": 286, "cross_color_eval": 31055872, "fdct_call": 0, - "histogram_alloc": 171, - "histogram_pass": 927, + "histogram_alloc": 349, + "histogram_pass": 1753, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 78105246, - "match_hop": 474006536, + "match_compare": 119287667, + "match_hop": 787315683, "mb_complexity_fdct": 0, - "optimal_dp_step": 5373952, + "optimal_dp_step": 15073280, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 304640, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 31989736, + "resolve_walk": 68772359, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -910,7 +2168,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -922,20 +2194,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 42, + "color_cache_alloc": 28, "cross_color_eval": 491008, "fdct_call": 0, - "histogram_alloc": 76, - "histogram_pass": 580, + "histogram_alloc": 64, + "histogram_pass": 400, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 57300, + "match_hop": 57146, "mb_complexity_fdct": 0, "optimal_dp_step": 57344, "predict_chroma": 0, @@ -943,12 +2215,26 @@ "predictor_mode_eval": 4704, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 234752, + "resolve_walk": 176064, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -960,15 +2246,15 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 10, + "color_cache_alloc": 8, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 12, - "histogram_pass": 132, + "histogram_pass": 108, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -981,12 +2267,26 @@ "predictor_mode_eval": 224, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 57440, + "resolve_walk": 49248, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -998,33 +2298,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 2340598, - "cluster_scan": 1953408, + "cache_bits_build": 480, + "cluster_estimate": 1170341, + "cluster_scan": 997370, "coeff_token": 0, - "color_cache_alloc": 116, + "color_cache_alloc": 66, "cross_color_eval": 496128, "fdct_call": 0, - "histogram_alloc": 142, - "histogram_pass": 862, + "histogram_alloc": 139, + "histogram_pass": 619, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 0, - "match_hop": 81870, + "match_hop": 1269481, "mb_complexity_fdct": 0, - "optimal_dp_step": 81920, + "optimal_dp_step": 229376, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 569488, + "resolve_walk": 358808, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1036,20 +2350,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 30, + "color_cache_alloc": 20, "cross_color_eval": 7889408, "fdct_call": 0, - "histogram_alloc": 73, - "histogram_pass": 577, + "histogram_alloc": 61, + "histogram_pass": 397, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 2, - "match_hop": 919246, + "match_compare": 6, + "match_hop": 919802, "mb_complexity_fdct": 0, "optimal_dp_step": 917504, "predict_chroma": 0, @@ -1057,12 +2371,26 @@ "predictor_mode_eval": 75264, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3491584, + "resolve_walk": 2549504, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1074,15 +2402,15 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 5, + "color_cache_alloc": 4, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 11, - "histogram_pass": 131, + "histogram_pass": 107, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -1095,12 +2423,26 @@ "predictor_mode_eval": 3584, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 787712, + "resolve_walk": 656640, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1112,33 +2454,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 720, - "cluster_estimate": 2340900, - "cluster_scan": 2340872, + "cache_bits_build": 480, + "cluster_estimate": 1170450, + "cluster_scan": 1170450, "coeff_token": 0, - "color_cache_alloc": 70, + "color_cache_alloc": 41, "cross_color_eval": 7972864, "fdct_call": 0, - "histogram_alloc": 104, - "histogram_pass": 824, + "histogram_alloc": 120, + "histogram_pass": 600, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 4, - "match_hop": 1312794, + "match_compare": 144, + "match_hop": 20667529, "mb_complexity_fdct": 0, - "optimal_dp_step": 1310720, + "optimal_dp_step": 3670016, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 7433408, + "resolve_walk": 4778432, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1150,20 +2506,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 504, + "cache_bits_build": 336, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 30, + "color_cache_alloc": 18, "cross_color_eval": 31731200, "fdct_call": 0, - "histogram_alloc": 73, - "histogram_pass": 577, + "histogram_alloc": 60, + "histogram_pass": 396, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 64, - "match_hop": 3670004, + "match_compare": 48, + "match_hop": 3672650, "mb_complexity_fdct": 0, "optimal_dp_step": 3670016, "predict_chroma": 0, @@ -1171,12 +2527,26 @@ "predictor_mode_eval": 301056, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 13954048, + "resolve_walk": 9931776, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1188,20 +2558,20 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 120, + "cache_bits_build": 96, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 5, + "color_cache_alloc": 4, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 11, - "histogram_pass": 131, + "histogram_pass": 107, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, "match_compare": 11, - "match_hop": 787575, + "match_hop": 787551, "mb_complexity_fdct": 0, "optimal_dp_step": 524288, "predict_chroma": 0, @@ -1209,12 +2579,26 @@ "predictor_mode_eval": 14336, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 3150848, + "resolve_walk": 2626560, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1226,33 +2610,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, + "cache_bits_build": 504, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 40, + "color_cache_alloc": 24, "cross_color_eval": 435200, "fdct_call": 0, - "histogram_alloc": 101, - "histogram_pass": 857, + "histogram_alloc": 122, + "histogram_pass": 626, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 1261724, - "match_hop": 1958774, + "match_compare": 2483280, + "match_hop": 3909056, "mb_complexity_fdct": 0, - "optimal_dp_step": 82432, + "optimal_dp_step": 230912, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 120251, + "resolve_walk": 118746, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1264,7 +2662,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 144, + "cache_bits_build": 120, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -1272,7 +2670,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 15, - "histogram_pass": 159, + "histogram_pass": 135, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -1285,12 +2683,26 @@ "predictor_mode_eval": 224, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 25208, + "resolve_walk": 25204, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1302,33 +2714,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, + "cache_bits_build": 504, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, - "color_cache_alloc": 40, + "color_cache_alloc": 24, "cross_color_eval": 435200, "fdct_call": 0, - "histogram_alloc": 101, - "histogram_pass": 857, + "histogram_alloc": 122, + "histogram_pass": 626, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 1261724, - "match_hop": 1958774, + "match_compare": 2483280, + "match_hop": 3909056, "mb_complexity_fdct": 0, - "optimal_dp_step": 82432, + "optimal_dp_step": 230912, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 120251, + "resolve_walk": 118746, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1340,33 +2766,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 4322, - "cluster_scan": 38, + "cache_bits_build": 636, + "cluster_estimate": 3362, + "cluster_scan": 30, "coeff_token": 0, - "color_cache_alloc": 56, + "color_cache_alloc": 32, "cross_color_eval": 6963200, "fdct_call": 0, - "histogram_alloc": 173, - "histogram_pass": 929, + "histogram_alloc": 187, + "histogram_pass": 823, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 16142732, - "match_hop": 33939384, + "match_compare": 32138024, + "match_hop": 67739760, "mb_complexity_fdct": 0, - "optimal_dp_step": 1318912, + "optimal_dp_step": 3694592, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 1974471, + "resolve_walk": 1950125, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1378,7 +2818,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 144, + "cache_bits_build": 120, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -1386,7 +2826,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 15, - "histogram_pass": 159, + "histogram_pass": 135, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -1399,12 +2839,26 @@ "predictor_mode_eval": 3584, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 403059, + "resolve_walk": 403025, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1416,33 +2870,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 4322, - "cluster_scan": 38, + "cache_bits_build": 636, + "cluster_estimate": 3362, + "cluster_scan": 30, "coeff_token": 0, - "color_cache_alloc": 56, + "color_cache_alloc": 32, "cross_color_eval": 6963200, "fdct_call": 0, - "histogram_alloc": 173, - "histogram_pass": 929, + "histogram_alloc": 187, + "histogram_pass": 823, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 16142732, - "match_hop": 33939384, + "match_compare": 32138024, + "match_hop": 67739760, "mb_complexity_fdct": 0, - "optimal_dp_step": 1318912, + "optimal_dp_step": 3694592, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 1974471, + "resolve_walk": 1950125, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1454,33 +2922,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 4600, - "cluster_scan": 54, + "cache_bits_build": 636, + "cluster_estimate": 3528, + "cluster_scan": 38, "coeff_token": 0, - "color_cache_alloc": 56, + "color_cache_alloc": 32, "cross_color_eval": 27852800, "fdct_call": 0, - "histogram_alloc": 173, - "histogram_pass": 929, + "histogram_alloc": 187, + "histogram_pass": 823, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 40176012, - "match_hop": 136625604, + "match_compare": 79770408, + "match_hop": 272694600, "mb_complexity_fdct": 0, - "optimal_dp_step": 5275648, + "optimal_dp_step": 14778368, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 304640, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 7868523, + "resolve_walk": 7777649, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1492,7 +2974,7 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 144, + "cache_bits_build": 120, "cluster_estimate": 0, "cluster_scan": 0, "coeff_token": 0, @@ -1500,7 +2982,7 @@ "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 15, - "histogram_pass": 159, + "histogram_pass": 135, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, @@ -1513,12 +2995,26 @@ "predictor_mode_eval": 14336, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 1612185, + "resolve_walk": 1612055, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1530,33 +3026,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 12968, - "cluster_scan": 2652, + "cache_bits_build": 1332, + "cluster_estimate": 95896, + "cluster_scan": 60333, "coeff_token": 0, - "color_cache_alloc": 45, + "color_cache_alloc": 132, "cross_color_eval": 437760, "fdct_call": 0, - "histogram_alloc": 182, - "histogram_pass": 938, + "histogram_alloc": 354, + "histogram_pass": 1686, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 4203174, - "match_hop": 4418830, + "match_compare": 7004386, + "match_hop": 8555384, "mb_complexity_fdct": 0, - "optimal_dp_step": 83968, + "optimal_dp_step": 235520, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 190178, + "resolve_walk": 219898, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1594,7 +3104,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1606,33 +3130,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 12968, - "cluster_scan": 2652, + "cache_bits_build": 1332, + "cluster_estimate": 95896, + "cluster_scan": 60333, "coeff_token": 0, - "color_cache_alloc": 45, + "color_cache_alloc": 132, "cross_color_eval": 437760, "fdct_call": 0, - "histogram_alloc": 182, - "histogram_pass": 938, + "histogram_alloc": 354, + "histogram_pass": 1686, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 4203174, - "match_hop": 4418830, + "match_compare": 7004386, + "match_hop": 8555384, "mb_complexity_fdct": 0, - "optimal_dp_step": 83968, + "optimal_dp_step": 235520, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 4760, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 190178, + "resolve_walk": 219898, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1644,33 +3182,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 52369, - "cluster_scan": 8600, + "cache_bits_build": 1296, + "cluster_estimate": 1267160, + "cluster_scan": 1205127, "coeff_token": 0, - "color_cache_alloc": 30, + "color_cache_alloc": 135, "cross_color_eval": 7006720, "fdct_call": 0, - "histogram_alloc": 182, - "histogram_pass": 938, + "histogram_alloc": 345, + "histogram_pass": 1641, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 233019512, - "match_hop": 84022746, + "match_compare": 442555282, + "match_hop": 160151722, "mb_complexity_fdct": 0, - "optimal_dp_step": 1343488, + "optimal_dp_step": 3768320, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 2837552, + "resolve_walk": 3075920, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1708,7 +3260,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1720,33 +3286,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 52369, - "cluster_scan": 8600, + "cache_bits_build": 1296, + "cluster_estimate": 1267160, + "cluster_scan": 1205127, "coeff_token": 0, - "color_cache_alloc": 30, + "color_cache_alloc": 135, "cross_color_eval": 7006720, "fdct_call": 0, - "histogram_alloc": 182, - "histogram_pass": 938, + "histogram_alloc": 345, + "histogram_pass": 1641, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 233019512, - "match_hop": 84022746, + "match_compare": 442555282, + "match_hop": 160151722, "mb_complexity_fdct": 0, - "optimal_dp_step": 1343488, + "optimal_dp_step": 3768320, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 76160, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 2837552, + "resolve_walk": 3075920, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1758,33 +3338,47 @@ "counts": { "bool_read": 0, "bool_renorm": 0, - "cache_bits_build": 756, - "cluster_estimate": 89033, - "cluster_scan": 17914, + "cache_bits_build": 1260, + "cluster_estimate": 1305203, + "cluster_scan": 1226670, "coeff_token": 0, - "color_cache_alloc": 51, + "color_cache_alloc": 144, "cross_color_eval": 28027392, "fdct_call": 0, - "histogram_alloc": 182, - "histogram_pass": 938, + "histogram_alloc": 338, + "histogram_pass": 1598, "idct_call": 0, "kmeans_compare": 0, "loop_filter_edge": 0, - "match_compare": 2089135856, - "match_hop": 342916833, + "match_compare": 3504799334, + "match_hop": 653378416, "mb_complexity_fdct": 0, - "optimal_dp_step": 5373952, + "optimal_dp_step": 15073280, "predict_chroma": 0, "predict_luma": 0, "predictor_mode_eval": 304640, "proba_opt_node": 0, "quantize_call": 0, - "resolve_walk": 11326287, + "resolve_walk": 12184703, "sse_block": 0, "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1822,7 +3416,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1860,7 +3468,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1870,26 +3492,26 @@ "edge": 256, "method": "decode", "counts": { - "bool_read": 39018, - "bool_renorm": 31389, + "bool_read": 28082, + "bool_renorm": 22170, "cache_bits_build": 0, "cluster_estimate": 0, "cluster_scan": 0, - "coeff_token": 5877, + "coeff_token": 5376, "color_cache_alloc": 0, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 3983, + "idct_call": 2915, "kmeans_compare": 0, - "loop_filter_edge": 28743, + "loop_filter_edge": 34524, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 0, "optimal_dp_step": 0, "predict_chroma": 512, - "predict_luma": 976, + "predict_luma": 1366, "predictor_mode_eval": 0, "proba_opt_node": 0, "quantize_call": 0, @@ -1898,7 +3520,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1908,26 +3544,26 @@ "edge": 512, "method": "decode", "counts": { - "bool_read": 141974, - "bool_renorm": 112591, + "bool_read": 100851, + "bool_renorm": 79053, "cache_bits_build": 0, "cluster_estimate": 0, "cluster_scan": 0, - "coeff_token": 22882, + "coeff_token": 20980, "color_cache_alloc": 0, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 14939, + "idct_call": 11205, "kmeans_compare": 0, - "loop_filter_edge": 111644, + "loop_filter_edge": 135460, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 0, "optimal_dp_step": 0, "predict_chroma": 2048, - "predict_luma": 3544, + "predict_luma": 4699, "predictor_mode_eval": 0, "proba_opt_node": 0, "quantize_call": 0, @@ -1936,7 +3572,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -1974,7 +3624,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2012,7 +3676,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2050,7 +3728,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2088,7 +3780,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2126,7 +3832,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2164,7 +3884,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2174,8 +3908,8 @@ "edge": 64, "method": "decode", "counts": { - "bool_read": 14145, - "bool_renorm": 10583, + "bool_read": 11338, + "bool_renorm": 8286, "cache_bits_build": 0, "cluster_estimate": 0, "cluster_scan": 0, @@ -2185,9 +3919,9 @@ "fdct_call": 0, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 381, + "idct_call": 372, "kmeans_compare": 0, - "loop_filter_edge": 20, + "loop_filter_edge": 123, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 0, @@ -2202,7 +3936,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2212,26 +3960,26 @@ "edge": 256, "method": "decode", "counts": { - "bool_read": 200515, - "bool_renorm": 153158, + "bool_read": 156894, + "bool_renorm": 121345, "cache_bits_build": 0, "cluster_estimate": 0, "cluster_scan": 0, - "coeff_token": 6145, + "coeff_token": 6146, "color_cache_alloc": 0, "cross_color_eval": 0, "fdct_call": 0, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 6082, + "idct_call": 5916, "kmeans_compare": 0, - "loop_filter_edge": 593, + "loop_filter_edge": 2953, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 0, "optimal_dp_step": 0, "predict_chroma": 512, - "predict_luma": 4081, + "predict_luma": 4066, "predictor_mode_eval": 0, "proba_opt_node": 0, "quantize_call": 0, @@ -2240,7 +3988,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2250,8 +4012,8 @@ "edge": 512, "method": "decode", "counts": { - "bool_read": 795474, - "bool_renorm": 607953, + "bool_read": 619789, + "bool_renorm": 478771, "cache_bits_build": 0, "cluster_estimate": 0, "cluster_scan": 0, @@ -2261,9 +4023,9 @@ "fdct_call": 0, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 24318, + "idct_call": 23608, "kmeans_compare": 0, - "loop_filter_edge": 2234, + "loop_filter_edge": 12318, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 0, @@ -2278,7 +4040,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2316,7 +4092,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2354,7 +4144,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2392,7 +4196,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2430,7 +4248,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 64 + "upsample_row": 64, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2468,7 +4300,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 256 + "upsample_row": 256, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2506,7 +4352,21 @@ "token_cost_walk": 0, "token_partition_walk": 0, "trellis_eval": 0, - "upsample_row": 512 + "upsample_row": 512, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2543,8 +4403,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 27129, - "upsample_row": 0 + "trellis_eval": 9200, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2582,7 +4456,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2619,8 +4507,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 27129, - "upsample_row": 0 + "trellis_eval": 9200, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2641,15 +4543,15 @@ "fdct_call": 31256, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 30088, + "idct_call": 27028, "kmeans_compare": 2048, - "loop_filter_edge": 28743, + "loop_filter_edge": 34524, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 4096, "optimal_dp_step": 0, "predict_chroma": 2946, - "predict_luma": 47249, + "predict_luma": 47639, "predictor_mode_eval": 0, "proba_opt_node": 1056, "quantize_call": 28121, @@ -2657,8 +4559,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 595534, - "upsample_row": 0 + "trellis_eval": 208360, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2696,7 +4612,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2717,15 +4647,15 @@ "fdct_call": 31256, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 30088, + "idct_call": 27028, "kmeans_compare": 2048, - "loop_filter_edge": 28743, + "loop_filter_edge": 34524, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 4096, "optimal_dp_step": 0, "predict_chroma": 2946, - "predict_luma": 47249, + "predict_luma": 47639, "predictor_mode_eval": 0, "proba_opt_node": 1056, "quantize_call": 28121, @@ -2733,8 +4663,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 595534, - "upsample_row": 0 + "trellis_eval": 208360, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2755,15 +4699,15 @@ "fdct_call": 128024, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 120094, + "idct_call": 108833, "kmeans_compare": 8192, - "loop_filter_edge": 111644, + "loop_filter_edge": 135460, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 16384, "optimal_dp_step": 0, "predict_chroma": 12034, - "predict_luma": 188761, + "predict_luma": 189916, "predictor_mode_eval": 0, "proba_opt_node": 1056, "quantize_call": 115609, @@ -2771,8 +4715,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 2446292, - "upsample_row": 0 + "trellis_eval": 864060, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2810,7 +4768,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2847,8 +4819,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 109232, - "upsample_row": 0 + "trellis_eval": 102944, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2886,7 +4872,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2923,8 +4923,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 109232, - "upsample_row": 0 + "trellis_eval": 102944, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -2961,8 +4975,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 2041136, - "upsample_row": 0 + "trellis_eval": 1918638, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3000,7 +5028,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3037,8 +5079,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 2041136, - "upsample_row": 0 + "trellis_eval": 1918638, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3075,8 +5131,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 8389491, - "upsample_row": 0 + "trellis_eval": 7888015, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3114,7 +5184,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3151,8 +5235,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 115075, - "upsample_row": 0 + "trellis_eval": 108670, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3190,7 +5288,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3227,8 +5339,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 115075, - "upsample_row": 0 + "trellis_eval": 108670, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3265,8 +5391,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 2201279, - "upsample_row": 0 + "trellis_eval": 2070743, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3304,7 +5444,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3341,8 +5495,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 2201279, - "upsample_row": 0 + "trellis_eval": 2070743, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3379,8 +5547,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 9047718, - "upsample_row": 0 + "trellis_eval": 8505024, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3418,7 +5600,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3439,9 +5635,9 @@ "fdct_call": 1688, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 2115, + "idct_call": 2096, "kmeans_compare": 128, - "loop_filter_edge": 20, + "loop_filter_edge": 123, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 256, @@ -3455,8 +5651,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 62116, - "upsample_row": 0 + "trellis_eval": 41317, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3494,7 +5704,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3515,9 +5739,9 @@ "fdct_call": 1688, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 2115, + "idct_call": 2096, "kmeans_compare": 128, - "loop_filter_edge": 20, + "loop_filter_edge": 123, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 256, @@ -3531,8 +5755,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 62116, - "upsample_row": 0 + "trellis_eval": 41317, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3553,15 +5791,15 @@ "fdct_call": 31256, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 38218, + "idct_call": 37835, "kmeans_compare": 5120, - "loop_filter_edge": 593, + "loop_filter_edge": 2953, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 4096, "optimal_dp_step": 0, "predict_chroma": 2946, - "predict_luma": 50354, + "predict_luma": 50339, "predictor_mode_eval": 0, "proba_opt_node": 1056, "quantize_call": 28121, @@ -3569,8 +5807,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 1194126, - "upsample_row": 0 + "trellis_eval": 775689, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3608,7 +5860,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3629,15 +5895,15 @@ "fdct_call": 31256, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 38218, + "idct_call": 37835, "kmeans_compare": 5120, - "loop_filter_edge": 593, + "loop_filter_edge": 2953, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 4096, "optimal_dp_step": 0, "predict_chroma": 2946, - "predict_luma": 50354, + "predict_luma": 50339, "predictor_mode_eval": 0, "proba_opt_node": 1056, "quantize_call": 28121, @@ -3645,8 +5911,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 1194126, - "upsample_row": 0 + "trellis_eval": 775689, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3667,9 +5947,9 @@ "fdct_call": 128024, "histogram_alloc": 0, "histogram_pass": 0, - "idct_call": 155991, + "idct_call": 154378, "kmeans_compare": 20480, - "loop_filter_edge": 2234, + "loop_filter_edge": 12318, "match_compare": 0, "match_hop": 0, "mb_complexity_fdct": 16384, @@ -3683,8 +5963,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 4902604, - "upsample_row": 0 + "trellis_eval": 3186587, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3722,7 +6016,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3759,8 +6067,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 22938, - "upsample_row": 0 + "trellis_eval": 22789, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3798,7 +6120,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3835,8 +6171,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 22938, - "upsample_row": 0 + "trellis_eval": 22789, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3873,8 +6223,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 434586, - "upsample_row": 0 + "trellis_eval": 434437, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3912,7 +6276,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3949,8 +6327,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 434586, - "upsample_row": 0 + "trellis_eval": 434437, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -3987,8 +6379,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 1786266, - "upsample_row": 0 + "trellis_eval": 1786117, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4026,7 +6432,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4063,8 +6483,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 25859, - "upsample_row": 0 + "trellis_eval": 18321, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4102,7 +6536,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4139,8 +6587,22 @@ "sse_block": 2739, "token_cost_walk": 1753, "token_partition_walk": 3, - "trellis_eval": 25859, - "upsample_row": 0 + "trellis_eval": 18321, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4177,8 +6639,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 492253, - "upsample_row": 0 + "trellis_eval": 347958, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4216,7 +6692,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4253,8 +6743,22 @@ "sse_block": 44355, "token_cost_walk": 32473, "token_partition_walk": 3, - "trellis_eval": 492253, - "upsample_row": 0 + "trellis_eval": 347958, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4291,8 +6795,22 @@ "sse_block": 177795, "token_cost_walk": 133017, "token_partition_walk": 3, - "trellis_eval": 2024619, - "upsample_row": 0 + "trellis_eval": 1430026, + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } }, { @@ -4330,7 +6848,21 @@ "token_cost_walk": 0, "token_partition_walk": 1, "trellis_eval": 0, - "upsample_row": 0 + "upsample_row": 0, + "vp8l_backref_pixel": 0, + "vp8l_backref_unit": 0, + "vp8l_bit_refill": 0, + "vp8l_cache_insert": 0, + "vp8l_cache_lookup": 0, + "vp8l_cross_color_pixel": 0, + "vp8l_entropy_select": 0, + "vp8l_huffman_second_level": 0, + "vp8l_huffman_symbol": 0, + "vp8l_literal_pixel": 0, + "vp8l_pack_pixel": 0, + "vp8l_palette_pixel": 0, + "vp8l_predictor_pixel": 0, + "vp8l_subtract_green_pixel": 0 } } ] diff --git a/crates/webpkit-cli/README.md b/crates/webpkit-cli/README.md index 7dc888b..d438f4b 100644 --- a/crates/webpkit-cli/README.md +++ b/crates/webpkit-cli/README.md @@ -80,9 +80,10 @@ strips it. Use `-metadata none` (or `--metadata none`) to strip on encode, or **Preprocessing and size targets** (`-crop`/`-resize`/`-size` on `cwebp`, `--crop`/`--resize`/`--target-size` on `webp`) are done tool-side on the decoded -pixels, exactly where libwebp's `cwebp` does them. Output **dimensions** match -libwebp for the same arguments; **pixels do not** — crop is exact, but resize uses -our own resampler, not libwebp's rescaler. `--target-size` bisects lossy quality +pixels, exactly where libwebp's `cwebp` does them. Output **dimensions and +pixels** match libwebp for the same arguments: crop is an exact window copy and +resize is a bit-exact safe Rust port of `WebPRescaler`, including transparent +edge handling. `--target-size` bisects lossy quality (shown under `-v`), a transparent alternative to libwebp's opaque internal multi-pass; `-pass`/`--pass` additionally exposes the entropy-refinement passes directly. @@ -104,15 +105,18 @@ multi-pass; `-pass`/`--pass` additionally exposes the entropy-refinement passes | `cwebp in.png -o out.webp -lossless -m 6` | same | `-m`/`-z`/`-q` = effort in lossless mode | | `cwebp in.png -o out.webp -near_lossless 60` | same | near-lossless preprocessing (implies lossless) | | `cwebp in.png -o out.webp -metadata none` | same | strip all | -| `cwebp in.png -o out.webp -crop x y w h` | same | supported; output **dimensions** match libwebp, **pixels differ** (own crop) | -| `cwebp in.png -o out.webp -resize w h` | same | supported; dimensions match libwebp, pixels differ (own resampler, not libwebp's); `0` on one axis keeps aspect | +| `cwebp in.png -o out.webp -crop x y w h` | same | supported; exact window copy, pixel-identical | +| `cwebp in.png -o out.webp -resize w h` | same | supported; bit-exact `WebPRescaler` port, pixel-identical; `0` on one axis keeps aspect | | `cwebp in.png -o out.webp -size 200000` | same | supported; a codec-native bisection over lossy quality to hit the byte budget (`-v` shows the search) | | `cwebp in.png -o out.webp -psnr 40` | same | supported; a PSNR floor for the quality search | | `cwebp in.png -o out.webp -pass 6` | same | supported; entropy-refinement passes (`1..=10`; `1` is byte-identical to a single pass) | | `cwebp in.png -o out.webp -preset photo -sns ...` / `-f` / `-sharpness` / `-segments` / `-sharp_yuv` / `-alpha_q` / `-jpeg_like` / `-partition_limit` / `-exact` | same | supported; psychovisual/RD tuning knobs and content presets, each byte-neutral at its default | -| `cwebp in.png -o out.webp -blend_alpha` / `-hint` / `-af` / `-pre` / `-map` | *(error)* | the true residue webpkit does not model (rejected, not silently ignored) | +| `cwebp in.png -o out.webp -hint graph` | same | typed as `ImageHint`; byte-neutral for lossy, matching libwebp 1.6.0 | +| `cwebp in.png -o out.webp -af` / `-map` | same | accepted for drop-in compatibility, ignored with a warning (quality/diagnostic only; ADR-0004) | +| `cwebp in.png -o out.webp -blend_alpha` | *(error)* | background compositing remains explicitly rejected (ADR-0004) | | `dwebp in.webp -o out.png` | same | decodes VP8L or VP8; default PNG | | `dwebp in.webp -o out.ppm -ppm` | same | netpbm output (also `-pam`) | +| `dwebp in.webp -o out.png -dither 50` / `-alpha_dither` | same | libwebp-compatible chroma dithering and quantized-alpha level smoothing | | `dwebp in.webp -o out.bmp -bmp` / `-tiff` | same | via the `image` crate (`formats` feature; rejected under `--no-default-features`) | | `dwebp in.webp -o out.yuv -yuv` / `-pgm` | same | native YUV 4:2:0 planes; lossy input only (lossless is a clear error) | | `webpmux -set icc profile.icc in.webp -o out.webp` | `webp meta set` | rewrite ICC/Exif/XMP without touching pixels | diff --git a/crates/webpkit-cli/src/cli/cwebp.rs b/crates/webpkit-cli/src/cli/cwebp.rs index d08fe72..55eebc2 100644 --- a/crates/webpkit-cli/src/cli/cwebp.rs +++ b/crates/webpkit-cli/src/cli/cwebp.rs @@ -9,7 +9,7 @@ use std::{ffi::OsString, path::PathBuf, process::ExitCode}; -use webpkit::{AlphaFilterMode, AlphaMethod, Image, LossyTuning, Preset}; +use webpkit::{AlphaFilterMode, AlphaMethod, Image, ImageHint, LossyTuning, Preset}; use crate::{ codec::EncodeMode, @@ -47,6 +47,8 @@ enum Disposition { CompatNoop { /// Whether the flag consumes the next argument. takes_value: bool, + /// A one-line stderr note when the flag drops an optimization or diagnostic the user might expect to act; `None` for runtime toggles ignored silently. + warn: Option<&'static str>, }, /// Not supported; rejected with a tailored `cause` and `help` under a caret. Rejected { @@ -65,12 +67,6 @@ struct FlagSpec { disposition: Disposition, } -/// A helpful line, factored out so the two generic-tuning rejections share one text. -const TUNE_HELP: &[&str] = &[ - "the encoder is tuned through effort and quality only:", - " cwebp -m <0-6> -q <0-100> -o ", -]; - /// Shorthand for an [`Disposition::Active`] flag with a rendered help line. const fn active(usage: &'static str, blurb: &'static str) -> Disposition { Disposition::Active(Some(HelpEntry { usage, blurb })) @@ -250,17 +246,27 @@ const FLAGS: &[FlagSpec] = &[ disposition: active("-version", "print version"), }, // Accepted for libwebp compatibility, ignored here. + // Runtime/threading toggles a user does not expect to change output, so they are ignored silently (no warn). FlagSpec { name: "-low_memory", - disposition: Disposition::CompatNoop { takes_value: false }, + disposition: Disposition::CompatNoop { + takes_value: false, + warn: None, + }, }, FlagSpec { name: "-noasm", - disposition: Disposition::CompatNoop { takes_value: false }, + disposition: Disposition::CompatNoop { + takes_value: false, + warn: None, + }, }, FlagSpec { name: "-mt", - disposition: Disposition::CompatNoop { takes_value: false }, + disposition: Disposition::CompatNoop { + takes_value: false, + warn: None, + }, }, // The true residue of genuinely-unsupported flags. FlagSpec { @@ -276,16 +282,18 @@ const FLAGS: &[FlagSpec] = &[ }, FlagSpec { name: "-hint", - disposition: Disposition::Rejected { - cause: GENERIC_TUNE_CAUSE, - help: TUNE_HELP, - }, + disposition: active("-hint ", "image type hint: picture, photo, or graph"), }, + // Auto-filter is a quality-only optimization (auto-tuning the deblocking strength); + // accepted so drop-in scripts run, warned so its effect is not silently assumed. FlagSpec { name: "-af", - disposition: Disposition::Rejected { - cause: GENERIC_TUNE_CAUSE, - help: TUNE_HELP, + disposition: Disposition::CompatNoop { + takes_value: false, + warn: Some( + "`-af` (auto-filter) is not modeled; the in-loop filter strength is taken \ + from -f (or the default)", + ), }, }, FlagSpec { @@ -295,18 +303,16 @@ const FLAGS: &[FlagSpec] = &[ "preprocessing filter: 0=none, 1=segment-map smoothing (2=dithering unsupported)", ), }, + // `-map` only prints a per-block debug visualization; it has no effect on the encoded bytes, so it is accepted (consuming its integer) and warned rather than errored. FlagSpec { name: "-map", - disposition: Disposition::Rejected { - cause: GENERIC_TUNE_CAUSE, - help: TUNE_HELP, + disposition: Disposition::CompatNoop { + takes_value: true, + warn: Some("`-map` is a debug visualization that is unavailable; it is ignored"), }, }, ]; -/// The shared cause for the internal-tuning-knob rejections. -const GENERIC_TUNE_CAUSE: &str = "this is an internal encoder-tuning knob webpkit does not expose."; - /// The [`FlagSpec`] for `name`, or `None` when the flag is unknown. fn spec(name: &str) -> Option<&'static FlagSpec> { FLAGS.iter().find(|f| f.name == name) @@ -384,6 +390,8 @@ struct Config { /// `-preset `: a content preset expanded into a base [`LossyTuning`] that the /// explicit knobs above override. preset: Option, + /// `-hint `: caller-supplied image classification. + image_hint: Option, /// `-exact`: preserve the RGB under fully-transparent pixels (webpkit's default). exact: bool, /// `-jpeg_like`: bias quantization toward a JPEG-like size curve. @@ -428,6 +436,9 @@ impl Config { let mut tuning = self .preset .map_or_else(LossyTuning::default, Preset::tuning); + if let Some(hint) = self.image_hint { + tuning = tuning.with_image_hint(hint); + } if let Some(sns) = self.sns { tuning = tuning.with_sns_strength(sns); } @@ -502,7 +513,7 @@ fn run(args: &[OsString]) -> Result<(), CliError> { // Crop-then-resize preprocessing. Project from the header first so an // out-of-bounds crop fails before the image is even decoded. - let pipeline = Pipeline::new(config.crop, config.resize); + let pipeline = Pipeline::new(config.crop, config.resize).with_exact(config.exact); if !pipeline.is_empty() && let Some(dims) = format::dimensions_of(&bytes, format) { @@ -630,6 +641,9 @@ fn parse(args: &[OsString]) -> Result { // A content preset: expanded into a base `LossyTuning`, overridden by any // explicit knob above. `default` (or no preset) is byte-identical. "-preset" => config.preset = Some(parse_preset(&value(args, &mut index, "-preset")?)?), + "-hint" => { + config.image_hint = Some(parse_image_hint(&value(args, &mut index, "-hint")?)?); + }, // Luminance-guided chroma subsampling (a boolean flag), mapped onto // `LossyTuning::with_sharp_yuv`. Off by default, so omitting it is byte-identical. "-sharp_yuv" => config.sharp_yuv = true, @@ -693,10 +707,13 @@ fn parse(args: &[OsString]) -> Result { // skipped (consuming its value if any), a rejected flag draws its caret and // cause, and an unrecognized dash-flag draws a did-you-mean. other => match spec(other).map(|f| &f.disposition) { - Some(Disposition::CompatNoop { takes_value }) => { + Some(Disposition::CompatNoop { takes_value, warn }) => { if *takes_value { let _ = value_os(args, &mut index, other)?; } + if let Some(note) = warn { + crate::report::warn(note); + } }, Some(Disposition::Rejected { cause, help }) => { return Err(reject(&rendered, index, other, cause, help)); @@ -751,6 +768,18 @@ fn parse_preset(text: &str) -> Result { } } +/// Parse a `-hint` name into an [`ImageHint`] classification. +fn parse_image_hint(text: &str) -> Result { + match text { + "picture" => Ok(ImageHint::Picture), + "photo" => Ok(ImageHint::Photo), + "graph" => Ok(ImageHint::Graph), + _ => Err(CliError::Usage(format!( + "`-hint` expects picture, photo, or graph, got `{text}`" + ))), + } +} + /// Drop the alpha channel: force every pixel opaque so the encoder omits alpha /// (`argb_has_alpha` then sees no transparency), mirroring `cwebp -noalpha`. fn strip_alpha(image: &Image) -> Result { @@ -943,15 +972,13 @@ mod tests { #[test] fn rejected_residue_is_exactly_the_true_unsupported_flags() { - // The rejected set is the true residue only: the genuinely-unsupported knobs. - // Everything the audit called out (`-preset`, `-jpeg_like`, `-partition_limit`, - // `-exact`) and P6b's rate control (`-size`/`-psnr`/`-pass`) is now Active, as is - // `-pre` (bit 0 = segment-map smoothing; the dithering bit is rejected at parse). + // The rejected set is the true residue only: `-blend_alpha` changes source pixels by compositing. + // `-hint` is represented by `ImageHint`; `-af` and `-map` are compatibility no-ops, and `-pre`'s dithering bit is rejected at parse rather than by table disposition. let rejected: Vec<&str> = FLAGS .iter() .filter(|f| matches!(f.disposition, Disposition::Rejected { .. })) .map(|f| f.name) .collect(); - assert_eq!(rejected, ["-blend_alpha", "-hint", "-af", "-map"]); + assert_eq!(rejected, ["-blend_alpha"]); } } diff --git a/crates/webpkit-cli/src/cli/dwebp.rs b/crates/webpkit-cli/src/cli/dwebp.rs index 89aa9ab..2fa063f 100644 --- a/crates/webpkit-cli/src/cli/dwebp.rs +++ b/crates/webpkit-cli/src/cli/dwebp.rs @@ -7,10 +7,9 @@ use std::{ffi::OsString, path::PathBuf, process::ExitCode}; -use webpkit::{Image, PixelLayout}; +use webpkit::{DecodeOptions, Image, PixelLayout}; use crate::{ - codec, diag::{self, Diagnostic}, error::CliError, format::{self, OutputFormat}, @@ -99,9 +98,10 @@ struct Config { crop: Option, /// `-scale`/`-resize w h`, applied after `-crop` (a `0` axis keeps aspect). resize: Option, - /// A dithering flag was requested; this exact decoder has nothing to dither, so a - /// verbose note explains the no-op rather than the tool staying silent. - dither_requested: bool, + /// `-dither N`: lossy VP8 chroma-plane dithering strength. + dithering_strength: u8, + /// `-alpha_dither`: smoothing strength for level-quantized `ALPH`. + alpha_dithering_strength: u8, flip: bool, alpha: bool, verbose: u8, @@ -124,6 +124,11 @@ fn run(args: &[OsString]) -> Result<(), CliError> { let source = Source::from_arg(&input); let sink = Sink::from_arg(&output); let bytes = source.read()?; + let options = DecodeOptions::new() + .layout(PixelLayout::Rgba8) + .max_pixels(webpkit::DEFAULT_MAX_PIXELS) + .with_dithering_strength(config.dithering_strength) + .with_alpha_dithering_strength(config.alpha_dithering_strength); // The YUV planes are the lossy decoder's native reconstruction, so this path // is distinct from the RGBA one (there is no `Image` to flip or recolor here). if let Some(layout) = config.yuv { @@ -135,7 +140,7 @@ fn run(args: &[OsString]) -> Result<(), CliError> { layout.flag(), ))); } - let yuv = decode_yuv(&bytes, layout)?; + let yuv = decode_yuv(&bytes, layout, &options)?; let out = match layout { YuvOut::Planar => format::yuv::write_yuv(&yuv), YuvOut::Pgm => format::yuv::write_pgm(&yuv), @@ -151,22 +156,12 @@ fn run(args: &[OsString]) -> Result<(), CliError> { )); return Ok(()); } - let mut image = codec::decode( - &bytes, - PixelLayout::Rgba8, - Some(webpkit::DEFAULT_MAX_PIXELS), - )?; + let mut image = webpkit::decode_with(&bytes, &options)?; // Geometry first (crop, then scale), on the decoded RGBA via the core rescaler. let pipeline = Pipeline::new(config.crop, config.resize); if !pipeline.is_empty() { image = pipeline.apply(image)?; } - if config.dither_requested { - reporter.detail( - "-dither/-alpha_dither have no effect: this decoder reconstructs the exact \ - stored pixels, so there is no lossy banding to hide", - ); - } if config.alpha { image = alpha_as_gray(&image)?; } @@ -250,10 +245,7 @@ fn parse(args: &[OsString]) -> Result { let height = value_u32(args, &mut index, &token)?; config.resize = Some(Resize::new(width, height)?); }, - // Dithering hides banding in *lossy* reconstruction; this decoder rebuilds - // the exact stored pixels, so there is nothing to perturb. The flags are - // accepted (and their strength validated) rather than rejected, and are a - // documented no-op — see the `-v` note emitted at decode time. + // Chroma dithering strength is applied during VP8 plane reconstruction. "-dither" => { let strength = value_u32(args, &mut index, "-dither")?; if strength > 100 { @@ -261,10 +253,10 @@ fn parse(args: &[OsString]) -> Result { "`-dither` strength is 0-100, got `{strength}`" ))); } - config.dither_requested |= strength > 0; + config.dithering_strength = u8::try_from(strength).unwrap_or(100); }, - "-alpha_dither" => config.dither_requested = true, - "-nodither" => {}, + "-alpha_dither" => config.alpha_dithering_strength = 100, + "-nodither" => config.dithering_strength = 0, #[cfg(feature = "formats")] "-bmp" => config.format = Some(OutputFormat::Bmp), #[cfg(feature = "formats")] @@ -317,8 +309,12 @@ fn reject(args: &[String], index: usize, flag: &str) -> CliError { /// Decode a lossy WebP to its native YUV 4:2:0 planes, turning the lossless or /// animated case — which has no YUV form — into a message that names the RGBA /// formats to reach for instead. Other decode failures pass through unchanged. -fn decode_yuv(bytes: &[u8], layout: YuvOut) -> Result { - webpkit::decode_yuv(bytes).map_err(|err| match err { +fn decode_yuv( + bytes: &[u8], + layout: YuvOut, + options: &DecodeOptions, +) -> Result { + webpkit::decode_yuv_with_options(bytes, options).map_err(|err| match err { webpkit::Error::UnsupportedFeature => CliError::Rejected(Box::new( Diagnostic::new(format!( "`{}` needs a lossy WebP; this input is lossless or animated", diff --git a/crates/webpkit-cli/src/preprocess.rs b/crates/webpkit-cli/src/preprocess.rs index 1d80219..cee36e5 100644 --- a/crates/webpkit-cli/src/preprocess.rs +++ b/crates/webpkit-cli/src/preprocess.rs @@ -4,7 +4,7 @@ //! These are not codec knobs. In libwebp `-crop`/`-resize` are `cwebp` tool-side //! pixel operations run before the encoder ever sees the image, so this layer does //! the same thing in the same place — on a decoded RGBA buffer. Crop is exact byte -//! selection; resize resamples with the `image` crate's high-quality filter. +//! selection; resize runs the core's `WebPRescaler` port. //! //! Two entry points. [`Pipeline::project`] maps input dimensions to output //! dimensions with no pixels at all, so an out-of-bounds crop can be refused from a @@ -12,9 +12,10 @@ //! agree, and a test pins that they do. //! //! Both crop and resize delegate to `webpkit`'s core geometry engine — the resize is -//! the bit-exact `WebPRescaler` port, so the pixels now match libwebp's `cwebp -//! -resize` byte-for-byte (not just the dimensions). The core is zero-dependency, so -//! `--resize` works even under `--no-default-features`. +//! the bit-exact `WebPRescaler` port, so the pixels match libwebp's `cwebp -resize` +//! byte-for-byte (not just the dimensions), in both the default black-matted mode and +//! the un-matted one `-exact` selects. The core is zero-dependency, so `--resize` +//! works even under `--no-default-features`. use webpkit::{Dimensions, Image}; @@ -144,6 +145,7 @@ enum Stage { #[derive(Debug, Default)] pub(crate) struct Pipeline { stages: Vec, + exact: bool, } impl Pipeline { @@ -157,7 +159,17 @@ impl Pipeline { if let Some(resize) = resize { stages.push(Stage::Resize(resize)); } - Self { stages } + Self { + stages, + exact: false, + } + } + + /// Select the transparency-preserving resampler, mirroring `cwebp -resize -exact`. + #[must_use] + pub(crate) const fn with_exact(mut self, exact: bool) -> Self { + self.exact = exact; + self } /// Whether the pipeline is a no-op (no crop and no resize). @@ -198,7 +210,7 @@ impl Pipeline { for stage in &self.stages { image = match stage { Stage::Crop(crop) => apply_crop(&image, *crop)?, - Stage::Resize(resize) => apply_resize(&image, *resize)?, + Stage::Resize(resize) => apply_resize(&image, *resize, self.exact)?, }; } Ok(image) @@ -216,10 +228,15 @@ fn apply_crop(image: &Image, crop: Crop) -> Result { } /// Resize `image` to the projected target with the core geometry's bit-exact -/// `WebPRescaler` port — byte-identical to libwebp's `cwebp -resize`. -fn apply_resize(image: &Image, resize: Resize) -> Result { +/// `WebPRescaler` port — byte-identical to libwebp's `cwebp -resize`. `exact` selects +/// the un-matted variant `cwebp` uses under `-exact`. +fn apply_resize(image: &Image, resize: Resize, exact: bool) -> Result { let out_dims = resize.project(image.dimensions())?; - Ok(image.resize(out_dims)) + Ok(if exact { + image.resize_exact(out_dims) + } else { + image.resize(out_dims) + }) } /// Parse a base-10 `u32`, trimming surrounding whitespace. diff --git a/crates/webpkit-cli/tests/dropins.rs b/crates/webpkit-cli/tests/dropins.rs index 2c9ae75..8a0f6d2 100644 --- a/crates/webpkit-cli/tests/dropins.rs +++ b/crates/webpkit-cli/tests/dropins.rs @@ -74,30 +74,35 @@ fn cwebp_near_lossless_rejects_out_of_range() { /// column in the reconstructed command line. #[test] fn cwebp_points_a_caret_at_the_rejected_flag() { - // `-map` is a preprocessing knob that stays rejected (unlike `-sns`/`-crop`/`-pre`, - // now live). - let out = run("cwebp", &["-map", "2", "-o", "-"], vec![]); + // `-blend_alpha` composites onto a background color — a pixel-changing preprocessing step this encoder does not model, so it stays a hard error (unlike `-af`/`-map`, now accepted-as-noop). + // See ADR-0004. + let out = run("cwebp", &["-blend_alpha", "0xffffff", "-o", "-"], vec![]); assert_eq!(out.status.code(), Some(2)); let err = stderr(&out); // Find the echoed command line and the caret line beneath it, and check the - // carets sit *exactly* under `-map`. A substring assertion would tolerate a + // carets sit *exactly* under `-blend_alpha`. A substring assertion would tolerate a // caret shifted one column right (the actual column becomes a substring of a // wider run of spaces), so this compares real offsets. let lines: Vec<&str> = err.lines().collect(); let command = lines .iter() - .position(|l| l.contains("cwebp -map 2 -o -")) + .position(|l| l.contains("cwebp -blend_alpha 0xffffff -o -")) .expect("the command line is echoed"); let caret_line = lines.get(command + 1).expect("a caret line follows"); - let flag_col = lines[command].find("-map").expect("`-map` is in the echo"); + let flag_col = lines[command] + .find("-blend_alpha") + .expect("`-blend_alpha` is in the echo"); let caret_col = caret_line.find('^').expect("a caret is drawn"); - assert_eq!(caret_col, flag_col, "the caret must start under `-map`"); + assert_eq!( + caret_col, flag_col, + "the caret must start under `-blend_alpha`" + ); assert_eq!( caret_line.trim(), - "^^^^", - "exactly four carets, one per character of `-map`: {caret_line:?}" + "^^^^^^^^^^^^", + "exactly twelve carets, one per character of `-blend_alpha`: {caret_line:?}" ); } @@ -358,6 +363,24 @@ fn pam_2x2_with_alpha() -> Vec { p } +fn pam_with_quantizable_alpha(width: u32, height: u32) -> Vec { + let mut pam = format!( + "P7\nWIDTH {width}\nHEIGHT {height}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n" + ) + .into_bytes(); + for y in 0..height { + for x in 0..width { + pam.extend_from_slice(&[ + 40, + 80, + 120, + u8::try_from((x * 17 + y * 29) & 0xff).unwrap_or(0), + ]); + } + } + pam +} + /// The 16-byte RGBA body a `dwebp -pam` of a 2x2 image ends with. fn pam_alpha_bytes(pam: &[u8]) -> [u8; 4] { let body = &pam[pam.len() - 16..]; @@ -563,6 +586,42 @@ fn cwebp_rejects_a_bad_preset_name() { assert!(err.contains("preset"), "names the flag: {err:?}"); } +#[test] +fn cwebp_image_hint_matches_the_libwebp_token_set() { + let ppm = gradient_ppm(16, 16); + let baseline = run("cwebp", &["-", "-o", "-", "-q", "80"], ppm.clone()); + assert!(baseline.status.success(), "baseline failed: {baseline:?}"); + for hint in ["picture", "photo", "graph"] { + let out = run( + "cwebp", + &["-", "-o", "-", "-q", "80", "-hint", hint], + ppm.clone(), + ); + assert!(out.status.success(), "-hint {hint} failed: {out:?}"); + assert_eq!( + out.stdout, baseline.stdout, + "-hint {hint} is typed metadata and byte-neutral" + ); + } + + let lossless = run("cwebp", &["-lossless", "-", "-o", "-"], ppm.clone()); + let lossless_graph = run( + "cwebp", + &["-lossless", "-hint", "graph", "-", "-o", "-"], + ppm.clone(), + ); + assert!(lossless.status.success() && lossless_graph.status.success()); + assert_eq!(lossless_graph.stdout, lossless.stdout); + + let default = run("cwebp", &["-hint", "default", "-", "-o", "-"], ppm.clone()); + assert_eq!(default.status.code(), Some(2)); + assert!(stderr(&default).contains("hint"), "{:?}", stderr(&default)); + + let bad = run("cwebp", &["-hint", "diagram", "-", "-o", "-"], ppm); + assert_eq!(bad.status.code(), Some(2)); + assert!(stderr(&bad).contains("hint"), "{:?}", stderr(&bad)); +} + /// `-jpeg_like` and `-partition_limit N` are accepted (were rejected) and bias the base /// quantizer, changing the output; the neutral `-partition_limit 0` is byte-identical. #[test] @@ -643,37 +702,72 @@ fn cwebp_rejects_blend_alpha() { assert!(err.contains("-noalpha"), "{err:?}"); } -/// `dwebp -dither` is now accepted (not rejected): the strength is range-validated, -/// and the flag is a documented no-op because this decoder reconstructs exact pixels. -/// An out-of-range strength is a usage error. +/// `dwebp -dither` and `-alpha_dither` reach their pixel reconstruction paths. +/// Strength is range-validated and `-nodither` can explicitly restore the default. #[test] fn dwebp_dither_is_accepted_and_range_checked() { - // 2x2 image encoded, then decoded with `-dither 50`: parsing succeeds (the flag is - // no longer a hard rejection) and the decode completes. - let raw: Vec = (0..16u8).collect(); - let webp = run( - "webp", - &[ - "encode", - "-", - "-o", - "-", - "--input-format", - "raw", - "--width", - "2", - "--height", - "2", - ], - raw, + let mut ppm = b"P6\n32 32\n255\n".to_vec(); + let colors = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]]; + for y in 0..32usize { + for x in 0..32usize { + ppm.extend_from_slice(&colors[(x / 16 + 2 * (y / 16)) % colors.len()]); + } + } + let webp = run("cwebp", &["-", "-o", "-", "-q", "100"], ppm); + assert!(webp.status.success(), "fixture encode failed: {webp:?}"); + let plain = run("dwebp", &["-", "-o", "-", "-pam"], webp.stdout.clone()); + let dithered = run( + "dwebp", + &["-dither", "100", "-", "-o", "-", "-pam"], + webp.stdout.clone(), ); - assert!(webp.status.success()); - let ok = run( + assert!(plain.status.success() && dithered.status.success()); + assert_ne!( + plain.stdout, dithered.stdout, + "-dither must change a rich-chroma fixture" + ); + let yuv_plain = run("dwebp", &["-", "-o", "-", "-yuv"], webp.stdout.clone()); + let yuv_dithered = run( "dwebp", - &["-dither", "50", "-", "-o", "-", "-pam"], + &["-dither", "100", "-", "-o", "-", "-yuv"], + webp.stdout.clone(), + ); + assert!(yuv_plain.status.success() && yuv_dithered.status.success()); + assert_ne!( + yuv_plain.stdout, yuv_dithered.stdout, + "-dither must reach native YUV output" + ); + let disabled = run( + "dwebp", + &["-dither", "100", "-nodither", "-", "-o", "-", "-pam"], webp.stdout, ); - assert!(ok.status.success(), "-dither must be accepted: {ok:?}"); + assert_eq!( + disabled.stdout, plain.stdout, + "-nodither restores strength zero" + ); + + let alpha_webp = run( + "cwebp", + &["-", "-o", "-", "-q", "90", "-alpha_q", "30"], + pam_with_quantizable_alpha(17, 19), + ); + assert!(alpha_webp.status.success(), "alpha encode: {alpha_webp:?}"); + let alpha_plain = run( + "dwebp", + &["-", "-o", "-", "-pam"], + alpha_webp.stdout.clone(), + ); + let alpha_dithered = run( + "dwebp", + &["-alpha_dither", "-", "-o", "-", "-pam"], + alpha_webp.stdout, + ); + assert!(alpha_plain.status.success() && alpha_dithered.status.success()); + assert_ne!( + alpha_plain.stdout, alpha_dithered.stdout, + "-alpha_dither must smooth a level-quantized ALPH plane" + ); // A strength above 100 is an honest usage error. let bad = run("dwebp", &["-dither", "200", "-", "-o", "-"], vec![0; 16]); diff --git a/crates/webpkit-cli/tests/snapshots/snapshot__cwebp.snap b/crates/webpkit-cli/tests/snapshots/snapshot__cwebp.snap index 2559b17..13e4bb9 100644 --- a/crates/webpkit-cli/tests/snapshots/snapshot__cwebp.snap +++ b/crates/webpkit-cli/tests/snapshots/snapshot__cwebp.snap @@ -39,4 +39,5 @@ Options: -v verbose output (repeatable) -color auto (default), always, or never -version print version + -hint image type hint: picture, photo, or graph -pre preprocessing filter: 0=none, 1=segment-map smoothing (2=dithering unsupported) diff --git a/crates/webpkit-libwebp-timing/Cargo.toml b/crates/webpkit-libwebp-timing/Cargo.toml new file mode 100644 index 0000000..47c2fdc --- /dev/null +++ b/crates/webpkit-libwebp-timing/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "webpkit-libwebp-timing" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +description = "In-process libwebp-sys encode/decode timing shims for the xtask scoreboard; the unsafe FFI is isolated here so xtask stays forbid-unsafe." +publish = false + +[features] +# Link libwebp-sys and expose the in-process encode/decode shims. +# Off by default, +# so a plain workspace build never compiles the vendored C reference library. +enabled = ["dep:libwebp-sys"] + +[dependencies] +libwebp-sys = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/crates/webpkit-libwebp-timing/src/lib.rs b/crates/webpkit-libwebp-timing/src/lib.rs new file mode 100644 index 0000000..ca1d928 --- /dev/null +++ b/crates/webpkit-libwebp-timing/src/lib.rs @@ -0,0 +1,308 @@ +//! In-process libwebp shims for the xtask `scoreboard` command's speed plane. +//! +//! The `unsafe` libwebp-sys FFI is isolated in this leaf crate so `xtask` keeps `#![forbid(unsafe_code)]` (the same split `webpkit-alloc-count` uses for its allocator). +//! Built empty unless the `enabled` feature links `libwebp-sys`; the scoreboard's libwebp columns are absent otherwise. + +#[cfg(feature = "enabled")] +#[allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + reason = "libwebp FFI takes/returns c_int dimensions and strides; the scoreboard \ + caps every edge via --max-edge so these bounded conversions cannot wrap" +)] +mod shims { + /// libwebp's reported decoder version as `"major.minor.patch"` (decoded from the packed hex `WebPGetDecoderVersion`), so the scoreboard states exactly which libwebp its in-process timings ran against. + #[must_use] + pub fn decoder_version() -> String { + // SAFETY: a nullary libwebp accessor returning a packed version int. + let v = unsafe { libwebp_sys::WebPGetDecoderVersion() } as u32; + format!("{}.{}.{}", (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff) + } + + fn checked_layout( + len: usize, + width: u32, + height: u32, + channels: usize, + ) -> Option<(i32, i32, i32)> { + if width == 0 || height == 0 { + return None; + } + let width_usize = usize::try_from(width).ok()?; + let height_usize = usize::try_from(height).ok()?; + let expected = width_usize + .checked_mul(height_usize)? + .checked_mul(channels)?; + if len != expected { + return None; + } + let width_i32 = i32::try_from(width).ok()?; + let height_i32 = i32::try_from(height).ok()?; + let stride = i32::try_from(width_usize.checked_mul(channels)?).ok()?; + Some((width_i32, height_i32, stride)) + } + + struct MemoryWriterGuard { + raw: libwebp_sys::WebPMemoryWriter, + } + + impl MemoryWriterGuard { + fn initialized() -> Self { + let mut raw = core::mem::MaybeUninit::::uninit(); + // SAFETY: libwebp initializes every field in the provided writer storage. + unsafe { libwebp_sys::WebPMemoryWriterInit(raw.as_mut_ptr()) }; + // SAFETY: the preceding call initialized every writer field. + let raw = unsafe { raw.assume_init() }; + Self { raw } + } + } + + impl Drop for MemoryWriterGuard { + fn drop(&mut self) { + // SAFETY: `raw` was initialized by libwebp and this guard clears it exactly once. + unsafe { libwebp_sys::WebPMemoryWriterClear(&raw mut self.raw) }; + } + } + + struct PictureGuard { + raw: libwebp_sys::WebPPicture, + } + + impl PictureGuard { + fn initialized() -> Result { + libwebp_sys::WebPPicture::new().map(|raw| Self { raw }) + } + } + + impl Drop for PictureGuard { + fn drop(&mut self) { + // SAFETY: `raw` was initialized by libwebp and this guard frees it exactly once. + unsafe { libwebp_sys::WebPPictureFree(&raw mut self.raw) }; + } + } + + /// Losslessly encode `rgba` with libwebp's simple-API default. + /// + /// Returns the encoded byte length (the buffer is freed here) — for the encode-speed loop and the size column; lossless fidelity is exact, so the bytes themselves are never needed. + #[must_use] + pub fn encode_lossless_len(rgba: &[u8], width: u32, height: u32) -> usize { + let Some((width, height, stride)) = checked_layout(rgba.len(), width, height, 4) else { + return 0; + }; + let mut out: *mut u8 = core::ptr::null_mut(); + // SAFETY: `rgba` holds `width*height*4` bytes at `stride`; libwebp writes the + // freshly allocated stream pointer into `out`. + let size = unsafe { + libwebp_sys::WebPEncodeLosslessRGBA(rgba.as_ptr(), width, height, stride, &raw mut out) + }; + if !out.is_null() { + // SAFETY: `out` was allocated by libwebp and is freed exactly once here. + unsafe { libwebp_sys::WebPFree(out.cast()) }; + } + size + } + + fn encode_lossless_best_impl(rgba: &[u8], width: u32, height: u32) -> usize { + let Some((width_i32, height_i32, stride)) = checked_layout(rgba.len(), width, height, 4) + else { + return 0; + }; + let Ok(mut config) = libwebp_sys::WebPConfig::new() else { + return 0; + }; + config.lossless = 1; + config.quality = 100.0; + config.method = 6; + config.thread_level = 0; + config.exact = 1; + // SAFETY: `config` is initialized by libwebp and all assigned fields are + // within the documented ranges. + if unsafe { libwebp_sys::WebPValidateConfig(&raw const config) } == 0 { + return 0; + } + + // Declaration order is intentional: locals drop in reverse order, so the picture releases its imported planes before the writer clears its buffer. + let mut writer = MemoryWriterGuard::initialized(); + let Ok(mut picture) = PictureGuard::initialized() else { + return 0; + }; + picture.raw.width = width_i32; + picture.raw.height = height_i32; + picture.raw.use_argb = 1; + // SAFETY: `rgba` contains `width * height * 4` bytes at `stride`. + if unsafe { + libwebp_sys::WebPPictureImportRGBA(&raw mut picture.raw, rgba.as_ptr(), stride) + } == 0 + { + return 0; + } + + picture.raw.writer = Some(libwebp_sys::WebPMemoryWrite); + // `writer` was declared before `picture` and is not moved after this pointer capture, so `custom_ptr` remains valid through the synchronous encode. + picture.raw.custom_ptr = (&raw mut writer.raw).cast(); + // SAFETY: config, picture, imported pixels, and writer remain live for this + // synchronous call. The callback uses `custom_ptr` only while `WebPEncode` + // is running and does not retain it. + let ok = unsafe { libwebp_sys::WebPEncode(&raw const config, &raw mut picture.raw) }; + if ok != 0 && !writer.raw.mem.is_null() { + writer.raw.size + } else { + 0 + } + } + + /// Losslessly encode `rgba` with the campaign reference settings: + /// `-lossless -exact -m 6 -q 100`, with `thread_level = 0`. + /// + /// It reads the libwebp memory-writer length before clearing that buffer and performs no Rust output copy, so timing and process-peak measurements use the same output ownership basis as webpkit. + #[must_use] + pub fn encode_lossless_best_len(rgba: &[u8], width: u32, height: u32) -> usize { + encode_lossless_best_impl(rgba, width, height) + } + + /// Lossily encode `rgb` (`width*height*3` bytes) with libwebp's simple-API default shaping at `quality`, returning the full WebP container bytes (for the size + fidelity pass). + /// Empty on encode failure. + #[must_use] + pub fn encode_lossy_bytes(rgb: &[u8], width: u32, height: u32, quality: f32) -> Vec { + let Some((width, height, stride)) = checked_layout(rgb.len(), width, height, 3) else { + return Vec::new(); + }; + if !quality.is_finite() || !(0.0..=100.0).contains(&quality) { + return Vec::new(); + } + let mut out: *mut u8 = core::ptr::null_mut(); + // SAFETY: `rgb` holds `width*height*3` bytes at `stride`; libwebp writes the + // freshly allocated stream pointer into `out`. + let size = unsafe { + libwebp_sys::WebPEncodeRGB(rgb.as_ptr(), width, height, stride, quality, &raw mut out) + }; + if out.is_null() || size == 0 { + return Vec::new(); + } + // SAFETY: on success `out` points at `size` valid bytes owned by libwebp. + let bytes = unsafe { core::slice::from_raw_parts(out, size) }.to_vec(); + // SAFETY: `out` was allocated by libwebp and is freed exactly once here. + unsafe { libwebp_sys::WebPFree(out.cast()) }; + bytes + } + + /// Lossily encode `rgb` at `quality`, returning the encoded byte length (the buffer is freed here). + /// The allocation-free variant of [`encode_lossy_bytes`] for the encode-speed loop. + #[must_use] + pub fn encode_lossy_len(rgb: &[u8], width: u32, height: u32, quality: f32) -> usize { + let Some((width, height, stride)) = checked_layout(rgb.len(), width, height, 3) else { + return 0; + }; + if !quality.is_finite() || !(0.0..=100.0).contains(&quality) { + return 0; + } + let mut out: *mut u8 = core::ptr::null_mut(); + // SAFETY: `rgb` holds `width*height*3` bytes at `stride`; libwebp writes the + // freshly allocated stream pointer into `out`. + let size = unsafe { + libwebp_sys::WebPEncodeRGB(rgb.as_ptr(), width, height, stride, quality, &raw mut out) + }; + if !out.is_null() { + // SAFETY: `out` was allocated by libwebp and is freed exactly once here. + unsafe { libwebp_sys::WebPFree(out.cast()) }; + } + size + } + + /// Lossily encode opaque-or-alpha-bearing RGBA input and return only the libwebp-owned output length. + /// + /// The surpass resource gate supplies an all-opaque buffer to both codecs so their timed input layout and retained input bytes are identical. + #[must_use] + pub fn encode_lossy_rgba_len(rgba: &[u8], width: u32, height: u32, quality: f32) -> usize { + let Some((width, height, stride)) = checked_layout(rgba.len(), width, height, 4) else { + return 0; + }; + if !quality.is_finite() || !(0.0..=100.0).contains(&quality) { + return 0; + } + let mut out: *mut u8 = core::ptr::null_mut(); + // SAFETY: `rgba` holds exactly `width*height*4` bytes at `stride`; + // libwebp writes the freshly allocated stream pointer into `out`. + let size = unsafe { + libwebp_sys::WebPEncodeRGBA(rgba.as_ptr(), width, height, stride, quality, &raw mut out) + }; + if !out.is_null() { + // SAFETY: `out` was allocated by libwebp and is freed exactly once. + unsafe { libwebp_sys::WebPFree(out.cast()) }; + } + size + } + + /// Decode `webp` with libwebp to RGBA, returning the decoded byte length (the buffer is freed here). + /// For the decode-speed loop; `0` on decode failure. + #[must_use] + pub fn decode_rgba_len(webp: &[u8]) -> usize { + let (mut w, mut h) = (0i32, 0i32); + // SAFETY: `webp`/`len` describe a valid buffer; libwebp allocates the output + // and writes the dimensions through the out-pointers. + let ptr = unsafe { + libwebp_sys::WebPDecodeRGBA(webp.as_ptr(), webp.len(), &raw mut w, &raw mut h) + }; + if ptr.is_null() { + return 0; + } + let len = usize::try_from(w) + .ok() + .zip(usize::try_from(h).ok()) + .and_then(|(width, height)| width.checked_mul(height)) + .and_then(|pixels| pixels.checked_mul(4)) + .unwrap_or(0); + // SAFETY: `ptr` was allocated by libwebp and is freed exactly once here. + unsafe { libwebp_sys::WebPFree(ptr.cast()) }; + len + } +} + +#[cfg(feature = "enabled")] +pub use shims::{ + decode_rgba_len, decoder_version, encode_lossless_best_len, encode_lossless_len, + encode_lossy_bytes, encode_lossy_len, encode_lossy_rgba_len, +}; + +#[cfg(all(test, feature = "enabled"))] +mod tests { + use super::{ + encode_lossless_best_len, encode_lossless_len, encode_lossy_bytes, encode_lossy_len, + encode_lossy_rgba_len, + }; + + #[test] + fn lossless_best_rejects_invalid_rgba_dimensions_before_ffi() { + assert_eq!(encode_lossless_best_len(&[], 1, 1), 0); + assert_eq!(encode_lossless_best_len(&[0; 4], 0, 1), 0); + assert_eq!(encode_lossless_best_len(&[0; 4], u32::MAX, 1), 0); + } + + #[test] + fn length_only_lossless_best_is_nonzero_and_deterministic() { + let rgba = [17, 29, 41, 255, 53, 67, 79, 255]; + let len = encode_lossless_best_len(&rgba, 2, 1); + assert_ne!(len, 0); + assert_eq!(encode_lossless_best_len(&rgba, 2, 1), len); + } + + #[test] + fn simple_shims_reject_invalid_layouts_before_ffi() { + assert_eq!(encode_lossless_len(&[0; 3], 1, 1), 0); + assert!(encode_lossy_bytes(&[0; 2], 1, 1, 75.0).is_empty()); + assert_eq!(encode_lossy_len(&[0; 3], 0, 1, 75.0), 0); + assert_eq!(encode_lossy_len(&[0; 3], 1, 1, f32::NAN), 0); + assert_eq!(encode_lossy_rgba_len(&[0; 3], 1, 1, 75.0), 0); + } + + #[test] + fn opaque_rgba_and_rgb_lossy_inputs_have_the_same_encoded_length() { + let rgb = [17, 29, 41, 53, 67, 79]; + let rgba = [17, 29, 41, 255, 53, 67, 79, 255]; + assert_eq!( + encode_lossy_rgba_len(&rgba, 2, 1, 75.0), + encode_lossy_len(&rgb, 2, 1, 75.0) + ); + } +} diff --git a/crates/webpkit-lossy-conformance/conformance-results-lossy.json b/crates/webpkit-lossy-conformance/conformance-results-lossy.json index b980a13..66b5769 100644 --- a/crates/webpkit-lossy-conformance/conformance-results-lossy.json +++ b/crates/webpkit-lossy-conformance/conformance-results-lossy.json @@ -5,12 +5,138 @@ "level": "must", "passed": true }, + { + "case": "chromah_vgrad_32x32", + "feature": "chroma-mode-h", + "level": "must", + "passed": true + }, + { + "case": "chromav_hgrad_32x32", + "feature": "chroma-mode-v", + "level": "must", + "passed": true + }, + { + "case": "col_1x63", + "feature": "col-edge", + "level": "must", + "passed": true + }, + { + "case": "edge_17x9_color", + "feature": "chroma-edge", + "level": "must", + "passed": true + }, + { + "case": "edge_23x31_color", + "feature": "chroma-edge", + "level": "must", + "passed": true + }, + { + "case": "filter_none_48x48", + "feature": "no-filter", + "level": "must", + "passed": true + }, + { + "case": "filter_sharp_48x48", + "feature": "loop-filter-sharp", + "level": "must", + "passed": true + }, + { + "case": "filter_simple_48x48", + "feature": "loop-filter-simple", + "level": "must", + "passed": true + }, + { + "case": "filter_strong_48x48", + "feature": "loop-filter-normal", + "level": "must", + "passed": true + }, + { + "case": "full_gradient_64x64", + "feature": "mb-aligned", + "level": "must", + "passed": true + }, + { + "case": "full_noise_64x64", + "feature": "mb-aligned-detail", + "level": "must", + "passed": true + }, { "case": "gradient_17x13_q80", "feature": "chroma-edge", "level": "must", "passed": true }, + { + "case": "i4x4_noise_32x32", + "feature": "intra-4x4", + "level": "must", + "passed": true + }, + { + "case": "i4x4_stripe_48x48", + "feature": "intra-4x4", + "level": "must", + "passed": true + }, + { + "case": "intra16dc_solid_32x32", + "feature": "intra16-dc", + "level": "must", + "passed": true + }, + { + "case": "intra16h_vgrad_32x32", + "feature": "intra16-h", + "level": "must", + "passed": true + }, + { + "case": "intra16tm_dgrad_32x32", + "feature": "intra16-tm", + "level": "must", + "passed": true + }, + { + "case": "intra16v_hgrad_32x32", + "feature": "intra16-v", + "level": "must", + "passed": true + }, + { + "case": "method0_fast_32x32", + "feature": "fast-method", + "level": "must", + "passed": true + }, + { + "case": "method6_trellis_32x32", + "feature": "trellis", + "level": "must", + "passed": true + }, + { + "case": "narrow_3x64", + "feature": "narrow", + "level": "must", + "passed": true + }, + { + "case": "narrow_64x3", + "feature": "narrow", + "level": "must", + "passed": true + }, { "case": "noise_32x24_q30", "feature": "intra-4x4", @@ -22,5 +148,77 @@ "feature": "tiny-odd", "level": "must", "passed": true + }, + { + "case": "q100_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "q10_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "q30_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "q50_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "q75_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "q90_color_32x32", + "feature": "quantizer", + "level": "must", + "passed": true + }, + { + "case": "row_63x1", + "feature": "row-edge", + "level": "must", + "passed": true + }, + { + "case": "seg_multi_64x48", + "feature": "segmentation-multi", + "level": "must", + "passed": true + }, + { + "case": "seg_single_64x48", + "feature": "segmentation-single", + "level": "must", + "passed": true + }, + { + "case": "tiny_1x1", + "feature": "tiny", + "level": "must", + "passed": true + }, + { + "case": "tiny_2x2", + "feature": "tiny", + "level": "must", + "passed": true + }, + { + "case": "tiny_5x5", + "feature": "tiny", + "level": "must", + "passed": true } ] diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/expected.rgba new file mode 100644 index 0000000..0411e3b --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/expected.rgba @@ -0,0 +1,31 @@ +�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿ�úÿùÿùÿùÿùÿ}øÿ}øÿ}øÿ}øÿ€ùÿ€ùÿ€ùÿ€ùÿ€ùÿ€ùÿ€ùÿ€ùÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿ‚÷ÿõÿõÿõÿõÿ€÷ÿ€÷ÿ€÷ÿ€÷ÿƒøÿƒøÿƒøÿƒøÿƒøÿƒøÿƒøÿƒøÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ‚íÿ€íÿ€íÿ€íÿìÿìÿìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿ�ìÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿãÿ�äÿäÿäÿäÿ€æÿ€æÿ€æÿ‚æÿ�äÿ�äÿ�äÿ�äÿ�äÿ�äÿ�äÿ�äÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ�"áÿ~!àÿ~!àÿ~!àÿ~!àÿ~!àÿ~!àÿ~!àÿ~!àÿ àÿ àÿ àÿ àÿ àÿ àÿ àÿ àÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ|)Ùÿ|)Ùÿ|)Ùÿ|)Ùÿ~*Úÿ~*Úÿ~*Úÿ~*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ*Úÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ3Ñÿ3Ñÿ3Ñÿ3Ñÿ~2Ðÿ~2Ðÿ~2Ðÿ~2Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ€1Ðÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ}9Æÿ~:Çÿ~:Çÿ~:Çÿ~:Çÿ~:Çÿ~:Çÿ~:Çÿ~:Çÿ€9Çÿ€9Çÿ€9Çÿ€9Çÿ€9Çÿ€9Çÿ€9Çÿ€9Çÿ~CÀÿ~CÀÿ~CÀÿ~CÀÿ~CÀÿ~CÀÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿ~D¾ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿB½ÿ|J¶ÿ|J¶ÿ|J¶ÿ|J¶ÿ|J¶ÿ|K´ÿ|K´ÿ|K´ÿ|K´ÿ|K²ÿ|K²ÿ|K²ÿ|K²ÿ|K²ÿ|K²ÿ}J²ÿI´ÿI´ÿI´ÿI´ÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ€Jµÿ~Q«ÿ~Q«ÿ~Q«ÿ~Q«ÿ~Q«ÿ~R©ÿ~R©ÿ~R©ÿ~R©ÿ~R§ÿ~R§ÿ~R§ÿ~R§ÿ~R§ÿ~R§ÿ~R©ÿ�S¬ÿ�S¬ÿ�S¬ÿ�S¬ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ€R«ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y£ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿ~Y¡ÿZ¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ�Y¤ÿ‚c¢ÿ‚c¢ÿ‚c¢ÿ‚c¢ÿ‚c¢ÿ‚c¢ÿ‚c ÿ‚c ÿ‚c ÿ‚c ÿ‚džÿ‚džÿ‚džÿ‚džÿ‚džÿ‚c ÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ€bžÿ�išÿ�išÿ�išÿ�išÿ�išÿ�i˜ÿ�i˜ÿ�i˜ÿ�i˜ÿ�j–ÿ�j–ÿ�j–ÿ�j–ÿ�j–ÿ�j–ÿ�j–ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ€k™ÿ‚r�ÿ‚r�ÿ‚r�ÿ‚r�ÿ‚r�ÿ‚rŽÿ‚rŽÿ‚rŽÿ‚rŽÿ‚sŒÿ‚sŒÿ‚sŒÿ‚sŒÿ‚sŒÿ‚sŒÿ‚rŽÿ�sŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿtŽÿ�y…ÿ�y…ÿ�y…ÿ�y…ÿ�y…ÿ�y…ÿ�y…ÿ�yƒÿ�yƒÿ�yƒÿ�yƒÿ�yƒÿ�yƒÿ�yƒÿ�yƒÿ€zƒÿ€zƒÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ~z…ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„zÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ‚„|ÿ�„|ÿ�„|ÿ�„|ÿ�„|ÿ�„|ÿ�„|ÿ�„|ÿ�„|ÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ‚Šsÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€‹sÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ“lÿ“lÿ“lÿ“lÿ“lÿ“lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ€’lÿ“lÿ“lÿ“lÿ“lÿ“lÿ“lÿ“lÿ�™cÿ�™cÿ�™cÿ�™cÿ�™cÿšcÿšcÿšcÿšcÿ}›cÿ}›cÿ}›cÿ}›cÿ}›cÿ}›cÿšcÿšcÿšcÿšcÿšcÿšcÿšcÿšcÿšcÿšcÿ}›cÿ}›cÿ}›cÿ}›cÿ}›cÿ}›cÿ}›cÿ‚¤[ÿ‚¤[ÿ‚¤[ÿ‚¤[ÿ‚¤[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ€¥[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ~¦[ÿ€­Rÿ€­Rÿ€­Rÿ€­Rÿ€­Rÿ€­Rÿ­Rÿ­Rÿ­Rÿ­Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ­Rÿ­Rÿ­Rÿ­Rÿ­Rÿ­Rÿ­Rÿ­Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ}®Rÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ´Lÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ}µLÿ~»Bÿ~»Bÿ~»Bÿ~»Bÿ~»Bÿ~»Bÿ~»Bÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ}¼@ÿ~»@ÿ~»@ÿ~»@ÿ~»@ÿ~»@ÿ~»@ÿ}¼@ÿ}¼@ÿ}¼Bÿ}¼Bÿ}¼Bÿ}¼Bÿ}¼Bÿ}¼Bÿ}¼Bÿ€Ç;ÿ€Ç;ÿ€Ç;ÿ€Ç;ÿ€Ç;ÿ€Ç;ÿ€Ç;ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿÈ9ÿ€Ç9ÿ€Ç9ÿ€Ç9ÿ€Ç9ÿ€Ç9ÿ€Ç9ÿÈ9ÿÈ9ÿÈ;ÿÈ;ÿÈ;ÿÈ;ÿÈ;ÿÈ;ÿÈ;ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î/ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Î1ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ)ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Õ+ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ~Ý"ÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€æÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ€íÿ„õÿ„õÿ„õÿ„õÿ„õÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö +ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿ„ö ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü +ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿˆü ÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/input.vp8 new file mode 100644 index 0000000..e9c6711 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/meta.toml new file mode 100644 index 0000000..d12c5e8 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/chromah_vgrad_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "chroma-mode-h" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/expected.rgba new file mode 100644 index 0000000..c86fe51 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/input.vp8 new file mode 100644 index 0000000..9c4ffc2 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/meta.toml new file mode 100644 index 0000000..27cfba5 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/chromav_hgrad_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "chroma-mode-v" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/expected.rgba new file mode 100644 index 0000000..98afc66 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/input.vp8 new file mode 100644 index 0000000..efb4a12 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/meta.toml new file mode 100644 index 0000000..80f89b9 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/col_1x63/meta.toml @@ -0,0 +1,5 @@ +feature = "col-edge" +level = "must" +width = 1 +height = 63 +note = "1x63 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/expected.rgba new file mode 100644 index 0000000..5ca2f92 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/input.vp8 new file mode 100644 index 0000000..c6772af Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/meta.toml new file mode 100644 index 0000000..fa2dfaa --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/edge_17x9_color/meta.toml @@ -0,0 +1,5 @@ +feature = "chroma-edge" +level = "must" +width = 17 +height = 9 +note = "17x9 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/expected.rgba new file mode 100644 index 0000000..5be055f Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/input.vp8 new file mode 100644 index 0000000..02a6526 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/meta.toml new file mode 100644 index 0000000..5613d19 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/edge_23x31_color/meta.toml @@ -0,0 +1,5 @@ +feature = "chroma-edge" +level = "must" +width = 23 +height = 31 +note = "23x31 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/expected.rgba new file mode 100644 index 0000000..dfc7217 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/expected.rgba @@ -0,0 +1 @@ +ÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/input.vp8 new file mode 100644 index 0000000..61c9ad1 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/meta.toml new file mode 100644 index 0000000..8a3d5f4 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_none_48x48/meta.toml @@ -0,0 +1,5 @@ +feature = "no-filter" +level = "must" +width = 48 +height = 48 +note = "48x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 55 -f 0 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/expected.rgba new file mode 100644 index 0000000..dfc7217 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/expected.rgba @@ -0,0 +1 @@ +ÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/input.vp8 new file mode 100644 index 0000000..b8f3fad Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/meta.toml new file mode 100644 index 0000000..1a15fb3 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_sharp_48x48/meta.toml @@ -0,0 +1,5 @@ +feature = "loop-filter-sharp" +level = "must" +width = 48 +height = 48 +note = "48x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 55 -strong -f 60 -sharpness 7 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/expected.rgba new file mode 100644 index 0000000..dfc7217 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/expected.rgba @@ -0,0 +1 @@ +ÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/input.vp8 new file mode 100644 index 0000000..d73a9f4 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/meta.toml new file mode 100644 index 0000000..385e479 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_simple_48x48/meta.toml @@ -0,0 +1,5 @@ +feature = "loop-filter-simple" +level = "must" +width = 48 +height = 48 +note = "48x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 55 -nostrong -f 40 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/expected.rgba new file mode 100644 index 0000000..dfc7217 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/expected.rgba @@ -0,0 +1 @@ +ÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿÜÜÜÿÜÜÜÿÜÜÜÿÜÜÜÿÿÿÿÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/input.vp8 new file mode 100644 index 0000000..df6d699 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/meta.toml new file mode 100644 index 0000000..b823d04 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/filter_strong_48x48/meta.toml @@ -0,0 +1,5 @@ +feature = "loop-filter-normal" +level = "must" +width = 48 +height = 48 +note = "48x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 55 -strong -f 40 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/expected.rgba new file mode 100644 index 0000000..15484d8 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/expected.rgba @@ -0,0 +1,16 @@ +ÿÿ ÿ + + +ÿ ÿÿÿÿÿÿÿÿÿÿÿ!!!ÿ"""ÿ###ÿ&&&ÿ)))ÿ***ÿ+++ÿ000ÿ111ÿ333ÿ444ÿ999ÿ:::ÿ;;;ÿ>>>ÿAAAÿBBBÿDDDÿEEEÿHHHÿKKKÿLLLÿMMMÿRQRÿSSSÿTTTÿUUUÿZZZÿ[[[ÿ\\\ÿ^^^ÿbbbÿcccÿdddÿeeeÿiiiÿkkkÿlllÿmmmÿqqqÿrrrÿtttÿvvvÿyyyÿzzzÿ{{{ÿ}}}ÿÿÿÿÿ ÿ + + +ÿ ÿÿÿÿÿÿÿÿÿÿÿ!!!ÿ"""ÿ###ÿ&&&ÿ)))ÿ***ÿ+++ÿ000ÿ111ÿ333ÿ444ÿ999ÿ:::ÿ;;;ÿ>>>ÿAAAÿBBBÿDDDÿEEEÿHHHÿKKKÿLLLÿMMMÿRQRÿSSSÿTTTÿUUUÿZZZÿ[[[ÿ\\\ÿ^^^ÿbbbÿcccÿdddÿeeeÿiiiÿkkkÿlllÿmmmÿqqqÿrrrÿtttÿvvvÿyyyÿzzzÿ{{{ÿ}}}ÿÿÿÿÿ ÿ ÿÿÿÿÿÿÿÿÿÿÿ!!!ÿ"""ÿ$$$ÿ%%%ÿ)))ÿ+++ÿ,,,ÿ---ÿ222ÿ333ÿ444ÿ666ÿ:::ÿ;;;ÿ===ÿ???ÿBBBÿDDDÿEEEÿFFFÿIIIÿLLLÿMMMÿNNNÿSSSÿTTTÿUUUÿVVVÿ[[[ÿ\\\ÿ]]]ÿ___ÿcccÿdddÿeeeÿfffÿjjjÿlllÿmmmÿoooÿrrrÿsssÿvvvÿwwwÿzzzÿ{{{ÿ}}}ÿ~~~ÿ€€€ÿ€€€ÿÿÿ ÿ ÿÿÿÿÿÿÿÿÿÿ!!!ÿ"""ÿ###ÿ%%%ÿ&&&ÿ***ÿ,,,ÿ---ÿ///ÿ333ÿ444ÿ666ÿ777ÿ;;;ÿ===ÿ>>>ÿ@@@ÿBBBÿDDDÿFFFÿGGGÿKKKÿMMMÿNNNÿOOOÿTTTÿUUUÿVVVÿWWWÿ\\\ÿ]]]ÿ^^^ÿaaaÿcccÿdddÿfffÿhhhÿkkkÿmmmÿoooÿpppÿsssÿtttÿwwwÿxxxÿ{{{ÿ}}}ÿ~~~ÿÿ���ÿ���ÿÿÿ + + +ÿ ÿÿÿÿÿÿÿÿÿ"""ÿ$$$ÿ&&&ÿ(((ÿ&&&ÿ(((ÿ+++ÿ---ÿ///ÿ000ÿ444ÿ666ÿ888ÿ999ÿ>>>ÿ???ÿ@@@ÿBBBÿGGGÿHHHÿGGGÿHHHÿMMMÿOOOÿPPPÿRQRÿVVVÿWWWÿXXXÿZZZÿ^^^ÿ___ÿaaaÿcccÿhhhÿiiiÿhhhÿiiiÿmmmÿpppÿqqqÿrrrÿvvvÿwwwÿyyyÿzzzÿ~~~ÿÿ€€€ÿ���ÿ„„„ÿ„„„ÿÿ + + +ÿÿÿÿÿÿÿÿÿ!!!ÿ!!!ÿ###ÿ%%%ÿ(((ÿ)))ÿ***ÿ+++ÿ---ÿ000ÿ111ÿ222ÿ777ÿ888ÿ:::ÿ;;;ÿ@@@ÿAAAÿBBBÿEEEÿHHHÿIIIÿKKKÿLLLÿOOOÿRQRÿSSSÿTTTÿXXXÿZZZÿ[[[ÿ\\\ÿaaaÿbbbÿcccÿeeeÿiiiÿjjjÿkkkÿlllÿpppÿrrrÿsssÿtttÿxxxÿyyyÿ{{{ÿ}}}ÿ€€€ÿ���ÿ‚‚‚ÿ„„„ÿ†††ÿ†††ÿ + + +ÿÿÿÿÿÿÿÿÿÿ"""ÿ$$$ÿ&&&ÿ)))ÿ***ÿ+++ÿ---ÿ///ÿ111ÿ333ÿ444ÿ666ÿ:::ÿ;;;ÿ===ÿ>>>ÿBBBÿDDDÿEEEÿGGGÿKKKÿLLLÿMMMÿNNNÿRQRÿTTTÿUUUÿVVVÿ[[[ÿ\\\ÿ]]]ÿ^^^ÿcccÿdddÿeeeÿhhhÿkkkÿlllÿmmmÿoooÿrrrÿtttÿvvvÿwwwÿzzzÿ{{{ÿ~~~ÿÿ‚‚‚ÿ„„„ÿ………ÿ†††ÿˆˆˆÿˆˆˆÿÿÿÿÿÿÿÿÿÿÿ"""ÿ$$$ÿ(((ÿ***ÿ+++ÿ,,,ÿ---ÿ///ÿ222ÿ444ÿ666ÿ777ÿ;;;ÿ===ÿ>>>ÿ???ÿDDDÿEEEÿFFFÿHHHÿKKKÿLLLÿNNNÿOOOÿSSSÿUUUÿVVVÿWWWÿ\\\ÿ]]]ÿ^^^ÿ___ÿdddÿeeeÿfffÿiiiÿkkkÿlllÿoooÿpppÿsssÿvvvÿwwwÿxxxÿ{{{ÿ}}}ÿÿ€€€ÿ„„„ÿ………ÿ†††ÿ‡‡‡ÿ‰‰‰ÿ‰‰‰ÿÿÿÿÿÿÿÿÿÿ!!!ÿ###ÿ%%%ÿ***ÿ,,,ÿ---ÿ///ÿ///ÿ000ÿ333ÿ666ÿ777ÿ888ÿ===ÿ>>>ÿ???ÿ@@@ÿEEEÿFFFÿGGGÿIIIÿNNNÿOOOÿOOOÿPPPÿTTTÿVVVÿWWWÿXXXÿ]]]ÿ^^^ÿ___ÿaaaÿeeeÿfffÿhhhÿjjjÿoooÿpppÿpppÿqqqÿtttÿwwwÿxxxÿyyyÿ}}}ÿ~~~ÿ€€€ÿ���ÿ………ÿ†††ÿ‡‡‡ÿˆˆˆÿ‹‹‹ÿ‹‹‹ÿÿÿÿÿÿÿÿÿÿ###ÿ(((ÿ)))ÿ+++ÿ---ÿ000ÿ111ÿ111ÿ222ÿ666ÿ888ÿ999ÿ:::ÿ???ÿ@@@ÿAAAÿBBBÿGGGÿHHHÿIIIÿLLLÿOOOÿPPPÿRQRÿSSSÿVVVÿXXXÿZZZÿ[[[ÿ___ÿaaaÿbbbÿcccÿhhhÿiiiÿjjjÿlllÿpppÿqqqÿrrrÿsssÿwwwÿyyyÿzzzÿ{{{ÿÿ€€€ÿ‚‚‚ÿ„„„ÿ‡‡‡ÿˆˆˆÿ‰‰‰ÿ‹‹‹ÿ���ÿ���ÿÿÿÿÿÿÿ!!!ÿ"""ÿ$$$ÿ&&&ÿ)))ÿ+++ÿ---ÿ000ÿ111ÿ222ÿ444ÿ666ÿ888ÿ:::ÿ;;;ÿ===ÿAAAÿBBBÿEEEÿFFFÿKKKÿLLLÿMMMÿOOOÿRQRÿSSSÿUUUÿVVVÿZZZÿ\\\ÿ]]]ÿ^^^ÿcccÿdddÿeeeÿfffÿkkkÿlllÿmmmÿpppÿrrrÿsssÿvvvÿwwwÿzzzÿ}}}ÿ~~~ÿÿ‚‚‚ÿ„„„ÿ†††ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿ���ÿŽŽŽÿ���ÿ���ÿÿÿÿÿÿÿ!!!ÿ$$$ÿ%%%ÿ&&&ÿ)))ÿ+++ÿ///ÿ111ÿ222ÿ333ÿ444ÿ666ÿ999ÿ;;;ÿ===ÿ>>>ÿBBBÿDDDÿFFFÿGGGÿLLLÿMMMÿNNNÿPPPÿSSSÿTTTÿVVVÿWWWÿ[[[ÿ]]]ÿ^^^ÿ___ÿdddÿeeeÿfffÿhhhÿlllÿmmmÿoooÿqqqÿsssÿtttÿwwwÿxxxÿ{{{ÿ~~~ÿÿ€€€ÿ„„„ÿ………ÿ‡‡‡ÿˆˆˆÿŒŒŒÿ���ÿŽŽŽÿ���ÿ’’’ÿ’’’ÿÿÿÿÿÿ!!!ÿ"""ÿ$$$ÿ&&&ÿ(((ÿ***ÿ,,,ÿ111ÿ333ÿ666ÿ777ÿ666ÿ777ÿ;;;ÿ>>>ÿ???ÿ@@@ÿEEEÿFFFÿGGGÿHHHÿMMMÿNNNÿOOOÿRQRÿVVVÿWWWÿWWWÿXXXÿ\\\ÿ^^^ÿ___ÿaaaÿeeeÿfffÿhhhÿiiiÿmmmÿoooÿpppÿrrrÿwwwÿxxxÿxxxÿyyyÿ}}}ÿÿ€€€ÿ���ÿ………ÿ†††ÿˆˆˆÿ‰‰‰ÿ���ÿŽŽŽÿ���ÿ���ÿ“““ÿ“““ÿÿÿÿÿ!!!ÿ"""ÿ%%%ÿ&&&ÿ(((ÿ***ÿ///ÿ111ÿ333ÿ444ÿ777ÿ999ÿ:::ÿ:::ÿ>>>ÿAAAÿBBBÿDDDÿGGGÿIIIÿKKKÿLLLÿOOOÿRQRÿSSSÿTTTÿXXXÿ[[[ÿ[[[ÿ[[[ÿ^^^ÿbbbÿcccÿdddÿhhhÿjjjÿkkkÿlllÿpppÿqqqÿsssÿtttÿyyyÿzzzÿ{{{ÿ}}}ÿ€€€ÿ���ÿ‚‚‚ÿ„„„ÿˆˆˆÿ‰‰‰ÿŒŒŒÿ���ÿ���ÿ’’’ÿ“““ÿ”””ÿ–––ÿ–––ÿÿÿ!!!ÿ"""ÿ$$$ÿ&&&ÿ(((ÿ)))ÿ,,,ÿ///ÿ000ÿ333ÿ444ÿ777ÿ888ÿ:::ÿ;;;ÿ;;;ÿ???ÿBBBÿEEEÿEEEÿHHHÿKKKÿNNNÿNNNÿRQRÿTTTÿVVVÿWWWÿZZZÿ\\\ÿ]]]ÿ]]]ÿaaaÿdddÿfffÿfffÿjjjÿlllÿoooÿoooÿrrrÿtttÿvvvÿwwwÿzzzÿ{{{ÿÿ€€€ÿ‚‚‚ÿ………ÿ†††ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ���ÿ“““ÿ”””ÿ•••ÿ–––ÿ™™™ÿ™™™ÿÿÿ!!!ÿ###ÿ%%%ÿ(((ÿ)))ÿ+++ÿ---ÿ///ÿ111ÿ333ÿ666ÿ777ÿ888ÿ;;;ÿ===ÿ===ÿ@@@ÿDDDÿFFFÿFFFÿHHHÿLLLÿOOOÿOOOÿRQRÿUUUÿWWWÿWWWÿZZZÿ]]]ÿ^^^ÿ^^^ÿbbbÿeeeÿhhhÿhhhÿjjjÿmmmÿpppÿpppÿrrrÿtttÿwwwÿwwwÿzzzÿ{{{ÿÿ€€€ÿ„„„ÿ………ÿ†††ÿ‡‡‡ÿŒŒŒÿ���ÿ���ÿ���ÿ”””ÿ•••ÿ–––ÿ———ÿšššÿšššÿ!!!ÿ"""ÿ###ÿ%%%ÿ(((ÿ***ÿ,,,ÿ///ÿ000ÿ111ÿ333ÿ666ÿ777ÿ888ÿ===ÿ>>>ÿ>>>ÿ???ÿBBBÿEEEÿFFFÿGGGÿLLLÿNNNÿOOOÿPPPÿUUUÿVVVÿWWWÿZZZÿ^^^ÿ___ÿ___ÿaaaÿdddÿfffÿhhhÿiiiÿmmmÿoooÿpppÿqqqÿvvvÿwwwÿwwwÿxxxÿzzzÿÿ���ÿ‚‚‚ÿ………ÿ‡‡‡ÿˆˆˆÿ‰‰‰ÿ���ÿŽŽŽÿ���ÿ’’’ÿ–––ÿ———ÿ™™™ÿšššÿœœœÿœœœÿ!!!ÿ"""ÿ&&&ÿ(((ÿ(((ÿ***ÿ000ÿ111ÿ000ÿ333ÿ888ÿ777ÿ888ÿ:::ÿ???ÿ@@@ÿ@@@ÿDDDÿGGGÿHHHÿGGGÿKKKÿPPPÿRQRÿPPPÿTTTÿXXXÿZZZÿXXXÿ\\\ÿaaaÿbbbÿbbbÿeeeÿiiiÿjjjÿiiiÿlllÿqqqÿrrrÿqqqÿtttÿyyyÿyyyÿxxxÿ{{{ÿÿ‚‚‚ÿ‚‚‚ÿ„„„ÿ‡‡‡ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ’’’ÿ”””ÿ•••ÿšššÿ›››ÿœœœÿ���ÿ   ÿ   ÿ"""ÿ%%%ÿ(((ÿ***ÿ+++ÿ111ÿ222ÿ333ÿ444ÿ888ÿ999ÿ888ÿ:::ÿ???ÿAAAÿ@@@ÿAAAÿGGGÿIIIÿHHHÿKKKÿOOOÿSSSÿRQRÿUUUÿWWWÿ[[[ÿZZZÿ\\\ÿaaaÿcccÿbbbÿcccÿiiiÿkkkÿjjjÿmmmÿpppÿsssÿrrrÿvvvÿxxxÿ{{{ÿ{{{ÿ{{{ÿ€€€ÿ‚‚‚ÿ„„„ÿ………ÿ†††ÿˆˆˆÿŒŒŒÿ���ÿŽŽŽÿ’’’ÿ“““ÿ•••ÿ–––ÿ›››ÿœœœÿ���ÿžžžÿ¡¡¡ÿ¡¡¡ÿ%%%ÿ(((ÿ)))ÿ,,,ÿ///ÿ111ÿ222ÿ666ÿ777ÿ888ÿ999ÿ;;;ÿ>>>ÿBBBÿDDDÿDDDÿEEEÿGGGÿIIIÿLLLÿOOOÿRQRÿSSSÿUUUÿWWWÿZZZÿ[[[ÿ]]]ÿ^^^ÿcccÿdddÿeeeÿfffÿjjjÿjjjÿmmmÿpppÿrrrÿsssÿvvvÿxxxÿzzzÿ}}}ÿ~~~ÿ~~~ÿ‚‚‚ÿ„„„ÿ………ÿ†††ÿ‡‡‡ÿ‹‹‹ÿ���ÿŽŽŽÿ���ÿ”””ÿ•••ÿ———ÿ™™™ÿ���ÿžžžÿ   ÿ¡¡¡ÿ£££ÿ£££ÿ(((ÿ(((ÿ+++ÿ---ÿ000ÿ111ÿ333ÿ666ÿ888ÿ999ÿ:::ÿ???ÿAAAÿFFFÿFFFÿGGGÿGGGÿHHHÿKKKÿOOOÿRQRÿSSSÿTTTÿWWWÿXXXÿ[[[ÿ\\\ÿ___ÿbbbÿeeeÿfffÿhhhÿhhhÿiiiÿkkkÿpppÿqqqÿsssÿtttÿxxxÿyyyÿ{{{ÿ~~~ÿÿÿ„„„ÿ†††ÿ‡‡‡ÿ‡‡‡ÿˆˆˆÿŒŒŒÿŽŽŽÿ���ÿ���ÿ•••ÿ–––ÿ™™™ÿšššÿžžžÿ   ÿ¡¡¡ÿ¢¢¢ÿ¤¤¤ÿ¤¤¤ÿ(((ÿ***ÿ///ÿ000ÿ111ÿ222ÿ777ÿ888ÿ888ÿ:::ÿ>>>ÿBBBÿFFFÿIIIÿIIIÿIIIÿIIIÿLLLÿOOOÿRQRÿSSSÿTTTÿWWWÿZZZÿZZZÿ\\\ÿ___ÿcccÿeeeÿhhhÿiiiÿiiiÿjjjÿkkkÿpppÿrrrÿrrrÿtttÿxxxÿzzzÿzzzÿ}}}ÿÿ���ÿ‚‚‚ÿ‡‡‡ÿ‰‰‰ÿ‹‹‹ÿ‹‹‹ÿŒŒŒÿ���ÿ’’’ÿ“““ÿ”””ÿ™™™ÿšššÿœœœÿ���ÿ¢¢¢ÿ£££ÿ¤¤¤ÿ¥¥¥ÿ¨¨¨ÿ¨¨¨ÿ***ÿ///ÿ222ÿ333ÿ444ÿ777ÿ999ÿ:::ÿ;;;ÿ>>>ÿBBBÿDDDÿEEEÿHHHÿIIIÿIIIÿMMMÿOOOÿSSSÿTTTÿUUUÿWWWÿ[[[ÿ\\\ÿ]]]ÿ___ÿdddÿeeeÿeeeÿhhhÿiiiÿiiiÿlllÿpppÿsssÿtttÿvvvÿxxxÿ{{{ÿ}}}ÿ~~~ÿÿ���ÿ‚‚‚ÿ„„„ÿˆˆˆÿ‹‹‹ÿŒŒŒÿŒŒŒÿ���ÿ���ÿ“““ÿ”””ÿ•••ÿšššÿ›››ÿ���ÿžžžÿ£££ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ©©©ÿ©©©ÿ///ÿ111ÿ222ÿ444ÿ666ÿ888ÿ999ÿ;;;ÿ>>>ÿ@@@ÿBBBÿDDDÿFFFÿIIIÿKKKÿLLLÿOOOÿRQRÿSSSÿUUUÿWWWÿZZZÿ[[[ÿ]]]ÿ___ÿbbbÿdddÿeeeÿeeeÿiiiÿjjjÿkkkÿoooÿrrrÿrrrÿvvvÿxxxÿzzzÿ}}}ÿ~~~ÿ~~~ÿ€€€ÿ���ÿ„„„ÿ………ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿŽŽŽÿ’’’ÿ”””ÿ•••ÿ–––ÿ›››ÿœœœÿžžžÿ   ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ¨¨¨ÿªªªÿªªªÿ111ÿ222ÿ333ÿ666ÿ777ÿ999ÿ:::ÿ===ÿ???ÿAAAÿBBBÿGGGÿIIIÿMMMÿNNNÿNNNÿOOOÿSSSÿTTTÿVVVÿXXXÿ[[[ÿ\\\ÿ^^^ÿaaaÿcccÿeeeÿfffÿfffÿkkkÿmmmÿmmmÿpppÿrrrÿsssÿwwwÿyyyÿ{{{ÿ~~~ÿÿÿ���ÿ………ÿ‡‡‡ÿ‰‰‰ÿ���ÿŽŽŽÿ���ÿ���ÿ’’’ÿ”””ÿ–––ÿ———ÿ™™™ÿ���ÿžžžÿ¡¡¡ÿ¢¢¢ÿ§¦§ÿ¨¨¨ÿ©©©ÿªªªÿ¬¬¬ÿ¬¬¬ÿ222ÿ333ÿ777ÿ888ÿ888ÿ:::ÿ>>>ÿ???ÿ@@@ÿBBBÿFFFÿKKKÿNNNÿRQRÿRQRÿRQRÿRQRÿTTTÿWWWÿXXXÿZZZÿ\\\ÿ___ÿaaaÿbbbÿdddÿfffÿiiiÿjjjÿmmmÿpppÿpppÿrrrÿsssÿxxxÿyyyÿzzzÿ}}}ÿÿ€€€ÿ���ÿ‚‚‚ÿˆˆˆÿŒŒŒÿŽŽŽÿ’’’ÿ“““ÿ”””ÿ”””ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿ¢¢¢ÿ£££ÿ¥¥¥ÿ§¦§ÿ«««ÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ±±±ÿ±±±ÿ333ÿ777ÿ999ÿ:::ÿ;;;ÿ???ÿ@@@ÿBBBÿDDDÿFFFÿKKKÿLLLÿMMMÿPPPÿRQRÿRQRÿUUUÿWWWÿZZZÿ\\\ÿ]]]ÿaaaÿbbbÿdddÿeeeÿfffÿiiiÿjjjÿkkkÿoooÿpppÿqqqÿtttÿxxxÿzzzÿ}}}ÿ~~~ÿ~~~ÿ€€€ÿ���ÿ„„„ÿˆˆˆÿ���ÿŽŽŽÿŽŽŽÿ’’’ÿ“““ÿ”””ÿ–––ÿ———ÿšššÿœœœÿ���ÿžžžÿ£££ÿ¤¤¤ÿ§¦§ÿ¨¨¨ÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ°°°ÿ²²²ÿ²²²ÿ777ÿ999ÿ999ÿ;;;ÿ===ÿ@@@ÿAAAÿDDDÿFFFÿHHHÿKKKÿLLLÿNNNÿRQRÿTTTÿTTTÿWWWÿXXXÿ[[[ÿ]]]ÿ^^^ÿbbbÿdddÿeeeÿeeeÿhhhÿiiiÿkkkÿlllÿqqqÿrrrÿsssÿwwwÿyyyÿ{{{ÿ~~~ÿ~~~ÿ~~~ÿ€€€ÿ‚‚‚ÿ‡‡‡ÿ‹‹‹ÿ���ÿŽŽŽÿŽŽŽÿ“““ÿ”””ÿ•••ÿ–––ÿ———ÿ›››ÿ���ÿžžžÿ   ÿ¤¤¤ÿ¥¥¥ÿ¨¨¨ÿ©©©ÿ­­­ÿ¯¯¯ÿ°°°ÿ±±±ÿ³³³ÿ³³³ÿ999ÿ999ÿ:::ÿ===ÿ>>>ÿ@@@ÿBBBÿEEEÿGGGÿIIIÿKKKÿOOOÿRQRÿUUUÿVVVÿVVVÿWWWÿZZZÿ]]]ÿ^^^ÿ___ÿbbbÿeeeÿfffÿfffÿiiiÿlllÿoooÿqqqÿtttÿvvvÿvvvÿwwwÿyyyÿ}}}ÿ~~~ÿ~~~ÿÿ‚‚‚ÿ†††ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ���ÿ���ÿ”””ÿ–––ÿ———ÿ———ÿ™™™ÿœœœÿžžžÿ   ÿ¡¡¡ÿ¥¥¥ÿ§¦§ÿ©©©ÿªªªÿ¯¯¯ÿ°°°ÿ±±±ÿ²²²ÿ´´´ÿ´´´ÿ999ÿ:::ÿ>>>ÿ???ÿ@@@ÿAAAÿFFFÿGGGÿHHHÿKKKÿNNNÿSSSÿWWWÿZZZÿZZZÿZZZÿZZZÿ\\\ÿ___ÿaaaÿbbbÿcccÿfffÿhhhÿiiiÿjjjÿpppÿsssÿvvvÿxxxÿyyyÿyyyÿyyyÿ{{{ÿ~~~ÿÿÿ‚‚‚ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ’’’ÿ“““ÿ———ÿšššÿ›››ÿ›››ÿœœœÿ   ÿ¢¢¢ÿ£££ÿ¤¤¤ÿ©©©ÿªªªÿ¬¬¬ÿ­­­ÿ²²²ÿ³³³ÿ´´´ÿ¶¶¶ÿ¸¸¸ÿ¸¸¸ÿ:::ÿ>>>ÿ@@@ÿAAAÿDDDÿFFFÿHHHÿIIIÿKKKÿNNNÿSSSÿTTTÿVVVÿXXXÿZZZÿZZZÿ\\\ÿ___ÿbbbÿcccÿdddÿeeeÿhhhÿiiiÿkkkÿpppÿtttÿtttÿtttÿwwwÿyyyÿyyyÿ{{{ÿ}}}ÿ€€€ÿ€€€ÿ‚‚‚ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿ���ÿŽŽŽÿ’’’ÿ“““ÿ”””ÿ™™™ÿ›››ÿœœœÿœœœÿ���ÿ¡¡¡ÿ£££ÿ¤¤¤ÿ¥¥¥ÿªªªÿ«««ÿ­­­ÿ¯¯¯ÿ³³³ÿ´´´ÿ¶¶¶ÿ···ÿ¹¹¹ÿ¹¹¹ÿ>>>ÿ@@@ÿAAAÿDDDÿEEEÿGGGÿIIIÿLLLÿNNNÿPPPÿSSSÿTTTÿWWWÿZZZÿZZZÿ\\\ÿ]]]ÿaaaÿcccÿdddÿeeeÿhhhÿiiiÿjjjÿoooÿsssÿtttÿtttÿvvvÿxxxÿzzzÿzzzÿ}}}ÿÿ€€€ÿ���ÿ†††ÿ‰‰‰ÿ���ÿ���ÿ���ÿ���ÿ“““ÿ”””ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿžžžÿ¢¢¢ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ«««ÿ¬¬¬ÿ¯¯¯ÿ°°°ÿ³³³ÿ´´´ÿ¶¶¶ÿ···ÿ¹¹¹ÿ¹¹¹ÿ@@@ÿAAAÿDDDÿEEEÿGGGÿIIIÿKKKÿMMMÿOOOÿRQRÿSSSÿVVVÿWWWÿZZZÿ[[[ÿ^^^ÿaaaÿdddÿfffÿhhhÿhhhÿkkkÿlllÿmmmÿqqqÿvvvÿwwwÿxxxÿxxxÿ{{{ÿ~~~ÿÿ€€€ÿ„„„ÿ„„„ÿ………ÿˆˆˆÿŒŒŒÿŽŽŽÿ���ÿ���ÿ“““ÿ•••ÿ–––ÿ———ÿ���ÿ   ÿ¡¡¡ÿ¡¡¡ÿ¢¢¢ÿ¥¥¥ÿ¨¨¨ÿ©©©ÿªªªÿ­­­ÿ¯¯¯ÿ±±±ÿ²²²ÿ···ÿ¸¸¸ÿ¹¹¹ÿºººÿ½½½ÿ½½½ÿAAAÿBBBÿFFFÿHHHÿHHHÿKKKÿNNNÿPPPÿRQRÿSSSÿVVVÿXXXÿXXXÿ[[[ÿ^^^ÿbbbÿeeeÿhhhÿjjjÿkkkÿlllÿpppÿqqqÿrrrÿtttÿyyyÿzzzÿ{{{ÿ}}}ÿ€€€ÿ‚‚‚ÿ‚‚‚ÿ………ÿ‡‡‡ÿˆˆˆÿˆˆˆÿŒŒŒÿ���ÿ’’’ÿ“““ÿ”””ÿ———ÿšššÿ›››ÿœœœÿ¡¡¡ÿ£££ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿªªªÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ²²²ÿ³³³ÿ¶¶¶ÿ···ÿºººÿ»»»ÿ½½½ÿ¾¾¾ÿÀÀÀÿÀÀÀÿBBBÿFFFÿHHHÿKKKÿLLLÿOOOÿPPPÿSSSÿTTTÿVVVÿZZZÿZZZÿZZZÿ^^^ÿbbbÿdddÿeeeÿiiiÿkkkÿlllÿmmmÿqqqÿrrrÿsssÿvvvÿzzzÿ{{{ÿ}}}ÿ~~~ÿ���ÿ„„„ÿ„„„ÿ†††ÿˆˆˆÿ‰‰‰ÿ‰‰‰ÿ���ÿ���ÿ“““ÿ”””ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿ¢¢¢ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ¨¨¨ÿ«««ÿ­­­ÿ¯¯¯ÿ°°°ÿ³³³ÿ´´´ÿ···ÿ¸¸¸ÿ»»»ÿ½½½ÿ¾¾¾ÿ¿¿¿ÿÁÁÁÿÁÁÁÿFFFÿHHHÿIIIÿLLLÿMMMÿPPPÿRQRÿTTTÿVVVÿXXXÿZZZÿ[[[ÿ]]]ÿbbbÿcccÿdddÿfffÿiiiÿkkkÿlllÿmmmÿqqqÿrrrÿsssÿvvvÿzzzÿ{{{ÿ}}}ÿ~~~ÿ���ÿ„„„ÿ„„„ÿ†††ÿˆˆˆÿ‰‰‰ÿ‰‰‰ÿ���ÿ���ÿ“““ÿ”””ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿ¢¢¢ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ¨¨¨ÿ«««ÿ­­­ÿ¯¯¯ÿ°°°ÿ³³³ÿ´´´ÿ···ÿ¸¸¸ÿ»»»ÿ½½½ÿ¾¾¾ÿ¿¿¿ÿÁÁÁÿÁÁÁÿHHHÿIIIÿLLLÿMMMÿNNNÿPPPÿSSSÿUUUÿWWWÿZZZÿ[[[ÿ^^^ÿbbbÿfffÿfffÿfffÿiiiÿkkkÿmmmÿoooÿpppÿsssÿtttÿvvvÿxxxÿ}}}ÿ~~~ÿÿ€€€ÿ………ÿ†††ÿ‡‡‡ÿˆˆˆÿŒŒŒÿŒŒŒÿ���ÿ���ÿ”””ÿ–––ÿ———ÿ™™™ÿœœœÿžžžÿ   ÿ¡¡¡ÿ¥¥¥ÿ¨¨¨ÿ©©©ÿªªªÿ«««ÿ¯¯¯ÿ±±±ÿ²²²ÿ³³³ÿ···ÿ¸¸¸ÿºººÿ»»»ÿ¿¿¿ÿÀÀÀÿÁÁÁÿÂÂÂÿÅÅÅÿÅÅÅÿIIIÿKKKÿNNNÿOOOÿPPPÿRQRÿVVVÿWWWÿXXXÿ[[[ÿ^^^ÿbbbÿfffÿjjjÿkkkÿkkkÿlllÿoooÿqqqÿrrrÿsssÿwwwÿxxxÿyyyÿ{{{ÿ€€€ÿ���ÿ‚‚‚ÿ„„„ÿˆˆˆÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ���ÿ”””ÿ———ÿšššÿ›››ÿœœœÿ   ÿ¢¢¢ÿ£££ÿ¤¤¤ÿ©©©ÿ«««ÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ²²²ÿ´´´ÿ¶¶¶ÿ···ÿºººÿ»»»ÿ¾¾¾ÿ¿¿¿ÿÂÂÂÿÄÄÄÿÅÅÅÿÆÆÆÿÈÈÈÿÈÈÈÿKKKÿNNNÿPPPÿSSSÿTTTÿWWWÿXXXÿZZZÿ\\\ÿ^^^ÿbbbÿcccÿfffÿkkkÿlllÿlllÿmmmÿpppÿrrrÿsssÿtttÿxxxÿyyyÿzzzÿ}}}ÿ���ÿ‚‚‚ÿ„„„ÿ………ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ���ÿ’’’ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿ¡¡¡ÿ£££ÿ¤¤¤ÿ¥¥¥ÿªªªÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ°°°ÿ³³³ÿ¶¶¶ÿ···ÿ¸¸¸ÿ»»»ÿ½½½ÿ¿¿¿ÿÀÀÀÿÄÄÄÿÅÅÅÿÆÆÆÿÇÇÇÿÉÉÉÿÉÉÉÿNNNÿPPPÿRQRÿTTTÿUUUÿXXXÿZZZÿ[[[ÿ^^^ÿbbbÿcccÿdddÿfffÿkkkÿlllÿlllÿmmmÿpppÿrrrÿsssÿtttÿxxxÿyyyÿzzzÿ}}}ÿ���ÿ‚‚‚ÿ„„„ÿ………ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ���ÿ’’’ÿ•••ÿ™™™ÿ›››ÿœœœÿ���ÿ¡¡¡ÿ£££ÿ¤¤¤ÿ¥¥¥ÿªªªÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ°°°ÿ³³³ÿ¶¶¶ÿ···ÿ¸¸¸ÿ»»»ÿ½½½ÿ¿¿¿ÿÀÀÀÿÄÄÄÿÅÅÅÿÆÆÆÿÇÇÇÿÉÉÉÿÉÉÉÿPPPÿRQRÿTTTÿUUUÿVVVÿXXXÿ[[[ÿ^^^ÿ___ÿcccÿdddÿfffÿiiiÿlllÿoooÿoooÿpppÿrrrÿvvvÿwwwÿxxxÿ{{{ÿ}}}ÿ~~~ÿ€€€ÿ………ÿ†††ÿ‡‡‡ÿˆˆˆÿ���ÿŽŽŽÿ���ÿ���ÿ”””ÿ”””ÿ•••ÿ™™™ÿœœœÿžžžÿ   ÿ¡¡¡ÿ¤¤¤ÿ§¦§ÿ¨¨¨ÿ©©©ÿ­­­ÿ°°°ÿ±±±ÿ²²²ÿ³³³ÿ···ÿ¹¹¹ÿºººÿ»»»ÿ¿¿¿ÿÀÀÀÿÂÂÂÿÄÄÄÿÇÇÇÿÈÈÈÿÉÉÉÿËËËÿÍÍÍÿÍÍÍÿRQRÿSSSÿVVVÿWWWÿXXXÿZZZÿ^^^ÿbbbÿbbbÿdddÿhhhÿjjjÿlllÿpppÿrrrÿrrrÿsssÿvvvÿyyyÿzzzÿ{{{ÿÿ€€€ÿ���ÿ„„„ÿˆˆˆÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ’’’ÿ“““ÿ”””ÿ———ÿ———ÿ™™™ÿœœœÿ   ÿ¢¢¢ÿ£££ÿ¤¤¤ÿ¨¨¨ÿªªªÿ«««ÿ¬¬¬ÿ±±±ÿ³³³ÿ´´´ÿ¶¶¶ÿ···ÿºººÿ½½½ÿ¾¾¾ÿ¿¿¿ÿÂÂÂÿÄÄÄÿÆÆÆÿÇÇÇÿËËËÿÌÌÌÿÍÍÍÿÎÎÎÿÐÐÐÿÐÐÐÿSSSÿVVVÿXXXÿZZZÿ\\\ÿ^^^ÿbbbÿcccÿeeeÿhhhÿkkkÿmmmÿoooÿrrrÿsssÿsssÿvvvÿyyyÿ{{{ÿ}}}ÿ~~~ÿ���ÿ‚‚‚ÿ„„„ÿ†††ÿ‹‹‹ÿŒŒŒÿ���ÿŽŽŽÿ’’’ÿ”””ÿ”””ÿ———ÿšššÿ›››ÿ›››ÿžžžÿ¢¢¢ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿªªªÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ³³³ÿ¶¶¶ÿ···ÿ···ÿ¸¸¸ÿ½½½ÿ¿¿¿ÿÀÀÀÿÁÁÁÿÅÅÅÿÆÆÆÿÈÈÈÿÉÉÉÿÍÍÍÿÎÎÎÿÏÏÏÿÐÐÐÿÓÓÓÿÓÓÓÿVVVÿXXXÿZZZÿ[[[ÿ^^^ÿbbbÿcccÿdddÿhhhÿkkkÿlllÿmmmÿoooÿrrrÿsssÿsssÿvvvÿyyyÿ{{{ÿ}}}ÿ~~~ÿ���ÿ‚‚‚ÿ„„„ÿ†††ÿ‹‹‹ÿŒŒŒÿ���ÿŽŽŽÿ’’’ÿ”””ÿ”””ÿ———ÿšššÿ›››ÿ›››ÿžžžÿ£££ÿ¥¥¥ÿ¥¥¥ÿ§¦§ÿªªªÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ³³³ÿ¶¶¶ÿ···ÿ···ÿ¸¸¸ÿ½½½ÿ¿¿¿ÿÀÀÀÿÁÁÁÿÅÅÅÿÆÆÆÿÈÈÈÿÉÉÉÿÍÍÍÿÎÎÎÿÏÏÏÿÐÐÐÿÓÓÓÿÓÓÓÿXXXÿZZZÿ[[[ÿ^^^ÿ___ÿcccÿdddÿhhhÿiiiÿlllÿlllÿmmmÿoooÿsssÿtttÿvvvÿwwwÿyyyÿ{{{ÿ~~~ÿÿ‚‚‚ÿ„„„ÿ………ÿ‡‡‡ÿŒŒŒÿ���ÿŽŽŽÿ���ÿ”””ÿ–––ÿ———ÿ———ÿ›››ÿœœœÿœœœÿ   ÿ¤¤¤ÿ§¦§ÿ§¦§ÿ¨¨¨ÿ«««ÿ­­­ÿ¯¯¯ÿ°°°ÿ´´´ÿ¸¸¸ÿ¹¹¹ÿºººÿ»»»ÿ¾¾¾ÿÀÀÀÿÁÁÁÿÂÂÂÿÆÆÆÿÇÇÇÿÉÉÉÿËËËÿÎÎÎÿÏÏÏÿÐÐÐÿÒÒÒÿÔÔÔÿÔÔÔÿZZZÿ[[[ÿ^^^ÿbbbÿbbbÿdddÿhhhÿkkkÿkkkÿlllÿoooÿqqqÿrrrÿwwwÿwwwÿxxxÿyyyÿ}}}ÿ~~~ÿ���ÿ‚‚‚ÿ†††ÿ‡‡‡ÿˆˆˆÿ‹‹‹ÿ���ÿ���ÿ’’’ÿ“““ÿ–––ÿ™™™ÿšššÿšššÿ���ÿžžžÿ   ÿ£££ÿ¨¨¨ÿªªªÿªªªÿ«««ÿ¯¯¯ÿ±±±ÿ²²²ÿ³³³ÿ¸¸¸ÿºººÿ»»»ÿ¾¾¾ÿ¿¿¿ÿÁÁÁÿÄÄÄÿÅÅÅÿÆÆÆÿÉÉÉÿËËËÿÍÍÍÿÎÎÎÿÒÒÒÿÓÓÓÿÔÔÔÿÕÕÕÿ×××ÿ×××ÿ[[[ÿ^^^ÿbbbÿcccÿeeeÿhhhÿkkkÿmmmÿmmmÿpppÿqqqÿsssÿtttÿxxxÿzzzÿzzzÿ}}}ÿÿ‚‚‚ÿ„„„ÿ………ÿˆˆˆÿ‰‰‰ÿ‹‹‹ÿ���ÿ’’’ÿ“““ÿ”””ÿ•••ÿšššÿ›››ÿœœœÿžžžÿ¢¢¢ÿ¢¢¢ÿ£££ÿ§¦§ÿ«««ÿ­­­ÿ­­­ÿ¯¯¯ÿ²²²ÿ´´´ÿ¶¶¶ÿ···ÿ»»»ÿ½½½ÿ¾¾¾ÿÀÀÀÿÁÁÁÿÅÅÅÿÇÇÇÿÈÈÈÿÉÉÉÿÍÍÍÿÎÎÎÿÐÐÐÿÒÒÒÿÕÕÕÿÖÖÖÿ×××ÿÙÙÙÿÛÛÛÿÛÛÛÿ^^^ÿbbbÿcccÿdddÿhhhÿkkkÿmmmÿoooÿoooÿqqqÿsssÿtttÿvvvÿyyyÿzzzÿzzzÿ}}}ÿ€€€ÿ‚‚‚ÿ………ÿ†††ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ“““ÿ”””ÿ•••ÿ–––ÿšššÿ›››ÿœœœÿžžžÿ¢¢¢ÿ¢¢¢ÿ£££ÿ§¦§ÿ«««ÿ­­­ÿ­­­ÿ¯¯¯ÿ²²²ÿ´´´ÿ¶¶¶ÿ···ÿ»»»ÿ½½½ÿ¾¾¾ÿÀÀÀÿÁÁÁÿÅÅÅÿÇÇÇÿÈÈÈÿÉÉÉÿÍÍÍÿÎÎÎÿÐÐÐÿÒÒÒÿÕÕÕÿÖÖÖÿ×××ÿÙÙÙÿÛÛÛÿÛÛÛÿbbbÿcccÿdddÿhhhÿjjjÿlllÿoooÿpppÿqqqÿsssÿtttÿvvvÿwwwÿ{{{ÿ~~~ÿÿÿ���ÿ………ÿ†††ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ”””ÿ•••ÿ–––ÿ———ÿ���ÿžžžÿ   ÿ¡¡¡ÿ¤¤¤ÿ¤¤¤ÿ¥¥¥ÿ©©©ÿ­­­ÿ°°°ÿ°°°ÿ±±±ÿ´´´ÿ···ÿ¸¸¸ÿ¹¹¹ÿ¾¾¾ÿÁÁÁÿÂÂÂÿÁÁÁÿÂÂÂÿÆÆÆÿÈÈÈÿÉÉÉÿËËËÿÎÎÎÿÏÏÏÿÒÒÒÿÓÓÓÿÖÖÖÿ×××ÿÙÙÙÿÚÚÚÿÜÜÜÿÜÜÜÿcccÿdddÿhhhÿkkkÿkkkÿmmmÿpppÿqqqÿrrrÿsssÿxxxÿyyyÿzzzÿÿ€€€ÿ���ÿ���ÿ………ÿ‡‡‡ÿ‰‰‰ÿ‹‹‹ÿŽŽŽÿ���ÿ���ÿ“““ÿ———ÿ™™™ÿšššÿ›››ÿ   ÿ¡¡¡ÿ¢¢¢ÿ£££ÿ§¦§ÿ§¦§ÿ¨¨¨ÿ«««ÿ°°°ÿ²²²ÿ²²²ÿ³³³ÿ···ÿ¹¹¹ÿºººÿ»»»ÿÀÀÀÿÄÄÄÿÅÅÅÿÄÄÄÿÅÅÅÿÈÈÈÿËËËÿÌÌÌÿÍÍÍÿÐÐÐÿÒÒÒÿÔÔÔÿÕÕÕÿÙÙÙÿÚÚÚÿÛÛÛÿÜÜÜÿÞÞÞÿÞÞÞÿdddÿhhhÿkkkÿmmmÿoooÿoooÿrrrÿsssÿvvvÿxxxÿzzzÿ{{{ÿ}}}ÿ€€€ÿ„„„ÿ„„„ÿ………ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿ���ÿ���ÿ’’’ÿ“““ÿ•••ÿšššÿ›››ÿœœœÿ���ÿ¢¢¢ÿ¤¤¤ÿ¤¤¤ÿ§¦§ÿ©©©ÿ«««ÿ¬¬¬ÿ¯¯¯ÿ³³³ÿ¶¶¶ÿ¶¶¶ÿ···ÿºººÿ½½½ÿ¾¾¾ÿ¿¿¿ÿÄÄÄÿÅÅÅÿÆÆÆÿÈÈÈÿÉÉÉÿÌÌÌÿÎÎÎÿÏÏÏÿÐÐÐÿÔÔÔÿÕÕÕÿ×××ÿÙÙÙÿÜÜÜÿÝÝÝÿÞÞÞÿàààÿâââÿâââÿhhhÿkkkÿmmmÿoooÿoooÿpppÿrrrÿtttÿwwwÿzzzÿ{{{ÿ}}}ÿÿ‚‚‚ÿ„„„ÿ„„„ÿ†††ÿ‰‰‰ÿ‹‹‹ÿ���ÿŽŽŽÿ’’’ÿ“““ÿ”””ÿ–––ÿ›››ÿœœœÿ���ÿžžžÿ¢¢¢ÿ¤¤¤ÿ¤¤¤ÿ§¦§ÿ©©©ÿ«««ÿ¬¬¬ÿ¯¯¯ÿ³³³ÿ¶¶¶ÿ¶¶¶ÿ···ÿºººÿ½½½ÿ¾¾¾ÿ¿¿¿ÿÄÄÄÿÅÅÅÿÆÆÆÿÈÈÈÿÉÉÉÿÌÌÌÿÎÎÎÿÏÏÏÿÐÐÐÿÔÔÔÿÕÕÕÿ×××ÿÙÙÙÿÜÜÜÿÝÝÝÿÞÞÞÿàààÿâââÿâââÿkkkÿlllÿoooÿoooÿoooÿqqqÿsssÿwwwÿxxxÿzzzÿ{{{ÿ}}}ÿ€€€ÿ………ÿ†††ÿ‡‡‡ÿˆˆˆÿ‹‹‹ÿŽŽŽÿŽŽŽÿ���ÿ“““ÿ”””ÿ•••ÿ———ÿœœœÿ���ÿžžžÿ   ÿ¤¤¤ÿ§¦§ÿ¨¨¨ÿ©©©ÿ¬¬¬ÿ¬¬¬ÿ­­­ÿ°°°ÿ´´´ÿ···ÿ···ÿ¸¸¸ÿ»»»ÿ¾¾¾ÿ¿¿¿ÿÀÀÀÿÅÅÅÿÇÇÇÿÈÈÈÿÉÉÉÿËËËÿÍÍÍÿÏÏÏÿÐÐÐÿÒÒÒÿÕÕÕÿÖÖÖÿÙÙÙÿÚÚÚÿÝÝÝÿÞÞÞÿàààÿáááÿãããÿãããÿlllÿmmmÿoooÿpppÿqqqÿsssÿwwwÿzzzÿzzzÿzzzÿ~~~ÿÿ„„„ÿ‡‡‡ÿ‰‰‰ÿ‰‰‰ÿŒŒŒÿ���ÿ’’’ÿ’’’ÿ“““ÿ–––ÿ———ÿ™™™ÿ›››ÿ   ÿ¡¡¡ÿ¢¢¢ÿ£££ÿ¨¨¨ÿ©©©ÿªªªÿ¬¬¬ÿ°°°ÿ¯¯¯ÿ°°°ÿ³³³ÿ¸¸¸ÿºººÿºººÿ»»»ÿ¿¿¿ÿÁÁÁÿÂÂÂÿÄÄÄÿÈÈÈÿËËËÿÌÌÌÿÌÌÌÿÍÍÍÿÐÐÐÿÓÓÓÿÔÔÔÿÕÕÕÿÙÙÙÿÚÚÚÿÜÜÜÿÝÝÝÿáááÿâââÿãããÿäääÿçççÿçççÿoooÿoooÿqqqÿrrrÿtttÿwwwÿzzzÿ}}}ÿ}}}ÿ~~~ÿ€€€ÿ���ÿ………ÿ‰‰‰ÿŒŒŒÿŒŒŒÿ���ÿ���ÿ“““ÿ”””ÿ•••ÿ™™™ÿšššÿ›››ÿ���ÿ¢¢¢ÿ£££ÿ¤¤¤ÿ¥¥¥ÿªªªÿ«««ÿ¬¬¬ÿ­­­ÿ±±±ÿ±±±ÿ²²²ÿ¶¶¶ÿºººÿ½½½ÿ½½½ÿ¾¾¾ÿÁÁÁÿÄÄÄÿÅÅÅÿÆÆÆÿËËËÿÍÍÍÿÎÎÎÿÎÎÎÿÏÏÏÿÓÓÓÿÕÕÕÿÖÖÖÿ×××ÿÛÛÛÿÜÜÜÿÞÞÞÿàààÿãããÿäääÿåååÿçççÿéééÿéééÿpppÿqqqÿrrrÿsssÿwwwÿzzzÿ}}}ÿ~~~ÿ~~~ÿÿ���ÿ‚‚‚ÿ†††ÿ‹‹‹ÿŒŒŒÿŒŒŒÿŽŽŽÿ���ÿ”””ÿ•••ÿ–––ÿšššÿ›››ÿœœœÿžžžÿ£££ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿªªªÿ¬¬¬ÿ¬¬¬ÿ¯¯¯ÿ²²²ÿ²²²ÿ³³³ÿ···ÿ»»»ÿ¾¾¾ÿ¾¾¾ÿ¿¿¿ÿÂÂÂÿÅÅÅÿÆÆÆÿÇÇÇÿÌÌÌÿÍÍÍÿÎÎÎÿÏÏÏÿÐÐÐÿÔÔÔÿÖÖÖÿ×××ÿÙÙÙÿÜÜÜÿÝÝÝÿàààÿáááÿäääÿåååÿçççÿèèèÿêêêÿêêêÿpppÿrrrÿsssÿwwwÿxxxÿ{{{ÿ~~~ÿ~~~ÿ~~~ÿ€€€ÿ‚‚‚ÿ†††ÿ‰‰‰ÿ���ÿ���ÿ���ÿ’’’ÿ”””ÿ–––ÿ———ÿ™™™ÿœœœÿ���ÿžžžÿ¡¡¡ÿ¥¥¥ÿ§¦§ÿ¨¨¨ÿ©©©ÿ­­­ÿ¯¯¯ÿ°°°ÿ²²²ÿ´´´ÿ¶¶¶ÿ···ÿ¹¹¹ÿ¾¾¾ÿÀÀÀÿÀÀÀÿÁÁÁÿÅÅÅÿÇÇÇÿÈÈÈÿÉÉÉÿÎÎÎÿÐÐÐÿÒÒÒÿÓÓÓÿÔÔÔÿÖÖÖÿÙÙÙÿÚÚÚÿÛÛÛÿÞÞÞÿàààÿâââÿãããÿçççÿèèèÿéééÿêêêÿìììÿìììÿqqqÿsssÿwwwÿzzzÿzzzÿ}}}ÿ~~~ÿÿ€€€ÿ‚‚‚ÿ†††ÿ‰‰‰ÿŽŽŽÿ’’’ÿ”””ÿ”””ÿ•••ÿ———ÿ›››ÿœœœÿ���ÿ¡¡¡ÿ¢¢¢ÿ£££ÿ¥¥¥ÿªªªÿ«««ÿ¬¬¬ÿ­­­ÿ²²²ÿ³³³ÿ´´´ÿ¶¶¶ÿ¹¹¹ÿ¹¹¹ÿºººÿ¾¾¾ÿÂÂÂÿÅÅÅÿÅÅÅÿÆÆÆÿÉÉÉÿÌÌÌÿÍÍÍÿÎÎÎÿÓÓÓÿÕÕÕÿÖÖÖÿÖÖÖÿ×××ÿÛÛÛÿÝÝÝÿÞÞÞÿàààÿãããÿäääÿçççÿèèèÿëëëÿìììÿîîîÿïïïÿñññÿñññÿsssÿwwwÿzzzÿ}}}ÿ~~~ÿ~~~ÿ€€€ÿ���ÿ„„„ÿ†††ÿ‰‰‰ÿ‹‹‹ÿŽŽŽÿ“““ÿ•••ÿ•••ÿ–––ÿ™™™ÿœœœÿ���ÿžžžÿ¢¢¢ÿ£££ÿ¤¤¤ÿ§¦§ÿ«««ÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ³³³ÿ´´´ÿ¶¶¶ÿ···ÿºººÿºººÿ»»»ÿ¿¿¿ÿÄÄÄÿÆÆÆÿÆÆÆÿÇÇÇÿËËËÿÍÍÍÿÎÎÎÿÏÏÏÿÔÔÔÿÖÖÖÿ×××ÿ×××ÿÙÙÙÿÜÜÜÿÞÞÞÿàààÿáááÿäääÿåååÿèèèÿéééÿìììÿîîîÿïïïÿðððÿòòòÿòòòÿwwwÿzzzÿ}}}ÿ~~~ÿ~~~ÿÿ���ÿ‚‚‚ÿ†††ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ“““ÿ•••ÿ•••ÿ–––ÿ™™™ÿœœœÿ���ÿžžžÿ¢¢¢ÿ£££ÿ¤¤¤ÿ§¦§ÿ«««ÿ¬¬¬ÿ­­­ÿ¯¯¯ÿ³³³ÿ´´´ÿ¶¶¶ÿ···ÿºººÿºººÿ»»»ÿ¿¿¿ÿÄÄÄÿÆÆÆÿÆÆÆÿÇÇÇÿËËËÿÍÍÍÿÎÎÎÿÏÏÏÿÔÔÔÿÖÖÖÿ×××ÿ×××ÿÙÙÙÿÜÜÜÿÞÞÞÿàààÿáááÿäääÿåååÿèèèÿéééÿìììÿîîîÿïïïÿðððÿòòòÿòòòÿzzzÿ{{{ÿ~~~ÿ~~~ÿ~~~ÿ€€€ÿ‚‚‚ÿ†††ÿ‡‡‡ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ���ÿ”””ÿ–––ÿ–––ÿ™™™ÿ›››ÿžžžÿ   ÿ¡¡¡ÿ¤¤¤ÿ¥¥¥ÿ§¦§ÿ©©©ÿ­­­ÿ¯¯¯ÿ°°°ÿ±±±ÿ¶¶¶ÿ···ÿ¸¸¸ÿ¹¹¹ÿ½½½ÿ½½½ÿ¾¾¾ÿÁÁÁÿÆÆÆÿÈÈÈÿÈÈÈÿÉÉÉÿÍÍÍÿÏÏÏÿÐÐÐÿÒÒÒÿÖÖÖÿÙÙÙÿÚÚÚÿÚÚÚÿÛÛÛÿÞÞÞÿáááÿâââÿãããÿçççÿèèèÿêêêÿëëëÿïïïÿðððÿñññÿòòòÿõõõÿõõõÿ{{{ÿ}}}ÿ~~~ÿÿ€€€ÿ‚‚‚ÿ†††ÿ‰‰‰ÿ‰‰‰ÿŒŒŒÿ���ÿ’’’ÿ”””ÿ———ÿšššÿšššÿœœœÿžžžÿ¢¢¢ÿ£££ÿ¤¤¤ÿ¨¨¨ÿ©©©ÿªªªÿ¬¬¬ÿ±±±ÿ²²²ÿ³³³ÿ´´´ÿ¹¹¹ÿºººÿ»»»ÿ½½½ÿÀÀÀÿÀÀÀÿÁÁÁÿÅÅÅÿÉÉÉÿÌÌÌÿÌÌÌÿÍÍÍÿÐÐÐÿÓÓÓÿÔÔÔÿÕÕÕÿÚÚÚÿÜÜÜÿÝÝÝÿÝÝÝÿÞÞÞÿâââÿäääÿåååÿçççÿêêêÿëëëÿîîîÿïïïÿòòòÿóóóÿõõõÿöööÿøøøÿøøøÿ}}}ÿ~~~ÿ€€€ÿ���ÿ‚‚‚ÿ†††ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿ���ÿ“““ÿ”””ÿ•••ÿ™™™ÿ›››ÿ›››ÿ���ÿ   ÿ£££ÿ¤¤¤ÿ¥¥¥ÿ©©©ÿªªªÿ«««ÿ­­­ÿ²²²ÿ³³³ÿ´´´ÿ¶¶¶ÿºººÿ»»»ÿ½½½ÿ¾¾¾ÿÁÁÁÿÁÁÁÿÂÂÂÿÆÆÆÿËËËÿÍÍÍÿÍÍÍÿÎÎÎÿÒÒÒÿÔÔÔÿÕÕÕÿÖÖÖÿÛÛÛÿÝÝÝÿÞÞÞÿÞÞÞÿàààÿãããÿåååÿçççÿèèèÿëëëÿìììÿïïïÿðððÿóóóÿõõõÿöööÿ÷÷÷ÿùùùÿùùùÿ~~~ÿ€€€ÿ���ÿ‚‚‚ÿ………ÿ‰‰‰ÿ‹‹‹ÿŒŒŒÿŽŽŽÿ“““ÿ”””ÿ”””ÿ•••ÿ™™™ÿ›››ÿ›››ÿ���ÿ   ÿ£££ÿ¤¤¤ÿ¥¥¥ÿ©©©ÿªªªÿ«««ÿ­­­ÿ²²²ÿ³³³ÿ´´´ÿ¶¶¶ÿºººÿ»»»ÿ½½½ÿ¾¾¾ÿÁÁÁÿÁÁÁÿÂÂÂÿÆÆÆÿËËËÿÍÍÍÿÍÍÍÿÎÎÎÿÒÒÒÿÔÔÔÿÕÕÕÿÖÖÖÿÛÛÛÿÝÝÝÿÞÞÞÿÞÞÞÿàààÿãããÿåååÿçççÿèèèÿëëëÿìììÿïïïÿðððÿóóóÿõõõÿöööÿ÷÷÷ÿùùùÿùùùÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/input.vp8 new file mode 100644 index 0000000..9adfecd Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/meta.toml new file mode 100644 index 0000000..cd23413 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/full_gradient_64x64/meta.toml @@ -0,0 +1,5 @@ +feature = "mb-aligned" +level = "must" +width = 64 +height = 64 +note = "64x64 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/expected.rgba new file mode 100644 index 0000000..56dbde0 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/input.vp8 new file mode 100644 index 0000000..1d9a016 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/meta.toml new file mode 100644 index 0000000..22a584c --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/full_noise_64x64/meta.toml @@ -0,0 +1,5 @@ +feature = "mb-aligned-detail" +level = "must" +width = 64 +height = 64 +note = "64x64 synthetic source; input.vp8 via cwebp 1.6.0 `-q 30 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/expected.rgba new file mode 100644 index 0000000..eb669f3 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/input.vp8 new file mode 100644 index 0000000..eb5488c Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/meta.toml new file mode 100644 index 0000000..b803055 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_noise_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "intra-4x4" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 30 -m 6 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/expected.rgba new file mode 100644 index 0000000..73305ee --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/expected.rgba @@ -0,0 +1 @@ +áááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿáááÿÞÞÞÿÿÿÞÞÞÿÜÜÜÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿÝÝÝÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿÝÝÝÿÛÛÛÿÿÿáááÿâââÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿàààÿáááÿÿÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/input.vp8 new file mode 100644 index 0000000..41bf82d Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/meta.toml new file mode 100644 index 0000000..aa05f99 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/i4x4_stripe_48x48/meta.toml @@ -0,0 +1,5 @@ +feature = "intra-4x4" +level = "must" +width = 48 +height = 48 +note = "48x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 40 -m 6 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/expected.rgba new file mode 100644 index 0000000..efe4003 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/expected.rgba @@ -0,0 +1 @@ +a€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿb¡ÿa€¡ÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€Ÿÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿ_�¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿa€¡ÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/input.vp8 new file mode 100644 index 0000000..ff151c3 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/meta.toml new file mode 100644 index 0000000..cf8d836 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/intra16dc_solid_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "intra16-dc" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/expected.rgba new file mode 100644 index 0000000..3234877 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/input.vp8 new file mode 100644 index 0000000..415a530 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/meta.toml new file mode 100644 index 0000000..e8e69d0 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/intra16h_vgrad_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "intra16-h" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/expected.rgba new file mode 100644 index 0000000..ba525b2 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/input.vp8 new file mode 100644 index 0000000..18ac93e Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/meta.toml new file mode 100644 index 0000000..e83a5ee --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/intra16tm_dgrad_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "intra16-tm" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/expected.rgba new file mode 100644 index 0000000..9350b54 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/input.vp8 new file mode 100644 index 0000000..9174c20 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/meta.toml new file mode 100644 index 0000000..79821ef --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/intra16v_hgrad_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "intra16-v" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 92 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/expected.rgba new file mode 100644 index 0000000..88392a3 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/input.vp8 new file mode 100644 index 0000000..ffb753c Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/meta.toml new file mode 100644 index 0000000..8d9e0f6 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/method0_fast_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "fast-method" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -m 0 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/expected.rgba new file mode 100644 index 0000000..ca90f46 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/input.vp8 new file mode 100644 index 0000000..f6770df Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/meta.toml new file mode 100644 index 0000000..f440109 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/method6_trellis_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "trellis" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -m 6 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/expected.rgba new file mode 100644 index 0000000..0bd4a82 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/input.vp8 new file mode 100644 index 0000000..bb3693e Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/meta.toml new file mode 100644 index 0000000..388a629 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_3x64/meta.toml @@ -0,0 +1,5 @@ +feature = "narrow" +level = "must" +width = 3 +height = 64 +note = "3x64 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/expected.rgba new file mode 100644 index 0000000..0e70ab9 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/input.vp8 new file mode 100644 index 0000000..85f6bd2 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/meta.toml new file mode 100644 index 0000000..9524976 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/narrow_64x3/meta.toml @@ -0,0 +1,5 @@ +feature = "narrow" +level = "must" +width = 64 +height = 3 +note = "64x3 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/expected.rgba new file mode 100644 index 0000000..1e7f441 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/input.vp8 new file mode 100644 index 0000000..73d2413 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/meta.toml new file mode 100644 index 0000000..4047ceb --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q100_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 100 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/expected.rgba new file mode 100644 index 0000000..cba86d6 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/input.vp8 new file mode 100644 index 0000000..6f37f2c Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/meta.toml new file mode 100644 index 0000000..f2dddef --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q10_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 10 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/expected.rgba new file mode 100644 index 0000000..41312d8 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/input.vp8 new file mode 100644 index 0000000..e05cbe0 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/meta.toml new file mode 100644 index 0000000..85b2fb4 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q30_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 30 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/expected.rgba new file mode 100644 index 0000000..d41af6b Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/input.vp8 new file mode 100644 index 0000000..2bdc433 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/meta.toml new file mode 100644 index 0000000..3bac456 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q50_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/expected.rgba new file mode 100644 index 0000000..906e698 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/input.vp8 new file mode 100644 index 0000000..28dea22 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/meta.toml new file mode 100644 index 0000000..09895c7 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q75_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/expected.rgba new file mode 100644 index 0000000..b7f1daf Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/input.vp8 new file mode 100644 index 0000000..e97cb77 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/meta.toml new file mode 100644 index 0000000..bf72e68 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/q90_color_32x32/meta.toml @@ -0,0 +1,5 @@ +feature = "quantizer" +level = "must" +width = 32 +height = 32 +note = "32x32 synthetic source; input.vp8 via cwebp 1.6.0 `-q 90 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/expected.rgba new file mode 100644 index 0000000..8ea3b90 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/input.vp8 new file mode 100644 index 0000000..7317bfa Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/meta.toml new file mode 100644 index 0000000..4c2eab0 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/row_63x1/meta.toml @@ -0,0 +1,5 @@ +feature = "row-edge" +level = "must" +width = 63 +height = 1 +note = "63x1 synthetic source; input.vp8 via cwebp 1.6.0 `-q 75 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/expected.rgba new file mode 100644 index 0000000..2389da5 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/input.vp8 new file mode 100644 index 0000000..1aa1397 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/meta.toml new file mode 100644 index 0000000..92a6a5b --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/seg_multi_64x48/meta.toml @@ -0,0 +1,5 @@ +feature = "segmentation-multi" +level = "must" +width = 64 +height = 48 +note = "64x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -segments 4 -sns 90 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/expected.rgba new file mode 100644 index 0000000..6334261 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/input.vp8 new file mode 100644 index 0000000..6e8414f Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/meta.toml new file mode 100644 index 0000000..53863dc --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/seg_single_64x48/meta.toml @@ -0,0 +1,5 @@ +feature = "segmentation-single" +level = "must" +width = 64 +height = 48 +note = "64x48 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -segments 1 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/expected.rgba new file mode 100644 index 0000000..079c28a --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/expected.rgba @@ -0,0 +1 @@ +a¥ÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/input.vp8 new file mode 100644 index 0000000..55a496e Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/meta.toml new file mode 100644 index 0000000..b63992b --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_1x1/meta.toml @@ -0,0 +1,5 @@ +feature = "tiny" +level = "must" +width = 1 +height = 1 +note = "1x1 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/expected.rgba new file mode 100644 index 0000000..bc95079 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/expected.rgba differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/input.vp8 new file mode 100644 index 0000000..480f9e0 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/meta.toml new file mode 100644 index 0000000..1f8f8ee --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_2x2/meta.toml @@ -0,0 +1,5 @@ +feature = "tiny" +level = "must" +width = 2 +height = 2 +note = "2x2 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/expected.rgba b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/expected.rgba new file mode 100644 index 0000000..834c1a2 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/expected.rgba @@ -0,0 +1 @@ +²²´ÿ“�†ÿŸ�qÿ·…†ÿÐj¸ÿ`UbÿáÛÖÿ•™tÿËÁ«ÿ¡s ÿR3Tÿd_cÿsžfÿyº{ÿ™ØÂÿzežÿ¶¸ÓÿÅùÕÿrÃÿeÃzÿguÉÿ9IŠÿ–­Çÿf†iÿ•ÄYÿ \ No newline at end of file diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/input.vp8 b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/input.vp8 new file mode 100644 index 0000000..ab36752 Binary files /dev/null and b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/input.vp8 differ diff --git a/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/meta.toml b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/meta.toml new file mode 100644 index 0000000..49aad54 --- /dev/null +++ b/crates/webpkit-lossy-conformance/fixtures/decode/tiny_5x5/meta.toml @@ -0,0 +1,5 @@ +feature = "tiny" +level = "must" +width = 5 +height = 5 +note = "5x5 synthetic source; input.vp8 via cwebp 1.6.0 `-q 50 -noalpha`, expected.rgba via dwebp 1.6.0 `-pam` (header stripped)" diff --git a/crates/webpkit/README.md b/crates/webpkit/README.md index 80262ea..d1dea9b 100644 --- a/crates/webpkit/README.md +++ b/crates/webpkit/README.md @@ -43,7 +43,9 @@ encode → decode round-trip. `decode` is **safe on untrusted input by default**: it caps the canvas at `DEFAULT_MAX_PIXELS` before allocating, so a hostile header cannot exhaust memory. Use `decode_with` + `DecodeOptions::max_pixels` for a different cap, or -`DecodeOptions::unbounded` to lift it for trusted input. +`DecodeOptions::unbounded` to lift it for trusted input. The same options choose +layout and metadata reads and offer opt-in (`0..=100`, default `0`) lossy chroma +and level-quantized `ALPH` dithering strengths. ## Optional features diff --git a/crates/webpkit/examples/vs_libwebp.rs b/crates/webpkit/examples/vs_libwebp.rs index 3fea420..b22f09e 100644 --- a/crates/webpkit/examples/vs_libwebp.rs +++ b/crates/webpkit/examples/vs_libwebp.rs @@ -14,7 +14,8 @@ //! Fairness: encode is effort-matched (our Fast/Balanced/Best vs libwebp method //! 1/4/6) at quality 75 on identical pixels; decode is timed on each codec's own //! output (VP8 decode is bit-exact per RFC 6386, so cross-decoding is redundant); -//! libwebp is called in-process, so there is no CLI startup or file-IO skew. +//! lossy size compares the `VP8 ` payload on both sides; libwebp is called +//! in-process, so there is no CLI startup or file-IO skew. #![allow( clippy::cast_precision_loss, clippy::cast_possible_truncation, @@ -22,16 +23,19 @@ clippy::cast_possible_wrap, clippy::unwrap_used, clippy::expect_used, + clippy::panic, clippy::print_stdout, clippy::print_stderr, reason = "developer measurement example (not shipped library code): it prints \ its progress and the report path, unwraps on local file I/O, and casts \ - image dimensions to the libwebp FFI's i32 — all fine for a demo binary" + image dimensions to the libwebp FFI's i32; an unexpected reference \ + codec is a hard measurement failure — all fine for a demo binary" )] use std::hint::black_box; use std::time::Instant; +use webpkit::container::reader::{ImageChunk, locate_image}; use webpkit::lossy::{Dimensions, Effort, ImageRef, LossyConfig, PixelLayout}; use webpkit_samples::{Content, render}; @@ -93,6 +97,10 @@ fn measure(content: Content) -> Row { let lw_container = libwebp_encode(&rgb, EDGE, EDGE, f32::from(QUALITY), 6); let (lw_rgba, _lww, _lwh) = libwebp_decode(&lw_container); let lossy_lw_psnr = psnr_rgb(rgba, &lw_rgba); + let lossy_lw_bytes = match locate_image(&lw_container).expect("valid libwebp WebP container") { + ImageChunk::Lossy(payload) => payload.len(), + ImageChunk::Lossless(_) => panic!("libwebp lossy output unexpectedly contains VP8L"), + }; // ---- lossless size (ours deepest effort vs libwebp lossless) ---- let ll_cfg = webpkit::lossless::EncoderConfig::default() @@ -128,7 +136,7 @@ fn measure(content: Content) -> Row { Row { name: content.name(), lossy_ours_bytes: ours_payload.len(), - lossy_lw_bytes: lw_container.len(), + lossy_lw_bytes, lossy_ours_psnr, lossy_lw_psnr, ll_ours_bytes: ll_ours.len(), diff --git a/crates/webpkit/examples/vs_libwebp_template.html b/crates/webpkit/examples/vs_libwebp_template.html index 4317039..811d1ef 100644 --- a/crates/webpkit/examples/vs_libwebp_template.html +++ b/crates/webpkit/examples/vs_libwebp_template.html @@ -181,11 +181,11 @@

Size & quality vs libwebp

Lossy — VP8, our Best vs libwebp method 6

-

At the same quality our scale quantizes more aggressively, so read size and PSNR together (not rate-distortion-matched). Size ratio is ours ÷ libwebp bytes; ΔPSNR is ours − libwebp dB.

+

At the same quality our scale quantizes more aggressively, so read size and PSNR together (not rate-distortion-matched). Both size columns are raw VP8 payload bytes; size ratio is ours ÷ libwebp; ΔPSNR is ours − libwebp dB.

- +
contentours byteslibwebp bytessize ratioours dBlibwebp dBΔPSNR
contentours VP8 byteslibwebp VP8 bytessize ratioours dBlibwebp dBΔPSNR
diff --git a/crates/webpkit/src/alpha.rs b/crates/webpkit/src/alpha.rs index e101a90..a9ee8e7 100644 --- a/crates/webpkit/src/alpha.rs +++ b/crates/webpkit/src/alpha.rs @@ -278,13 +278,149 @@ pub fn filter_plane(filter: AlphaFilter, plane: &[u8], width: usize, height: usi out } +/// Correction-table extent for the alpha smoothing fixed-point delta. +const ALPHA_LEVEL_LUT_SIZE: usize = (1 << 10) - 1; + +/// Smooth a level-quantized alpha plane with libwebp's `WebPDequantizeLevels` kernel. +/// +/// `strength` controls a square radius from `0..=4`. +/// This keeps libwebp's 16-bit wrapping prefix sums, vertically replicated edge, horizontally mirrored edge, and delayed output rows exactly. +/// Planes with at most two distinct levels, or whose selected radius is zero, are left unchanged. +#[allow( + clippy::too_many_lines, + reason = "the sliding-window state is kept in one function so its update order remains \ + visibly identical to libwebp's VFilter/HFilter/ApplyFilter sequence" +)] +pub(crate) fn dequantize_levels(plane: &mut [u8], width: usize, height: usize, strength: u8) { + if width == 0 || height == 0 || width.checked_mul(height) != Some(plane.len()) { + return; + } + let mut radius = 4 * usize::from(strength.min(100)) / 100; + radius = radius.min((width - 1) / 2).min((height - 1) / 2); + if radius == 0 { + return; + } + + let mut used = [false; 256]; + for &value in plane.iter() { + used[usize::from(value)] = true; + } + let levels: Vec = used + .iter() + .enumerate() + .filter_map(|(level, &present)| present.then_some(level)) + .collect(); + if levels.len() <= 2 { + return; + } + let min = levels[0]; + let max = *levels.last().unwrap_or(&min); + let min_dist = levels + .windows(2) + .map(|pair| pair[1] - pair[0]) + .min() + .unwrap_or(max - min); + + // The reference correction LUT is indexed over `average - (value << 2)`, hence `-1023..=1023`. + let mut correction = vec![0i16; 1 + 2 * ALPHA_LEVEL_LUT_SIZE]; + let threshold1 = min_dist << 2; + let threshold2 = (3 * threshold1) >> 2; + let delta = threshold1 - threshold2; + for i in 1..=ALPHA_LEVEL_LUT_SIZE { + let c = if i <= threshold2 { + i + } else if i < threshold1 { + threshold2 * (threshold1 - i) / delta + } else { + 0 + } >> 2; + let c = i16::try_from(c).unwrap_or(i16::MAX); + correction[ALPHA_LEVEL_LUT_SIZE + i] = c; + correction[ALPHA_LEVEL_LUT_SIZE - i] = -c; + } + + // Port the reference sliding-window state directly. + // `scratch[..end]` is a ring of horizontal prefix rows; `scratch[end..]` is the current vertical sum. + // The C implementation intentionally performs all of these operations modulo 16 bits. + let source = plane.to_vec(); + let side = 2 * radius + 1; + let scale = (1u32 << 18) / u32::try_from(side * side).unwrap_or(1); + let end = side * width; + let mut scratch = vec![0u16; (side + 1) * width]; + let mut average = vec![0u16; width]; + let mut cur = 0usize; + let mut top = end - width; + let mut src_y = 0usize; + let mut dst_y = 0usize; + + for row in -(isize::try_from(radius).unwrap_or(0))..isize::try_from(height).unwrap_or(0) { + let mut sum = 0u16; + for x in 0..width { + sum = sum.wrapping_add(u16::from(source[src_y * width + x])); + let new_value = scratch[top + x].wrapping_add(sum); + scratch[end + x] = new_value.wrapping_sub(scratch[cur + x]); + scratch[cur + x] = new_value; + } + top = cur; + cur += width; + if cur == end { + cur = 0; + } + if row >= 0 && row < isize::try_from(height - 1).unwrap_or(0) { + src_y += 1; + } + + if row < isize::try_from(radius).unwrap_or(0) { + continue; + } + + // Horizontal prefix differencing, including the reference's mirrored boundary formulas. + let input = &scratch[end..end + width]; + let mut x = 0usize; + while x <= radius { + let delta = input[x + radius - 1].wrapping_add(input[radius - x]); + average[x] = u16::try_from((u32::from(delta) * scale) >> 16).unwrap_or(u16::MAX); + x += 1; + } + while x < width - radius { + let delta = input[x + radius].wrapping_sub(input[x - radius - 1]); + average[x] = u16::try_from((u32::from(delta) * scale) >> 16).unwrap_or(u16::MAX); + x += 1; + } + while x < width { + let delta = input[width - 1] + .wrapping_mul(2) + .wrapping_sub(input[2 * width - 2 - radius - x]) + .wrapping_sub(input[x - radius - 1]); + average[x] = u16::try_from((u32::from(delta) * scale) >> 16).unwrap_or(u16::MAX); + x += 1; + } + + for x in 0..width { + let value = usize::from(plane[dst_y * width + x]); + if value <= min || value >= max { + continue; + } + let index = + isize::try_from(average[x]).unwrap_or(0) - isize::try_from(value << 2).unwrap_or(0); + let lut_index = + usize::try_from(isize::try_from(ALPHA_LEVEL_LUT_SIZE).unwrap_or(0) + index) + .unwrap_or(ALPHA_LEVEL_LUT_SIZE) + .min(2 * ALPHA_LEVEL_LUT_SIZE); + let corrected = i32::try_from(value).unwrap_or(0) + i32::from(correction[lut_index]); + plane[dst_y * width + x] = u8::try_from(corrected.clamp(0, 255)).unwrap_or(0); + } + dst_y += 1; + } +} + #[cfg(test)] mod tests { use proptest::prelude::*; use super::{ - AlphaCompression, AlphaFilter, AlphaHeader, build_header, filter_plane, parse_header, - unfilter, unfilter_row, + AlphaCompression, AlphaFilter, AlphaHeader, build_header, dequantize_levels, filter_plane, + parse_header, unfilter, unfilter_row, }; use crate::error::Error; @@ -492,6 +628,28 @@ mod tests { ); } + #[test] + fn dequantized_alpha_smoothing_is_gated_and_deterministic() { + let source = vec![ + 0, 0, 0, 0, 0, 0, 64, 64, 64, 0, 0, 64, 128, 64, 0, 0, 64, 64, 64, 0, 0, 0, 0, 0, 0, + ]; + let mut off = source.clone(); + dequantize_levels(&mut off, 5, 5, 0); + assert_eq!(off, source); + + let mut smoothed_a = source.clone(); + let mut smoothed_b = source.clone(); + dequantize_levels(&mut smoothed_a, 5, 5, 100); + dequantize_levels(&mut smoothed_b, 5, 5, 100); + assert_eq!(smoothed_a, smoothed_b); + assert_ne!(smoothed_a, source); + + let mut binary = vec![0, 255, 0, 255, 0, 255, 0, 255, 0]; + let binary_source = binary.clone(); + dequantize_levels(&mut binary, 3, 3, 100); + assert_eq!(binary, binary_source); + } + proptest! { /// Filtering an original plane and then [`unfilter`]ing the result is the /// identity, for every filter and random plane / dimensions — the forward diff --git a/crates/webpkit/src/geometry.rs b/crates/webpkit/src/geometry.rs index fd2bc95..c43a85c 100644 --- a/crates/webpkit/src/geometry.rs +++ b/crates/webpkit/src/geometry.rs @@ -1,18 +1,13 @@ -//! The shared crop/resize geometry engine, used by both the encode and decode -//! tool-side paths. +//! The shared crop/resize geometry engine, used by both the encode and decode tool-side paths. //! -//! Resizing is a bit-exact port of libwebp's `WebPRescaler` (`utils/rescaler.c` + -//! the generic `dsp/rescaler.c` import/export path): a fixed-point, -//! [`forbid(unsafe_code)`](crate), zero-dependency resampler whose output is -//! byte-identical to `cwebp`/`dwebp`. The `RFIX = 32` fixed-point shift, the -//! `fx`/`fy`/`fxy` factor derivation, the import-then-export phase ordering, and the -//! per-channel `u32`/`u64` accumulation all mirror the reference exactly, because an -//! off-by-one in the rounding or the phase order is a visible pixel difference. +//! Resizing is a bit-exact port of libwebp's `WebPRescaler` (`utils/rescaler.c` + the generic `dsp/rescaler.c` import/export path): a fixed-point, +//! [`forbid(unsafe_code)`](crate), zero-dependency resampler whose output is byte-identical to `cwebp`/`dwebp`. +//! The `RFIX = 32` fixed-point shift, the `fx`/`fy`/`fxy` factor derivation, the import-then-export phase ordering, and the per-channel `u32`/`u64` accumulation all mirror the reference exactly, because an off-by-one in the rounding or the phase order is a visible pixel difference. //! -//! `resize` follows `WebPPictureRescale`'s ARGB path: it black-mattes (premultiplies -//! the color channels by alpha), rescales all four channels interleaved, then removes -//! the premultiplication — so translucent edges interpolate the way libwebp's do. -//! `crop` is an exact byte-window copy. Both are integer-only and deterministic. +//! `resize` follows `WebPPictureRescale`'s ARGB path: it black-mattes (premultiplies the color channels by alpha), rescales all four channels interleaved, then removes the premultiplication — so translucent edges interpolate the way libwebp's do. +//! `resize_exact` is the variant `cwebp -resize -exact` runs, keeping the color stored under transparent pixels. +//! `crop` is an exact byte-window copy. +//! All are integer-only and deterministic. use crate::error::{Error, Result}; use crate::image::{Dimensions, Image, argb_has_alpha, pack_pixels, unpack_pixels}; @@ -26,8 +21,7 @@ const ONE: u64 = 1 << RFIX; const ROUNDER: u64 = ONE >> 1; /// `WEBP_RESCALER_FRAC(numer, denom)`: `numer / denom` in `RFIX` fixed-point, -/// truncated to `u32` (so an exact `1.0` wraps to `0`, matching the reference — the -/// callers that can hit it special-case the result). +/// truncated to `u32` (so an exact `1.0` wraps to `0`, matching the reference — the callers that can hit it special-case the result). #[allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, @@ -68,8 +62,7 @@ fn mult_alpha(x: u8, scale: u32) -> u32 { (((u64::from(x) * u64::from(scale)) + u64::from(M_HALF)) >> MFIX) as u32 } -/// `GetScale(a, inverse)`: the premultiply (`false`) or un-premultiply (`true`) -/// scale for a non-zero, non-opaque alpha `a`. +/// `GetScale(a, inverse)`: the premultiply (`false`) or un-premultiply (`true`) scale for a non-zero, non-opaque alpha `a`. const fn alpha_scale(a: u32, inverse: bool) -> u32 { if inverse { (255_u32 << MFIX) / a @@ -78,8 +71,7 @@ const fn alpha_scale(a: u32, inverse: bool) -> u32 { } } -/// `WebPMultARGBRow_C` for one native `0xAARRGGBB` pixel: black-matte -/// (`inverse == false`) or restore (`inverse == true`) the color channels by alpha. +/// `WebPMultARGBRow_C` for one native `0xAARRGGBB` pixel: black-matte (`inverse == false`) or restore (`inverse == true`) the color channels by alpha. /// Opaque and fully-transparent pixels are handled exactly as the reference. #[allow( clippy::cast_possible_truncation, @@ -108,8 +100,8 @@ fn mult_argb(argb: u32, inverse: bool) -> u32 { // WebPRescaler port (utils/rescaler.c + dsp/rescaler.c generic path) /// A single-plane fixed-point rescaler over `num_channels` interleaved `u8` lanes, -/// a direct port of libwebp's `WebPRescaler`. Owns its output plane and the two -/// `u32` work rows; driven by [`rescale_plane`]. +/// a direct port of libwebp's `WebPRescaler`. +/// Owns its output plane and the two `u32` work rows; driven by [`rescale_plane`]. #[allow( clippy::cast_possible_truncation, clippy::cast_sign_loss, @@ -152,9 +144,7 @@ struct Rescaler { fx/fy/fxy factor and A/B/I/J blend names are the reference's own" )] impl Rescaler { - /// `WebPRescalerInit`: derive the increments and fixed-point factors for a - /// `src_width x src_height` -> `dst_width x dst_height` rescale of `num_channels` - /// interleaved lanes, and allocate the output plane and work rows. + /// `WebPRescalerInit`: derive the increments and fixed-point factors for a `src_width x src_height` -> `dst_width x dst_height` rescale of `num_channels` interleaved lanes, and allocate the output plane and work rows. fn new( src_width: i32, src_height: i32, @@ -231,8 +221,8 @@ impl Rescaler { } /// `WebPRescalerImport`: consume rows from `src` (starting at its first byte, - /// advancing `src_stride` per row) until one output row is ready or `num_lines` - /// are taken. Returns the number of rows consumed. + /// advancing `src_stride` per row) until one output row is ready or `num_lines` are taken. + /// Returns the number of rows consumed. fn import(&mut self, num_lines: i32, src: &[u8], src_stride: usize) -> i32 { let mut total = 0; let mut off = 0; @@ -368,8 +358,7 @@ impl Rescaler { } } - /// `WebPRescalerExportRowShrink_C`: vertical area-average, carrying the leftover - /// fraction of each column back into `irow` for the next output row. + /// `WebPRescalerExportRowShrink_C`: vertical area-average, carrying the leftover fraction of each column back into `irow` for the next output row. fn export_row_shrink(&mut self) { let x_out_max = self.dst_width as usize * self.num_channels; let base = self.dst_y as usize * self.dst_stride; @@ -390,9 +379,7 @@ impl Rescaler { } } - /// The `fxy_scale == 0` special case: `src_height == dst_height`, a 1-wide source - /// expanded to at most 2 columns — the fraction cannot be represented, so `irow` - /// already holds the exact bytes. + /// The `fxy_scale == 0` special case: `src_height == dst_height`, a 1-wide source expanded to at most 2 columns — the fraction cannot be represented, so `irow` already holds the exact bytes. fn export_row_special(&mut self) { let n = self.num_channels * self.dst_width as usize; let base = self.dst_y as usize * self.dst_stride; @@ -404,8 +391,7 @@ impl Rescaler { } /// Rescale one `num_channels`-interleaved `u8` plane from `src` (packed, -/// `src_width * num_channels` bytes per row) to a fresh `dst_width x dst_height` -/// plane, driving the [`Rescaler`] exactly as libwebp's `RescalePlane`. +/// `src_width * num_channels` bytes per row) to a fresh `dst_width x dst_height` plane, driving the [`Rescaler`] exactly as libwebp's `RescalePlane`. #[allow( clippy::cast_sign_loss, reason = "the driver's dimensions are positive (validated by the caller), so the \ @@ -433,8 +419,7 @@ fn rescale_plane( //------------------------------------------------------------------------------ // Public geometry operations -/// A crop rectangle in source-pixel coordinates: the `width x height` window whose -/// top-left corner sits at `(x, y)`. +/// A crop rectangle in source-pixel coordinates: the `width x height` window whose top-left corner sits at `(x, y)`. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct Rect { x: u32, @@ -444,8 +429,8 @@ pub struct Rect { } impl Rect { - /// A crop window of `width x height` pixels with its top-left corner at - /// `(x, y)`. The window is validated against the source only at [`Image::crop`]. + /// A crop window of `width x height` pixels with its top-left corner at `(x, y)`. + /// The window is validated against the source only at [`Image::crop`]. #[must_use] pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self { Self { @@ -481,13 +466,11 @@ impl Rect { } } -/// Copy the exact `rect` sub-window out of `image`, preserving its layout and -/// metadata and recomputing `has_alpha` over the window. +/// Copy the exact `rect` sub-window out of `image`, preserving its layout and metadata and recomputing `has_alpha` over the window. /// /// # Errors /// -/// [`Error::InvalidDimensions`] if the rectangle is empty, or -/// [`Error::CropOutOfBounds`] if it does not lie fully inside `image`. +/// [`Error::InvalidDimensions`] if the rectangle is empty, or [`Error::CropOutOfBounds`] if it does not lie fully inside `image`. pub(crate) fn crop(image: &Image, rect: Rect) -> Result { if rect.width == 0 || rect.height == 0 { return Err(Error::InvalidDimensions); @@ -525,63 +508,83 @@ pub(crate) fn crop(image: &Image, rect: Rect) -> Result { )) } -/// Resize `image` to `target` with the bit-exact [`Rescaler`], following -/// `WebPPictureRescale`'s ARGB path: premultiply the color channels by alpha, rescale -/// all four channels, then remove the premultiplication. Layout and metadata carry -/// through; `has_alpha` is recomputed. Opaque images are unaffected by the matting, so -/// their color channels rescale directly. +/// Rescale a native-`0xAARRGGBB` plane through the [`Rescaler`]. +/// Little-endian bytes of `0xAARRGGBB` are `[B, G, R, A]` — the interleaving libwebp rescales when it casts its argb plane to bytes. #[allow( clippy::cast_possible_wrap, reason = "validated Dimensions are in 1..=16384, well within i32, so the u32 -> i32 \ casts handed to the rescaler never wrap" )] -#[must_use] -pub(crate) fn resize(image: &Image, target: Dimensions) -> Image { - let layout = image.layout(); - let src_w = image.width(); - let src_h = image.height(); - - // Native ARGB, black-matted so translucent colors interpolate as libwebp's do. - let mut argb = unpack_pixels(layout, image.as_bytes()); - for pixel in &mut argb { - *pixel = mult_argb(*pixel, false); - } - // Little-endian bytes of 0xAARRGGBB are [B, G, R, A] — the exact interleaving - // libwebp rescales when it casts its argb plane to bytes. +fn rescale_argb(argb: &[u32], src_w: u32, src_h: u32, target: Dimensions) -> Vec { let mut src_bytes = Vec::with_capacity(argb.len() * 4); - for pixel in &argb { + for pixel in argb { src_bytes.extend_from_slice(&pixel.to_le_bytes()); } - - let dst_bytes = rescale_plane( + rescale_plane( &src_bytes, src_w as i32, src_h as i32, 4, target.width() as i32, target.height() as i32, - ); + ) + .as_chunks::<4>() + .0 + .iter() + .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])) + .collect() +} - let mut out_argb: Vec = dst_bytes - .as_chunks::<4>() - .0 - .iter() - .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])) - .collect(); - for pixel in &mut out_argb { +/// `WebPPictureRescale`'s ARGB path over a native-ARGB plane: black-matte (premultiply the color channels by alpha), rescale all four channels interleaved, then remove the premultiplication. +/// Opaque pixels pass both matting steps unchanged. +fn rescale_matted(argb: &[u32], src_w: u32, src_h: u32, target: Dimensions) -> Vec { + let matted: Vec = argb.iter().map(|&p| mult_argb(p, false)).collect(); + let mut out = rescale_argb(&matted, src_w, src_h, target); + for pixel in &mut out { *pixel = mult_argb(*pixel, true); } - let has_alpha = argb_has_alpha(&out_argb); - let out_bytes = pack_pixels(layout, &out_argb); + out +} + +/// Rebuild an image around a rescaled ARGB plane, carrying layout and metadata and recomputing `has_alpha`. +fn rescaled_image(image: &Image, target: Dimensions, argb: &[u32]) -> Image { + let layout = image.layout(); Image::from_parts( target, layout, - out_bytes, - has_alpha, + pack_pixels(layout, argb), + argb_has_alpha(argb), image.metadata().clone(), ) } +/// Resize `image` to `target` with the bit-exact [`Rescaler`], following `WebPPictureRescale`'s ARGB path. +/// Layout and metadata carry through; `has_alpha` is recomputed. +#[must_use] +pub(crate) fn resize(image: &Image, target: Dimensions) -> Image { + let argb = unpack_pixels(image.layout(), image.as_bytes()); + let out = rescale_matted(&argb, image.width(), image.height(), target); + rescaled_image(image, target, &out) +} + +/// Resize `image` to `target` preserving the color stored under transparent pixels — +/// `cwebp -resize` under `-exact`. +/// It rescales a copy whose alpha is forced opaque to get un-weighted color, and keeps the alpha of the ordinary rescale; the opaque copy's own alpha can land a step below 255 and scale its color back up, so the two passes cannot be fused into one un-matted rescale. +#[must_use] +pub(crate) fn resize_exact(image: &Image, target: Dimensions) -> Image { + let argb = unpack_pixels(image.layout(), image.as_bytes()); + let (src_w, src_h) = (image.width(), image.height()); + let alpha = rescale_matted(&argb, src_w, src_h, target); + let opaque: Vec = argb.iter().map(|&p| p | 0xff00_0000).collect(); + let color = rescale_matted(&opaque, src_w, src_h, target); + let out: Vec = alpha + .iter() + .zip(&color) + .map(|(a, c)| (a & 0xff00_0000) | (c & 0x00ff_ffff)) + .collect(); + rescaled_image(image, target, &out) +} + #[cfg(test)] #[allow( clippy::cast_possible_truncation, @@ -589,7 +592,7 @@ pub(crate) fn resize(image: &Image, target: Dimensions) -> Image { reason = "tests: fixed small dimensions and hand-verified reference values" )] mod tests { - use super::{Rect, crop, mult_argb, rescale_plane, resize}; + use super::{Rect, crop, mult_argb, rescale_plane, resize, resize_exact}; use crate::image::{Dimensions, Image, PixelLayout}; fn dims(w: u32, h: u32) -> Dimensions { @@ -610,8 +613,7 @@ mod tests { #[test] fn plane_downscale_two_to_one_is_the_average() { - // [100, 200] -> one pixel = round-down average 150 (hand-computed against - // the reference's fixed-point path). + // [100, 200] -> one pixel = round-down average 150 (hand-computed against the reference's fixed-point path). let out = rescale_plane(&[100, 200], 2, 1, 1, 1, 1); assert_eq!(out, vec![150]); } @@ -654,6 +656,32 @@ mod tests { assert!(!out.has_alpha()); } + #[test] + fn exact_resize_keeps_color_the_matted_resize_blacks_out() { + // 2x1 fully transparent, both pixels the same color: matting collapses the color to 0, the exact variant carries it through. + let img = Image::new( + dims(2, 1), + PixelLayout::Rgba8, + vec![90, 140, 200, 0, 90, 140, 200, 0], + ) + .unwrap(); + assert_eq!(resize(&img, dims(1, 1)).as_bytes(), &[0, 0, 0, 0]); + assert_eq!( + resize_exact(&img, dims(1, 1)).as_bytes(), + &[90, 140, 200, 0] + ); + } + + #[test] + fn exact_and_matted_resize_agree_on_opaque_pixels() { + // Matting is the identity on an opaque plane, so both variants must coincide. + let img = opaque(2, 1, &[100, 110, 120, 255, 200, 210, 220, 255]); + assert_eq!( + resize(&img, dims(1, 1)).as_bytes(), + resize_exact(&img, dims(1, 1)).as_bytes() + ); + } + #[test] fn crop_selects_the_window() { // 4x4 with a per-pixel first byte = index; crop the inner 2x2 at (1,1). @@ -689,8 +717,7 @@ mod tests { #[test] fn mult_argb_opaque_and_transparent_are_fixed_points() { - // Opaque passes through; fully transparent collapses to 0; the round trip of a - // translucent pixel is stable (premultiply then un-premultiply). + // Opaque passes through; fully transparent collapses to 0; the round trip of a translucent pixel is stable (premultiply then un-premultiply). assert_eq!(mult_argb(0xff11_2233, false), 0xff11_2233); assert_eq!(mult_argb(0x0012_3456, false), 0); let translucent = 0x8011_2233; @@ -702,8 +729,8 @@ mod tests { #[test] fn crop_recomputes_alpha_over_the_window() { - // A 2x1 image: opaque pixel + translucent pixel. Cropping to just the opaque - // one must clear has_alpha. + // A 2x1 image: opaque pixel + translucent pixel. + // Cropping to just the opaque one must clear has_alpha. let img = Image::new( dims(2, 1), PixelLayout::Rgba8, diff --git a/crates/webpkit/src/image.rs b/crates/webpkit/src/image.rs index a78d2b3..50e718f 100644 --- a/crates/webpkit/src/image.rs +++ b/crates/webpkit/src/image.rs @@ -1,20 +1,16 @@ //! Public image model: validated [`Dimensions`], the byte-order [`PixelLayout`], -//! sidecar [`Metadata`], and the owned [`Image`] / borrowed [`ImageRef`] -//! containers. +//! sidecar [`Metadata`], and the owned [`Image`] / borrowed [`ImageRef`] containers. //! -//! Internally pixels are native `u32` ARGB (`0xAARRGGBB`); the conversion to and -//! from a caller's byte layout happens only here, so the rest of the codec never -//! deals with channel order. +//! Internally pixels are native `u32` ARGB (`0xAARRGGBB`); the conversion to and from a caller's byte layout happens only here, so the rest of the codec never deals with channel order. use crate::error::{Error, Result}; use crate::prelude::*; /// Largest image side any WebP bitstream can express, in pixels. /// -/// The VP8L header stores each side as `dimension - 1` in 14 bits, so a valid -/// dimension is `1..=MAX_DIMENSION` (`1 << 14` = 16384). The lossy `VP8` format -/// tops out slightly lower (16383); callers that need the tighter bound check it -/// themselves. This is the single source of truth for [`Dimensions`] validation. +/// The VP8L header stores each side as `dimension - 1` in 14 bits, so a valid dimension is `1..=MAX_DIMENSION` (`1 << 14` = 16384). +/// The lossy `VP8` format tops out slightly lower (16383); callers that need the tighter bound check it themselves. +/// This is the single source of truth for [`Dimensions`] validation. pub const MAX_DIMENSION: u32 = 1 << 14; /// A validated image size: both sides lie in `1..=16384`. @@ -27,8 +23,7 @@ pub struct Dimensions { impl TryFrom<(u32, u32)> for Dimensions { type Error = Error; - /// Validate a `(width, height)` pair, the tuple companion to - /// [`Dimensions::new`]. + /// Validate a `(width, height)` pair, the tuple companion to [`Dimensions::new`]. /// /// # Errors /// @@ -71,22 +66,17 @@ impl Dimensions { u64::from(self.width) * u64::from(self.height) } - /// Resolve a resize target where a `0` axis is derived from the other to keep - /// `self`'s aspect ratio, the rule `cwebp`/`dwebp` apply (libwebp's - /// `WebPRescalerGetScaledDimensions`). A derived axis is the *ceiling* of the - /// proportional value, matching libwebp exactly. + /// Resolve a resize target where a `0` axis is derived from the other to keep `self`'s aspect ratio, the rule `cwebp`/`dwebp` apply (libwebp's `WebPRescalerGetScaledDimensions`). + /// A derived axis is the *ceiling* of the proportional value, matching libwebp exactly. /// /// # Errors /// - /// [`Error::InvalidDimensions`] if both axes are `0`, or if a resolved axis is - /// `0` or exceeds `16384` — an over-range request is an honest error, never a - /// silent clamp. + /// [`Error::InvalidDimensions`] if both axes are `0`, or if a resolved axis is `0` or exceeds `16384` — an over-range request is an honest error, never a silent clamp. pub fn scaled(self, width: u32, height: u32) -> Result { let (sw, sh) = (u64::from(self.width), u64::from(self.height)); let mut w = u64::from(width); let mut h = u64::from(height); - // Order mirrors libwebp: width first, then height off the (possibly-derived) - // width, each derived axis the ceiling of the proportional value. + // Order mirrors libwebp: width first, then height off the (possibly-derived) width, each derived axis the ceiling of the proportional value. if w == 0 { w = (sw * h).div_ceil(sh); } @@ -101,10 +91,8 @@ impl Dimensions { /// The byte order of a pixel buffer at the API boundary. /// /// `#[non_exhaustive]`: unlike [`Effort`](crate::Effort) or [`Codec`](crate::Codec), -/// which are closed sets fixed by the WebP bitstream, this is an API-convenience -/// type that may gain further orderings (e.g. `Rgb8` or a 16-bit layout), so an -/// external `match` must carry a wildcard arm. The crate's own exhaustive matches -/// are same-crate and unaffected. +/// which are closed sets fixed by the WebP bitstream, this is an API-convenience type that may gain further orderings (e.g. `Rgb8` or a 16-bit layout), so an external `match` must carry a wildcard arm. +/// The crate's own exhaustive matches are same-crate and unaffected. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)] #[non_exhaustive] pub enum PixelLayout { @@ -181,11 +169,8 @@ pub fn argb_has_alpha(argb: &[u32]) -> bool { /// Optional sidecar metadata carried by a WebP extended (`VP8X`) container. /// -/// `#[non_exhaustive]`: WebP can carry further sidecar chunk kinds, so new fields -/// can be added without a breaking change (the existing fields stay `pub` to -/// read). Build one from [`Metadata::none`] and the `with_*` setters — these keep -/// the `with_` prefix (unlike the other builders' bare setters) because each shares -/// a name with the `pub` field it sets, which a bare method would shadow. +/// `#[non_exhaustive]`: WebP can carry further sidecar chunk kinds, so new fields can be added without a breaking change (the existing fields stay `pub` to read). +/// Build one from [`Metadata::none`] and the `with_*` setters — these keep the `with_` prefix (unlike the other builders' bare setters) because each shares a name with the `pub` field it sets, which a bare method would shadow. #[derive(Clone, PartialEq, Eq, Hash, Debug, Default)] #[non_exhaustive] pub struct Metadata { @@ -208,22 +193,24 @@ impl Metadata { } } - /// Set the ICC color profile (`ICCP`). Accepts a `Vec`, `Some(..)`, or - /// `None`; chain from [`Metadata::none`]. + /// Set the ICC color profile (`ICCP`). + /// Accepts a `Vec`, `Some(..)`, or `None`; chain from [`Metadata::none`]. #[must_use] pub fn with_icc_profile(mut self, icc_profile: impl Into>>) -> Self { self.icc_profile = icc_profile.into(); self } - /// Set the Exif sidecar (`EXIF`). Accepts a `Vec`, `Some(..)`, or `None`. + /// Set the Exif sidecar (`EXIF`). + /// Accepts a `Vec`, `Some(..)`, or `None`. #[must_use] pub fn with_exif(mut self, exif: impl Into>>) -> Self { self.exif = exif.into(); self } - /// Set the XMP sidecar (`XMP `). Accepts a `Vec`, `Some(..)`, or `None`. + /// Set the XMP sidecar (`XMP `). + /// Accepts a `Vec`, `Some(..)`, or `None`. #[must_use] pub fn with_xmp(mut self, xmp: impl Into>>) -> Self { self.xmp = xmp.into(); @@ -236,11 +223,9 @@ impl Metadata { self.icc_profile.is_none() && self.exif.is_none() && self.xmp.is_none() } - /// Fold `self` (a per-field *override*), the `inherited` source metadata, and a - /// [`MetadataPolicy`] into the effective metadata to embed. Per field: an - /// override value in `self` wins; otherwise the inherited value is kept, gated - /// by the policy. ICC is never gated — it is inherited (or replaced) but never - /// dropped; only the privacy-bearing Exif/XMP sidecars are gated. + /// Fold `self` (a per-field *override*), the `inherited` source metadata, and a [`MetadataPolicy`] into the effective metadata to embed. + /// Per field: an override value in `self` wins; otherwise the inherited value is kept, gated by the policy. + /// ICC is never gated — it is inherited (or replaced) but never dropped; only the privacy-bearing Exif/XMP sidecars are gated. #[must_use] pub fn resolve(&self, inherited: &Self, policy: MetadataPolicy) -> Self { let keep_private = matches!(policy, MetadataPolicy::Preserve); @@ -269,23 +254,19 @@ impl Metadata { /// How an encoder treats the metadata inherited from a source [`Image`]. /// -/// The ICC color profile is preserved under *every* policy — a WebP we emit never -/// silently loses color-correctness — so a policy governs only the -/// privacy-bearing Exif/XMP sidecars. +/// The ICC color profile is preserved under *every* policy — a WebP we emit never silently loses color-correctness — so a policy governs only the privacy-bearing Exif/XMP sidecars. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] #[non_exhaustive] pub enum MetadataPolicy { - /// Preserve all inherited metadata: ICC, Exif, and XMP. The default — kinder - /// than `cwebp`, which strips metadata by default. + /// Preserve all inherited metadata: ICC, Exif, and XMP. + /// The default — kinder than `cwebp`, which strips metadata by default. #[default] Preserve, - /// Preserve the ICC color profile but strip the privacy-bearing Exif and XMP - /// sidecars (they can embed GPS, timestamps, and device IDs). + /// Preserve the ICC color profile but strip the privacy-bearing Exif and XMP sidecars (they can embed GPS, timestamps, and device IDs). StripPrivate, } -/// A decoded image: pixels in a chosen [`PixelLayout`] plus size, alpha, and -/// sidecar [`Metadata`]. +/// A decoded image: pixels in a chosen [`PixelLayout`] plus size, alpha, and sidecar [`Metadata`]. #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct Image { dims: Dimensions, @@ -298,12 +279,8 @@ pub struct Image { impl Image { /// Assemble a validated owned image from pixel bytes in `layout` order. /// - /// The single public owned-[`Image`] constructor: it enforces the - /// `pixels.len() == width * height * 4` invariant (mirroring [`ImageRef::new`]) - /// and derives [`has_alpha`](Self::has_alpha) by scanning the alpha lane at - /// [`PixelLayout::alpha_byte_offset`], so a caller cannot build an `Image` whose - /// pixel buffer disagrees with its dimensions. Metadata starts empty; attach any - /// with [`Image::with_metadata`]. + /// The single public owned-[`Image`] constructor: it enforces the `pixels.len() == width * height * 4` invariant (mirroring [`ImageRef::new`]) and derives [`has_alpha`](Self::has_alpha) by scanning the alpha lane at [`PixelLayout::alpha_byte_offset`], so a caller cannot build an `Image` whose pixel buffer disagrees with its dimensions. + /// Metadata starts empty; attach any with [`Image::with_metadata`]. /// /// # Errors /// @@ -336,9 +313,7 @@ impl Image { }) } - /// Assemble an image from already-validated parts with an explicit `has_alpha` - /// and `metadata` — the crate-internal constructor for decode paths that already - /// know the alpha state (public callers use the validating [`Image::new`]). + /// Assemble an image from already-validated parts with an explicit `has_alpha` and `metadata` — the crate-internal constructor for decode paths that already know the alpha state (public callers use the validating [`Image::new`]). #[must_use] pub(crate) const fn from_parts( dims: Dimensions, @@ -356,9 +331,7 @@ impl Image { } } - /// Attach (or replace) the sidecar metadata, builder-style — used to surface - /// `VP8X` container metadata recovered alongside a decoded image so a - /// decode → re-encode round trip preserves it. + /// Attach (or replace) the sidecar metadata, builder-style — used to surface `VP8X` container metadata recovered alongside a decoded image so a decode → re-encode round trip preserves it. #[must_use] pub fn with_metadata(mut self, metadata: Metadata) -> Self { self.metadata = metadata; @@ -423,8 +396,7 @@ impl Image { } } - /// Overwrite the alpha lane of every pixel from a `width * height` alpha plane - /// (one byte per pixel), and set [`Self::has_alpha`] from the plane. + /// Overwrite the alpha lane of every pixel from a `width * height` alpha plane (one byte per pixel), and set [`Self::has_alpha`] from the plane. /// /// # Errors /// @@ -441,34 +413,37 @@ impl Image { Ok(()) } - /// Crop to the exact pixel window `rect`, returning a new image that copies just - /// that sub-rectangle (layout and metadata carried through, `has_alpha` - /// recomputed over the window). + /// Crop to the exact pixel window `rect`, returning a new image that copies just that sub-rectangle (layout and metadata carried through, `has_alpha` recomputed over the window). /// - /// The window must lie fully inside the image; there is no silent clamp. Crop is - /// the same core geometry the `cwebp`/`dwebp` tools apply. + /// The window must lie fully inside the image; there is no silent clamp. + /// Crop is the same core geometry the `cwebp`/`dwebp` tools apply. /// /// # Errors /// - /// [`Error::InvalidDimensions`] if `rect` is empty, or [`Error::CropOutOfBounds`] - /// if it does not fit inside the image. + /// [`Error::InvalidDimensions`] if `rect` is empty, or [`Error::CropOutOfBounds`] if it does not fit inside the image. pub fn crop(&self, rect: crate::geometry::Rect) -> Result { crate::geometry::crop(self, rect) } - /// Resize to `target` with the bit-exact fixed-point rescaler ported from - /// libwebp's `WebPRescaler`, returning a new image whose pixels match - /// `cwebp`/`dwebp`'s resampler byte-for-byte. + /// Resize to `target` with the bit-exact fixed-point rescaler ported from libwebp's `WebPRescaler`, returning a new image whose pixels match `cwebp`/`dwebp`'s resampler byte-for-byte. /// - /// Color channels are premultiplied by alpha before interpolation and restored - /// afterward (libwebp's ARGB `WebPPictureRescale` path), so translucent edges - /// resample cleanly; opaque images are unaffected by the matting. Use - /// [`Dimensions::scaled`] to derive `target` from a single axis while keeping - /// aspect. Layout and metadata carry through. + /// Color channels are premultiplied by alpha before interpolation and restored afterward (libwebp's ARGB `WebPPictureRescale` path), so translucent edges resample cleanly; opaque images are unaffected by the matting. + /// Use [`Dimensions::scaled`] to derive `target` from a single axis while keeping aspect. + /// Layout and metadata carry through. #[must_use] pub fn resize(&self, target: Dimensions) -> Self { crate::geometry::resize(self, target) } + + /// Resize to `target` without the alpha matting, preserving the color stored under transparent pixels — what `cwebp -resize` does under `-exact`. + /// + /// The color channels interpolate unweighted, so a translucent edge blends toward its neighbors' color rather than toward black. + /// Prefer [`Self::resize`] unless the hidden color is meaningful. + /// Layout and metadata carry through. + #[must_use] + pub fn resize_exact(&self, target: Dimensions) -> Self { + crate::geometry::resize_exact(self, target) + } } /// A borrowed view of pixels to encode: size, layout, and a byte slice. @@ -524,8 +499,7 @@ mod tests { }; proptest! { - /// For every layout, `pack`/`unpack` are mutual inverses in both - /// directions (the single channel-order boundary of the whole codec). + /// For every layout, `pack`/`unpack` are mutual inverses in both directions (the single channel-order boundary of the whole codec). #[test] fn pixel_layout_pack_unpack_round_trips( argb in any::(), @@ -633,8 +607,7 @@ mod tests { assert!(!argb_has_alpha(&[0xFF00_0000, 0xFFAA_BBCC])); // A non-opaque pixel is detected (pins `!=`, not `==`). assert!(argb_has_alpha(&[0xFF00_0000, 0x0000_0000])); - // A pixel opaque in the high byte but 0xFF in the low byte is still opaque - // — kills `>> -> <<`, which would inspect the low byte instead. + // A pixel opaque in the high byte but 0xFF in the low byte is still opaque — kills `>> -> <<`, which would inspect the low byte instead. assert!(!argb_has_alpha(&[0xFF00_00FF])); } @@ -691,9 +664,8 @@ mod tests { } } - /// The single source of truth for the `Metadata::resolve` fold: policy - /// (Preserve/StripPrivate) × field (icc/exif/xmp) × override presence. The - /// encoder configs delegate here, so their tests only check the delegation. + /// The single source of truth for the `Metadata::resolve` fold: policy (Preserve/StripPrivate) × field (icc/exif/xmp) × override presence. + /// The encoder configs delegate here, so their tests only check the delegation. #[test] fn resolve_truth_table() { // Preserve (no override): every inherited field is kept as-is. @@ -722,8 +694,7 @@ mod tests { Some(&[2][..]), ); - // An explicit override survives StripPrivate, but a non-overridden private - // slot is still dropped; ICC is never gated. + // An explicit override survives StripPrivate, but a non-overridden private slot is still dropped; ICC is never gated. let exif99 = Metadata { exif: Some(vec![99]), ..Metadata::none() @@ -758,8 +729,7 @@ mod tests { Some(&[77][..]), ); - // A present-but-empty blob (`Some(vec![])`) is a real value: the `.or_else` - // short-circuits on any `Some`, so it wins and is not normalized to `None`, + // A present-but-empty blob (`Some(vec![])`) is a real value: the `.or_else` short-circuits on any `Some`, so it wins and is not normalized to `None`, // under either policy — and it still upgrades to VP8X (non-empty). let empty_exif = Metadata { exif: Some(vec![]), @@ -805,8 +775,7 @@ mod tests { assert_eq!(borrowed.dimensions(), dims); } - /// The alpha lane lands at the layout's byte offset (`3`/`0`/`3`), the other - /// three channels are untouched, and a mixed plane flips `has_alpha` on. + /// The alpha lane lands at the layout's byte offset (`3`/`0`/`3`), the other three channels are untouched, and a mixed plane flips `has_alpha` on. #[test] fn apply_alpha_plane_writes_alpha_lane() { for (layout, off) in [ diff --git a/crates/webpkit/src/lib.rs b/crates/webpkit/src/lib.rs index ede2461..58ecc68 100644 --- a/crates/webpkit/src/lib.rs +++ b/crates/webpkit/src/lib.rs @@ -1,33 +1,22 @@ //! `webpkit` — a pure-Rust WebP codec: lossless (VP8L) and lossy (VP8) behind one API. //! -//! This is the umbrella crate. [`decode`] reads any still WebP file, inspecting -//! the container to route `VP8L` payloads to the lossless decoder and -//! `VP8 ` payloads to the lossy decoder; both return the shared -//! [`Image`] type. A lossy image's sibling `ALPH` alpha chunk is composited here, -//! where both codecs are in scope. The type-state [`Encoder`] writes output with -//! either codec — [`Encoder::lossless`] or [`Encoder::lossy`] — sharing the -//! effort/metadata knobs (only the lossy builder has a `quality`). The container -//! framing, image model, and error type are defined once in [`crate`] and -//! re-exported here. +//! This is the umbrella crate. +//! [`decode`] reads any still WebP file, inspecting the container to route `VP8L` payloads to the lossless decoder and `VP8 ` payloads to the lossy decoder; both return the shared [`Image`] type. +//! A lossy image's sibling `ALPH` alpha chunk is composited here, +//! where both codecs are in scope. +//! The type-state [`Encoder`] writes output with either codec — [`Encoder::lossless`] or [`Encoder::lossy`] — sharing the effort/metadata knobs (only the lossy builder has a `quality`). +//! The container framing, image model, and error type are defined once in [`crate`] and re-exported here. //! -//! Like the codec crates it wraps, this crate forbids `unsafe`, has zero required -//! runtime dependencies, and targets `no_std` (with `alloc`). +//! Like the codec crates it wraps, this crate forbids `unsafe`, has zero required runtime dependencies, and targets `no_std` (with `alloc`). //! //! # Status //! -//! Lossless decode/encode is complete. Lossy (VP8) decoding -//! reconstructs baseline key frames, composites a separate -//! `ALPH` alpha channel, and decodes lossy animations (frames dispatched into -//! the lossless codec's compositor). The unified [`IncrementalDecoder`] streams any still or -//! animation, row-streaming a bare lossy `VP8 ` still. Lossy -//! **encoding** ([`Encoder::lossy`]) writes a baseline `VP8 ` key frame, carrying a -//! lossless `ALPH` alpha plane for non-opaque images and ICC/Exif/XMP [`Metadata`] -//! via the extended `VP8X` container ([`Encoder::encode`] preserves a source -//! [`Image`]'s metadata by default). [`read_metadata`]/[`write_metadata`] inspect and -//! rewrite that sidecar metadata without touching the image bitstream (the -//! `webpmux` half), [`decode_yuv`] recovers a lossy still's native YUV 4:2:0 planes, -//! and [`Encoder::lossless`]'s `near_lossless` preprocessing trades a bounded -//! per-channel error for a smaller VP8L payload. +//! Lossless decode/encode is complete. +//! Lossy (VP8) decoding reconstructs baseline key frames, composites a separate `ALPH` alpha channel, and decodes lossy animations (frames dispatched into the lossless codec's compositor). +//! The unified [`IncrementalDecoder`] streams any still or animation, row-streaming a bare lossy `VP8 ` still. +//! Lossy **encoding** ([`Encoder::lossy`]) writes a baseline `VP8 ` key frame, carrying a lossless `ALPH` alpha plane for non-opaque images and ICC/Exif/XMP [`Metadata`] via the extended `VP8X` container ([`Encoder::encode`] preserves a source [`Image`]'s metadata by default). +//! [`read_metadata`]/[`write_metadata`] inspect and rewrite that sidecar metadata without touching the image bitstream (the `webpmux` half), [`decode_yuv`] recovers a lossy still's native YUV 4:2:0 planes, +//! and [`Encoder::lossless`]'s `near_lossless` preprocessing trades a bounded per-channel error for a smaller VP8L payload. #![doc = include_str!("../README.md")] #![forbid(unsafe_code)] #![cfg_attr(not(feature = "std"), no_std)] @@ -41,13 +30,9 @@ reason = "pub(crate) is our honest internal visibility; this nursery lint conflicts \ with the rustc unreachable_pub lint that we also enable" )] -// Without the `__internals` tooling feature the internal module tree is `pub(crate)` -// (the literal `pub(crate) mod` declarations below). Its cross-module `pub` items are then no longer -// reachable from the crate root (`unreachable_pub`), and the items that exist only -// for the external tooling/test crates read as unused from webpkit's own side -// (`dead_code`). Both are the intended shape of a published build: the curated -// facade is the whole public API, and the tooling surface is exercised only under -// `__internals` (by the bench/oracle/conformance crates), never from within the lib. +// Without the `__internals` tooling feature the internal module tree is `pub(crate)` (the literal `pub(crate) mod` declarations below). +// Its cross-module `pub` items are then no longer reachable from the crate root (`unreachable_pub`), and the items that exist only for the external tooling/test crates read as unused from webpkit's own side (`dead_code`). +// Both are the intended shape of a published build: the curated facade is the whole public API, and the tooling surface is exercised only under `__internals` (by the bench/oracle/conformance crates), never from within the lib. #![cfg_attr( not(feature = "__internals"), allow( @@ -73,26 +58,18 @@ use alloc::boxed::Box; use alloc::vec::Vec; // ---- module tree ------------------------------------------------------------ -// The bitstream-agnostic shell (formerly the `webpkit-core` crate): container -// framing, image model, error type, streaming vocabulary. Flattened at the crate -// root so its `crate::`-relative code is unchanged. NOT named `core` — that would -// shadow the `::core` std crate the no_std codecs use throughout. +// The bitstream-agnostic shell (formerly the `webpkit-core` crate): container framing, image model, error type, streaming vocabulary. +// Flattened at the crate root so its `crate::`-relative code is unchanged. +// NOT named `core` — that would shadow the `::core` std crate the no_std codecs use throughout. // -// These shell and codec modules are `pub(crate)` in a normal build: the curated -// re-exports below are the *entire* public API. Under the non-default `__internals` -// feature they become `#[doc(hidden)] pub` so this workspace's own test & tooling -// crates can reach into them module-first (they used to be separate public crates). -// That feature is off on crates.io / docs.rs, so a downstream user never sees — and -// can never depend on — the internals. (`encoder`/`interop`/`prelude` stay private -// regardless: their public items are already re-exported through the facade.) +// These shell and codec modules are `pub(crate)` in a normal build: the curated re-exports below are the *entire* public API. +// Under the non-default `__internals` feature they become `#[doc(hidden)] pub` so this workspace's own test & tooling crates can reach into them module-first (they used to be separate public crates). +// That feature is off on crates.io / docs.rs, so a downstream user never sees — and can never depend on — the internals. +// (`encoder`/`interop`/`prelude` stay private regardless: their public items are already re-exported through the facade.) // -// The default-build declarations are literal `pub(crate) mod` items so the `syn` -// source walk in cargo-mutants descends into every codec file — a macro-hidden mod -// tree is invisible to the mutation gate and would silently leave the whole codec -// unmutated. The `__internals` re-exposure is instead macro-generated: it is only -// active under a feature the mutation sweep never builds, and routing it through a -// macro keeps the walk from seeing a *second* declaration of each file (which would -// double every codec mutant). So the sweep sees exactly one copy per module. +// The default-build declarations are literal `pub(crate) mod` items so the `syn` source walk in cargo-mutants descends into every codec file — a macro-hidden mod tree is invisible to the mutation gate and would silently leave the whole codec unmutated. +// The `__internals` re-exposure is instead macro-generated: it is only active under a feature the mutation sweep never builds, and routing it through a macro keeps the walk from seeing a *second* declaration of each file (which would double every codec mutant). +// So the sweep sees exactly one copy per module. #[cfg(not(feature = "__internals"))] pub(crate) mod alpha; #[cfg(not(feature = "__internals"))] @@ -153,12 +130,10 @@ expose_internals! { // Public RIFF chunk-inspection facade (thin wrappers over `container::reader`). // The module stays private; its public items are re-exported at the crate root. mod chunk; -// The encode surface is public so the type-state markers keep a namespaced home -// (`webpkit::encoder::Lossless` etc.) instead of crowding the crate root, where -// `webpkit::Lossless` would collide with `webpkit::Codec::Lossless`. +// The encode surface is public so the type-state markers keep a namespaced home (`webpkit::encoder::Lossless` etc.) instead of crowding the crate root, where `webpkit::Lossless` would collide with `webpkit::Codec::Lossless`. pub mod encoder; -// Optional `image`-crate interop (TryFrom conversions on `Image`). The impls attach -// to the public types, so the module itself stays private. +// Optional `image`-crate interop (TryFrom conversions on `Image`). +// The impls attach to the public types, so the module itself stays private. #[cfg(feature = "image")] mod interop; mod prelude; @@ -178,15 +153,25 @@ pub use crate::mux::{AnimationMux, MuxFrame}; pub use crate::optimize::AnimationOptimizer; pub use crate::stream::{DEFAULT_MAX_PIXELS, DecodeOptions, ImageInfo, Progress, RowDrain}; pub use crate::yuv::YuvImage; -// Only the entry points and the codec selector reach the crate root; the type-state -// markers (`Empty`/`HasFrames`/`Lossless`/`Lossy`) stay in `webpkit::encoder`, named -// only in the rare code that spells out an encoder's type argument. + +/// Tooling-only build capability: whether this resolved crate graph enabled Rayon. +/// +/// Unlike a forwarding feature checked by a dependent crate, this observes Cargo feature unification at the codec crate itself. +#[cfg(feature = "__internals")] +#[doc(hidden)] +pub const INTERNAL_RAYON_ENABLED: bool = cfg!(feature = "rayon"); + +/// Tooling-only build capability: whether deterministic work counters are compiled in. +#[cfg(feature = "__internals")] +#[doc(hidden)] +pub const INTERNAL_WORK_COUNT_ENABLED: bool = cfg!(feature = "work-count"); + +// Only the entry points and the codec selector reach the crate root; the type-state markers (`Empty`/`HasFrames`/`Lossless`/`Lossy`) stay in `webpkit::encoder`, named only in the rare code that spells out an encoder's type argument. pub use encoder::{AnimCodec, AnimationEncoder, Encoder}; -// The lossy psychovisual tuning surface, exposed at the crate root because it appears -// in the facade `Encoder::::tuning` signature (the `lossy` module itself is -// `pub(crate)` in a normal build). +// The lossy psychovisual tuning surface, exposed at the crate root because it appears in the facade `Encoder::::tuning` signature (the `lossy` module itself is `pub(crate)` in a normal build). pub use crate::lossy::{ - AlphaFilterMode, AlphaMethod, Attempt, LossyParams, LossyTuning, Preset, RateSearch, RateTarget, + AlphaFilterMode, AlphaMethod, Attempt, ImageHint, LossyParams, LossyTuning, Preset, RateSearch, + RateTarget, }; // ---- imports used by the facade functions below ----------------------------- @@ -196,70 +181,55 @@ use crate::container::reader::{ImageChunk, read_container}; use crate::container::scan::{declared_len, is_complete, scan_chunks}; use crate::container::vp8x::{VP8X_PAYLOAD_LEN, Vp8xInfo}; -/// A lazy per-frame iterator over a WebP animation — each frame decoded by the -/// crate's internal both-codecs frame decoder (lossless `VP8L` or lossy `VP8 `). +/// A lazy per-frame iterator over a WebP animation — each frame decoded by the crate's internal both-codecs frame decoder (lossless `VP8L` or lossy `VP8 `). #[cfg(feature = "alloc")] pub type Frames<'a> = crate::anim::Frames<'a, WebpFrameDecoder>; -/// A compositing iterator that paints each animation frame onto the persistent -/// canvas. +/// A compositing iterator that paints each animation frame onto the persistent canvas. #[cfg(feature = "alloc")] pub type CompositedFrames<'a> = crate::anim::CompositedFrames<'a, WebpFrameDecoder>; -/// Decode a still WebP file — lossless (`VP8L`) or lossy (`VP8 `) — into an -/// [`Image`] (RGBA8 by default), dispatching on the container's image chunk. +/// Decode a still WebP file — lossless (`VP8L`) or lossy (`VP8 `) — into an [`Image`] (RGBA8 by default), dispatching on the container's image chunk. /// -/// A lossy `VP8 ` image accompanied by a sibling `ALPH` chunk is composited: the -/// opaque RGB is decoded by the lossy decoder, the alpha plane is decompressed (raw, or -/// a lossless `VP8L` stream) and spatially un-filtered, and the result is written -/// into the image's alpha channel. Lossless (`VP8L`) images carry their own alpha. +/// A lossy `VP8 ` image accompanied by a sibling `ALPH` chunk is composited: the opaque RGB is decoded by the lossy decoder, the alpha plane is decompressed (raw, or a lossless `VP8L` stream) and spatially un-filtered, and the result is written into the image's alpha channel. +/// Lossless (`VP8L`) images carry their own alpha. /// -/// An animated file returns its **first composited frame** (matching libwebp's -/// `WebPDecode`); use [`decode_frames`] to walk every frame. +/// An animated file returns its **first composited frame** (matching libwebp's `WebPDecode`); use [`decode_frames`] to walk every frame. /// /// # Untrusted input /// -/// Safe by default: this caps the canvas at [`DEFAULT_MAX_PIXELS`] before any -/// buffer is allocated, so a hostile header cannot exhaust memory. Raise the cap -/// with [`decode_with`] + [`DecodeOptions::max_pixels`], or remove it for trusted -/// input with [`DecodeOptions::unbounded`]. +/// Safe by default: this caps the canvas at [`DEFAULT_MAX_PIXELS`] before any buffer is allocated, so a hostile header cannot exhaust memory. +/// Raise the cap with [`decode_with`] + [`DecodeOptions::max_pixels`], or remove it for trusted input with [`DecodeOptions::unbounded`]. /// /// # Errors /// /// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, -/// [`Error::MissingImage`] when the file has no image chunk, or a -/// bitstream/container error from the selected decoder or the `ALPH` alpha stream. +/// [`Error::MissingImage`] when the file has no image chunk, or a bitstream/container error from the selected decoder or the `ALPH` alpha stream. pub fn decode(input: &[u8]) -> Result { decode_with(input, &DecodeOptions::default()) } -/// Decode a still WebP file into an [`Image`] with explicit [`DecodeOptions`] -/// (output layout, pixel limit), dispatching on the container's image chunk. +/// Decode a still WebP file into an [`Image`] with explicit [`DecodeOptions`] (output layout, pixel limit, metadata, and opt-in chroma / quantized-alpha dithering). +/// +/// Dispatches on the container's image chunk. /// -/// Symmetric with the codec crates' `decode_with`: the selected decoder enforces -/// `options.max_pixels` against the peeked header dimensions *before* any pixel or -/// canvas buffer is allocated, and the limit is propagated into a lossy image's -/// `ALPH` alpha decode. A default [`DecodeOptions`] caps at [`DEFAULT_MAX_PIXELS`]; -/// call [`DecodeOptions::unbounded`] to lift it for trusted input. An animated file -/// returns its **first composited frame** (matching [`decode`]). +/// Symmetric with the codec crates' `decode_with`: the selected decoder enforces `options.max_pixels` against the peeked header dimensions *before* any pixel or canvas buffer is allocated, and the limit is propagated into a lossy image's `ALPH` alpha decode. +/// A default [`DecodeOptions`] caps at [`DEFAULT_MAX_PIXELS`]; +/// call [`DecodeOptions::unbounded`] to lift it for trusted input. +/// An animated file returns its **first composited frame** (matching [`decode`]). /// /// # Errors /// -/// The same errors as [`decode`], plus [`Error::LimitExceeded`] when -/// `options.max_pixels` is exceeded. +/// The same errors as [`decode`], plus [`Error::LimitExceeded`] when `options.max_pixels` is exceeded. #[cfg(feature = "alloc")] pub fn decode_with(input: &[u8], options: &DecodeOptions) -> Result { - // One container walk yields the image chunk, any sibling `ALPH`, the `VP8X` - // header, and sidecar metadata — so the file is parsed exactly once (the codec - // is handed the located payload, not the whole file to re-parse). + // One container walk yields the image chunk, any sibling `ALPH`, the `VP8X` header, and sidecar metadata — so the file is parsed exactly once (the codec is handed the located payload, not the whole file to re-parse). let c = read_container(input, options.read_metadata)?; - // A clearly-animated file routes to its first composited frame, leaving the - // still-image path (and its exact error semantics) untouched otherwise. + // A clearly-animated file routes to its first composited frame, leaving the still-image path (and its exact error semantics) untouched otherwise. if c.animated { return first_composited_frame(input, options); } match c.image.ok_or(Error::MissingImage)? { - // A `VP8L` image encodes its own alpha, so any sibling `ALPH` does not - // apply; decode the located payload directly (no container re-parse). + // A `VP8L` image encodes its own alpha, so any sibling `ALPH` does not apply; decode the located payload directly (no container re-parse). ImageChunk::Lossless(payload) => { let image = crate::lossless::decode_vp8l(payload, options)?; // A VP8X canvas, if present, must agree with the decoded dimensions. @@ -274,9 +244,7 @@ pub fn decode_with(input: &[u8], options: &DecodeOptions) -> Result { let plane = alpha_plane_with(alph, image.width(), image.height(), options)?; image.apply_alpha_plane(&plane)?; } - // Surface any `VP8X` sidecar metadata (ICCP/EXIF/XMP) so a lossy - // decode → encode_image round trip preserves it, matching the lossless - // path (a bare `VP8 ` yields no metadata, leaving the image unchanged). + // Surface any `VP8X` sidecar metadata (ICCP/EXIF/XMP) so a lossy decode → encode_image round trip preserves it, matching the lossless path (a bare `VP8 ` yields no metadata, leaving the image unchanged). Ok(image.with_metadata(c.metadata)) }, } @@ -284,8 +252,7 @@ pub fn decode_with(input: &[u8], options: &DecodeOptions) -> Result { /// Decode a still WebP straight to its **RGBA8** pixels and [`Dimensions`]. /// -/// The raw-buffer companion to [`decode`], skipping the [`Image`] wrapper for -/// callers that only want the bytes (any embedded metadata is dropped). +/// The raw-buffer companion to [`decode`], skipping the [`Image`] wrapper for callers that only want the bytes (any embedded metadata is dropped). /// /// # Errors /// @@ -298,20 +265,15 @@ pub fn decode_rgba(input: &[u8]) -> Result<(Dimensions, Vec)> { /// Decode a **lossy** still WebP straight to its native **YUV 4:2:0** planes. /// -/// A lossy (`VP8 `) image is reconstructed in YUV before the YUV→RGB conversion -/// [`decode`] performs; this hands back those planes — byte-identical to libwebp's -/// `WebPDecodeYUV` — without that final step, for callers that consume YUV -/// directly (video pipelines, `dwebp -yuv`/`-pgm`). See [`YuvImage`] for the plane -/// shapes. +/// A lossy (`VP8 `) image is reconstructed in YUV before the YUV→RGB conversion [`decode`] performs; this hands back those planes — byte-identical to libwebp's `WebPDecodeYUV` — without that final step, for callers that consume YUV directly (video pipelines, `dwebp -yuv`/`-pgm`). +/// See [`YuvImage`] for the plane shapes. /// -/// This is lossy-still only: lossless (`VP8L`) has no YUV form, and an animation is -/// not a single still — both return [`Error::UnsupportedFeature`]. Any sibling -/// `ALPH` alpha is ignored (YUV carries only luma and chroma). +/// This is lossy-still only: lossless (`VP8L`) has no YUV form, and an animation is not a single still — both return [`Error::UnsupportedFeature`]. +/// Any sibling `ALPH` alpha is ignored (YUV carries only luma and chroma). /// /// # Untrusted input /// -/// Safe by default: like [`decode`], the canvas is capped at [`DEFAULT_MAX_PIXELS`] -/// before the reconstruction planes are allocated. +/// Safe by default: like [`decode`], the canvas is capped at [`DEFAULT_MAX_PIXELS`] before the reconstruction planes are allocated. /// /// # Examples /// @@ -327,28 +289,39 @@ pub fn decode_rgba(input: &[u8]) -> Result<(Dimensions, Vec)> { /// /// # Errors /// -/// [`Error::UnsupportedFeature`] for a lossless or animated input, the same -/// container errors as [`decode`], or a lossy bitstream error. +/// [`Error::UnsupportedFeature`] for a lossless or animated input, the same container errors as [`decode`], or a lossy bitstream error. #[cfg(feature = "alloc")] pub fn decode_yuv(input: &[u8]) -> Result { let options = DecodeOptions::default(); + decode_yuv_with_options_impl(input, &options) +} + +/// Option-aware companion used by the libwebp-compatible CLI. +/// +/// Kept out of the documented facade: the stable user-facing additions are the [`DecodeOptions`] setters, while this narrow bridge avoids enabling the tooling-only `__internals` feature in a published CLI dependency. +#[cfg(feature = "alloc")] +#[doc(hidden)] +pub fn decode_yuv_with_options(input: &[u8], options: &DecodeOptions) -> Result { + decode_yuv_with_options_impl(input, options) +} + +#[cfg(feature = "alloc")] +fn decode_yuv_with_options_impl(input: &[u8], options: &DecodeOptions) -> Result { let c = read_container(input, false)?; - // Only a lossy still has a native YUV reconstruction; an animation is not a - // single still and a lossless image never leaves RGBA. + // Only a lossy still has a native YUV reconstruction; an animation is not a single still and a lossless image never leaves RGBA. if c.animated { return Err(Error::UnsupportedFeature); } match c.image.ok_or(Error::MissingImage)? { - ImageChunk::Lossy(payload) => crate::lossy::decode_yuv_with(payload, &options), + ImageChunk::Lossy(payload) => crate::lossy::decode_yuv_with(payload, options), ImageChunk::Lossless(_) => Err(Error::UnsupportedFeature), } } /// Decode a still WebP read from any [`std::io::Read`] source into an [`Image`]. /// -/// The reader-based companion to [`decode`] (RGBA8 by default). The whole stream -/// is buffered before decoding; for push-based streaming use -/// [`incremental_decoder`]. +/// The reader-based companion to [`decode`] (RGBA8 by default). +/// The whole stream is buffered before decoding; for push-based streaming use [`incremental_decoder`]. /// /// # Errors /// @@ -362,10 +335,9 @@ pub fn decode_reader(mut reader: R) -> Result { /// Encode an **RGBA8** pixel buffer as a lossless (`VP8L`) WebP file. /// -/// The one-call companion to [`decode`], mirroring [`decode_rgba`] on the encode -/// side. Uses the default [`Effort`]; for a different effort tier, embedded -/// metadata, or a non-RGBA input layout, use the [`Encoder`] builder. `rgba` must -/// be exactly `width * height * 4` bytes. +/// The one-call companion to [`decode`], mirroring [`decode_rgba`] on the encode side. +/// Uses the default [`Effort`]; for a different effort tier, embedded metadata, or a non-RGBA input layout, use the [`Encoder`] builder. +/// `rgba` must be exactly `width * height * 4` bytes. /// /// # Examples /// @@ -381,8 +353,7 @@ pub fn decode_reader(mut reader: R) -> Result { /// # Errors /// /// [`Error::InvalidDimensions`] for a zero or over-large canvas, -/// [`Error::PixelBufferMismatch`] if `rgba`'s length is wrong, otherwise any -/// encode error. +/// [`Error::PixelBufferMismatch`] if `rgba`'s length is wrong, otherwise any encode error. #[cfg(feature = "alloc")] pub fn encode_lossless_rgba(width: u32, height: u32, rgba: &[u8]) -> Result> { let image = ImageRef::new(Dimensions::new(width, height)?, PixelLayout::Rgba8, rgba)?; @@ -391,9 +362,9 @@ pub fn encode_lossless_rgba(width: u32, height: u32, rgba: &[u8]) -> Result R Encoder::lossy().quality(quality).encode_ref(image) } -/// Decode an `ALPH` chunk payload (including its 1-byte header) into a -/// `width * height` alpha plane. +/// Decode an `ALPH` chunk payload (including its 1-byte header) into a `width * height` alpha plane. /// -/// Parses the header, decompresses the plane (raw bytes, or a lossless `VP8L` -/// stream via [`crate::lossless::decode_alpha`]), then reverses its spatial filter. The -/// un-filter is applied identically for both compression methods — the filter is -/// orthogonal to how the plane was stored. -#[cfg(feature = "alloc")] -fn alpha_plane(alph: &[u8], width: u32, height: u32) -> Result> { - alpha_plane_with(alph, width, height, &DecodeOptions::default()) -} - -/// Like [`alpha_plane`] but threading `options` so [`decode_with`] can propagate -/// its `max_pixels` limit into the lossless `ALPH` alpha decode (checked before the -/// plane is allocated). The plane is the same size as the already-limited image, so -/// this is defense in depth rather than the primary guard. +/// Parses the header, decompresses the plane (raw bytes, or a lossless `VP8L` stream via [`crate::lossless::decode_alpha`]), then reverses its spatial filter. +/// The un-filter is applied identically for both compression methods — the filter is orthogonal to how the plane was stored. +/// Decode an `ALPH` plane while threading `options`, including the pixel limit and alpha dithering strength. +/// The plane is the same size as the already-limited image, so the size check is defense in depth rather than the primary guard. #[cfg(feature = "alloc")] fn alpha_plane_with( alph: &[u8], @@ -428,8 +389,7 @@ fn alpha_plane_with( options: &DecodeOptions, ) -> Result> { let (w, h) = (width as usize, height as usize); - // A `width * height` that overflows `usize` is an out-of-range size, not a short - // input — the already-validated image dimensions keep this unreachable in practice. + // A `width * height` that overflows `usize` is an out-of-range size, not a short input — the already-validated image dimensions keep this unreachable in practice. let count = w.checked_mul(h).ok_or(Error::InvalidDimensions)?; let (header, data) = parse_header(alph)?; let mut plane = match header.compression { @@ -439,45 +399,44 @@ fn alpha_plane_with( }, }; unfilter(header.filter, &mut plane, w, h); + if header.preprocessing == 1 && options.alpha_dithering_strength > 0 { + crate::alpha::dequantize_levels(&mut plane, w, h, options.alpha_dithering_strength); + } Ok(plane) } /// Decode an animated WebP into a lazy [`Frames`] iterator. /// /// Handles both lossless (`VP8L`) and lossy (`VP8 ` + optional `ALPH`) frames — -/// the latter via the lossy decoder with alpha compositing, injected into the -/// lossless codec's animation walker. +/// the latter via the lossy decoder with alpha compositing, injected into the lossless codec's animation walker. /// /// # Untrusted input /// -/// Safe by default: like [`decode`], each frame's canvas is capped at -/// [`DEFAULT_MAX_PIXELS`] before allocation. Use [`decode_frames_with`] with -/// [`DecodeOptions::max_pixels`] to choose another cap, or -/// [`DecodeOptions::unbounded`] for trusted input. +/// Safe by default: like [`decode`], each frame's canvas is capped at [`DEFAULT_MAX_PIXELS`] before allocation. +/// Use [`decode_frames_with`] with [`DecodeOptions::max_pixels`] to choose another cap, or [`DecodeOptions::unbounded`] for trusted input. /// /// # Errors /// -/// [`Error::UnsupportedFeature`] if `input` is not an animation, or a -/// container/bitstream error from a frame. +/// [`Error::UnsupportedFeature`] if `input` is not an animation, or a container/bitstream error from a frame. #[cfg(feature = "alloc")] pub fn decode_frames(input: &[u8]) -> Result> { decode_frames_with(input, &DecodeOptions::default()) } /// Like [`decode_frames`] with explicit [`DecodeOptions`] (output layout, -/// per-frame pixel limit); the crate's both-codecs frame decoder is always wired in. +/// per-frame pixel limit, metadata, and opt-in chroma / quantized-alpha dithering). +/// +/// The crate's both-codecs frame decoder is always wired in. /// /// # Errors /// -/// The same as [`decode_frames`], plus [`Error::LimitExceeded`] when a frame -/// exceeds `options.max_pixels`. +/// The same as [`decode_frames`], plus [`Error::LimitExceeded`] when a frame exceeds `options.max_pixels`. #[cfg(feature = "alloc")] pub fn decode_frames_with<'a>(input: &'a [u8], options: &DecodeOptions) -> Result> { crate::anim::decode_frames_with_decoder(input, options, WebpFrameDecoder) } -/// A push-based [`IncrementalDecoder`] for still images and animations, with the -/// lossy-frame hook wired so animated `VP8 ` frames decode. +/// A push-based [`IncrementalDecoder`] for still images and animations, with the lossy-frame hook wired so animated `VP8 ` frames decode. #[cfg(feature = "alloc")] #[must_use] pub fn incremental_decoder() -> IncrementalDecoder { @@ -489,15 +448,12 @@ pub fn incremental_decoder() -> IncrementalDecoder { enum Backend { /// Not yet classified — buffering until the first image chunk is reachable. Undecided, - /// Lossless still, or any animation: the `lossless` codec's incremental decoder (with the - /// lossy-frame hook injected so animated `VP8 ` frames decode). Boxed to keep the - /// enum small — the lossless decoder dwarfs the other variants. + /// Lossless still, or any animation: the `lossless` codec's incremental decoder (with the lossy-frame hook injected so animated `VP8 ` frames decode). + /// Boxed to keep the enum small — the lossless decoder dwarfs the other variants. Lossless(Box>), /// A bare lossy `VP8 ` still (no `VP8X`): true row streaming via [`crate::lossy`]. Lossy(Box), - /// An extended lossy still (`VP8X` + `VP8 `, possibly with `ALPH`): alpha / - /// metadata compositing is not row-streamable byte-identically, so buffer the - /// whole file and finish with a one-shot [`decode`]. + /// An extended lossy still (`VP8X` + `VP8 `, possibly with `ALPH`): alpha / metadata compositing is not row-streamable byte-identically, so buffer the whole file and finish with a one-shot [`decode`]. Deferred, } @@ -512,13 +468,10 @@ enum Decision { /// A push-based decoder for **any** still WebP or animation. /// -/// Dispatches on the container kind: lossless stills and all animations stream -/// through the lossless decoder, a bare lossy `VP8 ` still row-streams through the -/// lossy decoder, and an extended lossy still (which may carry `ALPH` alpha) is buffered and -/// finished with a one-shot [`decode`]. The pixels and error semantics match -/// [`decode`] exactly; the only new streaming capability over the -/// lossless/animation paths is the bare lossy still. `Read`-free, so it works on -/// `no_std + alloc`. +/// Dispatches on the container kind: lossless stills and all animations stream through the lossless decoder, a bare lossy `VP8 ` still row-streams through the lossy decoder, and an extended lossy still (which may carry `ALPH` alpha) is buffered and finished with a one-shot [`decode`]. +/// Non-zero lossy chroma dithering likewise buffers a bare lossy still until the full-frame filter is complete and yields no early row views. +/// The pixels and error semantics match [`decode`] exactly; the only new streaming capability over the lossless/animation paths is the bare lossy still. +/// `Read`-free, so it works on `no_std + alloc`. #[cfg(feature = "alloc")] pub struct IncrementalDecoder { buf: Vec, @@ -536,7 +489,8 @@ impl IncrementalDecoder { Self::with_options(DecodeOptions::default()) } - /// A new decoder with the given options (output layout, per-image pixel limit). + /// A new decoder with the given options (layout, per-image pixel limit, + /// metadata, and opt-in chroma / quantized-alpha dithering). #[must_use] pub const fn with_options(options: DecodeOptions) -> Self { Self { @@ -552,11 +506,9 @@ impl IncrementalDecoder { /// /// # Errors /// - /// The same errors as [`decode`], surfaced as soon as the buffered bytes make - /// them detectable. + /// The same errors as [`decode`], surfaced as soon as the buffered bytes make them detectable. pub fn push(&mut self, chunk: &[u8]) -> Result { - // Once a back end is chosen, forward the raw chunk to it (its own buffer - // already holds everything up to here, replayed on the deciding push). + // Once a back end is chosen, forward the raw chunk to it (its own buffer already holds everything up to here, replayed on the deciding push). match &mut self.backend { Backend::Lossless(be) => return be.push(chunk), Backend::Lossy(be) => return be.push(chunk), @@ -572,8 +524,7 @@ impl IncrementalDecoder { self.buf.extend_from_slice(chunk); match classify(&self.buf)? { Decision::Lossless => { - // Inject the both-codecs frame decoder so animated `VP8 ` frames - // decode (a bare `lossless` decoder would reject them). + // Inject the both-codecs frame decoder so animated `VP8 ` frames decode (a bare `lossless` decoder would reject them). let mut be = crate::lossless::IncrementalDecoder::with_options_and_decoder( self.options.clone(), WebpFrameDecoder, @@ -597,10 +548,9 @@ impl IncrementalDecoder { self.drive_deferred() }, Decision::NeedMore => { - // A complete-but-unclassifiable buffer (e.g. a malformed container) - // takes the one-shot path, preserving its exact error / image. + // A complete-but-unclassifiable buffer (e.g. a malformed container) takes the one-shot path, preserving its exact error / image. if is_complete(&self.buf) { - self.image = Some(decode(&self.buf)?); + self.image = Some(decode_with(&self.buf, &self.options)?); Ok(Progress::Finished) } else { Ok(Progress::NeedMoreInput) @@ -612,7 +562,7 @@ impl IncrementalDecoder { /// Finish the deferred (extended-lossy) path once the whole RIFF is buffered. fn drive_deferred(&mut self) -> Result { if is_complete(&self.buf) { - self.image = Some(decode(&self.buf)?); + self.image = Some(decode_with(&self.buf, &self.options)?); Ok(Progress::Finished) } else { Ok(Progress::NeedMoreInput) @@ -629,10 +579,8 @@ impl IncrementalDecoder { } } - /// Borrow the finalized-but-not-yet-viewed rows of a streamed still image (a - /// non-consuming early view). `None` unless a row-streaming back end is active - /// (a lossless or bare-lossy still); the deferred and animation paths yield no - /// rows. + /// Borrow the finalized-but-not-yet-viewed rows of a streamed still image (a non-consuming early view). + /// `None` unless a row-streaming back end is active (a lossless or bare-lossy still); the deferred and animation paths yield no rows. pub fn drain_rows(&mut self) -> Option> { match &mut self.backend { Backend::Lossless(be) => be.drain_rows(), @@ -641,8 +589,7 @@ impl IncrementalDecoder { } } - /// Retrieve the complete decoded image (an animation's first composited frame) - /// once [`Progress::Finished`] has been reported. + /// Retrieve the complete decoded image (an animation's first composited frame) once [`Progress::Finished`] has been reported. /// /// # Errors /// @@ -654,7 +601,7 @@ impl IncrementalDecoder { match self.backend { Backend::Lossless(be) => be.into_image(), Backend::Lossy(be) => be.into_image(), - Backend::Undecided | Backend::Deferred => decode(&self.buf), + Backend::Undecided | Backend::Deferred => decode_with(&self.buf, &self.options), } } } @@ -676,8 +623,7 @@ fn classify(buf: &[u8]) -> Result { if buf[0..4] != FourCc::RIFF.0 || buf[8..12] != FourCc::WEBP.0 { return Err(Error::NotWebp); } - // The header of a non-animated `VP8X`, if seen before the image chunk: its - // presence means an extended (possibly alpha-bearing) lossy still is deferred. + // The header of a non-animated `VP8X`, if seen before the image chunk: its presence means an extended (possibly alpha-bearing) lossy still is deferred. let mut vp8x_info: Option = None; for chunk in scan_chunks(buf) { match chunk.id { @@ -698,18 +644,14 @@ fn classify(buf: &[u8]) -> Result { false, )); }, - // A lossless still (crate::lossless re-reads any VP8X sidecar) or an animation - // chunk: crate::lossless streams it. + // A lossless still (crate::lossless re-reads any VP8X sidecar) or an animation chunk: crate::lossless streams it. FourCc::VP8L | FourCc::ANIM | FourCc::ANMF => return Ok(Decision::Lossless), FourCc::VP8 => { if let Some(info) = vp8x_info { // Extended lossy (has VP8X); the chunk we just reached says VP8. return Ok(Decision::Deferred(info.with_codec(Codec::Lossy))); } - // Bare VP8: row-stream only if it spans the whole declared RIFF - // body (`chunk.next` is the padded end), so no trailing - // ALPH/metadata chunk can diverge the opaque stream from the - // one-shot decode; otherwise defer. + // Bare VP8: row-stream only if it spans the whole declared RIFF body (`chunk.next` is the padded end), so no trailing ALPH/metadata chunk can diverge the opaque stream from the one-shot decode; otherwise defer. return match declared_len(buf) { Some(riff_end) if chunk.next >= riff_end => Ok(Decision::Lossy), Some(_) => Ok(bare_deferred(buf.get(chunk.payload_start..))), @@ -722,9 +664,8 @@ fn classify(buf: &[u8]) -> Result { Ok(Decision::NeedMore) } -/// A bare `VP8 ` followed by trailing chunks (e.g. a malformed `ALPH` with no -/// `VP8X`): defer to the one-shot decode, peeking the VP8 dimensions for the -/// header report. `NeedMore` until the 10-byte VP8 header is buffered. +/// A bare `VP8 ` followed by trailing chunks (e.g. a malformed `ALPH` with no `VP8X`): defer to the one-shot decode, peeking the VP8 dimensions for the header report. +/// `NeedMore` until the 10-byte VP8 header is buffered. #[cfg(feature = "alloc")] fn bare_deferred(vp8_payload: Option<&[u8]>) -> Decision { vp8_payload @@ -736,8 +677,7 @@ fn bare_deferred(vp8_payload: Option<&[u8]>) -> Decision { }) } -/// Decode an animation's first composited frame as a still [`Image`], honoring -/// `options` (output layout, per-frame pixel limit). +/// Decode an animation's first composited frame as a still [`Image`], honoring `options` (output layout, per-frame pixel limit). #[cfg(feature = "alloc")] fn first_composited_frame(input: &[u8], options: &DecodeOptions) -> Result { decode_frames_with(input, options)? @@ -747,17 +687,13 @@ fn first_composited_frame(input: &[u8], options: &DecodeOptions) -> Result l) { return Err(Error::LimitExceeded { pixels, limit }); @@ -788,7 +723,7 @@ impl crate::stream::FrameDecoder for WebpFrameDecoder { return Err(Error::InvalidContainer); } if let Some(alph) = frame.alph { - let plane = alpha_plane(alph, dims.width(), dims.height())?; + let plane = alpha_plane_with(alph, dims.width(), dims.height(), options)?; for (pixel, &a) in argb.iter_mut().zip(&plane) { *pixel = (*pixel & 0x00FF_FFFF) | (u32::from(a) << 24); } @@ -803,30 +738,22 @@ impl crate::stream::FrameDecoder for WebpFrameDecoder { /// Whether `input` is an animated WebP. /// -/// A cheap header probe (a `VP8X` animation flag, or an `ANIM`/`ANMF` chunk) that -/// decodes no pixels and is codec-agnostic (it works for a lossy file the same as a -/// lossless one). +/// A cheap header probe (a `VP8X` animation flag, or an `ANIM`/`ANMF` chunk) that decodes no pixels and is codec-agnostic (it works for a lossy file the same as a lossless one). /// /// # Errors /// -/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or -/// [`Error::InvalidContainer`] for a malformed `VP8X`. +/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or [`Error::InvalidContainer`] for a malformed `VP8X`. pub fn is_animated(input: &[u8]) -> Result { crate::container::reader::is_animated(input) } -/// Read a WebP's header — dimensions, alpha, metadata, animation — without -/// decoding a single pixel. +/// Read a WebP's header — dimensions, alpha, metadata, animation — without decoding a single pixel. /// /// Answers the "what is this file" question that [`decode`] answers expensively: -/// the cost is a chunk walk and one bitstream header, so it is the same on a -/// 40-byte image and a 40-megapixel one, and it succeeds even when the pixel data -/// is truncated or corrupt. +/// the cost is a chunk walk and one bitstream header, so it is the same on a 40-byte image and a 40-megapixel one, and it succeeds even when the pixel data is truncated or corrupt. /// -/// This is the one-shot counterpart to [`Progress::HeaderReady`]. The incremental -/// decoder reports a header only as a side effect of streaming — a file small -/// enough to decode within one `push` never emits the event at all — so it cannot -/// be used as a probe. +/// This is the one-shot counterpart to [`Progress::HeaderReady`]. +/// The incremental decoder reports a header only as a side effect of streaming — a file small enough to decode within one `push` never emits the event at all — so it cannot be used as a probe. /// /// # Examples /// @@ -848,9 +775,7 @@ pub fn is_animated(input: &[u8]) -> Result { /// /// # Errors /// -/// [`Error::NotWebp`] if the container magic is wrong, [`Error::Truncated`] if the -/// header is not fully present, [`Error::MissingImage`] if no image chunk is -/// found, or the codec's error for a malformed bitstream header. +/// [`Error::NotWebp`] if the container magic is wrong, [`Error::Truncated`] if the header is not fully present, [`Error::MissingImage`] if no image chunk is found, or the codec's error for a malformed bitstream header. #[cfg(feature = "alloc")] pub fn probe(input: &[u8]) -> Result { if input.len() < 12 { @@ -860,10 +785,8 @@ pub fn probe(input: &[u8]) -> Result { return Err(Error::NotWebp); } - // The walk mirrors `read_container`, which is what `decode` uses: the first - // image chunk wins, and the sidecars are collected wherever they sit. Anything - // narrower would let `probe` and `decode` disagree about one file — an `ALPH` - // needs no `VP8X` to announce it, and it may follow the image chunk. + // The walk mirrors `read_container`, which is what `decode` uses: the first image chunk wins, and the sidecars are collected wherever they sit. + // Anything narrower would let `probe` and `decode` disagree about one file — an `ALPH` needs no `VP8X` to announce it, and it may follow the image chunk. let mut image: Option = None; let mut sidecar_alpha = false; let mut sidecar_metadata = false; @@ -876,10 +799,9 @@ pub fn probe(input: &[u8]) -> Result { .ok_or(Error::Truncated)?; let info = Vp8xInfo::parse(data)?; // An animation is fully described by its VP8X: probe returns here, - // before any per-frame chunk, so its alpha/metadata must come from - // the flags. Its frames each carry their own codec, so the file has - // none. A still VP8X only wraps flags the image chunk and sidecars - // below state authoritatively, so it is not kept. + // before any per-frame chunk, so its alpha/metadata must come from the flags. + // Its frames each carry their own codec, so the file has none. + // A still VP8X only wraps flags the image chunk and sidecars below state authoritatively, so it is not kept. if info.flags.is_animated() { return Ok(ImageInfo::new( info.canvas, @@ -904,19 +826,16 @@ pub fn probe(input: &[u8]) -> Result { }, FourCc::ALPH => sidecar_alpha = true, FourCc::ICCP | FourCc::EXIF | FourCc::XMP => sidecar_metadata = true, - // Animation chunks with no preceding animated VP8X are malformed; a - // well-formed animation returns above. + // Animation chunks with no preceding animated VP8X are malformed; a well-formed animation returns above. FourCc::ANIM | FourCc::ANMF => return Err(Error::InvalidContainer), _ => {}, } } let mut info = image.ok_or(Error::MissingImage)?; - // A still's alpha and metadata are exactly what `decode` finds: the VP8L - // header's alpha bit (peeked above) or a lossy sibling `ALPH`, and the - // metadata sidecar chunks. `decode` reads chunk presence, not the VP8X flags, - // so `probe` does too — otherwise a VP8X that advertises a chunk it does not - // carry would make the two disagree. + // A still's alpha and metadata are exactly what `decode` finds: the VP8L header's alpha bit (peeked above) or a lossy sibling `ALPH`, and the metadata sidecar chunks. + // `decode` reads chunk presence, not the VP8X flags, + // so `probe` does too — otherwise a VP8X that advertises a chunk it does not carry would make the two disagree. info.has_alpha |= sidecar_alpha; info.has_metadata |= sidecar_metadata; Ok(info) @@ -928,14 +847,10 @@ const fn vp8x_has_metadata(info: Vp8xInfo) -> bool { info.flags.has_icc() || info.flags.has_exif() || info.flags.has_xmp() } -/// Read a WebP file's sidecar [`Metadata`] — ICC profile, Exif, XMP — without -/// decoding a single pixel. +/// Read a WebP file's sidecar [`Metadata`] — ICC profile, Exif, XMP — without decoding a single pixel. /// -/// A chunk-level walk that collects the `ICCP`/`EXIF`/`XMP ` chunks wherever they -/// sit (still or animation): the read half of [`write_metadata`], and the metadata -/// companion to [`probe`]. It interprets no bitstream, so the cost is independent -/// of the image size, and it answers for a file whose pixel data would not decode -/// as long as the container framing is intact. +/// A chunk-level walk that collects the `ICCP`/`EXIF`/`XMP ` chunks wherever they sit (still or animation): the read half of [`write_metadata`], and the metadata companion to [`probe`]. +/// It interprets no bitstream, so the cost is independent of the image size, and it answers for a file whose pixel data would not decode as long as the container framing is intact. /// /// # Examples /// @@ -955,15 +870,13 @@ const fn vp8x_has_metadata(info: Vp8xInfo) -> bool { /// /// # Errors /// -/// [`Error::NotWebp`] for a bad `RIFF`/`WEBP` magic, or [`Error::Truncated`] for a -/// short or malformed container. +/// [`Error::NotWebp`] for a bad `RIFF`/`WEBP` magic, or [`Error::Truncated`] for a short or malformed container. #[cfg(feature = "alloc")] pub fn read_metadata(input: &[u8]) -> Result { let mut metadata = Metadata::none(); for chunk in crate::container::reader::chunks(input)? { let chunk = chunk?; - // First-wins, mirroring the `image.is_none()` VP8L guard: a duplicate - // `ICCP`/`EXIF`/`XMP ` must not silently override the earlier one. + // First-wins, mirroring the `image.is_none()` VP8L guard: a duplicate `ICCP`/`EXIF`/`XMP ` must not silently override the earlier one. match chunk.id { FourCc::ICCP if metadata.icc_profile.is_none() => { metadata.icc_profile = Some(chunk.data.to_vec()); @@ -979,12 +892,9 @@ pub fn read_metadata(input: &[u8]) -> Result { /// Rewrite a WebP file's sidecar metadata — set, replace, or strip ICC/Exif/XMP — /// without re-encoding or even decoding the image bitstream. /// -/// The webpmux-style companion to [`read_metadata`]: the `VP8 `/`VP8L`/`ALPH`/ -/// `ANIM`/`ANMF` image chunks are copied through byte-for-byte, the old -/// `ICCP`/`EXIF`/`XMP `/`VP8X` are dropped, and a fresh `VP8X` plus the new -/// metadata chunks are emitted in the spec's order. Stills and animations alike are -/// handled. An empty `metadata` on a file that was already the simple form yields a -/// bare (`VP8X`-free) file again. +/// The webpmux-style companion to [`read_metadata`]: the `VP8 `/`VP8L`/`ALPH`/ `ANIM`/`ANMF` image chunks are copied through byte-for-byte, the old `ICCP`/`EXIF`/`XMP `/`VP8X` are dropped, and a fresh `VP8X` plus the new metadata chunks are emitted in the spec's order. +/// Stills and animations alike are handled. +/// An empty `metadata` on a file that was already the simple form yields a bare (`VP8X`-free) file again. /// /// # Examples /// @@ -1007,12 +917,10 @@ pub fn read_metadata(input: &[u8]) -> Result { /// /// # Errors /// -/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or the -/// errors of [`probe`] when the container header cannot be read. +/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or the errors of [`probe`] when the container header cannot be read. #[cfg(feature = "alloc")] pub fn write_metadata(input: &[u8], metadata: &Metadata) -> Result> { - // `probe` supplies the canvas, alpha, and animation facts from the header - // alone, so the container rewrite interprets no bitstream itself. + // `probe` supplies the canvas, alpha, and animation facts from the header alone, so the container rewrite interprets no bitstream itself. let info = probe(input)?; crate::container::mux::rewrite_metadata( input, @@ -1026,12 +934,11 @@ pub fn write_metadata(input: &[u8], metadata: &Metadata) -> Result> { /// Read an animation's canvas, loop count, frame count and total duration — /// without decoding a single frame. /// -/// The animation counterpart to [`probe`]. Every fact lives in the `VP8X`, -/// `ANIM` and `ANMF` headers, so the cost is a chunk walk however many frames -/// there are, and a file whose frame data is damaged still answers. +/// The animation counterpart to [`probe`]. +/// Every fact lives in the `VP8X`, +/// `ANIM` and `ANMF` headers, so the cost is a chunk walk however many frames there are, and a file whose frame data is damaged still answers. /// -/// libwebp's `WebPAnimInfo` is the same idea; [`decode_frames`] is what to use -/// when you actually want the pixels. +/// libwebp's `WebPAnimInfo` is the same idea; [`decode_frames`] is what to use when you actually want the pixels. /// /// ``` /// # fn main() -> Result<(), webpkit::Error> { @@ -1048,12 +955,10 @@ pub fn write_metadata(input: &[u8], metadata: &Metadata) -> Result> { /// # Errors /// /// [`Error::UnsupportedFeature`] if the file is not an animation, -/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or -/// [`Error::InvalidContainer`] for a malformed `ANIM`. +/// [`Error::NotWebp`]/[`Error::Truncated`] for a non-WebP or short input, or [`Error::InvalidContainer`] for a malformed `ANIM`. #[cfg(feature = "alloc")] pub fn probe_animation(input: &[u8]) -> Result { - // Lifted so a canvas too large to composite can still be described; nothing - // here allocates per pixel. + // Lifted so a canvas too large to composite can still be described; nothing here allocates per pixel. let options = DecodeOptions::new().unbounded(); Ok(decode_frames_with(input, &options)?.anim_info()) } @@ -1078,9 +983,8 @@ mod tests { /// A 32x32 image of busy pixels, encoded with `make`. /// - /// Deliberately not a flat fill: a flat 64x64 encodes to 28 bytes, so half of - /// it is header and a truncation test on it would be measuring the header - /// rather than the body. Busy pixels give a body worth truncating. + /// Deliberately not a flat fill: a flat 64x64 encodes to 28 bytes, so half of it is header and a truncation test on it would be measuring the header rather than the body. + /// Busy pixels give a body worth truncating. fn sample(make: fn(ImageRef<'_>) -> Vec) -> Vec { let rgba: Vec = (0..dims().width() * dims().height() * 4) .map(|i| (i * 7 % 251) as u8) @@ -1134,8 +1038,8 @@ mod tests { } /// An animation's frames each carry their own image chunk and need not agree, - /// so the container header cannot answer for the file. `None` says that; it - /// does not mean "unknown codec". + /// so the container header cannot answer for the file. + /// `None` says that; it does not mean "unknown codec". #[test] fn probe_leaves_an_animations_codec_unanswered() { let canvas = Dimensions::new(16, 8).unwrap(); @@ -1158,8 +1062,8 @@ mod tests { assert_eq!(probe(&bytes).unwrap().codec, None); } - /// One file, two ways of asking: a probe and a stream must not disagree about - /// what coded it. Nothing else keeps these two paths in step. + /// One file, two ways of asking: a probe and a stream must not disagree about what coded it. + /// Nothing else keeps these two paths in step. #[test] fn probe_and_streaming_agree_on_the_codec() { for bytes in [lossless_bytes(), lossy_bytes()] { @@ -1182,9 +1086,7 @@ mod tests { } } - /// A bare `VP8 ` + `ALPH` with no `VP8X`: alpha announced by a chunk that no - /// header flag mentions, which is the shape - /// `composites_a_raw_alpha_chunk_onto_a_lossy_image` pins for `decode`. + /// A bare `VP8 ` + `ALPH` with no `VP8X`: alpha announced by a chunk that no header flag mentions, which is the shape `composites_a_raw_alpha_chunk_onto_a_lossy_image` pins for `decode`. fn lossy_with_bare_alpha() -> Vec { let vp8_key_frame = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; let mut alph = vec![0x00u8]; @@ -1195,8 +1097,7 @@ mod tests { riff_envelope(&body) } - /// Metadata written *after* the image chunk, so a walk that stops at the image - /// never sees it. + /// Metadata written *after* the image chunk, so a walk that stops at the image never sees it. fn lossless_with_trailing_metadata() -> Vec { let rgba = vec![0x11u8; 4 * 4 * 4]; let dims = Dimensions::new(4, 4).unwrap(); @@ -1210,15 +1111,13 @@ mod tests { .unwrap() } - /// The invariant, not an example of it: two ways of asking one file must not - /// disagree. `probe` is a promise about what `decode` will produce, so every - /// field they share is checked over every container shape the crate builds — + /// The invariant, not an example of it: two ways of asking one file must not disagree. + /// `probe` is a promise about what `decode` will produce, so every field they share is checked over every container shape the crate builds — /// simple, extended, sidecar-alpha-without-a-flag, trailing metadata. /// - /// This exists because `probe` shipped believing a `VP8X` flag was "the only - /// place to look" for alpha. That is true of the streaming classifier it was - /// copied from, which has not buffered the trailing chunks yet. It is false of - /// a probe, which is handed the whole file. + /// This exists because `probe` shipped believing a `VP8X` flag was "the only place to look" for alpha. + /// That is true of the streaming classifier it was copied from, which has not buffered the trailing chunks yet. + /// It is false of a probe, which is handed the whole file. #[test] fn probe_never_contradicts_decode() { let cases: [(&str, Vec); 4] = [ @@ -1247,8 +1146,8 @@ mod tests { } } - /// The honest boundary: a probe needs the header. Cutting into it must fail - /// rather than invent dimensions. + /// The honest boundary: a probe needs the header. + /// Cutting into it must fail rather than invent dimensions. #[test] fn probe_fails_when_the_truncation_reaches_the_header() { assert!(probe(&lossless_bytes()[..14]).is_err()); @@ -1305,10 +1204,8 @@ mod tests { } } - /// `probe_animation` reports the exact frame count and the sum of the frame - /// durations, read from the `ANMF` headers — no decode. Distinct durations so - /// the total is not reproducible by dropping or multiplying frames: 3 frames of - /// 100/150/200 ms are 3 and 450, not 1, not 100·150·200. + /// `probe_animation` reports the exact frame count and the sum of the frame durations, read from the `ANMF` headers — no decode. + /// Distinct durations so the total is not reproducible by dropping or multiplying frames: 3 frames of 100/150/200 ms are 3 and 450, not 1, not 100·150·200. #[test] fn probe_animation_counts_frames_and_sums_durations() { let canvas = Dimensions::new(8, 6).unwrap(); @@ -1327,8 +1224,7 @@ mod tests { assert_eq!(info.total_duration_ms, 450); } - /// `decode_yuv` recovers a lossy still's native 4:2:0 planes; odd sides pin the - /// ceil-halved chroma dimensions and the `y = w*h`, `u = v = cw*ch` plane sizes. + /// `decode_yuv` recovers a lossy still's native 4:2:0 planes; odd sides pin the ceil-halved chroma dimensions and the `y = w*h`, `u = v = cw*ch` plane sizes. #[test] fn decode_yuv_recovers_4_2_0_plane_shapes_for_a_lossy_still() { let (w, h) = (7u32, 5u32); @@ -1367,9 +1263,8 @@ mod tests { assert_eq!(decode_yuv(&bytes), Err(Error::UnsupportedFeature)); } - /// Metadata announced only by a sidecar chunk, with no `VP8X` at all: `probe` - /// must report it, matching `decode`, which reads the chunk. Pins the - /// `ICCP|EXIF|XMP` collection arm and the `has_metadata` fold. + /// Metadata announced only by a sidecar chunk, with no `VP8X` at all: `probe` must report it, matching `decode`, which reads the chunk. + /// Pins the `ICCP|EXIF|XMP` collection arm and the `has_metadata` fold. #[test] fn probe_reads_metadata_from_a_sidecar_without_a_vp8x() { // A bare `VP8L` still (no `VP8X`), then a trailing `EXIF` chunk. @@ -1383,9 +1278,8 @@ mod tests { ); } - /// An animation's alpha and metadata come from its `VP8X` flags, since `probe` - /// returns at the animated header before any per-frame chunk. Pins - /// `vp8x_has_metadata` per flag — a single flag must still report metadata, + /// An animation's alpha and metadata come from its `VP8X` flags, since `probe` returns at the animated header before any per-frame chunk. + /// Pins `vp8x_has_metadata` per flag — a single flag must still report metadata, /// which `&&` between the three would deny. #[test] fn probe_reads_animation_metadata_from_vp8x_flags() { @@ -1396,8 +1290,7 @@ mod tests { push_chunk(&mut body, FourCc::VP8X, &Vp8xInfo::build(flags, canvas)); riff_envelope(&body) }; - // Each metadata kind alone must report metadata: `&&` between the three - // flags would deny a single one. + // Each metadata kind alone must report metadata: `&&` between the three flags would deny a single one. let cases = [ Metadata { icc_profile: Some(vec![1]), @@ -1421,8 +1314,7 @@ mod tests { assert!(info.is_animated && !info.has_metadata); } - /// `ANIM`/`ANMF` chunks with no preceding animated `VP8X` are malformed and - /// rejected, not read as a still. + /// `ANIM`/`ANMF` chunks with no preceding animated `VP8X` are malformed and rejected, not read as a still. #[test] fn probe_rejects_animation_chunks_without_an_animated_vp8x() { let mut body = Vec::new(); @@ -1433,8 +1325,8 @@ mod tests { )); } - /// The first image chunk wins: a second one is ignored, so `probe` reports the - /// first's dimensions. Pins the `image.is_none()` guards. + /// The first image chunk wins: a second one is ignored, so `probe` reports the first's dimensions. + /// Pins the `image.is_none()` guards. #[test] fn probe_reports_the_first_image_chunk_when_two_are_present() { let first = lossless_bytes(); // 32x32 @@ -1454,10 +1346,8 @@ mod tests { ); } - /// The same first-image-wins guard on the lossy `VP8 ` arm: two `VP8 ` chunks - /// of different sizes, and `probe` must report the first's dimensions. The - /// `VP8L`-only case above never exercises the `VP8 ` arm, so its `image.is_none()` - /// guard needs its own two-chunk fixture. + /// The same first-image-wins guard on the lossy `VP8 ` arm: two `VP8 ` chunks of different sizes, and `probe` must report the first's dimensions. + /// The `VP8L`-only case above never exercises the `VP8 ` arm, so its `image.is_none()` guard needs its own two-chunk fixture. #[test] fn probe_reports_the_first_lossy_image_chunk_when_two_are_present() { let first = lossy_bytes(); // 32x32 @@ -1487,19 +1377,19 @@ mod tests { assert!(matches!(probe(b"tiny"), Err(Error::Truncated))); } - /// The length guard is `< 12`, and 12 is the smallest input that carries the - /// full `RIFF....WEBP` magic. 11 bytes is too short to even check the magic - /// (`Truncated`); 12 bytes is long enough, so a wrong magic is `NotWebp`, not - /// `Truncated`. Pins the boundary so `<` cannot become `<=`. + /// The length guard is `< 12`, and 12 is the smallest input that carries the full `RIFF....WEBP` magic. + /// 11 bytes is too short to even check the magic (`Truncated`); 12 bytes is long enough, so a wrong magic is `NotWebp`, not `Truncated`. + /// Pins the boundary so `<` cannot become `<=`. #[test] fn probe_length_guard_is_exactly_twelve() { assert!(matches!(probe(&[0u8; 11]), Err(Error::Truncated))); assert!(matches!(probe(&[0u8; 12]), Err(Error::NotWebp))); } - /// The magic check rejects when *either* `RIFF` or `WEBP` is wrong (`||`). A - /// file with a correct `RIFF` but a wrong `WEBP` fourcc must still be `NotWebp`; - /// with `&&` it would slip past the guard. The mirror case (wrong `RIFF`, + /// The magic check rejects when *either* `RIFF` or `WEBP` is wrong (`||`). + /// A file with a correct `RIFF` but a wrong `WEBP` fourcc must still be `NotWebp`; + /// with `&&` it would slip past the guard. + /// The mirror case (wrong `RIFF`, /// right `WEBP`) covers the other operand. #[test] fn probe_rejects_a_half_correct_magic() { @@ -1538,9 +1428,7 @@ mod tests { #[test] fn round_trips_a_lossy_image_through_the_umbrella() { - // Encoder::lossy writes a VP8 key frame; decode() routes it back - // through crate::lossy and returns an image of the right shape (lossy, so the - // pixels are close but not identical — only dimensions/opacity are pinned). + // Encoder::lossy writes a VP8 key frame; decode() routes it back through crate::lossy and returns an image of the right shape (lossy, so the pixels are close but not identical — only dimensions/opacity are pinned). let mut rgba = Vec::new(); for y in 0..16u8 { for x in 0..16u8 { @@ -1565,10 +1453,8 @@ mod tests { #[test] fn lossy_alpha_round_trips_byte_exact_through_the_umbrella() { - // Encode a lossy image with a NON-TRIVIAL alpha channel (a radial-ish - // gradient plus fully-transparent and fully-opaque regions) and decode it - // back. Alpha is LOSSLESS, so the decoded alpha lane must equal the source - // byte-for-byte; the container must upgrade to the extended VP8X + ALPH form. + // Encode a lossy image with a NON-TRIVIAL alpha channel (a radial-ish gradient plus fully-transparent and fully-opaque regions) and decode it back. + // Alpha is LOSSLESS, so the decoded alpha lane must equal the source byte-for-byte; the container must upgrade to the extended VP8X + ALPH form. let (w, h) = (24u32, 20u32); let mut rgba = Vec::new(); let mut source_alpha = Vec::new(); @@ -1622,9 +1508,7 @@ mod tests { #[test] fn round_trips_each_lossy_effort_through_the_umbrella() { - // Every effort preset (the shared `Effort`, now common to both codecs) - // rides on the shared `Encoder::lossy` builder, so each effort must produce - // a decodable, correctly-sized, fully-opaque VP8 image. + // Every effort preset (the shared `Effort`, now common to both codecs) rides on the shared `Encoder::lossy` builder, so each effort must produce a decodable, correctly-sized, fully-opaque VP8 image. let mut rgba = Vec::new(); for y in 0..16u8 { for x in 0..16u8 { @@ -1656,10 +1540,8 @@ mod tests { #[test] fn encode_image_lossy_preserves_metadata_through_the_umbrella() { - // `Encoder::lossy().encode(&Image)` routes an `Image` carrying metadata to - // the lossy encoder, which upgrades to the extended VP8X form and emits the - // ICCP/EXIF/XMP chunks. The lossless side is exercised by crate::lossless's own - // tests; here we confirm the lossy encode preserves metadata by default. + // `Encoder::lossy().encode(&Image)` routes an `Image` carrying metadata to the lossy encoder, which upgrades to the extended VP8X form and emits the ICCP/EXIF/XMP chunks. + // The lossless side is exercised by crate::lossless's own tests; here we confirm the lossy encode preserves metadata by default. let mut rgba = Vec::new(); for y in 0..16u8 { for x in 0..16u8 { @@ -1695,9 +1577,7 @@ mod tests { #[test] fn lossy_decode_surfaces_vp8x_metadata_and_round_trips() { - // Decoding a lossy VP8X file must surface its ICCP/EXIF/XMP into the - // returned `Image`, so a decode → `encode_image` round trip preserves the - // color profile and sidecar metadata (matching the lossless path). + // Decoding a lossy VP8X file must surface its ICCP/EXIF/XMP into the returned `Image`, so a decode → `encode_image` round trip preserves the color profile and sidecar metadata (matching the lossless path). let mut rgba = Vec::new(); for y in 0..16u8 { for x in 0..16u8 { @@ -1741,8 +1621,7 @@ mod tests { #[test] fn dispatches_a_lossy_file_to_the_vp8_decoder() { - // A RIFF/`VP8 ` container with a valid key-frame header routes to - // crate::lossy, which reconstructs it to an image of the declared size. + // A RIFF/`VP8 ` container with a valid key-frame header routes to crate::lossy, which reconstructs it to an image of the declared size. let vp8_key_frame = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; let mut body = Vec::new(); push_chunk(&mut body, FourCc::VP8, &vp8_key_frame); @@ -1753,8 +1632,7 @@ mod tests { #[test] fn composites_a_raw_alpha_chunk_onto_a_lossy_image() { - // A `VP8 ` 16x16 key-frame header plus a raw (method=0, filter=NONE) `ALPH` - // plane must decode to an image whose alpha channel is the plane's bytes. + // A `VP8 ` 16x16 key-frame header plus a raw (method=0, filter=NONE) `ALPH` plane must decode to an image whose alpha channel is the plane's bytes. let vp8_key_frame = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; let mut alph = Vec::new(); alph.push(0x00u8); // method=0 (none), filter=0 (NONE), pre_processing=0 @@ -1779,8 +1657,7 @@ mod tests { #[test] fn rejects_a_non_webp_input() { - // At least 12 bytes so the RIFF/WEBP magic check runs (a shorter input is - // reported as `Truncated` before the magic is even examined). + // At least 12 bytes so the RIFF/WEBP magic check runs (a shorter input is reported as `Truncated` before the magic is even examined). assert_eq!( decode(b"definitely not a webp file").unwrap_err(), Error::NotWebp @@ -1789,11 +1666,8 @@ mod tests { #[test] fn unified_streams_a_bare_lossy_still_and_drains_rows() { - // A bare RIFF/`VP8 ` still routes to crate::lossy and row-streams: one-byte - // pushes reproduce the one-shot decode and expose rows via drain_rows. A - // real 32x24 stream (unlike a tiny header-only frame, whose payload only - // completes at EOF) sets up the still stream well before completion, so - // rows are genuinely drained incrementally. + // A bare RIFF/`VP8 ` still routes to crate::lossy and row-streams: one-byte pushes reproduce the one-shot decode and expose rows via drain_rows. + // A real 32x24 stream (unlike a tiny header-only frame, whose payload only completes at EOF) sets up the still stream well before completion, so rows are genuinely drained incrementally. let vp8 = include_bytes!("../tests/fixtures/noise_32x24_q30.vp8"); let mut body = Vec::new(); push_chunk(&mut body, FourCc::VP8, vp8); @@ -1814,8 +1688,7 @@ mod tests { #[test] fn unified_streams_a_lossless_still() { - // A VP8L still routes to crate::lossless and streams; the assembled image matches - // the one-shot decode. + // A VP8L still routes to crate::lossless and streams; the assembled image matches the one-shot decode. let rgba: Vec = (0..16u8).flat_map(|i| [i * 3, i * 5, i * 7, 255]).collect(); let dims = Dimensions::new(4, 4).unwrap(); let file = Encoder::lossless() @@ -1832,8 +1705,7 @@ mod tests { #[test] fn unified_defers_a_lossy_still_with_alpha() { - // `VP8 ` + `ALPH` (alpha present) is deferred to the one-shot decode: no - // rows stream, but into_image composites the alpha exactly like decode(). + // `VP8 ` + `ALPH` (alpha present) is deferred to the one-shot decode: no rows stream, but into_image composites the alpha exactly like decode(). let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; let mut alph = vec![0x00u8]; // method=0, filter=NONE alph.resize(1 + 16 * 16, 0x80); @@ -1855,10 +1727,47 @@ mod tests { assert_eq!(image.as_bytes(), expected.as_bytes()); } + #[test] + fn deferred_into_image_retains_alpha_dithering_options() { + let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; + let mut alph = vec![crate::alpha::build_header( + crate::alpha::AlphaCompression::None, + crate::alpha::AlphaFilter::None, + 1, + )]; + for y in 0..16u8 { + for x in 0..16u8 { + alph.push((x / 4 + y / 4).saturating_mul(51)); + } + } + let mut body = Vec::new(); + push_chunk(&mut body, FourCc::VP8, &vp8); + push_chunk(&mut body, FourCc::ALPH, &alph); + let file = riff_envelope(&body); + let options = DecodeOptions::new().with_alpha_dithering_strength(100); + let expected = decode_with(&file, &options).unwrap(); + assert_ne!( + expected.as_bytes(), + decode(&file).unwrap().as_bytes(), + "fixture must exercise alpha level smoothing" + ); + + // A complete file reports HeaderReady on the deciding push. + // Calling into_image immediately must still use the configured one-shot path. + let mut decoder = IncrementalDecoder::with_options(options); + assert!(matches!( + decoder.push(&file).unwrap(), + Progress::HeaderReady(_) + )); + assert_eq!( + decoder.into_image().unwrap().as_bytes(), + expected.as_bytes() + ); + } + #[test] fn decode_with_enforces_max_pixels_on_a_lossless_still() { - // The umbrella decode_with propagates the pixel limit to the lossless - // decoder, which rejects an 8x8 (64px) image *before* allocating pixels. + // The umbrella decode_with propagates the pixel limit to the lossless decoder, which rejects an 8x8 (64px) image *before* allocating pixels. let rgba: Vec = (0u8..64).flat_map(|i| [i, 0, 0, 255]).collect(); let dims = Dimensions::new(8, 8).unwrap(); let img = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap(); @@ -1870,8 +1779,7 @@ mod tests { limit: 10, } ); - // A 64px image is far under the default `DEFAULT_MAX_PIXELS` cap, so the - // default options still decode it. + // A 64px image is far under the default `DEFAULT_MAX_PIXELS` cap, so the default options still decode it. assert_eq!( decode_with(&file, &DecodeOptions::default()) .unwrap() @@ -1882,11 +1790,8 @@ mod tests { #[test] fn plain_decode_is_bounded_by_default_on_a_hostile_header() { - // Safe by default: a bare `decode` (no options) must reject a header that - // claims a huge canvas — here a 16383x16383 (≈268 Mpx) lossy `VP8 ` key - // frame, well past `DEFAULT_MAX_PIXELS` (100 Mpx) — *before* any plane is - // allocated, so only the 10-byte header is needed. A regression that let - // `decode` bypass the default cap would allocate ≈1 GiB here instead. + // Safe by default: a bare `decode` (no options) must reject a header that claims a huge canvas — here a 16383x16383 (≈268 Mpx) lossy `VP8 ` key frame, well past `DEFAULT_MAX_PIXELS` (100 Mpx) — *before* any plane is allocated, so only the 10-byte header is needed. + // A regression that let `decode` bypass the default cap would allocate ≈1 GiB here instead. let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0xFF, 0x3F, 0xFF, 0x3F]; let mut body = Vec::new(); push_chunk(&mut body, FourCc::VP8, &vp8); @@ -1896,8 +1801,7 @@ mod tests { if limit == crate::DEFAULT_MAX_PIXELS), "plain decode must enforce DEFAULT_MAX_PIXELS on an oversized header" ); - // `.unbounded()` is the explicit escape hatch: the same header now passes the - // pixel guard (and fails later on the truncated body, not on the cap). + // `.unbounded()` is the explicit escape hatch: the same header now passes the pixel guard (and fails later on the truncated body, not on the cap). assert!( !matches!( decode_with(&file, &DecodeOptions::default().unbounded()), @@ -1922,8 +1826,7 @@ mod tests { } ); - // An animation: decode_with routes to the first composited frame through - // decode_frames_with, which rejects a 2x2 (4px) frame under a 1-pixel cap. + // An animation: decode_with routes to the first composited frame through decode_frames_with, which rejects a 2x2 (4px) frame under a 1-pixel cap. let canvas = Dimensions::new(2, 2).unwrap(); let red = [255u8, 0, 0, 255].repeat(4); let meta = FrameMeta { @@ -1949,8 +1852,7 @@ mod tests { #[test] fn metadata_policy_is_reexported_from_the_umbrella() { - // The umbrella re-exports the shared `MetadataPolicy` so a caller can name - // the `Encoder::metadata_policy` argument type through `webpkit`. + // The umbrella re-exports the shared `MetadataPolicy` so a caller can name the `Encoder::metadata_policy` argument type through `webpkit`. let dims = Dimensions::new(1, 1).unwrap(); let metadata = Metadata { icc_profile: Some(vec![1]), @@ -1977,8 +1879,7 @@ mod tests { #[test] fn unified_streams_animation_frames() { - // An animation routes to crate::lossless's frame walker: each frame composites and - // exposes its canvas; into_image returns the first composited frame. + // An animation routes to crate::lossless's frame walker: each frame composites and exposes its canvas; into_image returns the first composited frame. let canvas = Dimensions::new(2, 2).unwrap(); let red = [255u8, 0, 0, 255].repeat(4); let blue = [0u8, 0, 255, 255].repeat(4); @@ -2026,9 +1927,7 @@ mod tests { assert_eq!(dec.into_image().unwrap().as_bytes(), expected.as_bytes()); } - /// `write_metadata` then `read_metadata` round-trips an arbitrary metadata set - /// on a still of either codec, and the decoded pixels are unchanged — the - /// facade's promise that the image bitstream is never touched. + /// `write_metadata` then `read_metadata` round-trips an arbitrary metadata set on a still of either codec, and the decoded pixels are unchanged — the facade's promise that the image bitstream is never touched. #[test] fn write_then_read_metadata_round_trips_on_a_still() { for bytes in [lossless_bytes(), lossy_bytes()] { @@ -2047,8 +1946,7 @@ mod tests { } } - /// Stripping metadata (an empty `Metadata`) leaves the pixels intact and the - /// output carrying nothing — even when the source was the extended form. + /// Stripping metadata (an empty `Metadata`) leaves the pixels intact and the output carrying nothing — even when the source was the extended form. #[test] fn write_metadata_strips_and_preserves_pixels() { let tagged = lossless_with_trailing_metadata(); @@ -2061,8 +1959,7 @@ mod tests { ); } - /// An animation survives a metadata rewrite: it stays animated, its metadata - /// round-trips, and its first composited frame is unchanged. + /// An animation survives a metadata rewrite: it stays animated, its metadata round-trips, and its first composited frame is unchanged. #[test] fn write_metadata_preserves_an_animation() { let canvas = Dimensions::new(16, 8).unwrap(); @@ -2095,8 +1992,7 @@ mod tests { ); } - /// `read_metadata` reports a non-WebP input rather than inventing empty - /// metadata, and reads a sidecar that sits after the image chunk. + /// `read_metadata` reports a non-WebP input rather than inventing empty metadata, and reads a sidecar that sits after the image chunk. #[test] fn read_metadata_validates_and_finds_trailing_sidecars() { assert!(matches!( @@ -2109,4 +2005,132 @@ mod tests { Some(&[9, 9, 9, 9][..]) ); } + + /// `classify` needs the full 12-byte `RIFF....WEBP` magic before it inspects the container: a sub-12-byte buffer is `NeedMore` (never an index panic), + /// and a complete-length buffer with the wrong magic is `NotWebp`. + /// Pins the `< 12` length guard against `<=`/`==`/`>=` (which would reject a valid 12-byte header) and `>` (which would index past a sub-12-byte buffer). + #[test] + fn classify_requires_twelve_bytes_before_the_magic_check() { + assert!(matches!( + super::classify(&[0u8; 4]), + Ok(super::Decision::NeedMore) + )); + assert!(matches!(super::classify(&[0u8; 12]), Err(Error::NotWebp))); + } + + /// `classify`'s magic check rejects when *either* `RIFF` or `WEBP` is wrong (`||`): a correct `RIFF` with a wrong `WEBP` fourcc, and the mirror, must be `NotWebp`. + /// With `&&` a half-correct magic would slip into the chunk walk. + #[test] + fn classify_rejects_a_half_correct_magic() { + assert!(matches!( + super::classify(b"RIFF\0\0\0\0XXXX"), + Err(Error::NotWebp) + )); + assert!(matches!( + super::classify(b"XXXX\0\0\0\0WEBP"), + Err(Error::NotWebp) + )); + } + + /// An extended lossy still (`VP8X` + `VP8 `) is `Deferred`, and its `ImageInfo` carries the `VP8X` metadata flags. + /// A single metadata flag (EXIF only) must still report `has_metadata` — pinning both the `VP8X` arm (whose deletion drops to the bare-`VP8 ` path, which reports no metadata) and the `has_icc() || has_exif() || has_xmp()` fold (which `&&` would deny for one flag). + #[test] + fn classify_defers_extended_lossy_and_reads_a_single_metadata_flag() { + let dims16 = Dimensions::new(16, 16).unwrap(); + let vp8x = Vp8xInfo::build(Vp8xFlags(0x08), dims16); // EXIF flag only + let vp8 = [0x10u8, 0x00, 0x00, 0x9d, 0x01, 0x2a, 16, 0, 16, 0]; + let mut body = Vec::new(); + push_chunk(&mut body, FourCc::VP8X, &vp8x); + push_chunk(&mut body, FourCc::VP8, &vp8); + let file = riff_envelope(&body); + match super::classify(&file) { + Ok(super::Decision::Deferred(info)) => { + assert!(info.has_metadata, "a single EXIF flag must report metadata"); + assert_eq!(info.dimensions, dims16); + assert_eq!(info.codec, Some(Codec::Lossy)); + }, + _ => panic!("classify must defer an extended lossy still"), + } + } + + /// The umbrella frame decoder's per-frame pixel guard is a strict `>`: a lossy frame whose pixel count *equals* the cap decodes, and one that *exceeds* it by a single pixel is `LimitExceeded`. + /// Pins `pixels > limit` against `==` (which would admit an over-cap frame) and `>=` (which would reject an exactly-at-cap frame). + /// This is the only guard for a lossy animation frame; a `VP8L` frame is checked inside the lossless decoder before this line. + #[test] + fn webp_frame_decoder_lossy_pixel_guard_is_strict_greater_than() { + use crate::container::reader::{ImageChunk, locate_image}; + use crate::stream::{FrameDecoder, FramePayload}; + + let rgba: Vec = (0..16u32 * 16 * 4).map(|i| (i * 7 % 251) as u8).collect(); + let webp = crate::encode_lossy_rgba(16, 16, &rgba, 80).unwrap(); + let ImageChunk::Lossy(vp8) = locate_image(&webp).unwrap() else { + panic!("encode_lossy_rgba must produce a VP8 chunk"); + }; + let payload = FramePayload { + vp8l: None, + vp8: Some(vp8), + alph: None, + dims: Dimensions::new(16, 16).unwrap(), + }; + let decoder = super::WebpFrameDecoder; + // 16*16 = 256 pixels. + // Exactly at the cap: decodes (kills `>` -> `>=`/`==`). + assert!( + decoder + .decode_frame(payload, &DecodeOptions::new().max_pixels(256)) + .is_ok() + ); + // One pixel over the cap: rejected (kills `>` -> `==`). + assert_eq!( + decoder + .decode_frame(payload, &DecodeOptions::new().max_pixels(255)) + .unwrap_err(), + Error::LimitExceeded { + pixels: 256, + limit: 255, + } + ); + } + + /// The umbrella `is_animated` facade reports the real answer, not a constant: a lossless still is `false`, an animation is `true`, and a non-WebP input is an error. + /// Pins the delegation so the body cannot collapse to `Ok(true)`. + #[test] + fn is_animated_facade_reports_the_real_answer() { + assert!(!super::is_animated(&lossless_bytes()).unwrap()); + assert!(super::is_animated(b"not a webp file").is_err()); + let canvas = Dimensions::new(8, 6).unwrap(); + let rgba = vec![0x30u8; 8 * 6 * 4]; + let frame = ImageRef::new(canvas, PixelLayout::Rgba8, &rgba).unwrap(); + let anim = crate::AnimationEncoder::new(canvas) + .add_frame(frame, frame_meta(canvas, 100)) + .unwrap() + .finish(); + assert!(super::is_animated(&anim).unwrap()); + } + + /// `read_metadata` is first-wins on duplicate sidecars: a second `ICCP`/`EXIF`/`XMP ` chunk must not override the first. + /// Pins each `.is_none()` guard — dropping it returns the last chunk, and negating it returns none at all, instead of the first. + #[test] + fn read_metadata_is_first_wins_on_duplicate_sidecars() { + let mut body = Vec::new(); + push_chunk(&mut body, FourCc::VP8L, &[0x2f]); + push_chunk(&mut body, FourCc::ICCP, &[1, 1]); + push_chunk(&mut body, FourCc::ICCP, &[2, 2]); + push_chunk(&mut body, FourCc::EXIF, &[3, 3]); + push_chunk(&mut body, FourCc::EXIF, &[4, 4]); + push_chunk(&mut body, FourCc::XMP, &[5, 5]); + push_chunk(&mut body, FourCc::XMP, &[6, 6]); + let file = riff_envelope(&body); + let md = super::read_metadata(&file).unwrap(); + assert_eq!(md.icc_profile.as_deref(), Some(&[1, 1][..])); + assert_eq!(md.exif.as_deref(), Some(&[3, 3][..])); + assert_eq!(md.xmp.as_deref(), Some(&[5, 5][..])); + } + + /// `version` returns the crate's real Cargo version, so it cannot be a stubbed `""` or an arbitrary string. + #[test] + fn version_reports_the_cargo_package_version() { + assert_eq!(super::version(), env!("CARGO_PKG_VERSION")); + assert_ne!(super::version(), ""); + } } diff --git a/crates/webpkit/src/lossless/bit_io/reader.rs b/crates/webpkit/src/lossless/bit_io/reader.rs index b27974a..9ee7a62 100644 --- a/crates/webpkit/src/lossless/bit_io/reader.rs +++ b/crates/webpkit/src/lossless/bit_io/reader.rs @@ -1,5 +1,7 @@ //! LSB-first bit reader over an in-memory byte slice. +use crate::lossless::work::work; + /// A little-endian, least-significant-bit-first reader over a byte slice. /// /// The VP8L bitstream is consumed LSB-first: the first bit read from a byte is @@ -111,6 +113,7 @@ impl<'a> BitReader<'a> { } self.bit_pos += n; while self.bit_pos >= 8 { + work!(Vp8lBitRefill); self.val >>= 8; if self.pos < self.buf.len() { self.val |= u64::from(self.buf[self.pos]) << 56; diff --git a/crates/webpkit/src/lossless/decoder.rs b/crates/webpkit/src/lossless/decoder.rs index da29f50..007c783 100644 --- a/crates/webpkit/src/lossless/decoder.rs +++ b/crates/webpkit/src/lossless/decoder.rs @@ -29,6 +29,7 @@ use crate::lossless::animation::{CompositedFrame, Frames, Vp8lFrameDecoder}; use crate::lossless::prelude::*; use crate::lossless::vp8l; use crate::lossless::vp8l::decode_incr::{Step, Vp8lStream}; +use crate::lossless::work::work; pub use crate::stream::{ DecodeOptions, DecodedFrame, FrameDecoder, FramePayload, ImageInfo, Progress, RowDrain, @@ -92,6 +93,7 @@ pub fn decode_vp8l(payload: &[u8], options: &DecodeOptions) -> Result { codec: Codec::Lossless, })?; let has_alpha = image::argb_has_alpha(&decoded.argb); + work!(Vp8lPackPixel, decoded.argb.len() as u64); let pixels = image::pack_pixels(options.layout, &decoded.argb); Ok(Image::from_parts( dims, @@ -548,6 +550,7 @@ impl IncrementalDecoder { let stop = (first_row + count) as usize * width; let rows_argb = &st.stream.ready()[start..stop]; st.has_alpha = st.has_alpha || image::argb_has_alpha(rows_argb); + work!(Vp8lPackPixel, rows_argb.len() as u64); let bytes = image::pack_pixels(st.layout, rows_argb); st.packed.extend_from_slice(&bytes); st.next_row = first_row + count; diff --git a/crates/webpkit/src/lossless/histogram.rs b/crates/webpkit/src/lossless/histogram.rs index 6ac9455..2177ed9 100644 --- a/crates/webpkit/src/lossless/histogram.rs +++ b/crates/webpkit/src/lossless/histogram.rs @@ -285,20 +285,12 @@ fn channel_bits(bins: &[u32]) -> (u64, u64) { (acc >> LOG2_FRAC_BITS, used) } -/// Shannon cost (whole bits) of a channel from its total count and the counts of -/// just its used (nonzero) symbols. Σ n·log2(total/n) = total·log2(total) − Σ n·log2(n), -/// fixed point — the O(used) sibling of [`channel_bits`], for the predictor's -/// per-tile mode-entropy scoring (avoids a 256-bin scan per mode). -pub(crate) fn shannon_bits(total: u64, used_counts: &[u32]) -> u64 { - if total == 0 { - return 0; - } - let mut acc = total * fixed_log2(total); - for &n in used_counts { - let n64 = u64::from(n); - acc -= n64 * fixed_log2(n64); - } - acc >> LOG2_FRAC_BITS +/// One bin's `n·log2(n)` term of a Shannon cost, in `fixed_log2`'s Q16 fixed point; `0` for an empty or single-count bin. +/// +/// A channel's cost is `total·log2(total) − Σ_bin bin_weight`, so merging a candidate into a running histogram changes the cost by a mode-invariant normalizer minus `Σ_bin [bin_weight(base + add) − bin_weight(base)]`. +/// The predictor's tile-mode search ranks candidates by that sum, touching only the bins the candidate hits instead of rescoring a 256-bin channel. +pub(crate) fn bin_weight(n: u64) -> u64 { + n * fixed_log2(n) } /// Shannon cost (whole bits) and used-symbol count of the element-wise SUM of two @@ -364,7 +356,7 @@ pub(crate) fn fixed_log2(x: u64) -> u64 { #[cfg(test)] mod tests { use super::{ - Histogram, LOG2_FRAC_BITS, auto_level, channel_bits, fixed_log2, header_bits, shannon_bits, + Histogram, LOG2_FRAC_BITS, auto_level, bin_weight, channel_bits, fixed_log2, header_bits, size_cap, }; use crate::effort::MAX_LEVEL; @@ -525,13 +517,37 @@ mod tests { } proptest! { - /// `shannon_bits(total, used_counts)` — fed the total and just the nonzero - /// bins — must equal the pixel-data term of the full-slice `channel_bits`. + /// `bin_weight` merge-gain must rank two equal-mass candidates exactly as merged `channel_bits` cost does: more gain is a cheaper merged channel. + /// The 1-bit guard skips pairs whose costs collide under Q16 truncation. #[test] - fn shannon_bits_matches_channel_bits(bins in prop::collection::vec(0u32..=5000, 0..=300)) { - let total: u64 = bins.iter().map(|&n| u64::from(n)).sum(); - let used: Vec = bins.iter().copied().filter(|&n| n > 0).collect(); - prop_assert_eq!(shannon_bits(total, &used), channel_bits(&bins).0); + fn merge_gain_ranks_like_merged_channel_cost( + base in prop::collection::vec(0u32..=500, 256..=256), + a_syms in prop::collection::vec(0u8..=255, 1..=200), + b_syms in prop::collection::vec(0u8..=255, 1..=200), + ) { + let histo = |syms: &[u8]| { + let mut h = vec![0u32; 256]; + for &s in syms.iter().take(a_syms.len().min(b_syms.len())) { + h[usize::from(s)] += 1; + } + h + }; + let (a, b) = (histo(&a_syms), histo(&b_syms)); + let gain = |add: &[u32]| -> u64 { + base.iter() + .zip(add) + .map(|(&x, &y)| { + bin_weight(u64::from(x) + u64::from(y)) - bin_weight(u64::from(x)) + }) + .sum() + }; + let merged = |add: &[u32]| -> u64 { + let sum: Vec = base.iter().zip(add).map(|(&x, &y)| x + y).collect(); + channel_bits(&sum).0 + }; + if merged(&a) + 1 < merged(&b) { + prop_assert!(gain(&a) > gain(&b)); + } } /// The entropy estimate is a lower bound on a real fixed-length coding: diff --git a/crates/webpkit/src/lossless/huffman/decode.rs b/crates/webpkit/src/lossless/huffman/decode.rs index 947d7a7..3472a95 100644 --- a/crates/webpkit/src/lossless/huffman/decode.rs +++ b/crates/webpkit/src/lossless/huffman/decode.rs @@ -7,6 +7,7 @@ use crate::lossless::constants::{ CODE_LENGTH_CODE_ORDER, CODE_LENGTH_CODES, CODE_LENGTH_EXTRA_BITS, CODE_LENGTH_REPEAT_OFFSETS, DEFAULT_CODE_LENGTH, HUFFMAN_TABLE_BITS, MAX_ALLOWED_CODE_LENGTH, }; +use crate::lossless::work::work; use crate::lossless::prelude::*; @@ -212,6 +213,7 @@ impl HuffmanTable { /// Decode one symbol, consuming its bits from `br`. pub(crate) fn read_symbol(&self, br: &mut BitReader<'_>) -> u16 { + work!(Vp8lHuffmanSymbol); let root_index = br.peek_bits(self.root_bits) as usize; // `root_index < 1 << root_bits <= arena.len()`: the root table always fills // the arena's first `1 << root_bits` slots. The direct index below is the one @@ -225,6 +227,7 @@ impl HuffmanTable { return entry.value; } // Second-level table. + work!(Vp8lHuffmanSecondLevel); br.consume(self.root_bits); let extra_bits = u32::from(entry.bits) - self.root_bits; let sub_index = root_index + entry.value as usize + br.peek_bits(extra_bits) as usize; diff --git a/crates/webpkit/src/lossless/lz77.rs b/crates/webpkit/src/lossless/lz77.rs index 4221c10..0d958d1 100644 --- a/crates/webpkit/src/lossless/lz77.rs +++ b/crates/webpkit/src/lossless/lz77.rs @@ -1,16 +1,14 @@ -//! LZ77 back-reference decode helpers: prefix codes with extra bits, and the -//! 2-D distance / plane-code mapping. Shared with the encoder (which uses the -//! inverse mappings), so both agree by construction. +//! LZ77 back-reference decode helpers: prefix codes with extra bits, and the 2-D distance / plane-code mapping. +//! Shared with the encoder (which uses the inverse mappings), so both agree by construction. +use alloc::vec::Vec; + use crate::lossless::bit_io::reader::BitReader; use crate::lossless::constants::{CODE_TO_PLANE, CODE_TO_PLANE_CODES}; -/// Decode a length or distance value from its prefix `symbol`, reading any extra -/// bits from `br`. Length and distance share this scheme (libwebp -/// `GetCopyLength` == `GetCopyDistance`). +/// Decode a length or distance value from its prefix `symbol`, reading any extra bits from `br`. +/// Length and distance share this scheme (libwebp `GetCopyLength` == `GetCopyDistance`). /// -/// Symbols `0..=3` are the literal values `1..=4` with no extra bits; larger -/// symbols encode `offset + extra + 1` where `offset` and the extra-bit count -/// grow geometrically. +/// Symbols `0..=3` are the literal values `1..=4` with no extra bits; larger symbols encode `offset + extra + 1` where `offset` and the extra-bit count grow geometrically. pub(crate) fn read_prefix_value(symbol: u32, br: &mut BitReader<'_>) -> u32 { if symbol < 4 { return symbol + 1; @@ -20,12 +18,9 @@ pub(crate) fn read_prefix_value(symbol: u32, br: &mut BitReader<'_>) -> u32 { offset + br.read_bits(extra_bits) + 1 } -/// Map a 1-based distance `plane_code` to a pixel distance for an image of width -/// `xsize`. +/// Map a 1-based distance `plane_code` to a pixel distance for an image of width `xsize`. /// -/// Plane codes `> 120` are literal distances (`plane_code - 120`); codes `1..=120` -/// index [`CODE_TO_PLANE`], whose packed byte gives a near-neighbor -/// `(yoffset, xoffset)` combined as `yoffset * xsize + xoffset`, clamped to `>= 1`. +/// Plane codes `> 120` are literal distances (`plane_code - 120`); codes `1..=120` index [`CODE_TO_PLANE`], whose packed byte gives a near-neighbor `(yoffset, xoffset)` combined as `yoffset * xsize + xoffset`, clamped to `>= 1`. #[allow( clippy::cast_possible_wrap, clippy::cast_possible_truncation, @@ -45,15 +40,10 @@ pub(crate) const fn plane_code_to_distance(xsize: u32, plane_code: u32) -> u32 { if dist >= 1 { dist as u32 } else { 1 } } -/// Encode a length or distance `value` (`>= 1`) into its prefix `symbol`, the -/// number of `extra_bits`, and the `extra_value` those bits carry. This is the -/// exact inverse of [`read_prefix_value`] (libwebp `VP8LPrefixEncode`): feeding -/// the returned `symbol` and `extra_value` back through `read_prefix_value` -/// reproduces `value`. +/// Encode a length or distance `value` (`>= 1`) into its prefix `symbol`, the number of `extra_bits`, and the `extra_value` those bits carry. +/// This is the exact inverse of [`read_prefix_value`] (libwebp `VP8LPrefixEncode`): feeding the returned `symbol` and `extra_value` back through `read_prefix_value` reproduces `value`. /// -/// Values `1..=4` are symbols `0..=3` with no extra bits (the `symbol < 4` fast -/// path); larger values split into the top two set bits of `value - 1` (the -/// `symbol`) and the remaining low bits (`extra_value`). +/// Values `1..=4` are symbols `0..=3` with no extra bits (the `symbol < 4` fast path); larger values split into the top two set bits of `value - 1` (the `symbol`) and the remaining low bits (`extra_value`). pub(crate) const fn prefix_encode(value: u32) -> (u32, u32, u32) { if value <= 4 { return (value - 1, 0, 0); @@ -70,19 +60,12 @@ pub(crate) const fn prefix_encode(value: u32) -> (u32, u32, u32) { ) } -/// Map a pixel `dist` (`>= 1`) to the smallest 1-based distance `plane_code` that -/// [`plane_code_to_distance`] turns back into `dist` for an image of width -/// `xsize` (libwebp `VP8LDistanceToPlaneCode`). +/// Map a pixel `dist` (`>= 1`) to the smallest 1-based distance `plane_code` that [`plane_code_to_distance`] turns back into `dist` for an image of width `xsize` (libwebp `VP8LDistanceToPlaneCode`). /// -/// The forward table is scanned in order so the smallest — hence -/// cheapest-to-code — plane code wins; distances with no near-neighbor encoding -/// fall back to the literal plane code `dist + 120`. Because the search reuses -/// the decoder's own [`plane_code_to_distance`], the round trip holds by -/// construction. +/// The forward table is scanned in order so the smallest — hence cheapest-to-code — plane code wins; distances with no near-neighbor encoding fall back to the literal plane code `dist + 120`. +/// Because the search reuses the decoder's own [`plane_code_to_distance`], the round trip holds by construction. /// -/// This O(120) scan is retained only as the byte-exact reference oracle for -/// [`PlaneCodeMap`], which the encoder builds once per width and looks up in -/// O(log n); `plane_code_map_matches_reference` pins the two equal. +/// This O(120) scan is retained only as the byte-exact reference oracle for [`PlaneCodeMap`], which the encoder builds once per width and looks up in O(log n); `plane_code_map_matches_reference` pins the two equal. #[cfg(test)] #[expect( clippy::cast_possible_truncation, @@ -99,19 +82,12 @@ pub(crate) fn distance_to_plane_code(xsize: u32, dist: u32) -> u32 { } /// A per-width reverse map from a pixel distance to its distance `plane_code`, -/// precomputed once so the encoder replaces [`distance_to_plane_code`]'s O(120) -/// linear rescan — run once per copy, and once per candidate per DP position — +/// precomputed once so the encoder replaces [`distance_to_plane_code`]'s O(120) linear rescan — run once per copy, and once per candidate per DP position — /// with an O(log n) lookup over the 120 near-neighbor distances. /// -/// `xsize` is fixed within each parse/emit, so the reachable near-neighbor -/// distances (and the plane code each maps to) are fixed too. The 120 `(distance, -/// plane_code)` pairs are held sorted by `(distance, plane_code)` in a fixed -/// stack array — no heap allocation, so building one per call stays cheap. A -/// lookup takes the FIRST pair with a matching distance; because equal distances -/// are ordered by ascending plane code, that first pair carries the smallest -/// plane code, reproducing the forward scan's first-match winner exactly. A -/// distance with no near-neighbor encoding matches none and falls back to the -/// literal plane code `dist + 120`, byte-identical to the scan. +/// `xsize` is fixed within each parse/emit, so the reachable near-neighbor distances (and the plane code each maps to) are fixed too. +/// The 120 `(distance, +/// plane_code)` pairs are held sorted by `(distance, plane_code)` in a fixed stack array — no heap allocation, so building one per call stays cheap. A lookup takes the FIRST pair with a matching distance; because equal distances are ordered by ascending plane code, that first pair carries the smallest plane code, reproducing the forward scan's first-match winner exactly. A distance with no near-neighbor encoding matches none and falls back to the literal plane code `dist + 120`, byte-identical to the scan. pub(crate) struct PlaneCodeMap { /// The 120 `(distance, plane_code)` pairs, sorted by `(distance, plane_code)`. entries: [(u32, u32); CODE_TO_PLANE_CODES], @@ -129,23 +105,19 @@ impl PlaneCodeMap { let plane_code = index as u32 + 1; *entry = (plane_code_to_distance(xsize, plane_code), plane_code); } - // Sorting by `(distance, plane_code)` puts the smallest plane code first - // within each equal-distance run, so the first match a lookup finds is the - // winner the ascending forward scan returns. + // Sorting by `(distance, plane_code)` puts the smallest plane code first within each equal-distance run, so the first match a lookup finds is the winner the ascending forward scan returns. entries.sort_unstable(); Self { entries } } - /// The distance `plane_code` for `dist` — identical to - /// [`distance_to_plane_code`] for the same `xsize`. + /// The distance `plane_code` for `dist` — identical to [`distance_to_plane_code`] for the same `xsize`. #[expect( clippy::cast_possible_truncation, reason = "CODE_TO_PLANE_CODES is 120 and every emitted distance is bounded \ by WINDOW_SIZE, so the miss-fallback cast is value-preserving" )] pub(crate) fn plane_code(&self, dist: u32) -> u32 { - // First index whose distance is `>= dist`; the array is sorted so that is - // the smallest-plane-code entry for `dist` when one exists. + // First index whose distance is `>= dist`; the array is sorted so that is the smallest-plane-code entry for `dist` when one exists. let index = self.entries.partition_point(|&(d, _)| d < dist); match self.entries.get(index) { Some(&(d, plane_code)) if d == dist => plane_code, @@ -154,6 +126,22 @@ impl PlaneCodeMap { } } +/// How many leading distance plane codes count as the *near* two-dimensional neighborhood: codes `1..=16` reach the same row and the three rows above within a `+/-3` column offset, and their prefix symbols carry at most one extra bit. +pub(crate) const NEAR_PLANE_CODES: u32 = 16; + +/// The distinct distances reachable through plane codes `1..=NEAR_PLANE_CODES` at width `xsize`, in ascending plane-code order and deduplicated (narrow images alias several codes onto the same distance). +/// Ascending order means a caller that only accepts a *strict* improvement keeps the smallest plane code among equal-length matches, so the cheapest encoding of a tie always wins. +pub(crate) fn near_distances(xsize: u32) -> Vec { + let mut dists = Vec::with_capacity(NEAR_PLANE_CODES as usize); + for plane_code in 1..=NEAR_PLANE_CODES { + let dist = plane_code_to_distance(xsize, plane_code); + if !dists.contains(&dist) { + dists.push(dist); + } + } + dists +} + #[cfg(test)] mod tests { use super::{ @@ -247,10 +235,7 @@ mod tests { #[test] fn plane_code_map_matches_reference_exhaustive() { - // Sweep a band of distances around every reachable near-neighbor multiple - // (`k * xsize`, `k <= 15`, x-offset within `-7..=8`) plus far misses, and - // assert the O(log n) map equals the O(120) reference scan bit-for-bit at - // every width — the tiny (1x1), tiny-width, and large (14-bit) extremes. + // Sweep a band of distances around every reachable near-neighbor multiple (`k * xsize`, `k <= 15`, x-offset within `-7..=8`) plus far misses, and assert the O(log n) map equals the O(120) reference scan bit-for-bit at every width — the tiny (1x1), tiny-width, and large (14-bit) extremes. for xsize in [1u32, 2, 3, 4, 7, 8, 16, 100, 1000, 16384] { let map = PlaneCodeMap::new(xsize); let mut dists = alloc::vec![1u32, 2, 100, 100_000, WINDOW_SIZE]; @@ -284,8 +269,7 @@ mod tests { } /// Round-trip a value through `prefix_encode` and the real decoder read, - /// exercising the actual `BitWriter` -> `BitReader` -> `read_prefix_value` - /// path (not just the algebra). + /// exercising the actual `BitWriter` -> `BitReader` -> `read_prefix_value` path (not just the algebra). fn round_trip_prefix(value: u32) -> u32 { let (symbol, extra_bits, extra_value) = prefix_encode(value); let mut bw = BitWriter::new(); @@ -299,8 +283,7 @@ mod tests { #[test] fn prefix_encode_inverts_read_prefix_value(value in 1u32..=1_048_576) { let (symbol, extra_bits, _) = prefix_encode(value); - // Extra bits always fit a single BitWriter call; symbols fit the - // widest (distance) alphabet. + // Extra bits always fit a single BitWriter call; symbols fit the widest (distance) alphabet. prop_assert!(extra_bits <= 24); prop_assert!(symbol < 40); prop_assert_eq!(round_trip_prefix(value), value); @@ -322,10 +305,8 @@ mod tests { prop_assert!(extra_bits <= 24); } - /// The memoized [`PlaneCodeMap`] returns byte-identical plane codes to the - /// [`distance_to_plane_code`] reference scan for every width/distance — - /// the mechanical proof the O(log n) lookup is behavior-neutral, sampling - /// the miss branch (far distances) as well as near-neighbor hits. + /// The memoized [`PlaneCodeMap`] returns byte-identical plane codes to the [`distance_to_plane_code`] reference scan for every width/distance — + /// the mechanical proof the O(log n) lookup is behavior-neutral, sampling the miss branch (far distances) as well as near-neighbor hits. #[test] fn plane_code_map_matches_reference( xsize in prop_oneof![ diff --git a/crates/webpkit/src/lossless/transform/cross_color.rs b/crates/webpkit/src/lossless/transform/cross_color.rs index f24a1c0..8d4e542 100644 --- a/crates/webpkit/src/lossless/transform/cross_color.rs +++ b/crates/webpkit/src/lossless/transform/cross_color.rs @@ -119,6 +119,7 @@ pub(crate) fn inverse_row(row: &mut [u32], y: usize, bits: u32, tile_data: &[u32 if width == 0 { return; } + work!(Vp8lCrossColorPixel, u64::from(width)); let tiles_per_row = subsample_size(width, bits) as usize; let tile_row = (y >> bits) * tiles_per_row; // All pixels in `[tx<> bits)` /// entries; the unused tail stays transparent `0x0000_0000` (not `ARGB_BLACK`). @@ -55,6 +56,7 @@ pub(crate) fn inverse(src: &[u32], dst_width: u32, bits: u32, palette: &[u32]) - #[must_use] pub(crate) fn inverse_row(src_row: &[u32], dst_width: u32, bits: u32, palette: &[u32]) -> Vec { let dst_w = dst_width as usize; + work!(Vp8lPalettePixel, u64::from(dst_width)); let bpp = 8u32 >> bits; let mut dst = vec![0u32; dst_w]; if bpp == 8 { diff --git a/crates/webpkit/src/lossless/transform/predictor.rs b/crates/webpkit/src/lossless/transform/predictor.rs index d68eb1c..cd08cdc 100644 --- a/crates/webpkit/src/lossless/transform/predictor.rs +++ b/crates/webpkit/src/lossless/transform/predictor.rs @@ -11,7 +11,7 @@ //! that `inverse` inverts it losslessly. use crate::lossless::constants::{ARGB_BLACK, subsample_size}; -use crate::lossless::histogram::shannon_bits; +use crate::lossless::histogram::bin_weight; use crate::lossless::prelude::*; use crate::lossless::transform::{add_pixels, sub_pixels}; use crate::lossless::work::work; @@ -22,6 +22,7 @@ pub(crate) fn inverse(argb: &mut [u32], width: u32, bits: u32, tile_data: &[u32] if width == 0 || argb.is_empty() { return; } + work!(Vp8lPredictorPixel, argb.len() as u64); let tiles_per_row = subsample_size(width, bits) as usize; let width = width as usize; let height = argb.len() / width; @@ -97,6 +98,7 @@ pub(crate) fn reconstruct_row_into( if width == 0 { return; } + work!(Vp8lPredictorPixel, width as u64); if y == 0 { // Row 0: origin against opaque black (mode 0), then left (mode 1). out[0] = add_pixels(residual[0], ARGB_BLACK); @@ -139,21 +141,26 @@ pub(crate) fn reconstruct_row_into( /// `tile_data` carries one pixel per tile with the mode packed as /// `(mode & 0x0f) << 8`, exactly where [`reconstruct_row`] reads it back. /// -/// Reused per-`forward` scratch for the predictor's per-tile entropy scoring: a -/// residual-byte histogram per channel (in `residual.to_le_bytes()` order — -/// blue, green, red, alpha), each paired with a touched-bin list so a mode's -/// accumulate + Shannon-finalize + reset is O(interior pixels), never a 256-bin -/// scan, plus a shared `used_counts` staging buffer. Allocated once in -/// [`forward`] and threaded through every `best_mode` call so mode scoring does -/// zero per-mode/per-tile heap work — every buffer is cleared and reused. +/// Reused per-`forward` scratch for the predictor's per-tile mode scoring: a +/// candidate residual-byte histogram per channel (in `residual.to_le_bytes()` +/// order — blue, green, red, alpha), each paired with a touched-bin list so a +/// mode's accumulate + score + reset is O(interior pixels), never a 256-bin scan, +/// plus the running image-wide histogram every candidate is scored against. +/// Allocated once in [`forward`] and threaded through every `best_mode` call so +/// mode scoring does zero per-mode/per-tile heap work. struct EntropyScratch { counts: [[u32; 256]; 4], touched: [Vec; 4], - used_counts: Vec, /// The interior pixels of the tile currently being scored, each as /// `(value, left, top_left, top, top_right)`. Gathered once per tile so all /// 14 modes reuse the same neighbor loads instead of re-reading `argb`. neighbors: Vec<(u32, u32, u32, u32, u32)>, + /// Per-channel residual-byte histogram of every tile already committed by [`forward`], in scan order. + /// One image-wide histogram is what actually gets entropy-coded, so tile modes are scored against it rather than in isolation. + committed: [[u32; 256]; 4], + /// How many committed tiles carry each mode. + /// The tile-mode sub-image is coded too, so a mode's own symbol cost is part of its score. + mode_counts: [u32; 16], } impl EntropyScratch { @@ -161,8 +168,9 @@ impl EntropyScratch { Self { counts: [[0u32; 256]; 4], touched: core::array::from_fn(|_| Vec::new()), - used_counts: Vec::new(), neighbors: Vec::new(), + committed: [[0u32; 256]; 4], + mode_counts: [0u32; 16], } } } @@ -172,24 +180,26 @@ impl EntropyScratch { /// use fixed modes 0/1/2 and are excluded, exactly as [`reconstruct_row`] /// decodes them. /// -/// Cost is the ENTROPY of the mode's residual over the tile interior: -/// `Σ_channel shannon_bits(total, used_counts)`, where `total` is the interior -/// pixel count (constant across modes) and `used_counts` are the residual-byte -/// histogram's nonzero bins. This rewards residuals that reduce to FEW distinct -/// symbols — a constant residual costs exactly 0 bits (a single symbol), so a -/// flat / periodic tile picks the LZ77-friendly mode instead of the one with -/// the tightest magnitude. The magnitude proxy this replaced misjudged those, -/// picking clip255-spiky gradient modes that break row-to-row back-references. +/// Cost is the mode's marginal contribution to the ONE image-wide residual +/// histogram that is actually entropy-coded: the increase in +/// `Σ_channel shannon(committed + tile)` over `Σ_channel shannon(committed)`, +/// where `scratch.committed` holds every earlier tile's residual bytes. Both +/// normalizer terms are mode-invariant (the interior pixel count is fixed), so +/// the argmin reduces to the argmax of `Σ_bin [w(committed+tile) - w(committed)]` +/// over the bins the tile touches. Scoring against the running histogram removes +/// the small-sample bias of a per-tile-local estimate and charges a mode for +/// fragmenting the global symbol distribution. /// -/// There is NO per-tile header term: a constant channel must score exactly 0 -/// so flat tiles tie. Ties resolve to the lowest mode index (strict `<` keeps -/// the first, ascending, winner), so a 0-entropy tile resolves to mode 1 -/// (mode 0 predicts opaque black, never constant on a non-black tile). +/// There is NO per-tile header term: a constant channel must contribute the same +/// weight for every mode so flat tiles tie. Ties resolve to the lowest mode index +/// (strict `>` keeps the first, ascending, winner). /// /// `scratch` is reused across every call so this allocates nothing; each mode /// only touches, then resets, the histogram bins it actually hit. The four /// neighbors are the same for every mode at a given pixel, so they are gathered -/// once into `scratch.neighbors` and reused across all 14 modes. +/// once into `scratch.neighbors` and reused across all 14 modes. The winning +/// mode's residual is folded into `scratch.committed` before returning, so +/// [`forward`]'s scan-order tile loop scores each tile against its predecessors. fn best_mode( argb: &[u32], width: usize, @@ -204,7 +214,6 @@ fn best_mode( let y0 = (ty << bits).max(1); let x1 = ((tx << bits) + tile).min(width); let y1 = ((ty << bits) + tile).min(height); - let total = (y1.saturating_sub(y0) * x1.saturating_sub(x0)) as u64; // Gather each interior pixel's value and its four neighbors once. They are // identical across all 14 modes, so this replaces 14 re-reads of `argb` per // pixel with a single pass — the scored order (row-major over the interior) @@ -224,7 +233,7 @@ fn best_mode( } } let mut best = 0u32; - let mut best_cost = u64::MAX; + let mut best_gain = 0u64; for mode in 0..=13u32 { work!(PredictorModeEval); // Accumulate the per-channel residual-byte histogram over the interior, @@ -239,29 +248,35 @@ fn best_mode( scratch.counts[c][idx] += 1; } } - // Finalize the Shannon cost per channel, then reset only touched bins. - let mut cost = 0u64; + // Merge gain against the committed histogram, then reset only touched bins. + let mut gain = 0u64; for c in 0..4 { - scratch.used_counts.clear(); let n = scratch.touched[c].len(); for k in 0..n { - let byte = scratch.touched[c][k]; - scratch - .used_counts - .push(scratch.counts[c][usize::from(byte)]); - } - cost += shannon_bits(total, &scratch.used_counts); - for k in 0..n { - let byte = scratch.touched[c][k]; - scratch.counts[c][usize::from(byte)] = 0; + let idx = usize::from(scratch.touched[c][k]); + let committed = u64::from(scratch.committed[c][idx]); + let merged = committed + u64::from(scratch.counts[c][idx]); + gain += bin_weight(merged) - bin_weight(committed); + scratch.counts[c][idx] = 0; } scratch.touched[c].clear(); } - if cost < best_cost { - best_cost = cost; + // The tile's own mode symbol lands in the mode sub-image histogram. + let seen = u64::from(scratch.mode_counts[mode as usize]); + gain += bin_weight(seen + 1) - bin_weight(seen); + if gain > best_gain { + best_gain = gain; best = mode; } } + scratch.mode_counts[best as usize] += 1; + // Commit the winner so later tiles score against the histogram they extend. + for &(cur, left, top_left, top, top_right) in &scratch.neighbors { + let residual = sub_pixels(cur, predict(best, left, top_left, top, top_right)); + for (c, &byte) in residual.to_le_bytes().iter().enumerate() { + scratch.committed[c][usize::from(byte)] += 1; + } + } best } @@ -440,9 +455,10 @@ mod tests { use super::{ EntropyScratch, add_sub_full, add_sub_half, average2, average3, average4, best_mode, clamped_add_subtract_full, clamped_add_subtract_half, clip255, forward, inverse, predict, - reconstruct_row_into, select, shannon_bits, sub_pixels, + reconstruct_row_into, select, sub_pixels, }; use crate::lossless::constants::subsample_size; + use crate::lossless::histogram::bin_weight; use proptest::prelude::*; /// Pre-hoist reference for [`best_mode`]: re-reads the four neighbors from @@ -462,9 +478,8 @@ mod tests { let y0 = (ty << bits).max(1); let x1 = ((tx << bits) + tile).min(width); let y1 = ((ty << bits) + tile).min(height); - let total = (y1.saturating_sub(y0) * x1.saturating_sub(x0)) as u64; let mut best = 0u32; - let mut best_cost = u64::MAX; + let mut best_gain = 0u64; for mode in 0..=13u32 { for y in y0..y1 { let row = y * width; @@ -485,28 +500,45 @@ mod tests { } } } - let mut cost = 0u64; + let mut gain = 0u64; for c in 0..4 { - scratch.used_counts.clear(); let n = scratch.touched[c].len(); for k in 0..n { - let byte = scratch.touched[c][k]; - scratch - .used_counts - .push(scratch.counts[c][usize::from(byte)]); - } - cost += shannon_bits(total, &scratch.used_counts); - for k in 0..n { - let byte = scratch.touched[c][k]; - scratch.counts[c][usize::from(byte)] = 0; + let idx = usize::from(scratch.touched[c][k]); + let committed = u64::from(scratch.committed[c][idx]); + let merged = committed + u64::from(scratch.counts[c][idx]); + gain += bin_weight(merged) - bin_weight(committed); + scratch.counts[c][idx] = 0; } scratch.touched[c].clear(); } - if cost < best_cost { - best_cost = cost; + let seen = u64::from(scratch.mode_counts[mode as usize]); + gain += bin_weight(seen + 1) - bin_weight(seen); + if gain > best_gain { + best_gain = gain; best = mode; } } + scratch.mode_counts[best as usize] += 1; + for y in y0..y1 { + let row = y * width; + for x in x0..x1 { + let i = row + x; + let residual = sub_pixels( + argb[i], + predict( + best, + argb[i - 1], + argb[i - width - 1], + argb[i - width], + argb[i - width + 1], + ), + ); + for (c, &byte) in residual.to_le_bytes().iter().enumerate() { + scratch.committed[c][usize::from(byte)] += 1; + } + } + } best } @@ -1207,30 +1239,25 @@ mod tests { #[test] fn forward_roundtrips_multi_tile_with_mixed_modes() { - // An 8x8 image at bits=2 is a 2x2 tile grid; the top rows form a - // horizontal gradient (tile-row 0 -> modes 1,1) and the bottom rows a - // per-column ramp (tile-row 1 -> modes 2,12), so tile_row is non-zero and - // adjacent tiles differ. This pins forward's residual-loop tile index - // (`tile_row + (x>>bits)`, line 304) and stride (`(y>>bits)*tiles_per_row`, - // line 301): any `+`/`*`/`/` there reads a different tile's mode than the - // untouched inverse, so the round-trip breaks. - let h = [0i32, 5, 9, 20, 26, 35, 41]; + // An 8x8 image at bits=2 is a 2x2 tile grid; the diagonal quadrants ramp horizontally (mode 1) and the anti-diagonal ones vertically (mode 2), so every tile differs from both its row and its column neighbor. + // This pins forward's residual-loop tile index (`tile_row + (x>>bits)`) and stride (`(y>>bits)*tiles_per_row`): any `+`/`*`/`/` there reads a different tile's mode than the untouched inverse, so the round-trip breaks. let img: Vec = (0..8i32) .flat_map(|y| { (0..8i32).map(move |x| { - let b = if y < 4 { + let horizontal = (x < 4) == (y < 4); + let b = if horizontal { (13 * x) & 0xff } else { - (7 * y + h[(x % 7) as usize]) & 0xff + (11 * y) & 0xff }; 0xff00_0000 | (b as u32) }) }) .collect(); let (residual, tile_data) = forward(&img, 8, 8, 2); - // Guard the premise: tile-row 0 and tile-row 1 must carry different modes. + // Guard the premise: no two adjacent tiles may share a mode. let modes: Vec = tile_data.iter().map(|&d| (d >> 8) & 0x0f).collect(); - assert_eq!(modes, vec![1, 1, 2, 12]); + assert_eq!(modes, vec![1, 2, 2, 1]); let mut round = residual; inverse(&mut round, 8, 2, &tile_data); assert_eq!(round, img); diff --git a/crates/webpkit/src/lossless/transform/subtract_green.rs b/crates/webpkit/src/lossless/transform/subtract_green.rs index f4bb337..501220e 100644 --- a/crates/webpkit/src/lossless/transform/subtract_green.rs +++ b/crates/webpkit/src/lossless/transform/subtract_green.rs @@ -3,6 +3,8 @@ //! `forward` ports libwebp `SubtractGreenFromBlueAndRed_C`; `inverse` ports //! `AddGreenToBlueAndRed_C`. +use crate::lossless::work::work; + /// Subtract-green forward: subtract the green channel from red and blue. /// /// Green and alpha are left unchanged. Each channel is subtracted independently @@ -30,6 +32,7 @@ pub(crate) fn inverse(pixels: &mut [u32]) { /// the whole-buffer [`inverse`]. Because the transform is pointwise, looping /// this over every row reproduces [`inverse`] byte-for-byte (proven in tests). pub(crate) fn inverse_row(row: &mut [u32]) { + work!(Vp8lSubtractGreenPixel, row.len() as u64); for pixel in row.iter_mut() { let green = (*pixel >> 8) & 0xff; let red_blue = (*pixel & 0x00ff_00ff).wrapping_add((green << 16) | green) & 0x00ff_00ff; diff --git a/crates/webpkit/src/lossless/vp8l/backref.rs b/crates/webpkit/src/lossless/vp8l/backref.rs index 9f92369..a4f9cdc 100644 --- a/crates/webpkit/src/lossless/vp8l/backref.rs +++ b/crates/webpkit/src/lossless/vp8l/backref.rs @@ -21,7 +21,7 @@ use crate::lossless::constants::{ WINDOW_SIZE, }; use crate::lossless::histogram::{Histogram, fixed_log2}; -use crate::lossless::lz77::{PlaneCodeMap, prefix_encode}; +use crate::lossless::lz77::{PlaneCodeMap, near_distances, prefix_encode}; use crate::lossless::prelude::*; use crate::lossless::work::work; @@ -134,6 +134,76 @@ fn parse_with_chain(pixels: &[u32], chain: &HashChain) -> Vec { tokens } +/// Greedy one-step-lazy parse restricted to the *near* distances of [`near_distances`]: a copy may only reference the pixel to its left, or a pixel within `+/-3` columns of the same position up to three rows above. +/// Every distance it emits therefore codes as a low plane code (one prefix symbol, at most one extra bit) instead of a raw offset. +/// +/// Positions with no near match become literals rather than reaching for a distant one, so this trades match coverage for distance cost. +/// It is a whole extra token stream, ranked against the others by real emitted bytes, never a replacement. +pub(crate) fn parse_local(pixels: &[u32], width: u32) -> Vec { + let n = pixels.len(); + if n < MIN_MATCH as usize { + return pixels.iter().map(|&p| Token::Literal(p)).collect(); + } + let dists = near_distances(width); + let mut tokens = Vec::new(); + let mut i = 0usize; + // Same carried-lookahead shape as `parse_with_chain`: the lazy probe at `i + 1` becomes the next iteration's current match when a literal is coded. + let mut here = find_near(pixels, i, &dists); + while i < n { + let ahead = if i + 1 < n { + find_near(pixels, i + 1, &dists) + } else { + None + }; + let matched = here.filter(|&(_, len)| ahead.is_none_or(|(_, next)| next <= len)); + if let Some((distance, length)) = matched { + tokens.push(Token::Copy { + length: length as u32, + distance: distance as u32, + }); + i += length; + here = find_near(pixels, i, &dists); // jumped past the lookahead + } else { + tokens.push(Token::Literal(pixels[i])); + i += 1; + here = ahead; + } + } + tokens +} + +/// The longest match at `i` over the fixed near-distance set `dists`, or `None` when none reaches [`MIN_MATCH`]. +/// `dists` is in ascending plane-code order and only a strict improvement replaces the winner, so equal-length matches keep the cheapest plane code. +fn find_near(pixels: &[u32], i: usize, dists: &[u32]) -> Option<(usize, usize)> { + let max_len = (MAX_COPY_LENGTH as usize).min(pixels.len() - i); + if max_len < MIN_MATCH as usize { + return None; + } + let (mut best_len, mut best_dist) = (0usize, 0usize); + for &dist in dists { + let d = dist as usize; + if d > i { + continue; // no earlier position at this distance yet + } + work!(MatchHop); + let pos = i - d; + // Quick reject: to strictly beat `best_len` this distance must at least match at index `best_len`. + // `best_len < max_len` (the loop breaks at the cap) and `pos < i`, so both reads are in bounds. + if pixels[pos + best_len] != pixels[i + best_len] { + continue; + } + let len = match_length(pixels, pos, i, max_len); + if len > best_len { + best_len = len; + best_dist = d; + if best_len >= max_len { + break; // cannot improve on the capped length + } + } + } + (best_len >= MIN_MATCH as usize).then_some((best_dist, best_len)) +} + /// Walk `tokens` in output order, applying a `cache_bits`-wide color cache /// (disabled when `0`) and resolving each unit to its bitstream symbols, driving /// `sink` once per emitted unit. @@ -545,12 +615,37 @@ fn channel_costs_arr(counts: &[u32]) -> [u64; N] { core::array::from_fn(|s| total_log2.saturating_sub(fixed_log2(u64::from(counts[s])))) } -/// The optimal-parse decision at one position: code a literal, or take a copy of -/// `len` at `dist`. Recorded per position by the backward DP and replayed forward. +/// The optimal-parse decision at one position, packed into one `u32`. +/// +/// Copies store zero-based `(distance, length)` in 20 + 12 bits. +/// The format bounds guarantee `distance <= WINDOW_SIZE < 2^20` and `length <= MAX_COPY_LENGTH == 2^12`; the all-ones word is therefore unused by a valid copy and represents a literal. +/// Keeping this scratch word-sized is especially important for the paired DP, which holds two decision arrays. #[derive(Clone, Copy)] -enum Choice { - Literal, - Copy { len: u32, dist: u32 }, +struct Choice(u32); + +const _: () = assert!(MAX_COPY_LENGTH == 1 << 12); +const _: () = assert!(WINDOW_SIZE < 1 << 20); + +impl Choice { + const LENGTH_BITS: u32 = 12; + const LENGTH_MASK: u32 = (1 << Self::LENGTH_BITS) - 1; + const LITERAL: Self = Self(u32::MAX); + + fn copy(len: u32, dist: u32) -> Self { + debug_assert!((MIN_MATCH..=MAX_COPY_LENGTH).contains(&len)); + debug_assert!((1..=WINDOW_SIZE).contains(&dist)); + Self(((dist - 1) << Self::LENGTH_BITS) | (len - 1)) + } + + const fn copy_parts(self) -> Option<(u32, u32)> { + if self.0 == Self::LITERAL.0 { + None + } else { + let len = (self.0 & Self::LENGTH_MASK) + 1; + let dist = (self.0 >> Self::LENGTH_BITS) + 1; + Some((len, dist)) + } + } } /// Cost-model-driven near-optimal LZ77 parse: a backward dynamic program that, @@ -558,24 +653,27 @@ enum Choice { /// self-information of the remaining stream, then reconstructs the token list /// forward. /// -/// The cost model is a pass-1 histogram of the already-computed `greedy_tokens` -/// (the same cache-agnostic distribution the greedy stream emits), so the DP -/// prices exactly what the encoder will code. The near/far distance tradeoff is -/// supplied by [`HashChain::find_candidates`]; the length/distance caps, -/// window, and overlap (RLE) handling are inherited from [`match_length`] via -/// that method. The result is a valid cache-agnostic [`Token`] stream — the same -/// `Literal` / `Copy` shape [`resolve`] already handles — so it round-trips by -/// construction, and (wired as a self-floored extra candidate) can only ever -/// replace the greedy stream when it is strictly smaller. +/// The cost model is a pass-1 histogram of the already-computed `seed_tokens`, so +/// the DP prices a distribution the encoder really emits. Which seed is used is a +/// free choice: the greedy stream reproduces its own distance spread, the +/// near-restricted [`parse_local`] stream biases the model toward cheap plane +/// codes, and re-seeding from a previous DP output iterates the model off its +/// starting point. The near/far distance tradeoff is supplied by +/// [`HashChain::find_candidates`]; the length/distance caps, window, and overlap +/// (RLE) handling are inherited from [`match_length`] via that method. The result +/// is a valid cache-agnostic [`Token`] stream — the same `Literal` / `Copy` shape +/// [`resolve`] already handles — so it round-trips by construction, and (wired as +/// a self-floored extra candidate) can only ever replace the seed when it is +/// strictly smaller. pub(crate) fn parse_optimal( pixels: &[u32], width: u32, - greedy_tokens: &[Token], + seed_tokens: &[Token], chain: &HashChain, ) -> Vec { let n = pixels.len(); if n < MIN_MATCH as usize { - return greedy_tokens.to_vec(); + return seed_tokens.to_vec(); } // Backward pass: the `cost`/`chain`/`cands` scratch and the cost model exist // only to fill `choice`. Scope them so they drop before the forward @@ -584,11 +682,10 @@ pub(crate) fn parse_optimal( // `choice` escapes, so the recorded decisions — and the emitted tokens — are // unchanged. let choice = { - // Pass-1 histogram of the greedy stream, cache-agnostic (cache_bits = 0) — - // reusing the encoder's own model so the DP prices the identical - // distribution the emit pass sees before any cache decision. + // Pass-1 histogram of the seed stream, cache-agnostic (cache_bits = 0) — + // reusing the encoder's own model so the DP prices the identical distribution the emit pass sees before any cache decision. let histogram = - crate::lossless::vp8l::encode::RefModel::new(greedy_tokens, pixels, width).histogram(0); + crate::lossless::vp8l::encode::RefModel::new(seed_tokens, pixels, width).histogram(0); let cost_model = CostModel::from_histogram(&histogram); // `chain` is threaded in by the caller (built once by the greedy parse). It // is a pure function of `pixels`, so a shared chain is bit-identical to one @@ -607,7 +704,7 @@ pub(crate) fn parse_optimal( // the reads yields the identical values a full `cost[0..=n]` array would. let ring = (n + 1).min(MAX_COPY_LENGTH as usize + 1); let mut cost = vec![0u64; ring]; - let mut choice = vec![Choice::Literal; n]; + let mut choice = vec![Choice::LITERAL; n]; let mut cands: Vec<(u32, u32)> = Vec::new(); // Carries each position's per-distance match lengths to the next (`i - 1`) // iteration so `find_candidates` can shift them instead of re-scanning @@ -620,7 +717,7 @@ pub(crate) fn parse_optimal( let mut best = cost_model .literal_cost(pixels[i]) .saturating_add(cost[(i + 1) % ring]); - let mut best_choice = Choice::Literal; + let mut best_choice = Choice::LITERAL; chain.find_candidates(pixels, i, &mut cands, &mut memo); for &(dist, len) in &cands { let (length_symbol, length_bits, _) = prefix_encode(len); @@ -631,7 +728,7 @@ pub(crate) fn parse_optimal( .saturating_add(cost[(i + len as usize) % ring]); if c < best { best = c; - best_choice = Choice::Copy { len, dist }; + best_choice = Choice::copy(len, dist); } } cost[i % ring] = best; @@ -642,22 +739,109 @@ pub(crate) fn parse_optimal( choice }; - // Forward reconstruction from the recorded choices. + replay_choices(pixels, &choice) +} + +/// Run two optimal parses over the same pixels while sharing the hash-chain candidate walk. +/// +/// This is equivalent to two independent [`parse_optimal`] calls seeded by `seed_a` and `seed_b`: each state has its own cost model, cost ring, and strict-less-than decisions. +/// Only [`HashChain::find_candidates`] and its backward shift memo are shared, because their results depend solely on the pixels and position. +pub(crate) fn parse_optimal_pair( + pixels: &[u32], + width: u32, + seed_a: &[Token], + seed_b: &[Token], + chain: &HashChain, +) -> (Vec, Vec) { + let n = pixels.len(); + if n < MIN_MATCH as usize { + return (seed_a.to_vec(), seed_b.to_vec()); + } + + // Keep the two cost models, rings, and candidate scratch scoped to the backward pass. + // They are dead before either token stream is reconstructed, + // just as in the single-state path. + let (choice_a, choice_b) = { + let histogram_a = + crate::lossless::vp8l::encode::RefModel::new(seed_a, pixels, width).histogram(0); + let histogram_b = + crate::lossless::vp8l::encode::RefModel::new(seed_b, pixels, width).histogram(0); + let model_a = CostModel::from_histogram(&histogram_a); + let model_b = CostModel::from_histogram(&histogram_b); + let plane_map = PlaneCodeMap::new(width); + + let ring = (n + 1).min(MAX_COPY_LENGTH as usize + 1); + let mut cost_a = vec![0u64; ring]; + let mut cost_b = vec![0u64; ring]; + let mut choice_a = vec![Choice::LITERAL; n]; + let mut choice_b = vec![Choice::LITERAL; n]; + let mut cands: Vec<(u32, u32)> = Vec::new(); + let mut memo = ShiftMemo::default(); + + for i in (0..n).rev() { + // Preserve the work-counter meaning of two DP states even though their candidate discovery is now performed once. + work!(OptimalDpStep, 2); + let mut best_a = model_a + .literal_cost(pixels[i]) + .saturating_add(cost_a[(i + 1) % ring]); + let mut best_b = model_b + .literal_cost(pixels[i]) + .saturating_add(cost_b[(i + 1) % ring]); + let mut best_choice_a = Choice::LITERAL; + let mut best_choice_b = Choice::LITERAL; + + chain.find_candidates(pixels, i, &mut cands, &mut memo); + for &(dist, len) in &cands { + let (length_symbol, length_bits, _) = prefix_encode(len); + let plane_code = plane_map.plane_code(dist); + let (dist_symbol, dist_bits, _) = prefix_encode(plane_code); + + let candidate_a = model_a + .copy_cost(length_symbol, length_bits, dist_symbol, dist_bits) + .saturating_add(cost_a[(i + len as usize) % ring]); + if candidate_a < best_a { + best_a = candidate_a; + best_choice_a = Choice::copy(len, dist); + } + + let candidate_b = model_b + .copy_cost(length_symbol, length_bits, dist_symbol, dist_bits) + .saturating_add(cost_b[(i + len as usize) % ring]); + if candidate_b < best_b { + best_b = candidate_b; + best_choice_b = Choice::copy(len, dist); + } + } + + cost_a[i % ring] = best_a; + cost_b[i % ring] = best_b; + choice_a[i] = best_choice_a; + choice_b[i] = best_choice_b; + memo.advance(); + } + (choice_a, choice_b) + }; + + let tokens_a = replay_choices(pixels, &choice_a); + drop(choice_a); + let tokens_b = replay_choices(pixels, &choice_b); + (tokens_a, tokens_b) +} + +/// Forward reconstruction from a DP decision array. +fn replay_choices(pixels: &[u32], choice: &[Choice]) -> Vec { let mut tokens = Vec::new(); let mut i = 0usize; - while i < n { - match choice[i] { - Choice::Literal => { - tokens.push(Token::Literal(pixels[i])); - i += 1; - }, - Choice::Copy { len, dist } => { - tokens.push(Token::Copy { - length: len, - distance: dist, - }); - i += len as usize; - }, + while i < pixels.len() { + if let Some((len, dist)) = choice[i].copy_parts() { + tokens.push(Token::Copy { + length: len, + distance: dist, + }); + i += len as usize; + } else { + tokens.push(Token::Literal(pixels[i])); + i += 1; } } tokens @@ -665,7 +849,10 @@ pub(crate) fn parse_optimal( #[cfg(test)] mod tests { - use super::{Resolved, Token, parse, parse_lz77, parse_optimal, resolve}; + use super::{ + Choice, Resolved, Token, parse, parse_local, parse_lz77, parse_optimal, parse_optimal_pair, + resolve, + }; use crate::lossless::color_cache::ColorCache; use proptest::prelude::*; @@ -956,6 +1143,55 @@ mod tests { } } + /// Sharing one candidate walk across two cost states must be token-identical to running the same greedy- and local-seeded DPs independently. + /// The long cases cross `MAX_COPY_LENGTH` (4096) and exercise both RLE overlap and a short-period hash chain. + #[test] + fn parse_optimal_pair_matches_independent_calls() { + let tiny = vec![0x1122_3344u32; 3]; + let solid = vec![0xff22_4466u32; 4200]; + let periodic: Vec = (0u32..4200) + .map(|i| 0xff00_0000 | ((i % 7) * 0x0001_0101)) + .collect(); + + for (pixels, width) in [(&tiny, 1u32), (&solid, 60), (&periodic, 70)] { + let (greedy, chain) = parse_lz77(pixels); + let local = parse_local(pixels, width); + let expected_greedy = parse_optimal(pixels, width, &greedy, &chain); + let expected_local = parse_optimal(pixels, width, &local, &chain); + let (paired_greedy, paired_local) = + parse_optimal_pair(pixels, width, &greedy, &local, &chain); + + assert_eq!( + paired_greedy, + expected_greedy, + "greedy-seeded pair mismatch at {} pixels", + pixels.len() + ); + assert_eq!( + paired_local, + expected_local, + "local-seeded pair mismatch at {} pixels", + pixels.len() + ); + } + } + + #[test] + fn packed_choice_round_trips_format_bounds() { + use crate::lossless::constants::{MAX_COPY_LENGTH, MIN_MATCH, WINDOW_SIZE}; + + assert_eq!(core::mem::size_of::(), 4); + assert_eq!(Choice::LITERAL.copy_parts(), None); + for (len, dist) in [ + (MIN_MATCH, 1), + (MAX_COPY_LENGTH, 1), + (MIN_MATCH, WINDOW_SIZE), + (MAX_COPY_LENGTH, WINDOW_SIZE), + ] { + assert_eq!(Choice::copy(len, dist).copy_parts(), Some((len, dist))); + } + } + #[test] fn literal_cost_is_the_exact_four_channel_sum() { // `literal_cost` reads the four channel bytes of `argb` at the exact shifts @@ -1224,6 +1460,26 @@ mod tests { prop_assert_eq!(&decode_tokens(&parse(&pixels, true)), &pixels); } + /// `parse_local` is lossless, and every copy it emits carries one of the `NEAR_PLANE_CODES` leading distance plane codes — the property the whole candidate exists for. + #[test] + fn parse_local_round_trips_within_the_near_plane( + seed in any::(), + width in 1u32..=20, + rows in 1usize..=20, + palette in 1u32..=6, + ) { + use crate::lossless::lz77::{NEAR_PLANE_CODES, PlaneCodeMap}; + let pixels = seeded_pixels(seed, width as usize * rows, palette); + let tokens = super::parse_local(&pixels, width); + prop_assert_eq!(&decode_tokens(&tokens), &pixels); + let map = PlaneCodeMap::new(width); + for token in &tokens { + if let Token::Copy { distance, .. } = *token { + prop_assert!(map.plane_code(distance) <= NEAR_PLANE_CODES); + } + } + } + /// The guard-hoisted `find` returns the byte-identical `(best_dist, /// best_len)` to the pre-hoist reference at every position — the mechanical /// proof that hoisting the quick-reject guard load is behavior-neutral. diff --git a/crates/webpkit/src/lossless/vp8l/decode.rs b/crates/webpkit/src/lossless/vp8l/decode.rs index 8f30b94..da2515f 100644 --- a/crates/webpkit/src/lossless/vp8l/decode.rs +++ b/crates/webpkit/src/lossless/vp8l/decode.rs @@ -26,6 +26,7 @@ use crate::lossless::huffman::decode::{HuffmanTable, read_huffman_code}; use crate::lossless::lz77::{plane_code_to_distance, read_prefix_value}; use crate::lossless::prelude::*; use crate::lossless::transform::{cross_color, palette, predictor, subtract_green}; +use crate::lossless::work::work; use crate::lossless::{Codec, Error, Result}; /// A decoded VP8L image: native ARGB pixels (`0xAARRGGBB`), row-major. @@ -579,6 +580,7 @@ pub(crate) fn decode_one(br: &mut BitReader<'_>, st: &mut PixelCore) -> Result st.groups.first(), Some(e) => { + work!(Vp8lEntropySelect); let pos = st.pos as u32; let idx = select_group(Some(e), pos % st.width, pos / st.width).ok_or( Error::InvalidBitstream { @@ -601,12 +603,14 @@ pub(crate) fn decode_one(br: &mut BitReader<'_>, st: &mut PixelCore) -> Result, st: &mut PixelCore) -> Result, st: &mut PixelCore) -> Result Vec { let emit = |cache_bits, model: &RefModel<'_>, histogram: &Histogram| { @@ -237,69 +240,70 @@ fn search_lz77_variant( } best }; - // The greedy LZ77 stream. Kept alive as the DP seed and the meta-Huffman input; - // its hash chain is carried alongside so the DP reuses it instead of rebuilding - // the same pure-function-of-pixels chain (byte-identical either way). + // The parse candidates, in the fixed order they are folded in. + // The greedy LZ77 stream comes first and seeds the rest; its hash chain is carried alongside so every DP reuses it instead of rebuilding the same pure-function-of-pixels chain (byte-identical either way). let (tokens_lz77, chain) = parse_lz77(pixels); - let model_lz77 = RefModel::new(&tokens_lz77, pixels, working_width); - let (cache_lz77, hist_lz77) = best_cache_bits(&model_lz77); - keep_smaller(&mut best, emit(cache_lz77, &model_lz77, &hist_lz77)); + let mut parses = vec![tokens_lz77]; + // Cost-model-driven optimal parse, seeded from the greedy stream. + // Candidates are interned in the fixed ranking order. + // At Local breadth the greedy- and local-seeded DPs share their identical hash-chain candidate walk; their cost states and strict tie decisions remain independent. + // `intern_parse` returns the index of the equal-or-new stream, so a later parse re-seeds from `parses[idx]` (token-identical to the stream that produced it), leaving the emitted bytes unchanged. + if parse_breadth >= ParseBreadth::Optimal { + if parse_breadth >= ParseBreadth::Local { + let tokens_local = parse_local(pixels, working_width); + let (tokens_dp, dp_local) = + parse_optimal_pair(pixels, working_width, &parses[0], &tokens_local, &chain); + let dp_idx = intern_parse(&mut parses, tokens_dp); + // Preserve the historical candidate ranking exactly: greedy and its DP first, then local and its DP, then the re-seeded greedy DP. + intern_parse(&mut parses, tokens_local); + intern_parse(&mut parses, dp_local); + let dp_iter = parse_optimal(pixels, working_width, &parses[dp_idx], &chain); + intern_parse(&mut parses, dp_iter); + } else { + let tokens_dp = parse_optimal(pixels, working_width, &parses[0], &chain); + intern_parse(&mut parses, tokens_dp); + } + } + drop(chain); let ysize = pixels.len() as u32 / working_width; - // Cost-model-driven optimal parse: a self-floored extra candidate (chosen only - // when strictly smaller by real bytes), seeded from the greedy stream. - if optimal_parse { - let tokens_dp = parse_optimal(pixels, working_width, &tokens_lz77, &chain); - drop(chain); - let model_dp = RefModel::new(&tokens_dp, pixels, working_width); - let (cache_dp, hist_dp) = best_cache_bits(&model_dp); - keep_smaller(&mut best, emit(cache_dp, &model_dp, &hist_dp)); - if meta_huffman { - // A meta-Huffman shot on each of the greedy and DP streams — both - // self-floored against the single-group candidates already folded in. + // Every parse is a self-floored extra candidate: `keep_smaller` ranks by real emitted bytes, so a wider set can only shrink the result. + // The single-group emits run first, then the meta-Huffman shots, so each phase keeps the candidate order it is ranked in. + let mut models = Vec::with_capacity(parses.len()); + for tokens in &parses { + let model = RefModel::new(tokens, pixels, working_width); + let (cache_bits, hist) = best_cache_bits(&model); + keep_smaller(&mut best, emit(cache_bits, &model, &hist)); + models.push((model, cache_bits)); + } + if meta_huffman { + for (model, cache_bits) in &models { meta_shot( &mut best, header_width, header_height, alpha_used, plans, - cache_lz77, - &model_lz77, - pixels, - working_width, - ysize, - ); - meta_shot( - &mut best, - header_width, - header_height, - alpha_used, - plans, - cache_dp, - &model_dp, + *cache_bits, + model, pixels, working_width, ysize, ); } - } else if meta_huffman { - // No DP candidate at this breadth: the meta-Huffman shot rides the greedy - // stream alone. - meta_shot( - &mut best, - header_width, - header_height, - alpha_used, - plans, - cache_lz77, - &model_lz77, - pixels, - working_width, - ysize, - ); } best } +/// Intern `tokens` into the candidate parses: return the index of the first existing stream equal to it, or append it and return the new index. +/// Re-seeding a DP often converges back onto a stream already in the set; interning dedupes the redundant candidate (so it is never emitted twice) exactly as an append-if-absent would, and, because a duplicate is dropped here rather than retained, keeps the parse-build working set from holding every Local candidate at once. +/// The returned index lets a later parse re-seed from `parses[idx]`, which is token-identical to the stream that produced it, so the emitted bytes are unchanged. +fn intern_parse(parses: &mut Vec>, tokens: Vec) -> usize { + parses.iter().position(|p| *p == tokens).unwrap_or_else(|| { + parses.push(tokens); + parses.len() - 1 + }) +} + /// Fold a meta-Huffman candidate for `model` into `best` when [`meta::plan`] finds /// a beneficial grouping — self-floored, so it only wins by real emitted bytes. #[expect( @@ -353,7 +357,7 @@ fn search_lz77( plans, working_width, pixels, - true, + ParseBreadth::Optimal, false, ) } @@ -377,7 +381,7 @@ fn search_lz77_best( plans, working_width, pixels, - true, + ParseBreadth::Local, true, ) } @@ -396,6 +400,19 @@ const _: () = { } }; +/// How many LZ77 parse candidates a family search evaluates. +/// Ordered and nested: +/// each variant's candidate set contains the one below it, so the `>=` gates keep the effort schedule monotone. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum ParseBreadth { + /// The greedy LZ77 stream alone. + Greedy, + /// Plus the cost-model DP seeded from the greedy stream. + Optimal, + /// Plus the near-distance-restricted parse and the two DPs re-seeded from it and from the greedy-seeded DP. + Local, +} + /// How wide the Tier 3 search casts its net at one effort level: which tile sizes /// the predictor/cross-color families sweep, whether cross-color runs at all, and /// whether the per-family search adds the optimal-parse and meta-Huffman shots. @@ -408,8 +425,8 @@ struct Breadth { tile_bits: &'static [u32], /// Whether the cross-color family is evaluated. cross_color: bool, - /// Whether each family search adds the cost-model optimal-parse candidate. - optimal_parse: bool, + /// How many LZ77 parse candidates each family search evaluates. + parses: ParseBreadth, /// Whether each family search adds the meta-Huffman shots. meta_huffman: bool, } @@ -428,37 +445,37 @@ const fn schedule(level: u8) -> Breadth { 0 => Breadth { tile_bits: T4, cross_color: false, - optimal_parse: false, + parses: ParseBreadth::Greedy, meta_huffman: false, }, 1 => Breadth { tile_bits: T4, cross_color: true, - optimal_parse: true, + parses: ParseBreadth::Optimal, meta_huffman: false, }, 2 => Breadth { tile_bits: T34, cross_color: true, - optimal_parse: true, + parses: ParseBreadth::Optimal, meta_huffman: false, }, 3 => Breadth { tile_bits: T234, cross_color: true, - optimal_parse: true, + parses: ParseBreadth::Optimal, meta_huffman: false, }, 4 | 5 => Breadth { tile_bits: FULL, cross_color: true, - optimal_parse: true, + parses: ParseBreadth::Local, meta_huffman: false, }, _ => Breadth { tile_bits: FULL, cross_color: true, - optimal_parse: true, + parses: ParseBreadth::Local, meta_huffman: true, }, } @@ -527,7 +544,7 @@ fn family_search( plans, working_width, pixels, - breadth.optimal_parse, + breadth.parses, breadth.meta_huffman, ) } @@ -707,13 +724,13 @@ fn reduce_task_minima(minima: impl IntoIterator>>) -> Vec< /// plus the family currently being evaluated. /// /// Both paths preserve task order: the serial map folds families in emission -/// order, and `Vec::into_par_iter().map().collect()` is an +/// order, and the slice's `par_iter().map().collect()` is an /// `IndexedParallelIterator`, so `minima[i]` is always /// `keep_smallest(run_best_task(tasks[i], ..))` regardless of scheduling — hence /// rayon-on output equals rayon-off byte-for-byte. #[cfg(not(feature = "rayon"))] fn evaluate_best_tasks( - tasks: Vec, + tasks: &[BestTask], breadth: Breadth, width: u32, height: u32, @@ -722,15 +739,15 @@ fn evaluate_best_tasks( ) -> Vec { // Serial: `reduce_task_minima` drives the lazy map, so each family's streams // are produced, folded to one, and dropped before the next family runs. - reduce_task_minima( - tasks - .into_iter() - .map(|t| keep_smallest(run_best_task(t, breadth, width, height, alpha_used, argb))), - ) + reduce_task_minima(tasks.iter().copied().map(|task| { + keep_smallest(run_best_task( + task, breadth, width, height, alpha_used, argb, + )) + })) } #[cfg(feature = "rayon")] fn evaluate_best_tasks( - tasks: Vec, + tasks: &[BestTask], breadth: Breadth, width: u32, height: u32, @@ -738,14 +755,27 @@ fn evaluate_best_tasks( argb: &[u32], ) -> Vec { use rayon::prelude::*; - // Parallel: each worker folds its family to a single stream, so the collected - // vec holds one stream per non-empty family (not every candidate); `collect` - // keeps task order, so the fold below resolves ties identically to serial. - let minima: Vec>> = tasks - .into_par_iter() - .map(|t| keep_smallest(run_best_task(t, breadth, width, height, alpha_used, argb))) - .collect(); - reduce_task_minima(minima) + // A fixed two-family wave bounds simultaneous transform/model buffers without making the result depend on host thread count. + // Each indexed parallel collect preserves canonical task order, and every wave is reduced before the next starts, so tie resolution remains identical to the serial evaluator. + const TASKS_PER_WAVE: usize = 2; + let mut best = Vec::new(); + for wave in tasks.chunks(TASKS_PER_WAVE) { + let minima: Vec>> = wave + .par_iter() + .map(|&task| { + keep_smallest(run_best_task( + task, breadth, width, height, alpha_used, argb, + )) + }) + .collect(); + let candidate = reduce_task_minima(minima); + if best.is_empty() { + best = candidate; + } else if !candidate.is_empty() { + keep_smaller(&mut best, candidate); + } + } + best } /// Tier 3 encode at breadth `level` (`0..=9`): return the SMALLEST stream among the @@ -765,14 +795,8 @@ fn evaluate_best_tasks( pub(crate) fn encode_best_at(level: u8, width: u32, height: u32, argb: &[u32]) -> Vec { let breadth = schedule(level); let alpha_used = argb.iter().any(|&p| p >> 24 != 0xff); - evaluate_best_tasks( - build_best_tasks(breadth), - breadth, - width, - height, - alpha_used, - argb, - ) + let tasks = build_best_tasks(breadth); + evaluate_best_tasks(&tasks, breadth, width, height, alpha_used, argb) } /// Full-breadth ([`schedule`] level 9) Tier 3 encode — byte-for-byte the historical @@ -1813,8 +1837,8 @@ mod tests { ); // The rayon evaluator (feature on) must match the serial reference and // the public entry point, byte-for-byte. - let parallel = - evaluate_best_tasks(build_best_tasks(breadth), breadth, w, h, alpha, &pixels); + let tasks = build_best_tasks(breadth); + let parallel = evaluate_best_tasks(&tasks, breadth, w, h, alpha, &pixels); assert_eq!( serial, parallel, "rayon evaluation must equal serial, byte-for-byte" diff --git a/crates/webpkit/src/lossy.rs b/crates/webpkit/src/lossy.rs index 48490cf..2905bad 100644 --- a/crates/webpkit/src/lossy.rs +++ b/crates/webpkit/src/lossy.rs @@ -197,7 +197,7 @@ pub use encoder::{ }; pub use frame_header::FrameHeader; pub use rate::{Attempt, RateSearch, RateTarget}; -pub use tuning::{AlphaFilterMode, AlphaMethod, LossyTuning, Preset}; +pub use tuning::{AlphaFilterMode, AlphaMethod, ImageHint, LossyTuning, Preset}; use crate::lossy::prelude::*; @@ -234,7 +234,7 @@ pub fn decode(payload: &[u8]) -> Result { } /// Decode a WebP lossy `VP8` key-frame `payload` into an [`Image`] with explicit -/// [`DecodeOptions`] (output layout, pixel limit). +/// [`DecodeOptions`] (output layout, pixel limit, and opt-in chroma dithering). /// /// Symmetric with [`crate::lossless::decode_with`]: the header dimensions are /// checked against `options.max_pixels` *before* the reconstruction planes are @@ -248,7 +248,7 @@ pub fn decode(payload: &[u8]) -> Result { /// `options.max_pixels` is exceeded. pub fn decode_with(payload: &[u8], options: &DecodeOptions) -> Result { check_pixel_limit(payload, options)?; - let image = decode::decode_frame(payload)?; + let image = decode::decode_frame_with_dithering(payload, options.dithering_strength)?; Ok(repack(image, options.layout)) } @@ -268,9 +268,10 @@ pub fn decode_argb(payload: &[u8]) -> Result<(Dimensions, Vec)> { /// Decode a baseline VP8 key-frame to native ARGB pixels with explicit /// [`DecodeOptions`]. /// -/// Checks `options.max_pixels` before the planes are allocated. The ARGB output is -/// always native `0xAARRGGBB`, so `options.layout` is not consulted (use -/// [`decode_with`] for a byte layout). +/// Checks `options.max_pixels` before the planes are allocated and applies the +/// requested chroma dithering strength. The ARGB output is always native +/// `0xAARRGGBB`, so `options.layout` is not consulted (use [`decode_with`] for a +/// byte layout). /// /// # Errors /// @@ -278,13 +279,13 @@ pub fn decode_argb(payload: &[u8]) -> Result<(Dimensions, Vec)> { /// `options.max_pixels` is exceeded. pub fn decode_argb_with(payload: &[u8], options: &DecodeOptions) -> Result<(Dimensions, Vec)> { check_pixel_limit(payload, options)?; - let image = decode::decode_frame(payload)?; + let image = decode::decode_frame_with_dithering(payload, options.dithering_strength)?; let argb = crate::image::unpack_pixels(PixelLayout::Rgba8, image.as_bytes()); Ok((image.dimensions(), argb)) } /// Reconstruct a VP8 key-frame `payload` into native **YUV 4:2:0** planes as a -/// [`YuvImage`], stopping before the YUV→RGB conversion `decode` performs. +/// [`crate::YuvImage`], stopping before the YUV→RGB conversion `decode` performs. /// /// The luma plane is `width × height`; the chroma planes are /// `⌈width/2⌉ × ⌈height/2⌉`, all packed with no row padding. Checks @@ -296,9 +297,10 @@ pub fn decode_argb_with(payload: &[u8], options: &DecodeOptions) -> Result<(Dime /// # Errors /// /// The same errors as [`decode_with`]. -pub(crate) fn decode_yuv_with(payload: &[u8], options: &DecodeOptions) -> Result { +pub fn decode_yuv_with(payload: &[u8], options: &DecodeOptions) -> Result { check_pixel_limit(payload, options)?; - let (planes, w, h) = decode::reconstruct_to_planes(payload)?; + let (planes, w, h) = + decode::reconstruct_to_planes_with_dithering(payload, options.dithering_strength)?; let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); let dims = Dimensions::new( u32::try_from(w).map_err(|_| Error::InvalidDimensions)?, diff --git a/crates/webpkit/src/lossy/alpha.rs b/crates/webpkit/src/lossy/alpha.rs index 69d6498..8218374 100644 --- a/crates/webpkit/src/lossy/alpha.rs +++ b/crates/webpkit/src/lossy/alpha.rs @@ -80,6 +80,7 @@ pub(crate) fn compress_alpha( let (w, h) = (width as usize, height as usize); let mut plane = plane.to_vec(); quantize_levels(&mut plane, levels_for_quality(tuning.quality)); + let preprocessing = u8::from(tuning.quality < 100); let mut best: Option> = None; for filter in filter_candidates(tuning.filter, &plane, w, h) { let filtered = alpha::filter_plane(filter, &plane, w, h); @@ -88,17 +89,24 @@ pub(crate) fn compress_alpha( let lossless = crate::lossless::encode_alpha(&filtered, width, height); consider( &mut best, - assemble(AlphaCompression::Lossless, filter, &lossless), + assemble(AlphaCompression::Lossless, filter, preprocessing, &lossless), ); } // method 0: the filtered plane stored raw. consider( &mut best, - assemble(AlphaCompression::None, filter, &filtered), + assemble(AlphaCompression::None, filter, preprocessing, &filtered), ); } // width*height >= 1 guarantees at least one candidate (`None`/raw always fits). - best.unwrap_or_else(|| assemble(AlphaCompression::None, AlphaFilter::None, &plane)) + best.unwrap_or_else(|| { + assemble( + AlphaCompression::None, + AlphaFilter::None, + preprocessing, + &plane, + ) + }) } /// The spatial filters [`compress_alpha`] trials for `mode`: every filter for @@ -247,10 +255,15 @@ pub(crate) fn alph_chunk( } /// Build one `ALPH` payload: the 1-byte header (`compression`, `filter`, -/// pre-processing = 0) followed by the stored `data`. -fn assemble(compression: AlphaCompression, filter: AlphaFilter, data: &[u8]) -> Vec { +/// pre-processing marker followed by the stored `data`. +fn assemble( + compression: AlphaCompression, + filter: AlphaFilter, + preprocessing: u8, + data: &[u8], +) -> Vec { let mut out = Vec::with_capacity(1 + data.len()); - out.push(alpha::build_header(compression, filter, 0)); + out.push(alpha::build_header(compression, filter, preprocessing)); out.extend_from_slice(data); out } @@ -313,11 +326,11 @@ mod tests { let lossless = crate::lossless::encode_alpha(&filtered, width, height); super::consider( &mut best, - super::assemble(AlphaCompression::Lossless, filter, &lossless), + super::assemble(AlphaCompression::Lossless, filter, 0, &lossless), ); super::consider( &mut best, - super::assemble(AlphaCompression::None, filter, &filtered), + super::assemble(AlphaCompression::None, filter, 0, &filtered), ); } best.unwrap() diff --git a/crates/webpkit/src/lossy/decode.rs b/crates/webpkit/src/lossy/decode.rs index f5abf9f..45834ff 100644 --- a/crates/webpkit/src/lossy/decode.rs +++ b/crates/webpkit/src/lossy/decode.rs @@ -58,6 +58,8 @@ pub(crate) struct QuantMatrix { pub(crate) y2: [i32; 2], /// Chroma dequant factors. pub(crate) uv: [i32; 2], + /// Raw chroma AC quantizer index (`0..=127`), retained for dithering. + pub(crate) uv_quant: i32, } /// Frame-persistent entropy-coding probabilities (RFC §13, libwebp `VP8Proba`). @@ -231,7 +233,16 @@ impl Frame { /// # Errors /// /// [`Error::Truncated`] / [`Error::InvalidBitstream`] for a malformed stream. +#[cfg(any(test, feature = "oracle"))] pub(crate) fn reconstruct_to_planes(payload: &[u8]) -> Result<(Planes, usize, usize)> { + reconstruct_to_planes_with_dithering(payload, 0) +} + +/// Like [`reconstruct_to_planes`], with deterministic libwebp-compatible chroma dithering applied after the in-loop filter. +pub(crate) fn reconstruct_to_planes_with_dithering( + payload: &[u8], + dithering_strength: u8, +) -> Result<(Planes, usize, usize)> { let fh = FrameHeader::parse_key_frame(payload)?; let (width, height) = (usize::from(fh.width), usize::from(fh.height)); let mut frame = Frame::new(fh)?; @@ -258,6 +269,7 @@ pub(crate) fn reconstruct_to_planes(payload: &[u8]) -> Result<(Planes, usize, us let fstrengths = reconstruct::compute_fstrengths(&frame.segment, &frame.filter); let use_skip = frame.proba.use_skip; let mut finfo = vec![FInfo::default(); frame.mb_w * frame.mb_h]; + let mut dither = vec![0u8; frame.mb_w * frame.mb_h]; // Walk the macroblock grid: parse each row's modes (partition 0) and residual // coefficients (the row's token partition), then reconstruct the row. @@ -282,6 +294,11 @@ pub(crate) fn reconstruct_to_planes(payload: &[u8]) -> Result<(Planes, usize, us reconstruct::reconstruct_mb(&mut planes, block, mb_x, mb_y, frame.mb_w); finfo[mb_y * frame.mb_w + mb_x] = reconstruct::resolve_finfo(fstrengths, block, use_skip); + dither[mb_y * frame.mb_w + mb_x] = reconstruct::chroma_dither_amplitude( + frame.dqm[usize::from(block.segment)].uv_quant, + block.non_zero_uv, + dithering_strength, + ); } frame.init_scanline(); } @@ -293,6 +310,7 @@ pub(crate) fn reconstruct_to_planes(payload: &[u8]) -> Result<(Planes, usize, us frame.mb_h, frame.filter_type, ); + reconstruct::dither_chroma(&mut planes, &dither, frame.mb_w, frame.mb_h); Ok((planes, width, height)) } @@ -302,11 +320,19 @@ pub(crate) fn reconstruct_to_planes(payload: &[u8]) -> Result<(Planes, usize, us /// # Errors /// /// [`Error::Truncated`] / [`Error::InvalidBitstream`] for a malformed stream. +#[cfg(test)] pub(crate) fn decode_frame(payload: &[u8]) -> Result { let (planes, width, height) = reconstruct_to_planes(payload)?; reconstruct::to_image(&planes, width, height) } +/// Decode a VP8 key frame with explicit chroma dithering strength. +pub(crate) fn decode_frame_with_dithering(payload: &[u8], dithering_strength: u8) -> Result { + let (planes, width, height) = + reconstruct_to_planes_with_dithering(payload, dithering_strength)?; + reconstruct::to_image(&planes, width, height) +} + /// Parse just far enough into `payload` to report whether the frame codes /// per-macroblock skip (`proba.use_skip`); `None` for a malformed stream. The /// differential oracle uses this to prove a skip test is non-vacuous — that the diff --git a/crates/webpkit/src/lossy/decoder.rs b/crates/webpkit/src/lossy/decoder.rs index cee10b3..5225707 100644 --- a/crates/webpkit/src/lossy/decoder.rs +++ b/crates/webpkit/src/lossy/decoder.rs @@ -6,9 +6,11 @@ //! through [`IncrementalDecoder::drain_rows`]. A bare `VP8 ` still is always //! opaque (alpha lives only in the extended `VP8X` form), so the streamed rows //! and [`IncrementalDecoder::into_image`] are byte-identical to a one-shot -//! [`crate::lossy::decode`] of the same chunk. `Read`-free, so it works on -//! `no_std + alloc`. Animation and alpha compositing belong to the umbrella -//! `webp` crate, which routes only bare-lossy stills here. +//! [`crate::lossy::decode`] of the same chunk. Chroma dithering runs only after +//! the full-frame loop filter, so a non-zero dithering strength buffers the +//! complete image and deliberately yields no early row views. `Read`-free, so +//! it works on `no_std + alloc`. Animation and alpha compositing belong to the +//! umbrella `webp` crate, which routes only bare-lossy stills here. use crate::container::fourcc::FourCc; use crate::container::reader::{ImageChunk, locate_image_with_alpha}; @@ -155,7 +157,10 @@ impl StillState { /// /// The image is streamed row-by-row through a `Vp8Stream`; [`Self::drain_rows`] /// yields finalized rows early and [`Self::into_image`] returns the complete -/// picture. `Read`-free, so it works on `no_std + alloc`. +/// picture. With non-zero chroma dithering, reconstruction is buffered until the +/// whole frame is available and `drain_rows` returns `None`, because dithering is +/// applied after the full-frame loop filter. `Read`-free, so it works on +/// `no_std + alloc`. /// /// The pixel limit follows [`DecodeOptions`], the single source of truth: a /// default-constructed one caps at [`DEFAULT_MAX_PIXELS`](crate::DEFAULT_MAX_PIXELS) @@ -245,6 +250,16 @@ impl IncrementalDecoder { if let Some(limit) = self.options.max_pixels.filter(|&limit| pixels > limit) { return Err(Error::LimitExceeded { pixels, limit }); } + // Chroma dithering is defined over the fully filtered U/V planes and its deterministic PRNG advances in macroblock raster order. + // Rows already converted to RGBA by `Vp8Stream` therefore cannot be patched exactly. + // Preserve byte equality with the one-shot decoder by buffering this option until the complete-file branch above can run `decode_image`. + if self.options.dithering_strength != 0 { + if self.reported_header { + return Ok(Some(Progress::NeedMoreInput)); + } + self.reported_header = true; + return Ok(Some(Progress::HeaderReady(info))); + } let Some((start, end)) = locate_vp8(&self.buf) else { return Ok(Some(Progress::NeedMoreInput)); }; @@ -349,6 +364,8 @@ impl IncrementalDecoder { /// [`PixelLayout`], as a non-consuming early view. `None` if none are pending. /// The borrowed bytes are freed on the next [`Self::push`], yet /// [`Self::into_image`] still returns the complete image (see [`RowDrain`]). + /// A non-zero chroma dithering strength buffers the full frame and always + /// returns `None` here. pub fn drain_rows(&mut self) -> Option> { self.still.as_mut()?.take_drain() } @@ -382,7 +399,15 @@ impl IncrementalDecoder { // were freed (so `packed` still holds every row). Otherwise defer to // the one-shot decode, which reconstructs any freed rows from `buf` or // surfaces the same error. - Some(st) if st.done && st.packed_base == 0 && is_complete(&buf) => st.assemble(&buf), + Some(st) + if st.done + && st.packed_base == 0 + && options.dithering_strength == 0 + && options.alpha_dithering_strength == 0 + && is_complete(&buf) => + { + st.assemble(&buf) + }, _ => decode_image(&buf, &options), } } @@ -410,7 +435,7 @@ fn decode_image(buf: &[u8], options: &DecodeOptions) -> Result { if let Some(limit) = options.max_pixels.filter(|&limit| pixels > limit) { return Err(Error::LimitExceeded { pixels, limit }); } - let image = decode::decode_frame(payload)?; + let image = decode::decode_frame_with_dithering(payload, options.dithering_strength)?; if let Some(vp8x) = located.vp8x && vp8x.canvas != image.dimensions() { @@ -585,6 +610,57 @@ mod tests { assert_eq!(dec.into_image().unwrap().as_bytes(), expected); } + #[test] + fn dithered_incremental_decode_buffers_and_matches_one_shot() { + let (width, height) = (32u32, 32u32); + let dims = Dimensions::new(width, height).unwrap(); + let colors = [ + [255, 0, 0, 255], + [0, 255, 0, 255], + [0, 0, 255, 255], + [255, 255, 0, 255], + ]; + let mut rgba = Vec::with_capacity((width * height * 4) as usize); + for y in 0..height { + for x in 0..width { + rgba.extend_from_slice(&colors[((x / 16 + 2 * (y / 16)) % 4) as usize]); + } + } + let input = ImageRef::new(dims, PixelLayout::Rgba8, &rgba).unwrap(); + let config = LossyConfig::new().with_quality(100); + let payload = encode_vp8(input, &config).unwrap().1; + let options = DecodeOptions::new().with_dithering_strength(100); + let expected = decode_with(&payload, &options).unwrap(); + assert_ne!( + expected.as_bytes(), + decode(&payload).unwrap().as_bytes(), + "fixture must exercise chroma dithering" + ); + + let file = riff_vp8(&payload); + let mut dec = IncrementalDecoder::with_options(options); + let mut headers = 0u32; + let mut finished = false; + for chunk in file.chunks(7) { + match dec.push(chunk).unwrap() { + Progress::HeaderReady(_) => headers += 1, + Progress::RowsDecoded { .. } => { + panic!("dithered decode must not expose undithered rows") + }, + Progress::Finished => finished = true, + Progress::FrameComplete(_) => panic!("a still image emits no frames"), + Progress::NeedMoreInput => {}, + } + assert!( + dec.drain_rows().is_none(), + "dithered decode must not expose an undithered row view" + ); + } + assert_eq!(headers, 1, "HeaderReady must be reported exactly once"); + assert!(finished, "dithered stream never Finished"); + assert_eq!(dec.into_image().unwrap().as_bytes(), expected.as_bytes()); + } + #[test] fn progress_sequence_is_monotonic() { let file = riff_vp8(FIXTURE); diff --git a/crates/webpkit/src/lossy/frame.rs b/crates/webpkit/src/lossy/frame.rs index 28fa81d..4acb3c9 100644 --- a/crates/webpkit/src/lossy/frame.rs +++ b/crates/webpkit/src/lossy/frame.rs @@ -547,7 +547,11 @@ fn plan_segmentation( } } let (seg_ids, count, seg_a) = kmeans_segments(&activity, nb); - let seg_base_q = segment_base_qs(base_q, count, seg_a, tuning.sns_strength); + let mut pop = [0i64; 4]; + for &s in &seg_ids { + pop[usize::from(s)] += 1; + } + let seg_base_q = segment_base_qs(base_q, count, seg_a, pop, tuning.sns_strength); // If the SNS deltas collapse every segment to the same quantizer (uniform activity, // or the deltas cancel), segmentation buys nothing but header bytes — fall back. if count <= 1 || seg_base_q[..count].iter().all(|&q| q == base_q) { @@ -763,20 +767,51 @@ fn smooth_segment_map(seg_ids: &mut [u8], mb_w: usize, mb_h: usize) { } /// Map each of the `count` segments' representative activity to a base quantizer index -/// by the zero-centered SNS delta ([`sns_quant_delta`]) at `sns_strength`: a flat -/// (low-activity) segment is coded finer (negative delta), a busy one coarser (positive -/// delta), anchored on `base_q` and clamped into `0..=127`. A mid-activity segment or a -/// zero strength leaves the quantizer at `base_q`. Unused segments (`count..4`) keep -/// `base_q`. -fn segment_base_qs(base_q: i32, count: usize, seg_a: [i64; 4], sns_strength: u8) -> [i32; 4] { - let mut qs = [base_q; 4]; - for (q, &a) in qs.iter_mut().take(count).zip(&seg_a) { +/// by the zero-centered SNS delta ([`sns_quant_delta`]) at `sns_strength`, then subtract +/// the `pop`-weighted mean delta so the allocation redistributes bits *around* `base_q` +/// (mean-preserving) rather than dragging the whole frame finer or coarser. A flat +/// (low-activity) segment is coded finer relative to the frame mean and a busy one +/// coarser, anchored on `base_q` and clamped into `0..=127`. A zero strength, a +/// single populated segment, or activities that share the mean leaves every quantizer at +/// `base_q`. Unused segments (`count..4`) keep `base_q`. +fn segment_base_qs( + base_q: i32, + count: usize, + seg_a: [i64; 4], + pop: [i64; 4], + sns_strength: u8, +) -> [i32; 4] { + let mut deltas = [0i32; 4]; + for (d, &a) in deltas.iter_mut().take(count).zip(&seg_a) { let activity = u8::try_from(a.clamp(0, 255)).unwrap_or(NEUTRAL_ACTIVITY); - *q = (base_q + sns_quant_delta(activity, sns_strength)).clamp(0, 127); + *d = sns_quant_delta(activity, sns_strength); + } + // Population-weighted mean delta: the neutral point is the frame's own mean macroblock, so segmentation only shifts bits between segments. + let mut wsum = 0i64; + let mut psum = 0i64; + for (&d, &p) in deltas.iter().take(count).zip(&pop) { + wsum += i64::from(d) * p; + psum += p; + } + let mean_delta = round_div(wsum, psum); + let mut qs = [base_q; 4]; + for (q, &d) in qs.iter_mut().take(count).zip(&deltas) { + *q = (base_q + d - mean_delta).clamp(0, 127); } qs } +/// Round-to-nearest signed integer division (ties away from zero), or `0` when `den <= 0`. +/// Used to re-anchor the segment quantizer deltas on their population-weighted mean without floating point. +fn round_div(num: i64, den: i64) -> i32 { + if den <= 0 { + return 0; + } + let half = den / 2; + let adjusted = if num >= 0 { num + half } else { num - half }; + i32::try_from(adjusted / den).unwrap_or(0) +} + /// Derive the three segment-id decision-tree probabilities from the segment /// populations, so a macroblock's id costs about its information content. Each is the /// probability (`1..=255`, `256`-scaled) that the corresponding tree bit is zero. @@ -3912,8 +3947,8 @@ gradient fast 190 fdbe0b6ce4ec645c gradient balanced 89 44cc4831e2b014b5 gradient best 113 f507b9cd3a89366e noisy fast 7975 f970e253b081638d -noisy balanced 3551 01eb533d4d1d4989 -noisy best 3157 2a63efd863fa841d +noisy balanced 4357 d27ae40306795729 +noisy best 3913 1b804c5c8c70baaa flat fast 40 66b6a4ee87e184a3 flat balanced 39 2f5a1f125c0a5180 flat best 39 2f5a1f125c0a5180 @@ -3924,14 +3959,14 @@ stripe fast 1680 145aef26907ace38 stripe balanced 322 d0284fcbb5c77800 stripe best 118 00cc0a30e8a48b20 diagonal fast 415 46ac66199192122b -diagonal balanced 274 54c3293234733cd4 -diagonal best 337 462b55975bc359f2 +diagonal balanced 253 3c79a794ba1aa9ee +diagonal best 337 684b11c7b468114e mixed fast 4363 13644d2cc4723cd5 mixed balanced 1953 656fe4c084dd5c45 mixed best 2044 c347fc85c019f5f0 horizontal fast 667 81e58ac967acf023 -horizontal balanced 278 b98f0c9e1dfbcc25 -horizontal best 213 2e6a76a653010c3a"; +horizontal balanced 208 89cea00148cb7d12 +horizontal best 170 0432775a38b69227"; /// Golden exact-encode test — THE high-leverage encoder-decision net. Encodes a /// diverse set of fixed images (smooth gradient, AC-rich noise, flat blocks, @@ -4081,13 +4116,22 @@ horizontal best 213 2e6a76a653010c3a"; // tree_prob(1, 2) = (1*255/2).clamp(1,255) = 127. Pins the `* 255 / total` // scaling and the constant-return mutants. assert_eq!(super::tree_prob(1, 2), 127, "tree_prob(1,2)"); - // segment_base_qs: 2 segments with mean activity {64, 192} around base_q 40 at - // sns 100. sns_quant_delta = (activity-128)*100/512: flat 64 -> -12 (q 28), - // busy 192 -> +12 (q 52). Pins the zero-centered SNS delta end to end. + // segment_base_qs: 2 equal-population segments with mean activity {64, 192} + // around base_q 40 at sns 100. + // sns_quant_delta = (activity-128)*100/512: flat 64 -> -12, busy 192 -> +12; the pop-weighted mean delta is 0, so q = {28, 52}. assert_eq!( - super::segment_base_qs(40, 2, [64, 192, 0, 0], 100), + super::segment_base_qs(40, 2, [64, 192, 0, 0], [1, 1, 0, 0], 100), [28, 52, 40, 40], - "per-segment SNS base quantizers" + "per-segment SNS base quantizers (equal populations)" + ); + // Unequal populations re-anchor on the frame mean: deltas {-12, +12}, pop {3, 1} + // → mean delta round(-24/4) = -6, so both segments shift +6 coarser to {34, 58}, + // whose pop-weighted mean (34*3 + 58) / 4 = 40 = base_q. + // Pins mean preservation. + assert_eq!( + super::segment_base_qs(40, 2, [64, 192, 0, 0], [3, 1, 0, 0], 100), + [34, 58, 40, 40], + "mean-preserving per-segment SNS base quantizers" ); } @@ -4241,8 +4285,8 @@ horizontal best 213 2e6a76a653010c3a"; }; let (_p0, _t, default_total, optimized_total, _probas) = emit_best_partitions(&mb_plans, mb_w, mb_h, header, skip); - assert_eq!(default_total, 4979, "default candidate byte-length sum"); - assert_eq!(optimized_total, 3538, "optimized candidate byte-length sum"); + assert_eq!(default_total, 6378, "default candidate byte-length sum"); + assert_eq!(optimized_total, 4343, "optimized candidate byte-length sum"); } #[test] @@ -4470,10 +4514,10 @@ horizontal best 213 2e6a76a653010c3a"; /// under each of those mutations. Regenerate by running this test (it prints the /// actual table on mismatch) and pasting the block. const RD_GOLDEN_EXPECTED: &str = "\ -rd_rx0_ry5_d0_q48 c03bd74b0b752939 +rd_rx0_ry5_d0_q48 aa526e23c605dde9 rd_rx1_ry0_d0_q8 2dbc60b6031d7af4 rd_rx0_ry2_d0_q24 b6e5b4d5e6bc4ab2 -rd_rx0_ry5_d60_q24 a788b70bb1dfc28e"; +rd_rx0_ry5_d60_q24 9755bd280cef64b9"; #[test] fn rd_cost_arithmetic_golden() { diff --git a/crates/webpkit/src/lossy/header.rs b/crates/webpkit/src/lossy/header.rs index 5d2abaa..04d8db5 100644 --- a/crates/webpkit/src/lossy/header.rs +++ b/crates/webpkit/src/lossy/header.rs @@ -165,7 +165,8 @@ impl Frame { // x * 155 / 100, computed exactly as (x * 101581) >> 16. m.y2[1] = ((ac_quant(clip_q(q + dqy2_ac)) * 101_581) >> 16).max(8); m.uv[0] = dc_quant(clip_uv(q + dquv_dc)); - m.uv[1] = ac_quant(clip_q(q + dquv_ac)); + m.uv_quant = clip_q(q + dquv_ac); + m.uv[1] = ac_quant(m.uv_quant); } } diff --git a/crates/webpkit/src/lossy/reconstruct.rs b/crates/webpkit/src/lossy/reconstruct.rs index 2e487db..0c24af6 100644 --- a/crates/webpkit/src/lossy/reconstruct.rs +++ b/crates/webpkit/src/lossy/reconstruct.rs @@ -61,6 +61,177 @@ impl Planes { } } +/// libwebp's chroma-quantizer to maximum dithering-amplitude table. +const QUANT_TO_DITHER_AMP: [u8; 12] = [8, 7, 6, 4, 4, 2, 2, 2, 1, 1, 1, 1]; + +/// Derive one macroblock's chroma dithering amplitude. +/// +/// Rich chroma blocks (three or more coefficients in any 4×4 block) are not dithered. +/// The remaining amplitude depends on the transmitted chroma AC quantizer and the caller's `0..=100` strength. +/// Values below four have no visible fixed-point effect and are normalized to zero. +pub(crate) fn chroma_dither_amplitude(uv_quant: i32, non_zero_uv: u32, strength: u8) -> u8 { + if strength == 0 || non_zero_uv & 0xaaaa != 0 { + return 0; + } + let Ok(index) = usize::try_from(uv_quant) else { + return 0; + }; + let Some(&base) = QUANT_TO_DITHER_AMP.get(index) else { + return 0; + }; + let factor = u32::from(strength.min(100)) * 255 / 100; + let amplitude = (factor * u32::from(base)) >> 3; + if amplitude < 4 { + 0 + } else { + u8::try_from(amplitude).unwrap_or(u8::MAX) + } +} + +/// The deterministic subtractive generator used by libwebp's VP8 dithering. +struct DitherRandom { + table: [u32; 55], + index1: usize, + index2: usize, +} + +impl DitherRandom { + const INITIAL: [u32; 55] = [ + 0x0de1_5230, + 0x03b3_1886, + 0x775f_accb, + 0x1c88_626a, + 0x6838_5c55, + 0x14b3_b828, + 0x4a85_fef8, + 0x49dd_b84b, + 0x64fc_f397, + 0x5c55_0289, + 0x4a29_0000, + 0x0d7e_c1da, + 0x5940_b7ab, + 0x5492_577d, + 0x4e19_ca72, + 0x38d3_8c69, + 0x0c01_ee65, + 0x32a1_755f, + 0x5437_f652, + 0x5abb_2c32, + 0x0faa_57b1, + 0x73f5_33e7, + 0x685f_eeda, + 0x7563_cce2, + 0x6e99_0e83, + 0x4730_a7ed, + 0x4fc0_d9c6, + 0x496b_153c, + 0x4f14_03fa, + 0x541a_fb0c, + 0x7399_0b32, + 0x26d7_cb1c, + 0x6fcc_3706, + 0x2cbb_77d8, + 0x7576_2f2a, + 0x6425_ccdd, + 0x24b3_5461, + 0x0a7d_8715, + 0x2204_14a8, + 0x141e_bf67, + 0x56b4_1583, + 0x73e5_02e3, + 0x44ca_b16f, + 0x2826_4d42, + 0x73ba_aefb, + 0x0a50_ebed, + 0x1d6a_b6fb, + 0x0d3a_d40b, + 0x35db_3b68, + 0x2b08_1e83, + 0x77ce_6b95, + 0x5181_e5f0, + 0x7885_3bbc, + 0x009f_9494, + 0x27e5_ed3c, + ]; + + const fn new() -> Self { + Self { + table: Self::INITIAL, + index1: 0, + index2: 31, + } + } + + /// Return an 8-bit, 128-centered sample restricted by `amplitude`. + fn sample(&mut self, amplitude: u8) -> u8 { + let diff = self.table[self.index1].wrapping_sub(self.table[self.index2]) & 0x7fff_ffff; + self.table[self.index1] = diff; + self.index1 += 1; + if self.index1 == self.table.len() { + self.index1 = 0; + } + self.index2 += 1; + if self.index2 == self.table.len() { + self.index2 = 0; + } + let centered = (diff << 1).cast_signed() >> 24; + let restricted = (centered * i32::from(amplitude)) >> 8; + u8::try_from(restricted + 128).unwrap_or(0) + } +} + +/// Apply deterministic 8×8 U/V dithering to the reconstructed macroblock grid. +/// +/// `amplitudes` is row-major with one entry per macroblock. +/// The generator is advanced only for non-zero entries, in U-then-V raster order. +pub(crate) fn dither_chroma(planes: &mut Planes, amplitudes: &[u8], mb_w: usize, mb_h: usize) { + if amplitudes.iter().all(|&litude| amplitude == 0) { + return; + } + let mut random = DitherRandom::new(); + for mb_y in 0..mb_h { + for mb_x in 0..mb_w { + let amplitude = amplitudes.get(mb_y * mb_w + mb_x).copied().unwrap_or(0); + if amplitude == 0 { + continue; + } + let offset = (mb_y * 8 + 1) * planes.uv_stride + mb_x * 8 + 1; + dither_8x8( + &mut planes.u, + offset, + planes.uv_stride, + amplitude, + &mut random, + ); + dither_8x8( + &mut planes.v, + offset, + planes.uv_stride, + amplitude, + &mut random, + ); + } + } +} + +/// Apply one macroblock's fixed-point dither samples to a chroma plane. +fn dither_8x8( + plane: &mut [u8], + offset: usize, + stride: usize, + amplitude: u8, + random: &mut DitherRandom, +) { + for y in 0..8 { + for x in 0..8 { + let delta = (i32::from(random.sample(amplitude)) - 128 + 8) >> 4; + let index = offset + y * stride + x; + plane[index] = + u8::try_from((i32::from(plane[index]) + delta).clamp(0, 255)).unwrap_or(0); + } + } +} + /// Fill a plane's top border row with `127` and its left border column with /// `129` (the corner keeps the top value `127`). fn fill_borders(plane: &mut [u8], stride: usize, rows: usize) { @@ -445,13 +616,37 @@ mod tests { bare names mirror the reference for readability" )] - use super::{Planes, compute_fstrengths, fill_top_right_lane, filter_frame, resolve_finfo}; + use super::{ + Planes, chroma_dither_amplitude, compute_fstrengths, dither_chroma, fill_top_right_lane, + filter_frame, resolve_finfo, + }; use crate::lossy::decode::{FilterHeader, MbData, SegmentHeader}; use crate::lossy::loop_filter::FInfo; // ------------------------------------------------------------------------- // Helpers + #[test] + fn chroma_dither_is_gated_and_deterministic() { + assert_eq!(chroma_dither_amplitude(0, 0, 0), 0); + assert_eq!(chroma_dither_amplitude(12, 0, 100), 0); + assert_eq!(chroma_dither_amplitude(0, 0x0002, 100), 0); + assert_eq!(chroma_dither_amplitude(0, 0, 100), 255); + + let mut a = Planes::new(1, 1); + let mut b = Planes::new(1, 1); + a.u.fill(128); + a.v.fill(128); + b.u.fill(128); + b.v.fill(128); + dither_chroma(&mut a, &[255], 1, 1); + dither_chroma(&mut b, &[255], 1, 1); + assert_eq!(a.u, b.u); + assert_eq!(a.v, b.v); + assert!(a.u.iter().any(|&value| value != 128)); + assert!(a.v.iter().any(|&value| value != 128)); + } + /// Build a `SegmentHeader` with only the fields `compute_fstrengths` reads. fn segment( use_segment: bool, diff --git a/crates/webpkit/src/lossy/trellis.rs b/crates/webpkit/src/lossy/trellis.rs index eb9b81f..65f9d25 100644 --- a/crates/webpkit/src/lossy/trellis.rs +++ b/crates/webpkit/src/lossy/trellis.rs @@ -65,12 +65,9 @@ const INF: i64 = i64::MAX / 4; /// table. const TRELLIS_FREQ_WEIGHTING: bool = true; -/// Right shift turning `7 * q_ac^2` into the trellis `lambda`. libwebp's weighted base -/// is `(7 * q^2) >> 3`; we use `>> 2` — double the lambda, so the trellis charges the -/// rate more aggressively. That is a measured rate-distortion Pareto win over `cwebp` -/// default shaping on a real-image corpus (smaller at matched quality, reconstruction -/// still at or above `cwebp`). With [`TRELLIS_FREQ_WEIGHTING`] off an unweighted -/// squared error is balanced against the rate by a larger `>> 6` shift instead. +/// Right shift turning `7 * q_ac^2` into the trellis `lambda`. +/// `>> 2` doubles the lambda relative to libwebp's weighted base `(7 * q^2) >> 3`, charging rate more aggressively. +/// With [`TRELLIS_FREQ_WEIGHTING`] off an unweighted squared error is balanced against the rate by a larger `>> 6` shift instead. const LAMBDA_SHIFT: u32 = if TRELLIS_FREQ_WEIGHTING { 2 } else { 6 }; /// Per-frequency trellis distortion weights (libwebp `kWeightTrellis`, the `USE_TDISTO` @@ -382,7 +379,8 @@ pub(crate) fn trellis_quantize_block( let cands = &cands[..ncand]; for ci in 0..3 { - if prev_cost[ci] >= INF { + // Branch-and-bound: the cumulative cost only grows, so a context whose incoming cost already meets the best terminate-here total can never win (this also filters the INF unreachable state, since best_total <= INF). + if prev_cost[ci] >= best_total { continue; } let p = pb[BANDS[n]][ci]; diff --git a/crates/webpkit/src/lossy/tuning.rs b/crates/webpkit/src/lossy/tuning.rs index cefc880..bf2db72 100644 --- a/crates/webpkit/src/lossy/tuning.rs +++ b/crates/webpkit/src/lossy/tuning.rs @@ -79,6 +79,25 @@ pub enum Preset { Text, } +/// A caller-supplied content classification matching libwebp's `WebPImageHint`. +/// +/// libwebp currently consults this hint only in its lossless encoder. +/// webpkit stores it in [`LossyTuning`] so `cwebp -hint` is represented faithfully and future content-gated lossy decisions can use it without changing the public configuration surface. +/// It is neutral by default and does not remap quality. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +#[non_exhaustive] +pub enum ImageHint { + /// No caller-supplied classification. + #[default] + Default, + /// Digital picture, such as a portrait or indoor shot. + Picture, + /// Outdoor photograph with natural lighting. + Photo, + /// Discrete-tone graphic, chart, or map tile. + Graph, +} + impl Preset { /// Expand this preset into a base [`LossyTuning`] — the libwebp `WebPConfigPreset` /// shaping values (spatial-noise-shaping strength, in-loop filter strength/sharpness, @@ -151,6 +170,7 @@ pub enum AlphaFilterMode { one-knob-per-field map callers rely on" )] pub struct LossyTuning { + image_hint: ImageHint, sns_strength: u8, segments: u8, filter_strength: u8, @@ -182,6 +202,7 @@ impl LossyTuning { /// its neutral value, so an encode with this tuning is byte-identical to the /// pre-tuning encoder. pub(crate) const DEFAULT: Self = Self { + image_hint: ImageHint::Default, sns_strength: 50, segments: MAX_SEGMENTS, filter_strength: 60, @@ -213,6 +234,15 @@ impl LossyTuning { Self::default() } + /// Set the caller-supplied content classification. + /// + /// The hint is orthogonal to [`Preset`]: a preset expands into concrete shaping values, while this value remains a classification available to content-gated encoder decisions. + #[must_use] + pub const fn with_image_hint(mut self, hint: ImageHint) -> Self { + self.image_hint = hint; + self + } + /// Set the spatial-noise-shaping strength (`0..=100`, clamped): higher values move /// more bits toward visually sensitive (flat) regions. `0` disables shaping. #[must_use] @@ -440,7 +470,7 @@ const fn clamp(v: u8, lo: u8, hi: u8) -> u8 { #[cfg(test)] mod tests { - use super::{AlphaFilterMode, AlphaMethod, LossyTuning, Preset}; + use super::{AlphaFilterMode, AlphaMethod, ImageHint, LossyTuning, Preset}; #[test] fn default_is_the_cwebp_parity_baseline() { @@ -461,6 +491,7 @@ mod tests { assert_eq!(t.pass(), 1); assert!(!t.jpeg_like()); assert_eq!(t.partition_limit(), 0); + assert_eq!(t.image_hint, ImageHint::Default); assert_eq!(LossyTuning::new(), t, "new() equals default()"); } @@ -500,6 +531,21 @@ mod tests { assert_eq!(low.segments(), 1, "segments clamps up to 1"); } + #[test] + fn image_hint_round_trips_without_changing_other_tuning() { + let baseline = LossyTuning::new(); + for hint in [ + ImageHint::Default, + ImageHint::Picture, + ImageHint::Photo, + ImageHint::Graph, + ] { + let tuned = baseline.with_image_hint(hint); + assert_eq!(tuned.image_hint, hint); + assert_eq!(tuned.with_image_hint(ImageHint::Default), baseline); + } + } + #[test] fn alpha_setters_validate_and_round_trip() { let t = LossyTuning::new() diff --git a/crates/webpkit/src/stream.rs b/crates/webpkit/src/stream.rs index 2c6df56..47d3ff0 100644 --- a/crates/webpkit/src/stream.rs +++ b/crates/webpkit/src/stream.rs @@ -81,8 +81,11 @@ pub const DEFAULT_MAX_PIXELS: u64 = 100_000_000; /// /// Build one with [`DecodeOptions::new`] / [`Default`] and the consuming builder /// methods ([`layout`](Self::layout), [`max_pixels`](Self::max_pixels), -/// [`read_metadata`](Self::read_metadata)); the fields are private so new options -/// can be added without a breaking change (reinforced by `#[non_exhaustive]`). +/// [`read_metadata`](Self::read_metadata), +/// [`with_dithering_strength`](Self::with_dithering_strength), and +/// [`with_alpha_dithering_strength`](Self::with_alpha_dithering_strength)); the +/// fields are private so new options can be added without a breaking change +/// (reinforced by `#[non_exhaustive]`). /// /// A default-constructed `DecodeOptions` caps the canvas at /// [`DEFAULT_MAX_PIXELS`] — so a plain [`decode`](crate::decode) is safe on @@ -96,6 +99,10 @@ pub struct DecodeOptions { pub(crate) max_pixels: Option, /// Whether to extract ICC/Exif/XMP metadata. See [`read_metadata`](Self::read_metadata). pub(crate) read_metadata: bool, + /// Lossy VP8 chroma-plane dithering strength (`0..=100`). + pub(crate) dithering_strength: u8, + /// Quantized `ALPH` plane smoothing strength (`0..=100`). + pub(crate) alpha_dithering_strength: u8, } impl Default for DecodeOptions { @@ -104,6 +111,8 @@ impl Default for DecodeOptions { layout: PixelLayout::Rgba8, max_pixels: Some(DEFAULT_MAX_PIXELS), read_metadata: true, + dithering_strength: 0, + alpha_dithering_strength: 0, } } } @@ -157,6 +166,26 @@ impl DecodeOptions { self.read_metadata = read; self } + + /// Set lossy VP8 chroma dithering strength (`0..=100`, clamped). + /// + /// Dithering is a deterministic post-process over low-amplitude U/V macroblocks. + /// `0` (the default) preserves the exact decoded pixels of the normal path; `100` selects the full reference amplitude. + #[must_use] + pub const fn with_dithering_strength(mut self, strength: u8) -> Self { + self.dithering_strength = if strength > 100 { 100 } else { strength }; + self + } + + /// Set quantized-alpha dithering strength (`0..=100`, clamped). + /// + /// This only affects an `ALPH` chunk whose header declares level quantization. + /// It smooths the reconstructed alpha levels like libwebp's `alpha_dithering_strength`; ordinary lossless alpha is unchanged. + #[must_use] + pub const fn with_alpha_dithering_strength(mut self, strength: u8) -> Self { + self.alpha_dithering_strength = if strength > 100 { 100 } else { strength }; + self + } } /// A summary of an image, obtainable before a full decode. @@ -339,10 +368,26 @@ mod tests { let opts = DecodeOptions::new() .layout(PixelLayout::Bgra8) .max_pixels(1024) - .read_metadata(false); + .read_metadata(false) + .with_dithering_strength(73) + .with_alpha_dithering_strength(41); assert_eq!(opts.layout, PixelLayout::Bgra8); assert_eq!(opts.max_pixels, Some(1024)); assert!(!opts.read_metadata); + assert_eq!(opts.dithering_strength, 73); + assert_eq!(opts.alpha_dithering_strength, 41); + assert_eq!( + DecodeOptions::new() + .with_dithering_strength(u8::MAX) + .dithering_strength, + 100 + ); + assert_eq!( + DecodeOptions::new() + .with_alpha_dithering_strength(u8::MAX) + .alpha_dithering_strength, + 100 + ); } #[test] diff --git a/crates/webpkit/src/work_count.rs b/crates/webpkit/src/work_count.rs index 5e33830..6df1b2b 100644 --- a/crates/webpkit/src/work_count.rs +++ b/crates/webpkit/src/work_count.rs @@ -1,7 +1,7 @@ //! Deterministic algorithmic-work counters: a fixed array of process-global //! integer atomics, one slot per instrumented codec hot path. //! -//! Counts are a pure function of encoder input (they tally control-flow events, +//! Counts are a pure function of codec input (they tally control-flow events, //! never wall-clock time), so a [`snapshot`] is a byte-reproducible integer //! ledger across platforms, toolchains, AND optimization levels — unlike timing //! or peak memory. This module is the third, "work-cost" measurement plane @@ -9,19 +9,20 @@ //! //! It is compiled only behind the crate's optional `work-count` feature and is //! absent from every default/production build. Because the counters are -//! process-global statics, the driver (xtask `work`) MUST measure one encode at -//! a time: reset, encode, snapshot. Totals are order-independent (`fetch_add`), -//! so an in-encode `rayon` region is fine as long as the snapshot is read after -//! it joins. +//! process-global statics, the driver (xtask `work`) MUST measure one codec +//! operation at a time: reset, run, snapshot. Totals are order-independent +//! (`fetch_add`), so an in-encode `rayon` region is fine as long as the snapshot +//! is read after it joins. use core::sync::atomic::{AtomicU64, Ordering}; /// One instrumented hot path. /// /// The discriminant order is load-bearing: it fixes both the index into the -/// backing `SLOTS` array and the field order of the committed -/// `corpus/work.json` ledger. Only ever APPEND new variants — never reorder or -/// remove, or the ledger schema shifts under the drift gate. +/// backing `SLOTS` array and each snapshot value's mapping to the stable +/// [`field_names`] keys written to `corpus/work.json`. Only ever APPEND new +/// variants — never reorder or remove, or the ledger schema shifts under the +/// drift gate. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(usize)] pub enum Counter { @@ -95,10 +96,42 @@ pub enum Counter { LoopFilterEdge, /// A `yuv::upsample_one_row` fancy-upsample row (decode color conversion). UpsampleRow, + // ---- lossless codec (DECODE) ---- + // Append-only despite the semantic grouping: these follow the lossy decode + // slots because their discriminants were added later. Moving them beside the + // older lossless slots would silently remap every committed ledger value. + /// One byte-window shift while refilling the VP8L LSB-first bit reader. + Vp8lBitRefill, + /// One VP8L Huffman-table symbol lookup (header, nested, or pixel stream). + Vp8lHuffmanSymbol, + /// A VP8L Huffman lookup that descends from the root into a second-level table. + Vp8lHuffmanSecondLevel, + /// A meta-Huffman entropy-image group selection for one coded unit. + Vp8lEntropySelect, + /// One literal pixel committed by the VP8L pixel loop. + Vp8lLiteralPixel, + /// One VP8L LZ77 back-reference unit committed. + Vp8lBackrefUnit, + /// One pixel copied by a VP8L LZ77 back-reference (weighted by copy length). + Vp8lBackrefPixel, + /// One VP8L color-cache lookup. + Vp8lCacheLookup, + /// One VP8L color-cache insertion (weighted by inserted pixels). + Vp8lCacheInsert, + /// One pixel reconstructed by the VP8L predictor inverse. + Vp8lPredictorPixel, + /// One pixel processed by the VP8L cross-color inverse. + Vp8lCrossColorPixel, + /// One pixel processed by the VP8L subtract-green inverse. + Vp8lSubtractGreenPixel, + /// One output pixel expanded by the VP8L palette inverse. + Vp8lPalettePixel, + /// One decoded VP8L ARGB pixel packed into the requested byte layout. + Vp8lPackPixel, } /// Number of counter slots — equal to the ledger's integer-field count. -pub const N: usize = Counter::UpsampleRow as usize + 1; +pub const N: usize = Counter::Vp8lPackPixel as usize + 1; // A const-initializable seed for the fixed atomic array. `AtomicU64` has // interior mutability, so the const-item lint fires; it is used only to @@ -113,40 +146,58 @@ const ZERO: AtomicU64 = AtomicU64::new(0); static SLOTS: [AtomicU64; N] = [ZERO; N]; impl Counter { + #[inline] + fn bump_in(self, slots: &[AtomicU64; N]) { + slots[self as usize].fetch_add(1, Ordering::Relaxed); + } + + #[inline] + fn add_in(self, slots: &[AtomicU64; N], n: u64) { + slots[self as usize].fetch_add(n, Ordering::Relaxed); + } + /// Add 1 to this counter's slot. #[inline] pub fn bump(self) { - SLOTS[self as usize].fetch_add(1, Ordering::Relaxed); + self.bump_in(&SLOTS); } /// Add `n` to this counter's slot (for cost-weighted events such as an /// inner loop's trip count). #[inline] pub fn add(self, n: u64) { - SLOTS[self as usize].fetch_add(n, Ordering::Relaxed); + self.add_in(&SLOTS, n); } } -/// Zero every counter slot; call immediately before a measured encode. -pub fn reset() { - for slot in &SLOTS { +fn reset_slots(slots: &[AtomicU64; N]) { + for slot in slots { slot.store(0, Ordering::Relaxed); } } -/// Read every counter slot in [`Counter`] discriminant order. -/// -/// Call after the measured encode (and after any in-encode parallel region has -/// joined). The returned array aligns element-for-element with [`field_names`]. -#[must_use] -pub fn snapshot() -> [u64; N] { +fn snapshot_slots(slots: &[AtomicU64; N]) -> [u64; N] { let mut out = [0u64; N]; - for (dst, slot) in out.iter_mut().zip(SLOTS.iter()) { + for (dst, slot) in out.iter_mut().zip(slots.iter()) { *dst = slot.load(Ordering::Relaxed); } out } +/// Zero every counter slot; call immediately before a measured codec operation. +pub fn reset() { + reset_slots(&SLOTS); +} + +/// Read every counter slot in [`Counter`] discriminant order. +/// +/// Call after the measured operation (and after any in-encode parallel region has joined). +/// The returned array aligns element-for-element with [`field_names`]. +#[must_use] +pub fn snapshot() -> [u64; N] { + snapshot_slots(&SLOTS) +} + /// The stable, lowercase field name of each slot in [`Counter`] order. /// /// These drive the `corpus/work.json` JSON keys. Kept adjacent to the enum so @@ -187,32 +238,73 @@ pub const fn field_names() -> [&'static str; N] { "idct_call", "loop_filter_edge", "upsample_row", + // lossless codec (DECODE) + "vp8l_bit_refill", + "vp8l_huffman_symbol", + "vp8l_huffman_second_level", + "vp8l_entropy_select", + "vp8l_literal_pixel", + "vp8l_backref_unit", + "vp8l_backref_pixel", + "vp8l_cache_lookup", + "vp8l_cache_insert", + "vp8l_predictor_pixel", + "vp8l_cross_color_pixel", + "vp8l_subtract_green_pixel", + "vp8l_palette_pixel", + "vp8l_pack_pixel", ] } #[cfg(test)] mod tests { - use super::{Counter, N, field_names, reset, snapshot}; + use core::sync::atomic::AtomicU64; + + use super::{Counter, N, field_names, reset_slots, snapshot_slots}; #[test] fn field_names_align_with_slot_count() { + assert_eq!(N, 43); assert_eq!(field_names().len(), N); } - // A single test owns the process-global `SLOTS`, since cargo runs test - // functions on parallel threads and separate tests would race on it. + #[test] + fn vp8l_decode_field_names_align_with_discriminants() { + for (counter, name) in [ + (Counter::Vp8lBitRefill, "vp8l_bit_refill"), + (Counter::Vp8lHuffmanSymbol, "vp8l_huffman_symbol"), + (Counter::Vp8lHuffmanSecondLevel, "vp8l_huffman_second_level"), + (Counter::Vp8lEntropySelect, "vp8l_entropy_select"), + (Counter::Vp8lLiteralPixel, "vp8l_literal_pixel"), + (Counter::Vp8lBackrefUnit, "vp8l_backref_unit"), + (Counter::Vp8lBackrefPixel, "vp8l_backref_pixel"), + (Counter::Vp8lCacheLookup, "vp8l_cache_lookup"), + (Counter::Vp8lCacheInsert, "vp8l_cache_insert"), + (Counter::Vp8lPredictorPixel, "vp8l_predictor_pixel"), + (Counter::Vp8lCrossColorPixel, "vp8l_cross_color_pixel"), + (Counter::Vp8lSubtractGreenPixel, "vp8l_subtract_green_pixel"), + (Counter::Vp8lPalettePixel, "vp8l_palette_pixel"), + (Counter::Vp8lPackPixel, "vp8l_pack_pixel"), + ] { + assert_eq!(field_names()[counter as usize], name); + } + } + + // Exercise the counter primitives on local slots. + // Other all-feature tests legitimately use the process-global work ledger in parallel. #[test] fn bump_add_and_reset_hit_the_right_slots() { - reset(); - Counter::HistogramPass.bump(); - Counter::HistogramPass.bump(); - Counter::MatchCompare.add(40); - let snap = snapshot(); + let slots = core::array::from_fn(|_| AtomicU64::new(0)); + reset_slots(&slots); + Counter::HistogramPass.bump_in(&slots); + Counter::HistogramPass.bump_in(&slots); + Counter::MatchCompare.add_in(&slots, 40); + let snap = snapshot_slots(&slots); assert_eq!(snap[Counter::HistogramPass as usize], 2); assert_eq!(snap[Counter::MatchCompare as usize], 40); assert_eq!(snap[Counter::TokenPartitionWalk as usize], 0); - reset(); - assert_eq!(snapshot(), [0u64; N]); + reset_slots(&slots); + assert_eq!(snapshot_slots(&slots), [0u64; N]); } } diff --git a/crates/webpkit/tests/campaign_shapes.rs b/crates/webpkit/tests/campaign_shapes.rs new file mode 100644 index 0000000..3287f4e --- /dev/null +++ b/crates/webpkit/tests/campaign_shapes.rs @@ -0,0 +1,93 @@ +//! Synthetic campaign gates for shapes the square benchmark matrix does not cover. +#![forbid(unsafe_code)] +#![allow( + clippy::unwrap_used, + reason = "test fixtures use fixed, valid dimensions and fail immediately on setup errors" +)] + +use webpkit::lossless::{Effort, EncoderConfig, ImageRef, decode_rgba, encode}; +use webpkit::{Dimensions, Image, PixelLayout, decode, encode_lossy_rgba}; + +fn rgba(width: u32, height: u32, alpha: bool) -> Vec { + let mut out = + Vec::with_capacity(usize::try_from(u64::from(width) * u64::from(height) * 4).unwrap()); + for y in 0..height { + for x in 0..width { + let a = if alpha { + (x.wrapping_mul(17) + y.wrapping_mul(29)).to_le_bytes()[0] + } else { + 255 + }; + out.extend_from_slice(&[ + x.wrapping_mul(11).to_le_bytes()[0], + y.wrapping_mul(7).to_le_bytes()[0], + (x ^ y).wrapping_mul(13).to_le_bytes()[0], + a, + ]); + } + } + out +} + +#[test] +fn odd_portrait_landscape_and_alpha_round_trip_losslessly() { + for &(width, height, alpha) in &[(17, 31, true), (31, 17, false), (1, 37, true)] { + let dims = Dimensions::new(width, height).unwrap(); + let pixels = rgba(width, height, alpha); + let image = ImageRef::new(dims, PixelLayout::Rgba8, &pixels).unwrap(); + let config = EncoderConfig::new().with_effort(Effort::level(0)); + let encoded = encode(image, &config).unwrap(); + let (decoded_dims, decoded) = decode_rgba(&encoded).unwrap(); + assert_eq!(decoded_dims, dims); + assert_eq!(decoded, pixels); + } +} + +#[test] +fn odd_portrait_landscape_and_alpha_decode_through_lossy_container() { + for &(width, height, alpha) in &[(17, 31, true), (31, 17, false), (1, 37, true)] { + let pixels = rgba(width, height, alpha); + let encoded = encode_lossy_rgba(width, height, &pixels, 75).unwrap(); + let decoded = decode(&encoded).unwrap(); + assert_eq!( + decoded.dimensions(), + Dimensions::new(width, height).unwrap() + ); + assert_eq!(decoded.layout(), PixelLayout::Rgba8); + assert_eq!(decoded.as_bytes().len(), pixels.len()); + for (source, output) in pixels + .as_chunks::<4>() + .0 + .iter() + .zip(decoded.as_bytes().as_chunks::<4>().0.iter()) + { + assert_eq!( + output[3], source[3], + "the lossy container must preserve alpha semantics" + ); + } + } +} + +#[test] +fn large_portrait_and_landscape_scale_to_the_requested_longest_edge() { + for &(width, height, target_width, target_height) in + &[(2049, 1025, 1024, 513), (1025, 2049, 513, 1024)] + { + let dims = Dimensions::new(width, height).unwrap(); + let target = if width >= height { + dims.scaled(1024, 0).unwrap() + } else { + dims.scaled(0, 1024).unwrap() + }; + assert_eq!( + target, + Dimensions::new(target_width, target_height).unwrap() + ); + + let image = Image::new(dims, PixelLayout::Rgba8, rgba(width, height, true)).unwrap(); + let resized = image.resize_exact(target); + assert_eq!(resized.dimensions(), target); + assert!(resized.has_alpha()); + } +} diff --git a/crates/webpkit/tests/oracle.rs b/crates/webpkit/tests/oracle.rs index 2297083..9340999 100644 --- a/crates/webpkit/tests/oracle.rs +++ b/crates/webpkit/tests/oracle.rs @@ -41,6 +41,26 @@ fn libwebp_encode_lossy_rgba( quality: f32, alpha_compression: i32, alpha_filtering: i32, +) -> Vec { + libwebp_encode_lossy_rgba_with_alpha_quality( + rgba, + width, + height, + quality, + alpha_compression, + alpha_filtering, + 100, + ) +} + +fn libwebp_encode_lossy_rgba_with_alpha_quality( + rgba: &[u8], + width: u32, + height: u32, + quality: f32, + alpha_compression: i32, + alpha_filtering: i32, + alpha_quality: i32, ) -> Vec { let mut config = libwebp_sys::WebPConfig::new().unwrap(); config.lossless = 0; @@ -48,7 +68,7 @@ fn libwebp_encode_lossy_rgba( config.method = 4; config.alpha_compression = alpha_compression; config.alpha_filtering = alpha_filtering; - config.alpha_quality = 100; + config.alpha_quality = alpha_quality; // SAFETY: `config` is a fully-initialized WebPConfig. assert!( unsafe { libwebp_sys::WebPValidateConfig(&raw const config) } != 0, @@ -105,6 +125,45 @@ fn libwebp_decode_rgba(webp: &[u8]) -> (u32, u32, Vec) { (u32::try_from(w).unwrap(), u32::try_from(h).unwrap(), rgba) } +/// Decode through libwebp's advanced API so both decoder dithering controls are observable. +/// The output buffer is Rust-owned, avoiding allocator ownership at the FFI boundary. +fn libwebp_decode_rgba_with_dither( + webp: &[u8], + width: u32, + height: u32, + dithering_strength: i32, + alpha_dithering_strength: i32, +) -> Vec { + let rgba_stride = usize::try_from(width).unwrap().checked_mul(4).unwrap(); + let rgba_len = rgba_stride + .checked_mul(usize::try_from(height).unwrap()) + .unwrap(); + let rgba_stride = i32::try_from(rgba_stride).unwrap(); + let mut rgba = vec![0u8; rgba_len]; + let mut config = libwebp_sys::WebPDecoderConfig::new().unwrap(); + config.options.dithering_strength = dithering_strength; + config.options.alpha_dithering_strength = alpha_dithering_strength; + config.output.colorspace = libwebp_sys::WEBP_CSP_MODE::MODE_RGBA; + config.output.is_external_memory = 1; + config.output.u.RGBA = libwebp_sys::WebPRGBABuffer { + rgba: rgba.as_mut_ptr(), + stride: rgba_stride, + size: rgba.len(), + }; + // SAFETY: `webp` is live for the call; `config` is initialized and points to + // the exactly-sized Rust-owned RGBA buffer above. + let status = unsafe { libwebp_sys::WebPDecode(webp.as_ptr(), webp.len(), &raw mut config) }; + // SAFETY: `config.output` was initialized by libwebp. This is its required final + // cleanup call; external RGBA storage remains owned by the Rust `Vec`. + unsafe { libwebp_sys::WebPFreeDecBuffer(&raw mut config.output) }; + assert_eq!( + status, + libwebp_sys::VP8StatusCode::VP8_STATUS_OK, + "libwebp advanced decode failed" + ); + rgba +} + /// Deterministic synthetic RGBA (`width * height * 4`) whose alpha channel is an /// independent function of position (never derivable from the RGB), so a wrong /// alpha/green extraction cannot pass. `pattern` selects an alpha shape that steers @@ -204,6 +263,90 @@ fn decode_matches_libwebp_on_transparent_lossy_images() { ); } +#[test] +fn decoder_dithering_matches_libwebp_for_chroma_and_quantized_alpha() { + // Saturated 2x2 color cells give the chroma-dither gate plenty of non-zero AC energy. + // Quality 100 keeps UV quantization in libwebp's dither table. + let (w, h) = (32u32, 32u32); + let mut opaque = Vec::with_capacity(usize::try_from(w * h * 4).unwrap()); + let colors = [ + [255, 0, 0, 255], + [0, 255, 0, 255], + [0, 0, 255, 255], + [255, 255, 0, 255], + ]; + for y in 0..h { + for x in 0..w { + opaque + .extend_from_slice(&colors[usize::try_from((x / 16 + 2 * (y / 16)) % 4).unwrap()]); + } + } + let color_file = libwebp_encode_lossy_rgba(&opaque, w, h, 100.0, 0, 0); + let color_golden = libwebp_decode_rgba_with_dither(&color_file, w, h, 100, 0); + let color_plain = libwebp_decode_rgba_with_dither(&color_file, w, h, 0, 0); + let color_ours = webpkit::decode_with( + &color_file, + &webpkit::DecodeOptions::new().with_dithering_strength(100), + ) + .unwrap(); + assert_ne!( + color_golden, color_plain, + "fixture must exercise chroma dithering" + ); + assert_eq!(color_ours.as_bytes(), color_golden); + let mut color_incremental = webpkit::IncrementalDecoder::with_options( + webpkit::DecodeOptions::new().with_dithering_strength(100), + ); + color_incremental.push(&color_file).unwrap(); + assert_eq!( + color_incremental.into_image().unwrap().as_bytes(), + color_golden, + "incremental lossy decode must retain color dithering options" + ); + + // Alpha quality below 100 marks the ALPH stream for level dequantization. + // Its RGB is intentionally flat so only the alpha option can change output. + let (aw, ah) = (17u32, 19u32); + let mut transparent = synth_rgba(aw, ah, 2); + for px in transparent.chunks_exact_mut(4) { + px[0] = 40; + px[1] = 80; + px[2] = 120; + } + let alpha_file = + libwebp_encode_lossy_rgba_with_alpha_quality(&transparent, aw, ah, 90.0, 1, 2, 30); + let alph = locate_image_with_alpha(&alpha_file) + .unwrap() + .alpha + .expect("quantized alpha fixture has ALPH"); + assert_eq!( + parse_header(alph).unwrap().0.preprocessing, + 1, + "fixture must request alpha level dequantization" + ); + let alpha_golden = libwebp_decode_rgba_with_dither(&alpha_file, aw, ah, 0, 100); + let alpha_plain = libwebp_decode_rgba_with_dither(&alpha_file, aw, ah, 0, 0); + let alpha_ours = webpkit::decode_with( + &alpha_file, + &webpkit::DecodeOptions::new().with_alpha_dithering_strength(100), + ) + .unwrap(); + assert_ne!( + alpha_golden, alpha_plain, + "fixture must exercise alpha dequantization" + ); + assert_eq!(alpha_ours.as_bytes(), alpha_golden); + let mut alpha_incremental = webpkit::IncrementalDecoder::with_options( + webpkit::DecodeOptions::new().with_alpha_dithering_strength(100), + ); + alpha_incremental.push(&alpha_file).unwrap(); + assert_eq!( + alpha_incremental.into_image().unwrap().as_bytes(), + alpha_golden, + "deferred incremental decode must retain alpha dithering options" + ); +} + #[test] fn incremental_decodes_lossy_still_like_one_shot() { // An opaque lossy image encodes to a bare `VP8 ` (no `ALPH`), which the umbrella diff --git a/crates/webpkit/tests/oracle_rescale.rs b/crates/webpkit/tests/oracle_rescale.rs index 4121b46..b9b97b4 100644 --- a/crates/webpkit/tests/oracle_rescale.rs +++ b/crates/webpkit/tests/oracle_rescale.rs @@ -1,16 +1,13 @@ -//! Byte-exact differential oracle for the core geometry rescaler -//! (`crate::geometry`): cross-check [`Image::resize`] against libwebp's -//! `WebPPictureRescale` (the ARGB path `cwebp -resize` runs), in-process. +//! Byte-exact differential oracle for the core geometry rescaler (`crate::geometry`): cross-check [`Image::resize`] and [`Image::resize_exact`] against libwebp's `WebPPictureRescale` (the ARGB path `cwebp -resize` runs), +//! in-process. //! -//! Enabled only with `--features oracle` (which links `libwebp-sys` and the vendored -//! reference); never part of a normal build. +//! Enabled only with `--features oracle` (which links `libwebp-sys` and the vendored reference); never part of a normal build. //! -//! The resize contract is **byte-exact**: our fixed-point port of `WebPRescaler` -//! reproduces libwebp's output pixel-for-pixel, including the alpha black-matting -//! that `WebPPictureRescale` applies before interpolating. The alpha channel is -//! generated *independently* of the RGB so a wrong premultiply lane cannot pass -//! silently, and the size matrix covers upscaling, downscaling, non-integer ratios, -//! and single-axis (aspect) targets. +//! The resize contract is **byte-exact** in both modes. +//! [`Image::resize`] must equal `WebPPictureRescale`, including the alpha black-matting it applies before interpolating. +//! [`Image::resize_exact`] must equal what `cwebp -resize -exact` builds: the color channels of an opaque copy's rescale, with alpha from the ordinary rescale. +//! Alpha is generated *independently* of the RGB so a wrong premultiply lane cannot pass silently, four content patterns cover opaque, +//! hard-edged, and saturated-corner alpha, and the size matrix sweeps every (src, dst) pair in `1..=8` on both axes plus large, prime, and extreme-ratio pairs — so upscaling, downscaling, non-integer ratios, and single-axis targets are all pinned. #![cfg(feature = "oracle")] #![allow( @@ -26,8 +23,7 @@ use webpkit::{Dimensions, Image, PixelLayout}; -/// Rescale `rgba` (`width * height * 4` bytes) to `dst_w x dst_h` with libwebp's -/// `WebPPictureRescale` in ARGB mode, returning the result as RGBA8 bytes. +/// Rescale `rgba` (`width * height * 4` bytes) to `dst_w x dst_h` with libwebp's `WebPPictureRescale` in ARGB mode, returning the result as RGBA8 bytes. fn libwebp_rescale_rgba(rgba: &[u8], width: u32, height: u32, dst_w: u32, dst_h: u32) -> Vec { let mut picture = libwebp_sys::WebPPicture::new().unwrap(); picture.use_argb = 1; // ARGB path: import stores 0xAARRGGBB directly, no YUV. @@ -66,55 +62,194 @@ fn libwebp_rescale_rgba(rgba: &[u8], width: u32, height: u32, dst_w: u32, dst_h: out } -/// A synthetic RGBA image whose alpha varies on a different gradient than the RGB, so -/// the premultiply/rescale/un-premultiply lanes are all exercised distinctly. -fn synthetic(width: u32, height: u32) -> Vec { +/// The reference for `cwebp -resize -exact`: libwebp rescales a copy whose alpha has been forced opaque to get the un-weighted color, and keeps the alpha of the ordinary (matted) rescale. +fn libwebp_rescale_rgba_exact( + rgba: &[u8], + width: u32, + height: u32, + dst_w: u32, + dst_h: u32, +) -> Vec { + let matted = libwebp_rescale_rgba(rgba, width, height, dst_w, dst_h); + let mut opaque = rgba.to_vec(); + for px in opaque.chunks_exact_mut(4) { + px[3] = 0xff; + } + let color = libwebp_rescale_rgba(&opaque, width, height, dst_w, dst_h); + color + .chunks_exact(4) + .zip(matted.chunks_exact(4)) + .flat_map(|(c, m)| [c[0], c[1], c[2], m[3]]) + .collect() +} + +/// Four synthetic RGBA patterns. +/// Alpha always varies on a different law than the RGB, +/// so the premultiply/rescale/un-premultiply lanes are exercised distinctly. +fn content(kind: u32, width: u32, height: u32) -> Vec { let mut rgba = Vec::with_capacity((width * height * 4) as usize); + let mut state: u32 = 0x1234_5678 ^ (width << 8) ^ height; for y in 0..height { for x in 0..width { - let r = (x * 7 + y * 3) as u8; - let g = (x * 3 + y * 11 + 40) as u8; - let b = (x * 13 + y * 5 + 90) as u8; - // Alpha sweeps independently, including 0 and 255 corners. - let a = ((x + y) * 17 % 256) as u8; - rgba.extend_from_slice(&[r, g, b, a]); + state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + let px = match kind { + // Smooth color, alpha sweeping independently through 0 and 255. + 0 => [ + (x * 7 + y * 3) as u8, + (x * 3 + y * 11 + 40) as u8, + (x * 13 + y * 5 + 90) as u8, + ((x + y) * 17 % 256) as u8, + ], + // Fully opaque: the matting must be the identity here. + 1 => [ + (x * 5 + y * 9) as u8, + (x * 17 + y) as u8, + (x + y * 23) as u8, + 0xff, + ], + // Hard 0/255 alpha edges over saturated color: the widest matting gap. + 2 => [ + if (x / 2) % 2 == 0 { 255 } else { 0 }, + if (y / 3) % 2 == 0 { 255 } else { 0 }, + if (x ^ y) % 4 < 2 { 200 } else { 10 }, + if (x + y) % 2 == 0 { 255 } else { 0 }, + ], + // Pseudo-random, hitting the alpha 1 and 254 rounding corners. + _ => [ + (state >> 24) as u8, + (state >> 16) as u8, + (state >> 8) as u8, + match state % 8 { + 0 => 0, + 1 => 1, + 2 => 254, + 3 => 255, + v => (v * 31) as u8, + }, + ], + }; + rgba.extend_from_slice(&px); } } rgba } +/// Assert both resize modes against libwebp for one content/size case, returning whether the case carried any non-opaque pixel. +fn check(kind: u32, sw: u32, sh: u32, dw: u32, dh: u32) -> bool { + let rgba = content(kind, sw, sh); + let img = Image::new( + Dimensions::new(sw, sh).unwrap(), + PixelLayout::Rgba8, + rgba.clone(), + ) + .unwrap(); + let target = Dimensions::new(dw, dh).unwrap(); + + assert_eq!( + img.resize(target).as_bytes(), + libwebp_rescale_rgba(&rgba, sw, sh, dw, dh).as_slice(), + "resize k{kind} {sw}x{sh} -> {dw}x{dh} diverged from WebPPictureRescale" + ); + assert_eq!( + img.resize_exact(target).as_bytes(), + libwebp_rescale_rgba_exact(&rgba, sw, sh, dw, dh).as_slice(), + "resize_exact k{kind} {sw}x{sh} -> {dw}x{dh} diverged from `cwebp -resize -exact`" + ); + rgba.chunks_exact(4).any(|p| p[3] != 0xff) +} + #[test] -fn resize_matches_libwebp_byte_for_byte() { - // (src_w, src_h, dst_w, dst_h): upscale, downscale, non-integer, and aspect rows. +fn resize_matches_libwebp_over_the_small_size_matrix() { + // Every (src, dst) pair in 1..=8 on both axes: 1:1, both expansions, both shrinks, and every mixed/odd ratio a tiny plane can take. + let mut exercised_alpha = false; + for kind in 0..4 { + for sw in 1..=8 { + for sh in 1..=8 { + for dw in 1..=8 { + for dh in 1..=8 { + exercised_alpha |= check(kind, sw, sh, dw, dh); + } + } + } + } + } + assert!(exercised_alpha, "matrix must include non-opaque alpha"); +} + +#[test] +fn resize_matches_libwebp_at_scale_and_extremes() { + // Photographic sizes, primes, single-row/column planes, and ratios extreme enough to hit the `fxy_scale == 0` special case and the 16384 dimension ceiling. let cases = [ - (16, 16, 8, 8), // integer 2x downscale - (16, 16, 32, 32), // integer 2x upscale - (17, 13, 5, 9), // odd downscale, non-integer ratios both axes - (10, 10, 23, 7), // mixed up/down - (9, 9, 9, 9), // 1:1 (still routes through the matting path) - (7, 20, 20, 7), // transpose-ish aspect swap - (1, 8, 4, 3), // 1-wide source expansion - (32, 1, 5, 1), // 1-tall source shrink + (64, 64, 32, 32), + (64, 64, 63, 65), + (64, 64, 1, 1), + (64, 64, 127, 3), + (64, 64, 256, 256), + (100, 100, 33, 77), + (17, 13, 200, 190), + (97, 61, 300, 7), + (223, 149, 97, 211), + (640, 426, 320, 213), + (640, 426, 641, 425), + (1, 1, 512, 384), + (512, 384, 1, 1), + (997, 3, 13, 61), + (3, 997, 61, 13), + (256, 256, 255, 255), + (255, 255, 256, 256), + (16384, 1, 37, 1), + (1, 16384, 1, 37), ]; - let mut exercised_alpha = false; - for (sw, sh, dw, dh) in cases { - let rgba = synthetic(sw, sh); - exercised_alpha |= rgba.as_chunks::<4>().0.iter().any(|p| p[3] != 0xff); + for kind in 0..4 { + for (sw, sh, dw, dh) in cases { + check(kind, sw, sh, dw, dh); + } + } +} +#[test] +fn the_two_modes_agree_exactly_when_the_source_is_opaque() { + // Matting is the identity on an opaque plane, so `-exact` can only change pixels that carry transparency — a regression in either mode breaks this equality. + for (sw, sh, dw, dh) in [(16, 16, 7, 23), (9, 5, 5, 9), (33, 33, 64, 64)] { + let rgba = content(1, sw, sh); let img = Image::new( Dimensions::new(sw, sh).unwrap(), PixelLayout::Rgba8, rgba.clone(), ) .unwrap(); - let ours = img.resize(Dimensions::new(dw, dh).unwrap()); - let theirs = libwebp_rescale_rgba(&rgba, sw, sh, dw, dh); - + let target = Dimensions::new(dw, dh).unwrap(); assert_eq!( - ours.as_bytes(), - theirs.as_slice(), - "resize {sw}x{sh} -> {dw}x{dh} diverged from libwebp WebPPictureRescale" + img.resize(target).as_bytes(), + img.resize_exact(target).as_bytes(), + "opaque {sw}x{sh} -> {dw}x{dh}: the two modes must coincide" ); } - assert!(exercised_alpha, "matrix must include non-opaque alpha"); +} + +#[test] +fn exact_mode_keeps_the_color_under_transparent_pixels() { + // A fully transparent plane with a constant color: matting collapses it to black, + // the exact mode carries the color through untouched. + let mut rgba = Vec::new(); + for _ in 0..(12 * 12) { + rgba.extend_from_slice(&[200, 90, 30, 0]); + } + let img = Image::new(Dimensions::new(12, 12).unwrap(), PixelLayout::Rgba8, rgba).unwrap(); + let target = Dimensions::new(5, 7).unwrap(); + + assert!( + img.resize(target) + .as_bytes() + .chunks_exact(4) + .all(|p| p == [0, 0, 0, 0]), + "matted resize must black out fully transparent color" + ); + assert!( + img.resize_exact(target) + .as_bytes() + .chunks_exact(4) + .all(|p| p == [200, 90, 30, 0]), + "exact resize must preserve fully transparent color" + ); } diff --git a/docs/adr/0001-comparison-corpus-provenance.md b/docs/adr/0001-comparison-corpus-provenance.md new file mode 100644 index 0000000..38b24ca --- /dev/null +++ b/docs/adr/0001-comparison-corpus-provenance.md @@ -0,0 +1,38 @@ +--- +date: 2026-11-11 +--- + +# Comparison corpus provenance + +## Status + +Proposed + +## Context + +Committed comparison ledgers need public inputs that a clean checkout can reproduce. +Private photographs aren't available to public contributors. + +## Decision + +Build committed ledgers from the deterministic corpus in `crates/webpkit-samples`. +Keep private-image measurements local, with paths supplied at runtime and no private names or results committed. +Treat synthetic ledgers as regression checks rather than evidence of photographic compression quality. +Review compression changes using local category and total results before adopting them. + +## Alternatives + +A licensed public photographic corpus would improve reproducibility and representativeness, but requires provenance review and maintained fixtures. +Private inputs can't serve as a public gate. + +## Consequences + +Public regression checks remain reproducible. +Their synthetic content limits conclusions about photographs. +Reconsider the corpus when a redistributable photographic set is available. + +## Confirmation + +The workspace conformance tests check committed fixtures. +The measurement commands in [benchmarking](../benchmarking.md) separate local measurements from committed ledgers. +Performance adoption still requires representative measurements. diff --git a/docs/adr/0002-simd-unsafe-boundary.md b/docs/adr/0002-simd-unsafe-boundary.md new file mode 100644 index 0000000..8697f5b --- /dev/null +++ b/docs/adr/0002-simd-unsafe-boundary.md @@ -0,0 +1,36 @@ +--- +date: 2026-11-11 +--- + +# Portable scalar codec boundary + +## Status + +Proposed + +## Context + +The public crate forbids unsafe code and supports portable Rust with `no_std + alloc`. +CPU-specific intrinsics and runtime dispatch would add implementations and validation obligations. + +## Decision + +Keep product codecs in safe scalar Rust. +Pursue algorithmic work reduction, allocation reduction, and safe scalar optimization. +Keep unavoidable allocator operations and libwebp foreign function interfaces in unpublished tooling crates outside production dependencies. + +## Alternatives + +An isolated vector implementation could improve throughput, but would require processor dispatch, architecture coverage, and evidence that output remains deterministic. +Retaining one portable implementation preserves the existing boundary. + +## Consequences + +Product code keeps one portable implementation at the cost of possible vector speedups. +Local timing observations don't establish a universal performance ceiling. +Reconsider the boundary only through a reviewed successor supported by representative benchmarks and compatibility checks. + +## Confirmation + +The public crate's `forbid(unsafe_code)`, the host `no_std` build, and workspace tests enforce the current implementation. +[Benchmarking](../benchmarking.md) describes separate timing measurements. diff --git a/docs/adr/0003-rayon-default-and-parallel-ceiling.md b/docs/adr/0003-rayon-default-and-parallel-ceiling.md new file mode 100644 index 0000000..5db5d0f --- /dev/null +++ b/docs/adr/0003-rayon-default-and-parallel-ceiling.md @@ -0,0 +1,39 @@ +--- +date: 2026-11-11 +--- + +# Opt-in deterministic parallel encoding + +## Status + +Proposed + +## Context + +Lossy planning and lossless candidate evaluation support optional Rayon execution. +Entropy emission and scheduling costs limit the work that benefits from parallelism. + +## Decision + +Keep `rayon` outside the default feature set. +Preserve deterministic planning, commit order, and tie resolution across serial and parallel builds. +Use serial planning for small frames and the fast path without mode search. +Keep the supported Rust floor of 1.88. + +## Alternatives + +Default parallelism would simplify configuration for some consumers but add dependencies and require `std`. +Larger scheduling units require measurements demonstrating benefits without changing output. + +## Consequences + +Consumers explicitly enable parallelism and its dependency graph. +The default codec remains dependency-free. +No fixed speedup ceiling follows from the available source. +Reconsider defaults or scheduling when representative measurements justify the added cost. + +## Confirmation + +Workspace tests enable `webpkit/rayon` and compare serial and parallel results. +The host `no_std` check covers the allocation-only build. +Timing claims require the separate measurements described in [benchmarking](../benchmarking.md). diff --git a/docs/adr/0004-cwebp-residual-flags.md b/docs/adr/0004-cwebp-residual-flags.md new file mode 100644 index 0000000..ede9b34 --- /dev/null +++ b/docs/adr/0004-cwebp-residual-flags.md @@ -0,0 +1,40 @@ +--- +date: 2026-11-11 +--- + +# Explicit cwebp flag compatibility + +## Status + +Proposed + +## Context + +Drop-in callers need explicit handling of unsupported encoder flags. +Ignoring a requested pixel transformation would produce unintended output. + +## Decision + +Accept `-af` and `-map` as compatibility no-ops with stderr warnings. +Consume the integer argument to `-map`. +Keep existing runtime compatibility no-ops silent. +Reject `-blend_alpha` and the unsupported dithering bit of `-pre`. +Parse valid `-hint` names into `ImageHint`. +The current lossy emitter doesn't consult that classification. + +## Alternatives + +Rejecting every unsupported optimization breaks otherwise usable invocations. +Accepting every flag without warnings conceals missing transformations. +Adding each effect requires separate behavior and oracle tests. + +## Consequences + +Compatibility callers can continue with explicit warnings about omitted effects. +Pixel transformations remain errors until implemented. +Reconsider each disposition when its requested effect becomes available. + +## Confirmation + +The [flag table](../../crates/webpkit-cli/src/cli/cwebp.rs) defines the dispositions. +[Drop-in tests](../../crates/webpkit-cli/tests/dropins.rs) check warnings, argument validation, rejection, and byte-neutral hint handling for the covered inputs. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..f2a1e8d --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,17 @@ +# Architecture decision records + +[Comparison corpus provenance](0001-comparison-corpus-provenance.md) separates public regression fixtures from local private-image measurements. + +[Portable scalar codec boundary](0002-simd-unsafe-boundary.md) records the proposed product and tooling safety boundary. + +[Opt-in deterministic parallel encoding](0003-rayon-default-and-parallel-ceiling.md) records feature defaults and deterministic scheduling. + +[Explicit cwebp flag compatibility](0004-cwebp-residual-flags.md) records warnings, typed hints, and rejected transformations. + +[Affected-work selection](affected-work-selection.md) defines the ordinary check comparison boundary. +[Release-only heavy checks](release-only-heavy-checks.md) defines qualification timing. + +Recovered proposals keep their filenames and await owner acceptance. +New records use descriptive paths with a front-matter date. +Use Context, Decision, Alternatives, Consequences, and Confirmation sections. +Keep superseded records and link their successors. diff --git a/docs/benchmarking.md b/docs/benchmarking.md index 5cabdb9..ff37b2a 100644 --- a/docs/benchmarking.md +++ b/docs/benchmarking.md @@ -8,8 +8,8 @@ adding a perf number, a gate, or a benchmark. | Plane | What it measures | Reproducible? | Committed & gated? | Where | |-------|------------------|---------------|--------------------|-------| -| **Deterministic ledgers** | Encoded size, compression ratio, peak memory, decode/re-encode stability; **lossy** size / ratio / reconstruction SSE / peak memory | Yes — integer-only, byte-identical across runs | Yes — `committed == fresh` drift gate | `corpus/metrics.json`, `corpus/metrics-lossy.json`, `corpus/baseline.json` | -| **Deterministic work-cost** | Algorithmic work in the encoders (histogram passes, match-finder hops, cluster scans, fdct/quantize/trellis/SSE calls, …) **and the lossy decoder** (bool reads, coeff-token walks, IDCT / loop-filter / upsample calls) — a proxy for time | Yes — integer event tallies, toolchain- AND profile-independent | Yes — `committed == fresh` drift gate | `corpus/work.json` | +| **Deterministic ledgers** | Encoded size, compression ratio, peak memory, decode/re-encode stability; **lossy** size / ratio / reconstruction SSE / peak memory; **vs-libwebp** our-vs-cwebp size / centi-dB PSNR (requires pinned `cwebp`, fails closed) | Yes — integer-only, byte-identical across runs | Yes — `committed == fresh` drift gate | `corpus/metrics.json`, `corpus/metrics-lossy.json`, `corpus/vs-libwebp.json`, `corpus/baseline.json` | +| **Deterministic work-cost** | Algorithmic work in both encoders (histogram passes, match-finder hops, cluster scans, fdct/quantize/trellis/SSE calls, …) and both decoders (VP8L bit refills, Huffman/LZ/inverse-transform work; VP8 bool reads, coeff-token walks, IDCT / loop-filter / upsample calls) — a proxy for time | Yes — integer event tallies, toolchain- AND profile-independent | Yes — `committed == fresh` drift gate | `corpus/work.json` | | **Timing** | Wall-clock throughput (lossless AND lossy encode/decode) | No — noisy, hardware- and load-dependent | **Never** — local/dev only | `webpkit-bench` (criterion) | The **deterministic ledgers** are the load-bearing regression signal. Every @@ -31,12 +31,12 @@ Criterion benchmarks are a **local developer tool** for comparing two runs on one machine, never a merge gate. **Speed is gated deterministically, not by the clock.** To keep a *speed* -regression signal in CI without the noise, the encoders are instrumented with -integer **work counters** (the `work_count` module, behind the `work-count` -feature) — one per hot path -(histogram passes, LZ77 match-finder hops, meta-Huffman cluster scans, `fdct` / -`quantize` / `trellis` / `sse_block` calls, k-means comparisons, …). The counts -are a pure function of the input, so they are byte-reproducible across +regression signal in CI without the noise, both encoders and decoders are +instrumented with integer **work counters** (the `work_count` module, behind the +`work-count` feature) — one per hot path (histogram passes, LZ77 match-finder +hops, meta-Huffman cluster scans, `fdct` / `quantize` / `trellis` / `sse_block` +calls, VP8L Huffman/LZ/inverse-transform operations, VP8 bool reads, …). The +counts are a pure function of the input, so they are byte-reproducible across platforms, toolchains, and optimization levels, and `corpus/work.json` is drift-gated exactly like `corpus/metrics.json`. They are a *proxy* for time: an optimization that removes redundant work drops a counter, and the drop is a @@ -116,6 +116,64 @@ diff via `cargo run -p xtask -- metrics --lossy --explain`; it ends with a byte-invariance verdict (did the encoded bytes / `sse` move, or only peak memory?) — the lossy loop's "did ONLY my intended field change" check. +### cwebp comparison ledger — `just vs-libwebp` + +``` +just vs-libwebp # gate: fail if committed corpus/vs-libwebp.json drifts +just vs-libwebp-bless # (re)author it after an intended size/fidelity change +``` + +The committed, drift-gated counterpart of the print-only `metrics --vs-libwebp` aid: it gates `corpus/vs-libwebp.json`, one row per synthetic sample for our `l9` lossless (`Best`, capped to `edge <= 256`) versus single-threaded `cwebp -lossless -exact -m 6 -q 100` (`-mt` omitted), and per `(sample, quality)` for our zero-knob `AUTO` lossy versus `cwebp -q Q -noalpha` at qualities `50 / 75 / 90` (66 rows). +Every field is an integer: `our_bytes`, `cwebp_bytes`, `size_permille` (`our_bytes * 1000 / cwebp_bytes`; below 1000 = our stream is smaller), and `our_psnr_centidb` / `cwebp_psnr_centidb` — reconstruction PSNR as fixed-point centi-dB (`round(dB * 100)`; `9900` = identical, i.e. every lossless row). +**Both** outputs are decoded by *our own* decoder before the PSNR, so the fidelity column is apples-to-apples (lossless compares full-WebP sizes, lossy compares bare `VP8 ` payloads). +The pinned reference (`1.6.0`) is recorded in the top-level `libwebp_version`. + +Unlike the other ledgers this one **needs libwebp** (`cwebp`). +The gate/bless **fails closed** when `cwebp` is absent or does not report exactly `1.6.0`; a reference-free or version-mismatched run cannot validate or rewrite the ledger. +Its rows are integer sizes and centi-dB PSNR (no peak memory), which are deterministic and profile-independent, so the recipe builds `--release` for runtime budget only and needs **no `+1.96` pin** (cwebp's own output is pinned by the `1.6.0` version guard). +If a non-libwebp `cwebp` shadows libwebp on `PATH`, point `WEBPKIT_CWEBP` at the libwebp `1.6.0` binary. + +### Private matched-fidelity campaign — `surpass` + +``` +cargo run --release -p xtask --features scoreboard-libwebp -- \ + surpass --real --max-edge 1024 +``` + +`surpass` is the local, auditable comparison used for the libwebp-surpass gate. +It requires both the linked and command-line libwebp references to report exactly `1.6.0`; missing or mismatched references fail the command. +`--max-edge` reads source dimensions without decoding pixels, then asks cwebp to cap the longer image dimension while preserving aspect ratio. + +- **Lossless basis:** deepest webpkit effort versus single-threaded `cwebp -lossless -exact -m 6 -q 100` (`-mt` omitted), including the WebP container on both sides. +- **Lossy basis:** for each libwebp quality 50, 75, and 90, build webpkit's full quality 0–100 RD curve. + Select the smallest payload whose RGB-PSNR and SSIM are both at least the libwebp reconstruction. + Quality-number remapping alone cannot count as an improvement. +- **Coverage:** size and matched-fidelity RD are evaluated on every recognized image entry using a fixed four-worker corpus wave (each codec invocation remains scalar). + Before that expensive wave starts, all pinned cwebp lossy references are generated and validated so a reference-process failure is reported without discarding completed 101-point curves. + An unreadable recognized image fails measurement completion; unrelated files and subdirectories are excluded. + After that wave, timing and process-peak memory are measured serially for every recognized image without rebuilding its 101-point RD curve. + The schema-4 report records the worker count, all-image resource policy, and sample count. +- **Timing basis:** portable safe scalar webpkit (Rayon disabled), alternating paired A/B runs on each image: one deterministic warm-up per implementation, + then three AB/BA pairs (six timed samples per implementation). + The upper median is taken per image, then across each category and the full corpus. + Lossy encode timing supplies the same opaque RGBA8 buffer to both codecs (alpha forced to 255 and excluded from the comparison). +- **Memory basis:** every webpkit/libwebp encode and decode runs in a fresh invocation of the identical internal xtask worker executable and observer wrapper. + The parent records OS process peak working set on Windows or maximum RSS on Linux, so the process baseline is the same while allocations from one codec cannot leak into the other codec's sample. + Lossy encoders receive the same opaque RGBA8 input layout, and both lossy decoders receive the same WebP container rather than comparing a raw `VP8 ` payload with a container. +- **Environment:** OS, architecture, CPU model, logical CPU count, Rust toolchain, source commit and dirty state, corpus-worker count, timing-pair count, warm-up count, timed-sample count, and resolved scalar feature policy are recorded with the aggregates. +- **Privacy:** the report contains only `flat-icon`, `transparent`, `graphic`, + `photo`, and overall aggregates. + It records no source path, file name, or per-image result. + The only output is the gitignored `target/surpass/latest.json`. + +All size, time, and peak ratios are represented in integer permille and rounded up, so a value just above a threshold cannot be rounded into a pass. +The report evaluates the measurement thresholds directly: lossless median `<=100.5%` with at least half no larger than libwebp; matched-fidelity lossy median `<=105%` in every band and photo `<=103%`; scalar encode `<=2.0x`, +decode `<=1.5x`; and identical-worker peak memory `<=1.5x`. +`measurement_complete` is true only when every measurement threshold passes, +every recognized image completed size/RD evaluation, and every present category and every matched lossy band completed resources for every recognized image. +It is not the WK-400 completion bit: conformance, golden, oracle, fuzz, mutation, +and ledger gates remain separate requirements. + ### Work-cost ledger — `just work` ``` @@ -130,23 +188,25 @@ counters into both codecs and the xtask `work` command): event tallies that do not depend on the toolchain or optimization level, so any compiler reproduces them; `--release` is only for the runtime budget. This is the key simplification over the peak-memory ledger. -- **Both codecs, encode AND lossy decode** (schema `version: 2`) — every - `(sample, method)` is measured for both the `lossless` and `lossy` codecs - encode, plus **one `lossy-decode` row per sample** that isolates the VP8 decode - hot paths (`bool_read`, `bool_renorm`, `coeff_token`, `idct_call`, - `loop_filter_edge`, `upsample_row`, plus the shared `predict_*`). Counters name +- **Both codecs, encode AND decode** (schema `version: 3`) — every + `(sample, method)` is measured for both the `lossless` and `lossy` encoders, + plus one `lossless-decode` and one `lossy-decode` row per sample. The VP8L row + isolates bit-reader, Huffman/LZ, inverse-transform, and output-pack work; the + VP8 row isolates `bool_read`, `bool_renorm`, `coeff_token`, `idct_call`, + `loop_filter_edge`, `upsample_row`, plus shared `predict_*` work. Counters name the *operation*, not the phase; which pass populates them is fixed by the - reset→run→snapshot discipline, so the decode-only counters stay zero in the - encode rows and the encode-only counters stay zero in the decode rows (the two - shared-kernel counters `idct_call`/`loop_filter_edge` fire in both, since the - encoder reconstructs and deblocks its own output). The decode row is produced - from a fixed mid-quality `Balanced` encode (decode work is method-independent). + reset→run→snapshot discipline, so decode-only counters stay zero in encoder + rows and encode-only counters stay zero in decoder rows (the shared VP8 kernel + counters `idct_call`/`loop_filter_edge` fire in both, since that encoder + reconstructs and deblocks its own output). The VP8L decode row reuses the + sample's already-measured `AUTO` output; the VP8 row is produced from a fixed + mid-quality `Balanced` encode. - **`Best` capped to `edge <= 256`** — the counter increments slow the `Best` search several-fold, and its `Best@512` cases dominate the run; 64/256 fully capture `Best`'s algorithmic signature (the counts scale predictably with - size). `Fast`/`Balanced` run at every size. Plus the 18 `lossy-decode` rows. + size). `Fast`/`Balanced` run at every size. Plus 18 rows for each decoder. - **Serial by construction** — the counters are process-global statics, so the - `work` loop measures one encode at a time (reset → encode → snapshot). Totals + `work` loop measures one operation at a time (reset → run → snapshot). Totals are `fetch_add`-order-independent, so an in-encode `rayon` region does not perturb them, but the ledger is generated `rayon`-off regardless. @@ -304,6 +364,36 @@ or recorded** — keep a private corpus out of git and report only the category rollup. If a non-libwebp `cwebp` (e.g. this repo's own CLI) shadows libwebp on `PATH`, point `WEBPKIT_CWEBP` / `WEBPKIT_DWEBP` at the libwebp `1.6.0` binaries. +### Real-image multi-axis scoreboard — `scoreboard --real ` + +``` +cargo run --profile quick --features scoreboard-libwebp -p xtask -- scoreboard --real [--max-edge N] +``` + +One print-only scoreboard that puts **four axes** side by side over the real corpus — size, fidelity, encode/decode speed, and webpkit peak memory — rolled up by pixel-derived content category (`flat-icon` / `transparent` / `graphic` / `photo`, plus an `overall` row), with a **lossless** section and a **lossy** (`q75`) section. +Both sides run their **shipped defaults**: webpkit `AUTO`, libwebp's simple-API defaults (`WebPEncodeLosslessRGBA`, `WebPEncodeRGB`). +This deliberately **builds the scoreboard, not a verdict** — no win-condition is preset; the numbers describe what a user gets out of the box on identical resized pixels. + +**The columns are not equally rigorous, and the tool says so.** Read them by tier: + +- **Size — rigorous.** Exact encoded byte counts (`ours B`, `lib B`) and the integer-permille `size%` = `ours * 100 / lib` (`< 100` = ours smaller). + libwebp is the **in-process libwebp-sys reference**, which vendors the **same libwebp `1.6.0`** as the pinned `cwebp` binary (the tool prints the runtime version it linked), so the bytes are a true apples-to-apples default-vs-default comparison. +- **Fidelity (lossy) — rigorous.** `+dB` / `+SSIM` = ours − libwebp reconstruction PSNR / MSSIM (`> 0` = ours better). + **Both** encoders' `VP8 ` payloads are decoded by *webpkit's own* decoder, so the YUV→RGB path is identical on both sides (`cwebp -noalpha`-equivalent RGB basis; lossless is exact, so it has no fidelity columns). +- **webpkit peak memory — rigorous (exact).** `encKiB` / `decKiB` are the peak *additional* requested bytes during a webpkit encode / decode, measured by the `webpkit-alloc-count` counting allocator (the same instrument as the memory ledger), reported as the bucket's worst case. +- **Speed — the timing plane (noisy, approximate, never gated).** `encO/encL MB/s` (raw-input MB/s) and `decO/decL MP/s` (Mpixels/s) are the **median of repeated in-process passes**. libwebp is timed **in-process via libwebp-sys on identical buffers** (no process/startup/IO overhead), the most rigorous method available — + but it is still wall-clock: hardware- and load-dependent, non-deterministic, and reported for reading, never as a gate. + Encode times the same effort as the size column; decode times the **same `VP8`/WebP bytes** on both decoders (our payload, + wrapped in a container for libwebp). +- **libwebp peak memory — omitted, by construction.** libwebp allocates through C `malloc`, which bypasses the Rust counting allocator, so it **cannot** be measured on the same basis. + Rather than fabricate a number or mix in a coarse OS peak-RSS from a different process model, the scoreboard shows **no** libwebp memory column and states why. + +libwebp columns need the **`scoreboard-libwebp`** feature, which links the in-process reference; a plain build never compiles it, and without it the libwebp columns print `n/a` (webpkit size/speed/memory still show). +Like the other `--real` paths it is **print-only** (temp files in a dropped `tempfile::tempdir()`, no repo file written, **never gated**), needs `cwebp` only to **read** source images into RGBA, and **soft-skips** when it is absent. +**Privacy:** `` is a pure runtime argument and every image is bucketed by **pixels** (alpha presence, then a capped distinct-color count), never by filename, so **no file name is ever printed or recorded** — report only the category rollup. +Content note: the pixel classifier cannot separate a museum scan from an ordinary photograph (both are opaque, +high-distinct-color), so both land in the `photo` bucket. + ## Reading results **Compression (the ledger).** The measured impact of an encoder change is the @@ -330,11 +420,12 @@ its own output — the working-set high-water marks (measured by the process-wid counting allocator; the input buffer is excluded from the decode window). **Work (the ledger).** Each `corpus/work.json` row's `counts` map is the -algorithmic work of one `(codec, sample, method)` encode. Read it per counter: -a large or fast-growing count is a candidate hot path, and the measured impact -of an optimization is the **git diff of `corpus/work.json`** (a counter that -drops is work removed). Because the counts are a deterministic proxy for time, a -drop should track a wall-clock improvement in `just bench` — cross-check the two. +algorithmic work of one `(codec, sample, method)` operation. Encoder rows name +their method; decoder rows use `method = "decode"`. Read it per counter: a large +or fast-growing count is a candidate hot path, and the measured impact of an +optimization is the **git diff of `corpus/work.json`** (a counter that drops is +work removed). Because the counts are a deterministic proxy for time, a drop +should track a wall-clock improvement in `just bench` — cross-check the two. **Timing (criterion).** After `just bench`, open the HTML report at `target/criterion/**/report/index.html`. Read the throughput figures diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 29132be..9aec57c 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -14,31 +14,32 @@ name = "xtask" path = "src/main.rs" [features] -# Enables the deterministic algorithmic-work counters in both codecs and links -# the shared counter crate, so the `work` command can produce `corpus/work.json`. -# Off by default so the `metrics` job builds with zero counter code (keeping its -# toolchain-sensitive peak-memory numbers undisturbed). +# Enables the deterministic algorithmic-work counters in both codecs and links the shared counter crate, so the `work` command can produce `corpus/work.json`. +# Off by default so the `metrics` job builds with zero counter code (keeping its toolchain-sensitive peak-memory numbers undisturbed). work-count = ["webpkit/work-count"] -# Forward the encoder's parallel `Method::Best` evaluator so `bench-real` can time -# the serial-vs-rayon delta the CLI binaries ship with. Output is byte-identical -# either way (proven by `cargo test -p webpkit --features rayon`); this -# only affects wall-clock, never the gated size/work ledgers. +# Forward the encoder's parallel `Method::Best` evaluator so `bench-real` can time the serial-vs-rayon delta the CLI binaries ship with. +# Output is byte-identical either way (proven by `cargo test -p webpkit --features rayon`); this only affects wall-clock, never the gated size/work ledgers. rayon = ["webpkit/rayon"] +# Link the in-process libwebp-sys reference so `scoreboard` can time libwebp encode/decode on identical pixel buffers. +# Off by default so a plain workspace build never compiles the vendored C library; without it the scoreboard prints only the webpkit columns (libwebp size/fidelity/speed show `n/a`). +scoreboard-libwebp = ["webpkit-libwebp-timing/enabled"] [dependencies] anyhow = { workspace = true } cargo_metadata = { workspace = true } clap = { workspace = true } +image = { workspace = true } +png = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } toml = { workspace = true } -# Reaches the codec module-first (`webpkit::lossless::…`, `::lossy::…`) for -# fixture/ledger generation, so it needs the `__internals` tooling feature; the -# `work-count` feature (work-cost ledger) additionally implies it. +# Reaches the codec module-first (`webpkit::lossless::…`, `::lossy::…`) for fixture/ledger generation, so it needs the `__internals` tooling feature; the `work-count` feature (work-cost ledger) additionally implies it. webpkit = { path = "../crates/webpkit", features = ["__internals"] } webpkit-alloc-count = { path = "../crates/webpkit-alloc-count" } webpkit-ci-core = { path = "ci-core" } +# Always present but empty unless the `scoreboard-libwebp` feature turns on its `enabled` feature; isolates the libwebp-sys `unsafe` FFI so xtask stays forbid-unsafe. +webpkit-libwebp-timing = { path = "../crates/webpkit-libwebp-timing" } webpkit-lossless-conformance = { path = "../crates/webpkit-lossless-conformance" } webpkit-samples = { path = "../crates/webpkit-samples" } diff --git a/xtask/src/bench.rs b/xtask/src/bench.rs index 66c5046..da7def7 100644 --- a/xtask/src/bench.rs +++ b/xtask/src/bench.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result}; -use crate::libwebp::{check_version, cwebp_bin, run_cwebp}; +use crate::libwebp::{check_version, cwebp_bin, run_cwebp, run_cwebp_resized}; /// One real-image directory entry decoded to ARGB, ready for measurement. /// @@ -94,6 +94,123 @@ pub(crate) fn prepare_real_images( Ok(prepared) } +/// Prepare real images with the longest dimension capped to `max_edge`. +/// +/// Header-only readers establish native dimensions for supported source formats. +/// Oversized landscape images are then width-capped and oversized portrait images are height-capped, with the other dimension left at zero so cwebp preserves aspect ratio. +/// An unknown header format falls back to one native cwebp pass and, +/// only when oversized, one resized pass. +/// No source name is needed by the `surpass` consumer. +pub(crate) fn prepare_real_images_max_edge( + cwebp: &str, + real: &Path, + max_edge: u32, +) -> Result> { + let directory = + std::fs::read_dir(real).with_context(|| format!("reading {}", real.display()))?; + let mut files = Vec::new(); + for entry in directory { + let path = entry.context("reading real-image directory entry")?.path(); + if path.is_file() && is_real_image_path(&path) { + files.push(path); + } + } + files.sort(); + + let tmp = tempfile::tempdir().context("creating tempdir for real-image preparation")?; + let out_webp = tmp.path().join("out.webp"); + let mut prepared = Vec::with_capacity(files.len()); + for path in &files { + let name = path + .file_name() + .and_then(|value| value.to_str()) + .map_or_else(|| "".to_owned(), str::to_owned); + let known_dimensions = source_dimensions(path); + let resize = known_dimensions + .and_then(|(width, height)| longest_edge_resize(width, height, max_edge)); + if run_cwebp_resized(cwebp, path, &out_webp, resize).is_err() { + prepared.push(RealPrep::Skipped { name }); + continue; + } + let mut webp_bytes = std::fs::read(&out_webp).context("reading anonymous prepared WebP")?; + let (mut dims, mut rgba) = webpkit::lossless::decode_rgba(&webp_bytes)?; + // Unknown formats retain the old two-pass fallback: cwebp establishes dimensions once, then only oversized inputs are encoded a second time. + if known_dimensions.is_none() + && let Some(target) = longest_edge_resize(dims.width(), dims.height(), max_edge) + { + if run_cwebp_resized(cwebp, path, &out_webp, Some(target)).is_err() { + prepared.push(RealPrep::Skipped { name }); + continue; + } + webp_bytes = std::fs::read(&out_webp).context("reading anonymous resized WebP")?; + (dims, rgba) = webpkit::lossless::decode_rgba(&webp_bytes)?; + } + prepared.push(RealPrep::Ready(RealImage { + name, + dims, + rgba, + cwebp_size: u64::try_from(webp_bytes.len()).unwrap_or(u64::MAX), + })); + } + Ok(prepared) +} + +fn is_real_image_path(path: &Path) -> bool { + path.extension() + .and_then(|extension| extension.to_str()) + .is_some_and(|extension| { + matches!( + extension.to_ascii_lowercase().as_str(), + "jpg" + | "jpeg" + | "png" + | "webp" + | "tif" + | "tiff" + | "bmp" + | "pam" + | "pgm" + | "ppm" + | "pnm" + ) + }) +} + +/// Read only source dimensions, avoiding a full-resolution lossless cwebp pass. +fn source_dimensions(path: &Path) -> Option<(u32, u32)> { + let extension = path.extension()?.to_str()?.to_ascii_lowercase(); + match extension.as_str() { + "webp" => { + let bytes = std::fs::read(path).ok()?; + let info = webpkit::probe(&bytes).ok()?; + Some((info.dimensions.width(), info.dimensions.height())) + }, + "png" => { + let file = std::fs::File::open(path).ok()?; + let reader = png::Decoder::new(std::io::BufReader::new(file)) + .read_info() + .ok()?; + Some((reader.info().width, reader.info().height)) + }, + _ => image::ImageReader::open(path) + .ok()? + .with_guessed_format() + .ok()? + .into_dimensions() + .ok(), + } +} + +fn longest_edge_resize(width: u32, height: u32, max_edge: u32) -> Option<(u32, u32)> { + if max_edge == 0 || width.max(height) <= max_edge { + None + } else if width >= height { + Some((max_edge, 0)) + } else { + Some((0, max_edge)) + } +} + /// Run `op` `iters` times, returning the fastest wall-clock duration (min = least /// scheduler/cache noise). The op's result is discarded but its error is /// propagated, so a codec failure aborts the measurement instead of timing a @@ -241,3 +358,26 @@ pub(crate) fn bench_real(real: &Path, max_edge: u32, iters: u32, limit: usize) - } Ok(()) } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::{is_real_image_path, longest_edge_resize}; + + #[test] + fn longest_edge_resize_selects_the_oversized_axis() { + assert_eq!(longest_edge_resize(2049, 1025, 1024), Some((1024, 0))); + assert_eq!(longest_edge_resize(1025, 2049, 1024), Some((0, 1024))); + assert_eq!(longest_edge_resize(17, 31, 1024), None); + assert_eq!(longest_edge_resize(2049, 1025, 0), None); + } + + #[test] + fn real_image_filter_ignores_unrelated_directory_entries() { + assert!(is_real_image_path(Path::new("input.JPEG"))); + assert!(is_real_image_path(Path::new("input.webp"))); + assert!(!is_real_image_path(Path::new("notes.txt"))); + assert!(!is_real_image_path(Path::new("README"))); + } +} diff --git a/xtask/src/common.rs b/xtask/src/common.rs index 9f3333f..1af9c73 100644 --- a/xtask/src/common.rs +++ b/xtask/src/common.rs @@ -25,6 +25,11 @@ pub(crate) fn encode_fixtures_dir(root: &Path) -> PathBuf { root.join("crates/webpkit-lossless-conformance/fixtures/encode") } +/// Directory holding the lossy (VP8) decode conformance fixtures. +pub(crate) fn lossy_decode_fixtures_dir(root: &Path) -> PathBuf { + root.join("crates/webpkit-lossy-conformance/fixtures/decode") +} + /// Absolute path to the committed corpus directory (repo root `corpus/`). pub(crate) fn corpus_dir(root: &Path) -> PathBuf { root.join("corpus") diff --git a/xtask/src/fixtures.rs b/xtask/src/fixtures.rs index 2fce641..0c4a60d 100644 --- a/xtask/src/fixtures.rs +++ b/xtask/src/fixtures.rs @@ -7,11 +7,15 @@ use std::process::Command; use anyhow::{Context, Result, bail}; -use crate::common::{decode_fixtures_dir, encode_fixtures_dir, webpkit_encode, workspace_root}; +use crate::common::{ + decode_fixtures_dir, encode_fixtures_dir, lossy_decode_fixtures_dir, webpkit_encode, + workspace_root, +}; use crate::libwebp::{ - REQUIRED_LIBWEBP, check_version, cwebp_bin, dwebp_bin, img2webp_bin, run_cwebp, run_dwebp, - run_img2webp, webpmux_bin, webpmux_get_frame, webpmux_set, + REQUIRED_LIBWEBP, check_version, cwebp_bin, dwebp_bin, img2webp_bin, run_cwebp, + run_cwebp_lossy_args, run_dwebp, run_img2webp, webpmux_bin, webpmux_get_frame, webpmux_set, }; +use crate::metrics::extract_vp8_chunk; /// A single deterministic fixture: a synthetic source image plus its metadata. struct FixtureCase { @@ -715,6 +719,278 @@ fn generate_encode_case(case: &FixtureCase, encode_dir: &Path, dwebp: &str) -> R Ok(()) } +// ---- lossy (VP8) decode conformance fixtures ------------------------------ + +/// A single lossy fixture: synthetic content plus the `cwebp` VP8 knobs that steer libwebp down a specific key-frame decode path. +struct LossyCase { + /// Directory name / case identifier under the lossy `fixtures/decode/`. + name: &'static str, + /// The decode feature label recorded in the case's `meta.toml`. + feature: &'static str, + /// Image width in pixels (`<= 64`). + width: u32, + /// Image height in pixels (`<= 64`). + height: u32, + /// cwebp `-q` quality (`0..=100`). + quality: u8, + /// Extra cwebp VP8 knobs (`-m` / `-segments` / `-f` / `-strong` / …). + extra: &'static [&'static str], + /// Maps `(x, y, width, height)` to an `[R, G, B, A]` source pixel. + pixel: fn(u32, u32, u32, u32) -> [u8; 4], +} + +/// A 0..=255 ramp of `n` across `span + 1` steps (span-guarded for 1-px sides). +const fn ramp(n: u32, span: u32) -> u8 { + lo((n * 255) / if span == 0 { 1 } else { span }) +} + +/// Deterministic integer pseudo-noise word at `(col, row)` (mirrors the oracle's). +const fn noise_hash(col: u32, row: u32) -> u32 { + let mut s = col + .wrapping_mul(2_654_435_761) + .wrapping_add(row.wrapping_mul(40_503)) + .wrapping_add(0x9e37_79b9); + s ^= s >> 13; + s = s.wrapping_mul(0x85eb_ca6b); + s ^ (s >> 16) +} + +const fn g_solid(_col: u32, _row: u32, _width: u32, _height: u32) -> [u8; 4] { + [96, 128, 160, 255] +} +const fn g_hgrad(col: u32, _row: u32, width: u32, _height: u32) -> [u8; 4] { + let shade = ramp(col, width - 1); + [shade, shade, shade, 255] +} +const fn g_vgrad(_col: u32, row: u32, _width: u32, height: u32) -> [u8; 4] { + let shade = ramp(row, height - 1); + [shade, shade, shade, 255] +} +const fn g_dgrad(col: u32, row: u32, width: u32, height: u32) -> [u8; 4] { + let shade = ramp(col + row, (width - 1) + (height - 1)); + [shade, shade, shade, 255] +} +const fn g_checker4(col: u32, row: u32, _width: u32, _height: u32) -> [u8; 4] { + let shade = if (col / 4 + row / 4).is_multiple_of(2) { + 30 + } else { + 220 + }; + [shade, shade, shade, 255] +} +const fn g_vstripe2(col: u32, _row: u32, _width: u32, _height: u32) -> [u8; 4] { + let shade = if (col / 2).is_multiple_of(2) { 220 } else { 20 }; + [shade, shade, shade, 255] +} +const fn g_noise(col: u32, row: u32, _width: u32, _height: u32) -> [u8; 4] { + let hash = noise_hash(col, row); + [lo(hash), lo(hash >> 8), lo(hash >> 16), 255] +} +const fn g_colorfield(col: u32, row: u32, width: u32, height: u32) -> [u8; 4] { + [ + ramp(col, width - 1), + ramp(row, height - 1), + ramp(col + row, (width - 1) + (height - 1)), + 255, + ] +} +const fn g_color_hgrad(col: u32, _row: u32, width: u32, _height: u32) -> [u8; 4] { + let shade = ramp(col, width - 1); + [shade, 255 - shade, 128, 255] +} +const fn g_color_vgrad(_col: u32, row: u32, _width: u32, height: u32) -> [u8; 4] { + let shade = ramp(row, height - 1); + [128, shade, 255 - shade, 255] +} +const fn g_mixed(col: u32, row: u32, width: u32, height: u32) -> [u8; 4] { + if row < height / 2 { + let shade = ramp(col, width - 1); + [shade, shade / 2, 255 - shade, 255] + } else { + let hash = noise_hash(col, row); + [lo(hash), lo(hash >> 8), lo(hash >> 16), 255] + } +} + +/// The lossy fixture matrix: quality sweep, each intra-16 luma mode, chroma modes, intra-4×4, segmentation on/off, all three loop-filter types, fast vs trellis method, odd/near-edge shapes, tiny sides, and full 64×64 — all fed to cwebp so libwebp emits real VP8 key frames exercising each decode path. +#[rustfmt::skip] +fn lossy_cases() -> Vec { + macro_rules! case { + ($name:literal, $feature:literal, $w:expr, $h:expr, $q:expr, $extra:expr, $px:expr) => { + LossyCase { + name: $name, + feature: $feature, + width: $w, + height: $h, + quality: $q, + extra: $extra, + pixel: $px, + } + }; + } + vec![ + // Quantizer sweep on a color field: distinct quant indices / mode choices. + case!("q10_color_32x32", "quantizer", 32, 32, 10, &[], g_colorfield), + case!("q30_color_32x32", "quantizer", 32, 32, 30, &[], g_colorfield), + case!("q50_color_32x32", "quantizer", 32, 32, 50, &[], g_colorfield), + case!("q75_color_32x32", "quantizer", 32, 32, 75, &[], g_colorfield), + case!("q90_color_32x32", "quantizer", 32, 32, 90, &[], g_colorfield), + case!("q100_color_32x32", "quantizer", 32, 32, 100, &[], g_colorfield), + // Each intra-16×16 luma predictor, nudged by content at high quality. + case!("intra16dc_solid_32x32", "intra16-dc", 32, 32, 92, &[], g_solid), + case!("intra16v_hgrad_32x32", "intra16-v", 32, 32, 92, &[], g_hgrad), + case!("intra16h_vgrad_32x32", "intra16-h", 32, 32, 92, &[], g_vgrad), + case!("intra16tm_dgrad_32x32", "intra16-tm", 32, 32, 92, &[], g_dgrad), + // Chroma prediction driven by horizontal / vertical color ramps. + case!("chromav_hgrad_32x32", "chroma-mode-v", 32, 32, 92, &[], g_color_hgrad), + case!("chromah_vgrad_32x32", "chroma-mode-h", 32, 32, 92, &[], g_color_vgrad), + // Intra-4×4 (B_PRED): high detail, deep method, low quality. + case!("i4x4_noise_32x32", "intra-4x4", 32, 32, 30, &["-m", "6"], g_noise), + case!("i4x4_stripe_48x48", "intra-4x4", 48, 48, 40, &["-m", "6"], g_vstripe2), + // Segmentation: multi-segment vs a single forced segment. + case!("seg_multi_64x48", "segmentation-multi", 64, 48, 50, &["-segments", "4", "-sns", "90"], g_mixed), + case!("seg_single_64x48", "segmentation-single", 64, 48, 50, &["-segments", "1"], g_mixed), + // The three in-loop filter paths plus a max-sharpness variant. + case!("filter_none_48x48", "no-filter", 48, 48, 55, &["-f", "0"], g_checker4), + case!("filter_simple_48x48", "loop-filter-simple", 48, 48, 55, &["-nostrong", "-f", "40"], g_checker4), + case!("filter_strong_48x48", "loop-filter-normal", 48, 48, 55, &["-strong", "-f", "40"], g_checker4), + case!("filter_sharp_48x48", "loop-filter-sharp", 48, 48, 55, &["-strong", "-f", "60", "-sharpness", "7"], g_checker4), + // Fast (no trellis) vs deepest (trellis RD) method on identical content. + case!("method0_fast_32x32", "fast-method", 32, 32, 75, &["-m", "0"], g_colorfield), + case!("method6_trellis_32x32", "trellis", 32, 32, 75, &["-m", "6"], g_colorfield), + // Odd / near-edge shapes: partial chroma edges and 1-px sides. + case!("edge_23x31_color", "chroma-edge", 23, 31, 75, &[], g_colorfield), + case!("edge_17x9_color", "chroma-edge", 17, 9, 75, &[], g_colorfield), + case!("row_63x1", "row-edge", 63, 1, 75, &[], g_hgrad), + case!("col_1x63", "col-edge", 1, 63, 75, &[], g_vgrad), + case!("narrow_3x64", "narrow", 3, 64, 75, &[], g_dgrad), + case!("narrow_64x3", "narrow", 64, 3, 75, &[], g_dgrad), + // Tiny frames: single macroblock and sub-block sizes. + case!("tiny_1x1", "tiny", 1, 1, 50, &[], g_solid), + case!("tiny_2x2", "tiny", 2, 2, 50, &[], g_colorfield), + case!("tiny_5x5", "tiny", 5, 5, 50, &[], g_noise), + // Full 64×64 macroblock-aligned frames, smooth and high-detail. + case!("full_gradient_64x64", "mb-aligned", 64, 64, 75, &[], g_dgrad), + case!("full_noise_64x64", "mb-aligned-detail", 64, 64, 30, &[], g_noise), + ] +} + +/// Render a lossy case to raw row-major RGBA (opaque; cwebp `-noalpha` drops it). +fn synthesize_lossy(case: &LossyCase) -> Vec { + let mut buf = Vec::with_capacity((case.width * case.height * 4) as usize); + for y in 0..case.height { + for x in 0..case.width { + buf.extend_from_slice(&(case.pixel)(x, y, case.width, case.height)); + } + } + buf +} + +/// Generate one lossy fixture: cwebp authors a VP8 key frame, its `VP8 ` payload becomes `input.vp8`, and dwebp's `-pam` decode becomes the `expected.rgba` golden. +/// Fail-closed: `webpkit::lossy::decode` must reproduce the golden byte-for-byte before anything is committed. +fn generate_lossy_case( + case: &LossyCase, + decode_dir: &Path, + cwebp: &str, + dwebp: &str, +) -> Result<()> { + let case_dir = decode_dir.join(case.name); + std::fs::create_dir_all(&case_dir) + .with_context(|| format!("creating {}", case_dir.display()))?; + + let rgba = synthesize_lossy(case); + let tmp = tempfile::tempdir().context("creating tempdir for lossy source image")?; + let src_png = tmp.path().join("src.png"); + write_png(&src_png, case.width, case.height, &rgba)?; + + let input_webp = tmp.path().join("input.webp"); + run_cwebp_lossy_args(cwebp, &src_png, &input_webp, case.quality, case.extra)?; + let webp = + std::fs::read(&input_webp).with_context(|| format!("reading {}", input_webp.display()))?; + let payload = extract_vp8_chunk(&webp)?; + + let golden_pam = tmp.path().join("golden.pam"); + run_dwebp(dwebp, &input_webp, &golden_pam)?; + let expected = strip_pam_header(&std::fs::read(&golden_pam)?)?; + + // GATE (fail-closed): our decoder reproduces the libwebp golden byte-for-byte. + let image = webpkit::lossy::decode(payload) + .with_context(|| format!("webpkit::lossy decode of {}", case.name))?; + if (image.width(), image.height()) != (case.width, case.height) { + bail!( + "lossy fixture `{}`: decoded {}x{}, expected {}x{}", + case.name, + image.width(), + image.height(), + case.width, + case.height + ); + } + if image.as_bytes() != expected.as_slice() { + bail!( + "lossy fixture `{}`: webpkit::lossy decode != dwebp golden ({} vs {} bytes)", + case.name, + image.as_bytes().len(), + expected.len() + ); + } + + std::fs::write(case_dir.join("input.vp8"), payload) + .with_context(|| format!("writing input.vp8 for {}", case.name))?; + std::fs::write(case_dir.join("expected.rgba"), &expected) + .with_context(|| format!("writing expected.rgba for {}", case.name))?; + + let flags = if case.extra.is_empty() { + String::new() + } else { + format!(" {}", case.extra.join(" ")) + }; + let note = format!( + "{}x{} synthetic source; input.vp8 via cwebp {REQUIRED_LIBWEBP} \ + `-q {}{flags} -noalpha`, expected.rgba via dwebp {REQUIRED_LIBWEBP} `-pam` \ + (header stripped)", + case.width, case.height, case.quality + ); + let meta = format!( + "feature = \"{}\"\n\ + level = \"must\"\n\ + width = {}\n\ + height = {}\n\ + note = \"{note}\"\n", + case.feature, case.width, case.height + ); + std::fs::write(case_dir.join("meta.toml"), meta) + .with_context(|| format!("writing meta.toml for {}", case.name))?; + + println!( + "gen-lossy-fixtures: {} ({}x{} q{}) OK", + case.name, case.width, case.height, case.quality + ); + Ok(()) +} + +pub(crate) fn gen_lossy_fixtures() -> Result<()> { + let root = workspace_root()?; + let decode_dir = lossy_decode_fixtures_dir(&root); + let cwebp = cwebp_bin(); + let dwebp = dwebp_bin(); + + // Version guard: refuse to generate goldens with a non-pinned libwebp. + check_version(&cwebp, "cwebp", "WEBPKIT_CWEBP")?; + check_version(&dwebp, "dwebp", "WEBPKIT_DWEBP")?; + + let cases = lossy_cases(); + for case in &cases { + generate_lossy_case(case, &decode_dir, &cwebp, &dwebp)?; + } + println!( + "gen-lossy-fixtures: {} case(s) written to {}", + cases.len(), + decode_dir.display() + ); + Ok(()) +} + pub(crate) fn gen_fixtures() -> Result<()> { let root = workspace_root()?; let decode_dir = decode_fixtures_dir(&root); diff --git a/xtask/src/libwebp.rs b/xtask/src/libwebp.rs index 8a3b3e2..2761a66 100644 --- a/xtask/src/libwebp.rs +++ b/xtask/src/libwebp.rs @@ -78,6 +78,17 @@ pub(crate) fn run_cwebp( src: &Path, out_webp: &Path, resize: Option, +) -> Result<()> { + run_cwebp_resized(cwebp, src, out_webp, resize.map(|width| (width, 0))) +} + +/// Encode losslessly like [`run_cwebp`], with an optional explicit `(width, height)` resize. +/// A zero dimension asks cwebp to preserve aspect ratio from the other dimension. +pub(crate) fn run_cwebp_resized( + cwebp: &str, + src: &Path, + out_webp: &Path, + resize: Option<(u32, u32)>, ) -> Result<()> { let mut cmd = Command::new(cwebp); cmd.arg("-lossless") @@ -86,11 +97,11 @@ pub(crate) fn run_cwebp( .arg("6") .arg("-q") .arg("100") - .arg("-mt") - .arg("0") .arg("-quiet"); - if let Some(width) = resize { - cmd.arg("-resize").arg(width.to_string()).arg("0"); + if let Some((width, height)) = resize { + cmd.arg("-resize") + .arg(width.to_string()) + .arg(height.to_string()); } cmd.arg(src).arg("-o").arg(out_webp); let out = output_or_notfound(&mut cmd, ¬_found_msg("cwebp", "WEBPKIT_CWEBP"))?; @@ -104,6 +115,35 @@ pub(crate) fn run_cwebp( Ok(()) } +/// Encode `src` to `out_webp` with `cwebp -q Q -noalpha` plus `extra` VP8 knobs (e.g. `-m`, `-segments`, `-f`, `-strong`/`-nostrong`, `-sharpness`). +/// Omitting cwebp's flag-only `-mt` keeps the reference single-threaded. +pub(crate) fn run_cwebp_lossy_args( + cwebp: &str, + src: &Path, + out_webp: &Path, + quality: u8, + extra: &[&str], +) -> Result<()> { + let mut cmd = Command::new(cwebp); + cmd.arg("-q") + .arg(quality.to_string()) + .arg("-noalpha") + .arg("-quiet"); + for arg in extra { + cmd.arg(arg); + } + cmd.arg(src).arg("-o").arg(out_webp); + let out = output_or_notfound(&mut cmd, ¬_found_msg("cwebp", "WEBPKIT_CWEBP"))?; + if !out.status.success() { + bail!( + "cwebp (lossy) failed ({}): {}", + out.status, + String::from_utf8_lossy(&out.stderr).trim() + ); + } + Ok(()) +} + /// Decode `in_webp` to a PAM golden at `out_pam` via dwebp. pub(crate) fn run_dwebp(dwebp: &str, in_webp: &Path, out_pam: &Path) -> Result<()> { let mut cmd = Command::new(dwebp); @@ -202,15 +242,13 @@ pub(crate) fn webpmux_get_frame( } /// Encode `src` to `out_webp` with libwebp `cwebp -q Q` (lossy). `-noalpha` keeps -/// the output a bare VP8 container (our encoder likewise drops alpha), and -/// `-mt 0`/`-quiet` keep it deterministic and silent. +/// the output a bare VP8 container (our encoder likewise drops alpha), omitting +/// the flag-only `-mt` keeps it single-threaded, and `-quiet` keeps it silent. pub(crate) fn run_cwebp_lossy(cwebp: &str, src: &Path, out_webp: &Path, quality: u8) -> Result<()> { let mut cmd = Command::new(cwebp); cmd.arg("-q") .arg(quality.to_string()) .arg("-noalpha") - .arg("-mt") - .arg("0") .arg("-quiet") .arg(src) .arg("-o") @@ -225,3 +263,71 @@ pub(crate) fn run_cwebp_lossy(cwebp: &str, src: &Path, out_webp: &Path, quality: } Ok(()) } + +/// Verify the pinned cwebp `-hint` token set and its byte-neutral behavior. +/// +/// libwebp 1.6.0 accepts exactly `picture`, `photo`, and `graph`; the lossy encoder does not consult the hint, while the lossless encoder uses `graph` only for its initial writer allocation. +/// Both modes must therefore emit the same bytes as their no-hint baseline on this deterministic fixture. +pub(crate) fn verify_cwebp_image_hint(cwebp: &str) -> Result<()> { + let temp = tempfile::tempdir()?; + let source = temp.path().join("hint-oracle.png"); + let output = temp.path().join("hint-oracle.webp"); + let mut rgb = Vec::with_capacity(4 * 4 * 3); + for y in 0..4u8 { + for x in 0..4u8 { + rgb.extend_from_slice(&[ + x.saturating_mul(61), + y.saturating_mul(59), + x.wrapping_add(y).saturating_mul(29), + ]); + } + } + let file = std::fs::File::create(&source)?; + let mut encoder = png::Encoder::new(std::io::BufWriter::new(file), 4, 4); + encoder.set_color(png::ColorType::Rgb); + encoder.set_depth(png::BitDepth::Eight); + encoder.write_header()?.write_image_data(&rgb)?; + + for lossless in [false, true] { + let encode = |hint: Option<&str>| -> Result> { + let mut cmd = Command::new(cwebp); + if lossless { + cmd.args(["-lossless", "-exact", "-m", "6", "-q", "100"]); + } else { + cmd.args(["-q", "75", "-noalpha"]); + } + if let Some(hint) = hint { + cmd.args(["-hint", hint]); + } + cmd.arg("-quiet").arg(&source).arg("-o").arg(&output); + let result = output_or_notfound(&mut cmd, ¬_found_msg("cwebp", "WEBPKIT_CWEBP"))?; + if !result.status.success() { + bail!( + "cwebp -hint oracle failed ({}): {}", + result.status, + String::from_utf8_lossy(&result.stderr).trim() + ); + } + Ok(std::fs::read(&output)?) + }; + let baseline = encode(None)?; + for hint in ["picture", "photo", "graph"] { + let hinted = encode(Some(hint))?; + if hinted != baseline { + bail!("cwebp -hint {hint} unexpectedly changed encoded bytes"); + } + } + } + + let mut invalid = Command::new(cwebp); + invalid + .args(["-q", "75", "-noalpha", "-hint", "default", "-quiet"]) + .arg(&source) + .arg("-o") + .arg(&output); + let invalid = output_or_notfound(&mut invalid, ¬_found_msg("cwebp", "WEBPKIT_CWEBP"))?; + if invalid.status.success() { + bail!("cwebp 1.6.0 unexpectedly accepted literal `-hint default`"); + } + Ok(()) +} diff --git a/xtask/src/lint_comments.rs b/xtask/src/lint_comments.rs new file mode 100644 index 0000000..b5a1c1f --- /dev/null +++ b/xtask/src/lint_comments.rs @@ -0,0 +1,126 @@ +//! Comment-hygiene gate (`lint-comments`): fail on narrative / history / measurement-boast phrasing in library and CLI source comments. +//! The record for *why* a value was chosen belongs in Linear/ADR; a comment states the constraint/reason in one line. +//! Deliberately small and conservative — it flags a handful of unambiguous phrases, never legitimate constraint comments. + +use std::path::{Path, PathBuf}; + +use anyhow::{Result, bail}; + +use crate::common::workspace_root; + +/// Source trees scanned by the gate (product library + CLI), relative to the root. +const SCANNED_DIRS: [&str; 2] = ["crates/webpkit/src", "crates/webpkit-cli/src"]; + +/// A forbidden comment rule, matched case-insensitively against one comment line. +enum Rule { + /// The lowercased comment contains this exact phrase. + Phrase(&'static str), + /// The lowercased comment has `first` as a whole word strictly before a later whole-word `second` (ordered co-occurrence, so "win is measured" is exempt). + Ordered(&'static str, &'static str), +} + +/// The forbidden phrase set, each paired with the class it belongs to. +/// Kept small and unambiguous so the gate never fires on a legitimate constraint comment. +const FORBIDDEN: &[(Rule, &str)] = &[ + // Measurement-boast: a benchmarked "win" claim is Linear/ADR's job, not a comment's. + (Rule::Ordered("measured", "win"), "measurement-boast"), + // Comparison-boast against another encoder. + (Rule::Phrase("pareto win"), "comparison-boast"), + // Code-history narrative: what the code used to do is recoverable from git. + (Rule::Phrase("we used to"), "code-history"), + (Rule::Phrase("in the past"), "code-history"), + // Audience narrative in place of a constraint/reason. + (Rule::Phrase("for our consumers"), "audience-narrative"), +]; + +/// Scan the library and CLI source comments; fail (nonzero) on any forbidden hit. +pub(crate) fn lint_comments() -> Result<()> { + let root = workspace_root()?; + let mut files = Vec::new(); + for dir in SCANNED_DIRS { + collect_rs(&root.join(dir), &mut files)?; + } + files.sort(); + + let mut hits = 0usize; + for file in &files { + let text = std::fs::read_to_string(file)?; + for (lineno, line) in text.lines().enumerate() { + let Some(comment) = comment_text(line) else { + continue; + }; + let lower = comment.to_ascii_lowercase(); + for (rule, class) in FORBIDDEN { + if rule.matches(&lower) { + hits += 1; + let rel = file.strip_prefix(&root).unwrap_or(file); + println!( + "{}:{} [{class}] {}", + rel.display(), + lineno + 1, + comment.trim() + ); + break; + } + } + } + } + + if hits > 0 { + bail!( + "lint-comments: {hits} forbidden comment(s) — state the constraint in one line, \ + record the rationale in Linear/ADR" + ); + } + println!("lint-comments: {} files clean", files.len()); + Ok(()) +} + +/// Recursively gather `*.rs` paths under `dir` into `out`. +fn collect_rs(dir: &Path, out: &mut Vec) -> Result<()> { + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + if path.is_dir() { + collect_rs(&path, out)?; + } else if path.extension().is_some_and(|e| e == "rs") { + out.push(path); + } + } + Ok(()) +} + +/// The line-comment text of `line` (everything after the first `//`), or `None`. +/// A pragmatic scan: it does not track block comments or `//` inside string literals, which the forbidden phrases never appear in. +fn comment_text(line: &str) -> Option<&str> { + line.find("//").map(|i| &line[i + 2..]) +} + +impl Rule { + /// Whether this rule matches the already-lowercased comment text. + fn matches(&self, lower: &str) -> bool { + match *self { + Self::Phrase(p) => lower.contains(p), + Self::Ordered(a, b) => { + word_index(lower, a, 0).is_some_and(|end| word_index(lower, b, end).is_some()) + }, + } + } +} + +/// Start index of the first whole-word `word` in `hay` at or after `from`, where a whole word is not flanked by ASCII alphanumerics (so "win" ignores "window"). +/// Returns the index just past the match, for chaining ordered lookups. +fn word_index(hay: &str, word: &str, from: usize) -> Option { + let bytes = hay.as_bytes(); + let mut i = from; + while let Some(rel) = hay[i..].find(word) { + let start = i + rel; + let end = start + word.len(); + let before_ok = start == 0 || !bytes[start - 1].is_ascii_alphanumeric(); + let after_ok = end == bytes.len() || !bytes[end].is_ascii_alphanumeric(); + if before_ok && after_ok { + return Some(end); + } + i = start + 1; + } + None +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index f90ae29..1e49924 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -42,7 +42,10 @@ mod corpus; mod fixtures; mod ledger; mod libwebp; +mod lint_comments; mod metrics; +mod scoreboard; +mod surpass; mod work; use metrics::MetricsAction; @@ -74,6 +77,8 @@ enum Task { }, /// Invoke libwebp `cwebp`/`dwebp` to (re)generate golden fixtures. GenFixtures, + /// Invoke libwebp `cwebp`/`dwebp` to (re)generate the lossy VP8 decode conformance fixtures (`webpkit-lossy-conformance/fixtures/decode`). + GenLossyFixtures, /// Sweep the committed image corpus for decoder/encoder regressions. /// /// Default (gate) mode enforces the hard invariants and fails if the @@ -126,11 +131,21 @@ enum Task { #[arg(long)] explain: bool, }, + /// Compare our encoder against libwebp over the synthetic corpus and gate `corpus/vs-libwebp.json`. + /// + /// The committed, integer-only, drift-gated counterpart of the print-only `metrics --vs-libwebp` aid: our `l9` lossless and `AUTO` lossy sizes plus centi-dB reconstruction PSNR (both outputs decoded by our own decoder) versus `cwebp` on identical inputs. + /// Default (gate) mode fails if the committed ledger drifts; `--bless` rewrites it. + /// NEEDS libwebp (`cwebp`); it fails closed when that is absent or the wrong version. + VsLibwebp { + /// Rewrite `corpus/vs-libwebp.json` instead of gating against it. + #[arg(long)] + bless: bool, + }, /// Measure deterministic algorithmic-work counters and gate `corpus/work.json`. /// /// The third measurement plane (see `docs/benchmarking.md`): integer - /// event-tally counters instrumented in each codec's encode hot paths, a - /// toolchain- and profile-independent proxy for time. Default (gate) mode + /// event-tally counters instrumented in each codec's encode and decode hot + /// paths, a toolchain- and profile-independent proxy for time. Default (gate) mode /// fails if the committed ledger drifts from a fresh run; `--bless` rewrites /// it. Requires the `work-count` feature (`just work` builds it). Work { @@ -170,6 +185,53 @@ enum Task { #[arg(long, default_value_t = 0)] limit: usize, }, + /// Fail on narrative / history / measurement-boast phrasing in library and CLI source comments (`crates/webpkit/src`, `crates/webpkit-cli/src`). + /// + /// A small, conservative set of forbidden phrases: a comment states the constraint/reason in one line; the rationale record belongs in Linear/ADR. + /// Prints `file:line [class] text` for each hit and exits non-zero. + LintComments, + /// Print (never gate) a real-image multi-axis scoreboard of webpkit vs libwebp: + /// size, fidelity, encode/decode speed, and webpkit peak memory, rolled up by pixel-derived content category. + /// + /// Print-only: writes no repo file, bakes in no image path (the directory is a pure runtime argument), and soft-skips when `cwebp` (the source-image reader) is absent. + /// libwebp comparison columns need the `scoreboard-libwebp` feature (the in-process libwebp-sys reference); without it only webpkit columns show. + /// Size and fidelity are exact; speed is the noisy timing plane (median, never gated); libwebp peak memory is omitted (its C malloc bypasses our allocator). + Scoreboard { + /// Directory of real images to score (a pure runtime path; nothing is written into it and no path is recorded in the repo). + #[arg(long)] + real: PathBuf, + /// Cap each image's width via cwebp `-resize 0` (aspect preserved); `0` disables resizing (native resolution). + #[arg(long, default_value_t = 1024)] + max_edge: u32, + }, + /// Run the pinned libwebp surpass campaign comparison and save only anonymous category aggregates to `target/surpass/latest.json`. + Surpass { + /// Directory of private real images. + /// The path and file names are never printed or recorded. + #[arg(long)] + real: PathBuf, + /// Cap each image's longest edge while preserving aspect ratio. + #[arg(long, default_value_t = 1024)] + max_edge: u32, + }, + /// Internal subprocess used by `surpass` for like-for-like OS peak memory. + #[command(hide = true)] + SurpassWorker { + #[arg(long)] + kind: String, + #[arg(long)] + input: PathBuf, + #[arg(long, default_value_t = 0)] + width: u32, + #[arg(long, default_value_t = 0)] + height: u32, + #[arg(long, default_value_t = 0)] + quality: u8, + }, + /// Internal Windows worker entry whose request is passed as environment JSON. + #[cfg(target_os = "windows")] + #[command(hide = true)] + SurpassWorkerEnv, } fn main() -> Result<()> { @@ -177,6 +239,7 @@ fn main() -> Result<()> { match cli.command { Task::Ci { command } => ci_adapter::run(command), Task::GenFixtures => fixtures::gen_fixtures(), + Task::GenLossyFixtures => fixtures::gen_lossy_fixtures(), Task::CorpusSweep { bless } => corpus::corpus_sweep(bless), Task::Metrics { bless, @@ -203,6 +266,7 @@ fn main() -> Result<()> { metrics::metrics(action, vs_libwebp, real, max_edge) } }, + Task::VsLibwebp { bless } => metrics::vs_libwebp(bless), Task::Work { bless, explain } => work::work(bless, explain), Task::BenchReal { dir, @@ -210,5 +274,17 @@ fn main() -> Result<()> { iters, limit, } => bench::bench_real(&dir, max_edge, iters, limit), + Task::LintComments => lint_comments::lint_comments(), + Task::Scoreboard { real, max_edge } => scoreboard::scoreboard(&real, max_edge), + Task::Surpass { real, max_edge } => surpass::surpass(&real, max_edge), + Task::SurpassWorker { + kind, + input, + width, + height, + quality, + } => surpass::worker(&kind, &input, width, height, quality), + #[cfg(target_os = "windows")] + Task::SurpassWorkerEnv => surpass::worker_from_environment(), } } diff --git a/xtask/src/metrics.rs b/xtask/src/metrics.rs index 45178cb..ae37df8 100644 --- a/xtask/src/metrics.rs +++ b/xtask/src/metrics.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; +use webpkit::container::reader::{ImageChunk, locate_image}; use crate::bench::{RealImage, RealPrep, prepare_real_images}; use crate::common::{ @@ -14,7 +15,9 @@ use crate::common::{ }; use crate::fixtures::write_png; use crate::ledger::{FieldDelta, load_ledger, print_field_deltas}; -use crate::libwebp::{check_version, cwebp_bin, run_cwebp, run_cwebp_lossy}; +use crate::libwebp::{ + check_version, cwebp_bin, run_cwebp, run_cwebp_lossy, verify_cwebp_image_hint, +}; /// One committed compression-metric row: the encoded size and integer /// compression ratio for one synthetic sample image at one encoder method. @@ -710,7 +713,7 @@ fn explain_lossy_metrics(committed: &LossyMetricsLedger, fresh: &LossyMetricsLed /// crate forbids floating point (bit-determinism), but xtask is a CLI boundary, /// not the codec, so the `f64` PSNR math is allowed here (as in that test crate). /// `99.0` denotes an identical pair (the squared-error sum is integer-valued). -fn psnr_rgb(a: &[u8], b: &[u8]) -> f64 { +pub(crate) fn psnr_rgb(a: &[u8], b: &[u8]) -> f64 { let mut se = 0.0f64; let mut n = 0.0f64; for (pa, pb) in a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0) { @@ -750,25 +753,11 @@ fn sse_rgb(a: &[u8], b: &[u8]) -> u64 { /// Extract the payload of the first `VP8 ` chunk from a RIFF/WEBP container, so a /// (container-wrapped) `cwebp` lossy file can be decoded by `webpkit::lossy::decode`, /// which consumes a bare VP8 key-frame payload. Chunks are padded to even size. -fn extract_vp8_chunk(webp: &[u8]) -> Result> { - if webp.len() < 12 || &webp[0..4] != b"RIFF" || &webp[8..12] != b"WEBP" { - bail!("cwebp output is not a RIFF/WEBP container"); - } - let mut off = 12; - while off + 8 <= webp.len() { - let size = u32::from_le_bytes([webp[off + 4], webp[off + 5], webp[off + 6], webp[off + 7]]) - as usize; - let start = off + 8; - let end = start.checked_add(size).context("VP8 chunk size overflow")?; - if end > webp.len() { - bail!("truncated WEBP chunk"); - } - if &webp[off..off + 4] == b"VP8 " { - return Ok(webp[start..end].to_vec()); - } - off = end + (size & 1); // pad to the next even offset +pub(crate) fn extract_vp8_chunk(webp: &[u8]) -> Result<&[u8]> { + match locate_image(webp)? { + ImageChunk::Lossy(payload) => Ok(payload), + ImageChunk::Lossless(_) => bail!("cwebp lossy output contains VP8L, not VP8"), } - bail!("no `VP8 ` chunk in cwebp output (unexpected extended container)") } /// Gate / bless / explain the committed lossy size+quality ledger @@ -904,7 +893,7 @@ fn compare_lossy_vs_libwebp() -> Result<()> { .with_context(|| format!("reading {}", out_webp.display()))?; let cwebp_vp8 = extract_vp8_chunk(&cwebp_file)?; let cwebp_bytes = cwebp_vp8.len() as u64; - let cwebp_psnr = psnr_rgb(&sample.rgba, webpkit::lossy::decode(&cwebp_vp8)?.as_bytes()); + let cwebp_psnr = psnr_rgb(&sample.rgba, webpkit::lossy::decode(cwebp_vp8)?.as_bytes()); println!( " {id:<14} {q:>3} {ours_bytes:>10} {ours_psnr:>8.2} {cwebp_bytes:>11} {cwebp_psnr:>9.2}" @@ -980,7 +969,7 @@ fn classify_real(rgba: &[u8]) -> RealCategory { /// BT.601; alpha is ignored (the lossy codec drops it); pixels beyond the last full /// 8×8 block (a partial right/bottom strip) are not windowed. An image smaller than /// one window scores `1.0` (no measurable structural loss). -fn ssim_rgb(a: &[u8], b: &[u8], width: usize, height: usize) -> f64 { +pub(crate) fn ssim_rgb(a: &[u8], b: &[u8], width: usize, height: usize) -> f64 { // Stabilizers from the standard SSIM definition at 8-bit dynamic range L = 255: // C1 = (0.01 L)^2, C2 = (0.03 L)^2. const C1: f64 = 6.5025; @@ -1165,7 +1154,7 @@ fn compare_lossy_vs_libwebp_real(real: &Path, max_edge: u32) -> Result<()> { let cwebp_file = std::fs::read(&out_webp) .with_context(|| format!("reading {}", out_webp.display()))?; let cwebp_vp8 = extract_vp8_chunk(&cwebp_file)?; - let cwebp_dec = webpkit::lossy::decode(&cwebp_vp8)?; + let cwebp_dec = webpkit::lossy::decode(cwebp_vp8)?; let a = &mut acc[qi][cat.idx()]; a.n += 1; @@ -1209,3 +1198,289 @@ fn compare_lossy_vs_libwebp_real(real: &Path, max_edge: u32) -> Result<()> { } Ok(()) } + +/// Deep-effort lossless is capped to `edge <= 256` for the per-image `cwebp` shell-out runtime, mirroring the `--vs-libwebp` print aid's cap. +const VS_LOSSLESS_MAX_EDGE: u32 = 256; + +/// PSNR (RGB dB) as a fixed-point centi-dB integer (`round(dB * 100)`), so the committed comparison ledger carries no float and renders identical bytes. +#[expect( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + reason = "psnr_rgb is in [0.0, 99.0]; *100 rounds into [0, 9900] — non-negative, fits u64" +)] +fn psnr_centidb(a: &[u8], b: &[u8]) -> u64 { + (psnr_rgb(a, b) * 100.0).round() as u64 +} + +/// One committed cwebp/dwebp comparison row: our encoder vs libwebp at one `(sample, mode[, quality])`, all-integer so `corpus/vs-libwebp.json` is a pure textual diff of a fresh run (like `metrics.json`). +/// Field order fixes JSON keys. +/// +/// Both outputs are decoded by *our own* decoder for an apples-to-apples fidelity column: lossless rows compare full WebP file sizes, lossy rows compare bare `VP8 ` payloads (`cwebp -noalpha`, our alpha-dropping encoder). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct VsCase { + /// Stable sort key: `"{content}_{edge:03}/lossless"` or `".../lossy/q{quality:03}"`. + id: String, + /// Content archetype name (e.g. `"photo"`, `"gradient"`). + content: String, + /// Square edge length in pixels. + edge: u32, + /// `"lossless"` or `"lossy"`. + mode: String, + /// Encoder quality in `0..=100`; `0` on the lossless rows (no quality knob). + quality: u8, + /// Our encoded byte length (full WebP for lossless, `VP8 ` payload for lossy). + our_bytes: u64, + /// libwebp `cwebp` byte length on the same basis. + cwebp_bytes: u64, + /// `our_bytes * 1000 / cwebp_bytes`, integer floor (below 1000 = we are smaller). + size_permille: u64, + /// Our reconstruction PSNR (RGB) vs the source, centi-dB; `9900` = identical. + our_psnr_centidb: u64, + /// cwebp's reconstruction PSNR (RGB) vs the source, centi-dB (both decoded by our decoder). + cwebp_psnr_centidb: u64, +} + +/// The committed cwebp/dwebp comparison ledger, sorted by `id`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +struct VsLibwebpLedger { + /// Schema version (bump on any structural change). + version: u32, + /// The libwebp release every row was produced against (pinned, see `check_version`). + libwebp_version: String, + /// Per-(sample, mode[, quality]) rows, sorted by `id`. + cases: Vec, +} + +/// Measure one lossless `(sample)` row: our deepest-effort (`l9`) full-WebP size and reconstruction PSNR vs single-threaded `cwebp -lossless -exact -m 6 -q 100` (`-mt` omitted), both decoded by our own lossless decoder. +fn measure_vs_lossless( + sample: &webpkit_samples::Sample, + cwebp: &str, + src_png: &Path, + out_webp: &Path, + config: &webpkit::lossless::EncoderConfig, +) -> Result { + let dims = webpkit::lossless::Dimensions::new(sample.edge, sample.edge)?; + let image = webpkit::lossless::ImageRef::new( + dims, + webpkit::lossless::PixelLayout::Rgba8, + &sample.rgba, + )?; + let ours = webpkit::lossless::encode(image, config)?; + let (_d, our_rgba) = webpkit::lossless::decode_rgba(&ours)?; + + write_png(src_png, sample.edge, sample.edge, &sample.rgba)?; + run_cwebp(cwebp, src_png, out_webp, None)?; + let cwebp_file = + std::fs::read(out_webp).with_context(|| format!("reading {}", out_webp.display()))?; + let (_cd, cwebp_rgba) = webpkit::lossless::decode_rgba(&cwebp_file)?; + + let our_bytes = ours.len() as u64; + let cwebp_bytes = cwebp_file.len() as u64; + Ok(VsCase { + id: format!( + "{}/lossless", + webpkit_samples::sample_id(sample.content, sample.edge) + ), + content: sample.content.name().to_owned(), + edge: sample.edge, + mode: "lossless".to_owned(), + quality: 0, + our_bytes, + cwebp_bytes, + size_permille: our_bytes + .saturating_mul(1000) + .checked_div(cwebp_bytes) + .unwrap_or(0), + our_psnr_centidb: psnr_centidb(&sample.rgba, &our_rgba), + cwebp_psnr_centidb: psnr_centidb(&sample.rgba, &cwebp_rgba), + }) +} + +/// Measure one lossy `(sample, quality)` row: our zero-knob `AUTO` `VP8 ` payload size and reconstruction PSNR vs `cwebp -q Q -noalpha`, both decoded by our own lossy decoder (container overhead excluded on both sides). +fn measure_vs_lossy( + sample: &webpkit_samples::Sample, + quality: u8, + cwebp: &str, + src_png: &Path, + out_webp: &Path, +) -> Result { + let dims = webpkit::lossy::Dimensions::new(sample.edge, sample.edge)?; + let image = + webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, &sample.rgba)?; + let cfg = webpkit::lossy::LossyConfig::new() + .with_quality(quality) + .with_effort(webpkit::lossy::Effort::AUTO); + let (_d, ours) = webpkit::lossy::encode_vp8(image, &cfg)?; + let our_rgb = webpkit::lossy::decode(&ours)?; + + write_png(src_png, sample.edge, sample.edge, &sample.rgba)?; + run_cwebp_lossy(cwebp, src_png, out_webp, quality)?; + let cwebp_file = + std::fs::read(out_webp).with_context(|| format!("reading {}", out_webp.display()))?; + let cwebp_vp8 = extract_vp8_chunk(&cwebp_file)?; + let cwebp_rgb = webpkit::lossy::decode(cwebp_vp8)?; + + let our_bytes = ours.len() as u64; + let cwebp_bytes = cwebp_vp8.len() as u64; + Ok(VsCase { + id: format!( + "{}/lossy/q{quality:03}", + webpkit_samples::sample_id(sample.content, sample.edge) + ), + content: sample.content.name().to_owned(), + edge: sample.edge, + mode: "lossy".to_owned(), + quality, + our_bytes, + cwebp_bytes, + size_permille: our_bytes + .saturating_mul(1000) + .checked_div(cwebp_bytes) + .unwrap_or(0), + our_psnr_centidb: psnr_centidb(&sample.rgba, our_rgb.as_bytes()), + cwebp_psnr_centidb: psnr_centidb(&sample.rgba, cwebp_rgb.as_bytes()), + }) +} + +/// Compute a fresh cwebp/dwebp comparison ledger over the synthetic corpus: our `l9` lossless (capped to [`VS_LOSSLESS_MAX_EDGE`]) and our `AUTO` lossy at each of [`LOSSY_QUALITIES`], versus libwebp on the same inputs. +/// Integer-only, so the serialized ledger is byte-reproducible; cases are sorted by `id`. +fn compute_vs_libwebp(cwebp: &str) -> Result { + let tmp = tempfile::tempdir().context("creating tempdir for vs-libwebp ledger")?; + let src_png = tmp.path().join("src.png"); + let out_webp = tmp.path().join("out.webp"); + let lossless_cfg = + webpkit::lossless::EncoderConfig::new().with_effort(webpkit::lossless::Effort::level(9)); + + let mut cases = Vec::new(); + for sample in webpkit_samples::matrix() { + if sample.edge <= VS_LOSSLESS_MAX_EDGE { + cases.push(measure_vs_lossless( + &sample, + cwebp, + &src_png, + &out_webp, + &lossless_cfg, + )?); + } + for &quality in &LOSSY_QUALITIES { + cases.push(measure_vs_lossy( + &sample, quality, cwebp, &src_png, &out_webp, + )?); + } + } + cases.sort_by(|a, b| a.id.cmp(&b.id)); + Ok(VsLibwebpLedger { + version: 1, + libwebp_version: crate::libwebp::REQUIRED_LIBWEBP.to_owned(), + cases, + }) +} + +/// Describe the first case where a fresh comparison run diverges from the committed ledger (mirrors [`first_metrics_divergence`]). +fn first_vs_divergence(committed: &VsLibwebpLedger, fresh: &VsLibwebpLedger) -> String { + if committed.version != fresh.version { + return format!( + "schema version {} (committed) != {} (fresh)", + committed.version, fresh.version + ); + } + if committed.libwebp_version != fresh.libwebp_version { + return format!( + "libwebp version `{}` (committed) != `{}` (fresh)", + committed.libwebp_version, fresh.libwebp_version + ); + } + for (i, fc) in fresh.cases.iter().enumerate() { + match committed.cases.get(i) { + Some(cc) if cc == fc => {}, + Some(cc) if cc.id != fc.id => { + return format!( + "case #{i}: committed id `{}` != fresh id `{}`", + cc.id, fc.id + ); + }, + Some(_) => return format!("case `{}` values changed", fc.id), + None => return format!("case `{}` is new (not in committed ledger)", fc.id), + } + } + if let Some(extra) = committed.cases.get(fresh.cases.len()) { + return format!("committed ledger has extra case `{}`", extra.id); + } + "textual formatting only (no structural difference)".to_owned() +} + +/// Gate / bless the committed cwebp/dwebp comparison ledger `corpus/vs-libwebp.json` over the synthetic corpus. +/// +/// Unlike the print-only `--vs-libwebp` aid, this is a committed, byte-golden drift-gated artifact (like `corpus/metrics.json`): every field is an integer (sizes and centi-dB PSNR) and cwebp is pinned to [`crate::libwebp::REQUIRED_LIBWEBP`]. +/// It NEEDS the pinned libwebp reference. +/// A missing `cwebp` or version mismatch is a hard error: silently skipping the comparison would make a green gate unauditable. +pub(crate) fn vs_libwebp(bless: bool) -> Result<()> { + let cwebp = cwebp_bin(); + check_version(&cwebp, "cwebp", "WEBPKIT_CWEBP")?; + verify_cwebp_image_hint(&cwebp)?; + + let root = workspace_root()?; + let dir = corpus_dir(&root); + let path = dir.join("vs-libwebp.json"); + + let fresh = compute_vs_libwebp(&cwebp)?; + let fresh_json = format!("{}\n", serde_json::to_string_pretty(&fresh)?); + + if bless { + std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?; + std::fs::write(&path, &fresh_json) + .with_context(|| format!("writing {}", path.display()))?; + println!( + "vs-libwebp: blessed {} case(s) -> {}", + fresh.cases.len(), + path.display() + ); + return Ok(()); + } + + let committed = match std::fs::read_to_string(&path) { + Ok(text) => text, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => bail!( + "no vs-libwebp ledger at {} — run `just vs-libwebp-bless` and commit it first", + path.display() + ), + Err(e) => { + return Err(anyhow::Error::new(e).context(format!("reading {}", path.display()))); + }, + }; + if committed != fresh_json { + let detail = serde_json::from_str::(&committed).map_or_else( + |_| "committed ledger is not valid JSON".to_owned(), + |committed_ledger| first_vs_divergence(&committed_ledger, &fresh), + ); + bail!( + "vs-libwebp: ledger at {} is stale (first divergence: {}). \ + Run `just vs-libwebp-bless` and commit the updated file.", + path.display(), + detail + ); + } + println!("vs-libwebp: {} case(s) stable", fresh.cases.len()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::extract_vp8_chunk; + use webpkit::container::fourcc::FourCc; + use webpkit::container::writer::{push_chunk, riff_envelope}; + + #[test] + fn vp8_extraction_uses_the_shared_container_policy() { + let payload = [1, 2, 3, 4, 5]; + let mut body = Vec::new(); + push_chunk(&mut body, FourCc::VP8, &payload); + let lossy = riff_envelope(&body); + assert_eq!(extract_vp8_chunk(&lossy).unwrap(), payload); + + body.clear(); + push_chunk(&mut body, FourCc::VP8L, &[0x2f, 0, 0, 0, 0]); + let lossless = riff_envelope(&body); + assert!(extract_vp8_chunk(&lossless).is_err()); + } +} diff --git a/xtask/src/scoreboard.rs b/xtask/src/scoreboard.rs new file mode 100644 index 0000000..05ea898 --- /dev/null +++ b/xtask/src/scoreboard.rs @@ -0,0 +1,547 @@ +//! Real-image multi-axis scoreboard (`scoreboard --real `): a print-only, +//! never-gated per-category comparison of webpkit against libwebp on size, +//! fidelity, encode/decode speed, and webpkit peak memory. +//! +//! Rigor is uneven by column and stated as such (see `docs/benchmarking.md`): +//! size and fidelity are exact and deterministic; webpkit peak memory is exact (the counting allocator); speed is the noisy timing plane (median of repeats, +//! non-deterministic); libwebp peak memory is omitted (its C `malloc` is invisible to the Rust allocator). +//! libwebp numbers need the in-process reference behind the `scoreboard-libwebp` feature; without it only the webpkit columns are shown. + +use std::path::Path; +use std::time::Instant; + +use anyhow::{Context, Result}; + +use crate::bench::{RealImage, RealPrep, prepare_real_images}; +use crate::libwebp::{check_version, cwebp_bin}; +use crate::metrics::{extract_vp8_chunk, psnr_rgb, ssim_rgb}; + +/// libwebp reference, present only behind the `scoreboard-libwebp` feature. +/// The two module bodies give every call site one uniform signature; the stubs are reached only when [`lw::AVAILABLE`] is `false`, which gates every libwebp column. +#[cfg(feature = "scoreboard-libwebp")] +mod lw { + /// Whether the in-process libwebp reference is linked (the feature is on). + pub(super) const AVAILABLE: bool = true; + pub(super) use webpkit_libwebp_timing::{ + decode_rgba_len, decoder_version, encode_lossless_len, encode_lossy_bytes, encode_lossy_len, + }; +} +#[cfg(not(feature = "scoreboard-libwebp"))] +mod lw { + /// Whether the in-process libwebp reference is linked (the feature is off). + pub(super) const AVAILABLE: bool = false; + pub(super) const fn decoder_version() -> String { + String::new() + } + pub(super) const fn encode_lossless_len(_: &[u8], _: u32, _: u32) -> usize { + 0 + } + pub(super) const fn encode_lossy_bytes(_: &[u8], _: u32, _: u32, _: f32) -> Vec { + Vec::new() + } + pub(super) const fn encode_lossy_len(_: &[u8], _: u32, _: u32, _: f32) -> usize { + 0 + } + pub(super) const fn decode_rgba_len(_: &[u8]) -> usize { + 0 + } +} + +/// Passes each op is timed; the median is reported (odd count → true middle). +/// Kept small because the webpkit `AUTO` lossless encode is deep (Best-tier at ≤1MP); +/// timing is the noisy plane regardless (never gated). +const TIMING_ITERS: u32 = 3; + +/// The single quality the lossy section runs both encoders at (webpkit `AUTO`, +/// libwebp simple-API default shaping) — a representative default, not a sweep. +const LOSSY_QUALITY: u8 = 75; + +/// A real image's pixel-derived content category (never its filename), so the rollup names no private file. +/// The four buckets the corpus spans, mapped to the pixels that define them (see [`classify`]). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Cat { + /// Alpha present, small distinct-color count — flat UI icons. + FlatIcon, + /// Alpha present, large distinct-color count — transparent graphics/exports. + Transparent, + /// Opaque, small distinct-color count — flat charts / line art. + Graphic, + /// Opaque, large distinct-color count — photographs and museum scans. + Photo, +} + +impl Cat { + /// The categories in printed/aggregation order. + const ALL: [Self; 4] = [ + Self::FlatIcon, + Self::Transparent, + Self::Graphic, + Self::Photo, + ]; + + /// The row label used in the printed rollup. + const fn label(self) -> &'static str { + match self { + Self::FlatIcon => "flat-icon", + Self::Transparent => "transparent", + Self::Graphic => "graphic", + Self::Photo => "photo", + } + } + + /// Dense index into the per-category accumulator array. + const fn idx(self) -> usize { + match self { + Self::FlatIcon => 0, + Self::Transparent => 1, + Self::Graphic => 2, + Self::Photo => 3, + } + } +} + +/// Alpha-present flat content stays under this many distinct RGB triples. +const FLAT_ICON_MAX_COLORS: usize = 4096; + +/// Opaque photographic content exceeds this many distinct RGB triples; below it is flat opaque graphics (shared with the lossy AUTO sweep's threshold). +const GRAPHIC_MAX_COLORS: usize = 8192; + +/// Classify a decoded RGBA buffer from its pixels alone: transparency first, then a distinct-color count capped to the relevant threshold. +/// No filename is inspected. +fn classify(rgba: &[u8]) -> Cat { + let transparent = rgba.as_chunks::<4>().0.iter().any(|p| p[3] < 255); + let cap = if transparent { + FLAT_ICON_MAX_COLORS + } else { + GRAPHIC_MAX_COLORS + }; + let mut colors = std::collections::BTreeSet::new(); + let mut over = false; + for p in rgba.as_chunks::<4>().0 { + colors.insert([p[0], p[1], p[2]]); + if colors.len() > cap { + over = true; + break; + } + } + match (transparent, over) { + (true, false) => Cat::FlatIcon, + (true, true) => Cat::Transparent, + (false, false) => Cat::Graphic, + (false, true) => Cat::Photo, + } +} + +/// Running per-category totals for one mode (lossless or lossy). +/// Sizes are exact byte sums; speed is summed work / summed seconds (a size-weighted throughput); +/// memory is the category's worst-case webpkit peak; fidelity is averaged by `n`. +#[derive(Default, Clone, Copy)] +struct Acc { + /// Images folded into this bucket. + n: u32, + /// Summed webpkit encoded bytes (full WebP for lossless, `VP8 ` payload for lossy). + ours_bytes: u64, + /// Summed libwebp encoded bytes on the same basis (`0` if libwebp is absent). + lib_bytes: u64, + /// Summed (ours − libwebp) reconstruction PSNR dB, lossy only. + d_psnr: f64, + /// Summed (ours − libwebp) reconstruction MSSIM, lossy only. + d_ssim: f64, + /// Summed webpkit encode raw-input bytes and seconds (throughput = work/secs). + enc_ours_work: f64, + enc_ours_secs: f64, + /// Summed libwebp encode raw-input bytes and seconds. + enc_lib_work: f64, + enc_lib_secs: f64, + /// Summed decoded pixels and webpkit decode seconds. + dec_ours_px: f64, + dec_ours_secs: f64, + /// Summed decoded pixels and libwebp decode seconds. + dec_lib_px: f64, + dec_lib_secs: f64, + /// Worst-case webpkit encode / decode peak additional bytes across the bucket. + mem_enc: usize, + mem_dec: usize, +} + +impl Acc { + /// Fold this bucket into `other` (the `overall` rollup): sums add, memory maxes. + fn add_into(self, other: &mut Self) { + other.n += self.n; + other.ours_bytes += self.ours_bytes; + other.lib_bytes += self.lib_bytes; + other.d_psnr += self.d_psnr; + other.d_ssim += self.d_ssim; + other.enc_ours_work += self.enc_ours_work; + other.enc_ours_secs += self.enc_ours_secs; + other.enc_lib_work += self.enc_lib_work; + other.enc_lib_secs += self.enc_lib_secs; + other.dec_ours_px += self.dec_ours_px; + other.dec_ours_secs += self.dec_ours_secs; + other.dec_lib_px += self.dec_lib_px; + other.dec_lib_secs += self.dec_lib_secs; + other.mem_enc = other.mem_enc.max(self.mem_enc); + other.mem_dec = other.mem_dec.max(self.mem_dec); + } +} + +/// `num * 100 / den` as a one-decimal percent string (integer permille math, no float cast), or `-` when `den` is zero (libwebp absent). +fn pct(num: u64, den: u64) -> String { + if den == 0 { + return "-".to_owned(); + } + let tenths = num.saturating_mul(1000) / den; + format!("{}.{}", tenths / 10, tenths % 10) +} + +/// Throughput in millions of units per second, adaptive precision (mirrors the `bench-real` formatter). +/// `inf` guards a sub-tick zero duration; `-` a bucket with no timing (libwebp absent). +fn format_throughput(work: f64, secs: f64) -> String { + if work <= 0.0 { + return "-".to_owned(); + } + if secs <= 0.0 { + return "inf".to_owned(); + } + let v = work / secs / 1_000_000.0; + if v >= 100.0 { + format!("{v:.0}") + } else if v >= 1.0 { + format!("{v:.1}") + } else { + format!("{v:.3}") + } +} + +/// Median wall-clock seconds of `f` over [`TIMING_ITERS`] passes (`black_box`ing the result so the op is not optimized away). +/// The timing plane: noisy, never gated. +fn median_secs(mut f: impl FnMut() -> usize) -> f64 { + let mut times = Vec::new(); + for _ in 0..TIMING_ITERS { + let start = Instant::now(); + let n = f(); + let elapsed = start.elapsed(); + std::hint::black_box(n); + times.push(elapsed); + } + times.sort_unstable(); + times[times.len() / 2].as_secs_f64() +} + +/// Expand row-major RGBA to RGB (drop alpha), the input basis both lossy encoders see (webpkit drops alpha; libwebp is fed RGB). +fn rgba_to_rgb(rgba: &[u8]) -> Vec { + let mut rgb = Vec::with_capacity(rgba.len() / 4 * 3); + for p in rgba.as_chunks::<4>().0 { + rgb.extend_from_slice(&p[0..3]); + } + rgb +} + +/// Wrap a bare `VP8 ` payload in a minimal RIFF/WEBP container so libwebp's container decoder can time the exact same compressed bytes our payload decoder consumes (apples-to-apples decode speed on one stream). +fn wrap_vp8(payload: &[u8]) -> Vec { + let chunk_len = u32::try_from(payload.len()).unwrap_or(0); + let pad = payload.len() & 1; + let riff_size = u32::try_from(4 + 8 + payload.len() + pad).unwrap_or(0); + let mut out = Vec::with_capacity(8 + 4 + 8 + payload.len() + pad); + out.extend_from_slice(b"RIFF"); + out.extend_from_slice(&riff_size.to_le_bytes()); + out.extend_from_slice(b"WEBP"); + out.extend_from_slice(b"VP8 "); + out.extend_from_slice(&chunk_len.to_le_bytes()); + out.extend_from_slice(payload); + if pad == 1 { + out.push(0); + } + out +} + +/// Fold one image's lossless measurements into `acc`: webpkit `AUTO` vs libwebp's simple-API default, both on identical resized pixels. +/// Fidelity is exact (both reproduce the source), so only size, speed, and webpkit memory are recorded. +fn measure_lossless(img: &RealImage, acc: &mut Acc) -> Result<()> { + let (w, h) = (img.dims.width(), img.dims.height()); + let rgba = img.rgba.as_slice(); + let cfg = webpkit::lossless::EncoderConfig::new().with_effort(webpkit::lossless::Effort::AUTO); + + // One measured encode yields size, exact peak memory, and a genuine webpkit stream reused for the decode memory + timing (both decoders time these bytes). + let image = + webpkit::lossless::ImageRef::new(img.dims, webpkit::lossless::PixelLayout::Rgba8, rgba)?; + webpkit_alloc_count::reset_peak(); + let our_webp = webpkit::lossless::encode(image, &cfg)?; + acc.mem_enc = acc.mem_enc.max(webpkit_alloc_count::peak_since_reset()); + acc.ours_bytes += our_webp.len() as u64; + if lw::AVAILABLE { + acc.lib_bytes += lw::encode_lossless_len(rgba, w, h) as u64; + } + webpkit_alloc_count::reset_peak(); + let _ = webpkit::lossless::decode_rgba(&our_webp)?; + acc.mem_dec = acc.mem_dec.max(webpkit_alloc_count::peak_since_reset()); + + // Speed (timing plane). + let raw = f64::from(w) * f64::from(h) * 4.0; + let px = f64::from(w) * f64::from(h); + acc.enc_ours_work += raw; + acc.enc_ours_secs += median_secs(|| { + let image = + webpkit::lossless::ImageRef::new(img.dims, webpkit::lossless::PixelLayout::Rgba8, rgba) + .unwrap(); + webpkit::lossless::encode(image, &cfg).map_or(0, |v| v.len()) + }); + acc.dec_ours_px += px; + acc.dec_ours_secs += + median_secs(|| webpkit::lossless::decode_rgba(&our_webp).map_or(0, |v| v.1.len())); + if lw::AVAILABLE { + acc.enc_lib_work += raw; + acc.enc_lib_secs += median_secs(|| lw::encode_lossless_len(rgba, w, h)); + acc.dec_lib_px += px; + acc.dec_lib_secs += median_secs(|| lw::decode_rgba_len(&our_webp)); + } + + acc.n += 1; + Ok(()) +} + +/// Fold one image's lossy measurements into `acc`: webpkit `AUTO` vs libwebp default shaping at [`LOSSY_QUALITY`], both on RGB. +/// Both encoders' `VP8 ` payloads are decoded by webpkit's decoder for an apples-to-apples PSNR/SSIM. +fn measure_lossy(img: &RealImage, acc: &mut Acc) -> Result<()> { + let (w, h) = (img.dims.width(), img.dims.height()); + let (wu, hu) = (w as usize, h as usize); + let rgba = img.rgba.as_slice(); + let rgb = rgba_to_rgb(rgba); + let dims = webpkit::lossy::Dimensions::new(w, h)?; + let cfg = webpkit::lossy::LossyConfig::new() + .with_quality(LOSSY_QUALITY) + .with_effort(webpkit::lossy::Effort::AUTO); + + // Ours: one measured encode yields size, exact encode peak, and the payload reused for reconstruction + decode memory/timing. + let image = webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, rgba)?; + webpkit_alloc_count::reset_peak(); + let (_d, our_payload) = webpkit::lossy::encode_vp8(image, &cfg)?; + acc.mem_enc = acc.mem_enc.max(webpkit_alloc_count::peak_since_reset()); + acc.ours_bytes += our_payload.len() as u64; + webpkit_alloc_count::reset_peak(); + let our_dec = webpkit::lossy::decode(&our_payload)?; + acc.mem_dec = acc.mem_dec.max(webpkit_alloc_count::peak_since_reset()); + + // libwebp: size + reconstruction (its VP8 payload decoded by OUR decoder too). + if lw::AVAILABLE { + let lib_file = lw::encode_lossy_bytes(&rgb, w, h, f32::from(LOSSY_QUALITY)); + let lib_vp8 = extract_vp8_chunk(&lib_file)?; + acc.lib_bytes += lib_vp8.len() as u64; + let lib_dec = webpkit::lossy::decode(lib_vp8)?; + acc.d_psnr += psnr_rgb(rgba, our_dec.as_bytes()) - psnr_rgb(rgba, lib_dec.as_bytes()); + acc.d_ssim += + ssim_rgb(rgba, our_dec.as_bytes(), wu, hu) - ssim_rgb(rgba, lib_dec.as_bytes(), wu, hu); + } + + // Speed (timing plane). + // Decode times the SAME VP8 bytes on both sides: our payload for our decoder, the same payload wrapped in a container for libwebp. + let raw = f64::from(w) * f64::from(h) * 3.0; + let px = f64::from(w) * f64::from(h); + let container = wrap_vp8(&our_payload); + acc.enc_ours_work += raw; + acc.enc_ours_secs += median_secs(|| { + let image = + webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, rgba).unwrap(); + webpkit::lossy::encode_vp8(image, &cfg).map_or(0, |(_d, v)| v.len()) + }); + acc.dec_ours_px += px; + acc.dec_ours_secs += + median_secs(|| webpkit::lossy::decode(&our_payload).map_or(0, |i| i.as_bytes().len())); + if lw::AVAILABLE { + acc.enc_lib_work += raw; + acc.enc_lib_secs += + median_secs(|| lw::encode_lossy_len(&rgb, w, h, f32::from(LOSSY_QUALITY))); + acc.dec_lib_px += px; + acc.dec_lib_secs += median_secs(|| lw::decode_rgba_len(&container)); + } + + acc.n += 1; + Ok(()) +} + +/// Print the lossless section: one row per category plus `overall`. +fn print_lossless(acc: &[Acc; 4]) { + println!(); + println!( + "LOSSLESS (webpkit AUTO vs libwebp default; both reproduce the source, so no fidelity):" + ); + println!( + " {:<12} {:>3} {:>12} {:>12} {:>6} {:>9} {:>9} {:>9} {:>9} {:>8} {:>8}", + "category", + "n", + "ours B", + "lib B", + "size%", + "encO MB/s", + "encL MB/s", + "decO MP/s", + "decL MP/s", + "encKiB", + "decKiB", + ); + let mut overall = Acc::default(); + for cat in Cat::ALL { + print_lossless_row(cat.label(), acc[cat.idx()]); + acc[cat.idx()].add_into(&mut overall); + } + print_lossless_row("overall", overall); +} + +/// Print one lossless rollup row (skips an empty bucket). +fn print_lossless_row(label: &str, a: Acc) { + if a.n == 0 { + return; + } + let lib_b = if lw::AVAILABLE { + a.lib_bytes.to_string() + } else { + "n/a".to_owned() + }; + println!( + " {label:<12} {:>3} {:>12} {:>12} {:>6} {:>9} {:>9} {:>9} {:>9} {:>8} {:>8}", + a.n, + a.ours_bytes, + lib_b, + pct(a.ours_bytes, a.lib_bytes), + format_throughput(a.enc_ours_work, a.enc_ours_secs), + format_throughput(a.enc_lib_work, a.enc_lib_secs), + format_throughput(a.dec_ours_px, a.dec_ours_secs), + format_throughput(a.dec_lib_px, a.dec_lib_secs), + a.mem_enc / 1024, + a.mem_dec / 1024, + ); +} + +/// Print the lossy section: one row per category plus `overall`. +fn print_lossy(acc: &[Acc; 4]) { + println!(); + println!( + "LOSSY q{LOSSY_QUALITY} (webpkit AUTO vs libwebp default; bytes = VP8 payload, both decoded by webpkit):" + ); + println!( + " size% = ours*100/lib (<100 = ours smaller); +dB / +SSIM = ours - lib (>0 = ours better)" + ); + println!( + " {:<12} {:>3} {:>12} {:>12} {:>6} {:>7} {:>8} {:>9} {:>9} {:>9} {:>9} {:>8} {:>8}", + "category", + "n", + "ours B", + "lib B", + "size%", + "+dB", + "+SSIM", + "encO MB/s", + "encL MB/s", + "decO MP/s", + "decL MP/s", + "encKiB", + "decKiB", + ); + let mut overall = Acc::default(); + for cat in Cat::ALL { + print_lossy_row(cat.label(), acc[cat.idx()]); + acc[cat.idx()].add_into(&mut overall); + } + print_lossy_row("overall", overall); +} + +/// Print one lossy rollup row (skips an empty bucket). +fn print_lossy_row(label: &str, a: Acc) { + if a.n == 0 { + return; + } + let n = f64::from(a.n); + let (lib_b, d_db, d_ssim) = if lw::AVAILABLE { + ( + a.lib_bytes.to_string(), + format!("{:+.2}", a.d_psnr / n), + format!("{:+.4}", a.d_ssim / n), + ) + } else { + ("n/a".to_owned(), "n/a".to_owned(), "n/a".to_owned()) + }; + println!( + " {label:<12} {:>3} {:>12} {:>12} {:>6} {d_db:>7} {d_ssim:>8} {:>9} {:>9} {:>9} {:>9} {:>8} {:>8}", + a.n, + a.ours_bytes, + lib_b, + pct(a.ours_bytes, a.lib_bytes), + format_throughput(a.enc_ours_work, a.enc_ours_secs), + format_throughput(a.enc_lib_work, a.enc_lib_secs), + format_throughput(a.dec_ours_px, a.dec_ours_secs), + format_throughput(a.dec_lib_px, a.dec_lib_secs), + a.mem_enc / 1024, + a.mem_dec / 1024, + ); +} + +/// Print (never gate) the real-image multi-axis scoreboard over `real`. +/// +/// PRIVACY: `real` is a pure runtime path; images are aggregated by pixel-derived +/// [`Cat`] (never the filename) and no file name is printed or recorded. Every temp +/// file lives in a dropped `tempfile::tempdir()` (inside `prepare_real_images`), so +/// nothing is written into the repo. Needs `cwebp` to READ the source images into +/// RGBA; libwebp comparison columns additionally need the `scoreboard-libwebp` +/// feature (else they print `n/a`). Soft-skips when `cwebp` is absent. +pub(crate) fn scoreboard(real: &Path, max_edge: u32) -> Result<()> { + let cwebp = cwebp_bin(); + if let Err(e) = check_version(&cwebp, "cwebp", "WEBPKIT_CWEBP") { + println!("scoreboard: skipped ({e})"); + return Ok(()); + } + + println!("webpkit vs libwebp real-image scoreboard (printed only, NOT gated):"); + match (max_edge > 0).then_some(max_edge) { + Some(w) => println!(" input width capped to {w}px by cwebp (aspect preserved)"), + None => println!(" native resolution (no resize)"), + } + if lw::AVAILABLE { + println!( + " libwebp: in-process libwebp-sys {} reference, shipped simple-API defaults", + lw::decoder_version() + ); + } else { + println!( + " libwebp: NOT linked — rebuild with `--features scoreboard-libwebp` for libwebp columns" + ); + } + println!( + " size = exact bytes (rigorous); fidelity = exact, both decoded by webpkit (rigorous);" + ); + println!( + " speed = median of {TIMING_ITERS} in-process passes (timing plane: noisy, non-deterministic);" + ); + println!( + " encKiB/decKiB = webpkit peak additional alloc (exact); libwebp memory omitted (C malloc)." + ); + + let prepared = prepare_real_images(&cwebp, real, max_edge)?; + let mut lossless = [Acc::default(); 4]; + let mut lossy = [Acc::default(); 4]; + let mut skipped = 0usize; + + for item in &prepared { + let RealPrep::Ready(img) = item else { + skipped += 1; + continue; + }; + let cat = classify(&img.rgba); + measure_lossless(img, &mut lossless[cat.idx()]) + .with_context(|| format!("lossless measurement of a {} image", cat.label()))?; + measure_lossy(img, &mut lossy[cat.idx()]) + .with_context(|| format!("lossy measurement of a {} image", cat.label()))?; + } + + if lossless.iter().all(|a| a.n == 0) { + println!(); + println!(" (no comparable images in {})", real.display()); + return Ok(()); + } + print_lossless(&lossless); + print_lossy(&lossy); + if skipped > 0 { + println!(); + println!(" ({skipped} non-image file(s) skipped)"); + } + Ok(()) +} diff --git a/xtask/src/surpass.rs b/xtask/src/surpass.rs new file mode 100644 index 0000000..a85c179 --- /dev/null +++ b/xtask/src/surpass.rs @@ -0,0 +1,1533 @@ +//! Auditable real-image comparison against pinned libwebp (`surpass`). +//! +//! The report contains category rollups only. +//! Input paths, file names, and per-image measurements never cross the command boundary or enter the JSON. + +use std::collections::BTreeSet; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, anyhow, bail, ensure}; +use serde::{Deserialize, Serialize}; + +use crate::bench::{RealImage, RealPrep, prepare_real_images_max_edge}; +use crate::common::{fnv1a64, workspace_root}; +use crate::fixtures::write_png; +use crate::libwebp::{REQUIRED_LIBWEBP, check_version, cwebp_bin, verify_cwebp_image_hint}; +use crate::metrics::{extract_vp8_chunk, psnr_rgb, ssim_rgb}; + +#[cfg(feature = "scoreboard-libwebp")] +mod lw { + pub(super) const AVAILABLE: bool = true; + pub(super) use webpkit_libwebp_timing::{ + decode_rgba_len, decoder_version, encode_lossless_best_len, encode_lossy_bytes, + encode_lossy_rgba_len, + }; +} + +#[cfg(not(feature = "scoreboard-libwebp"))] +mod lw { + pub(super) const AVAILABLE: bool = false; + pub(super) const fn decoder_version() -> String { + String::new() + } + pub(super) const fn encode_lossless_best_len(_: &[u8], _: u32, _: u32) -> usize { + 0 + } + pub(super) const fn encode_lossy_bytes(_: &[u8], _: u32, _: u32, _: f32) -> Vec { + Vec::new() + } + pub(super) const fn encode_lossy_rgba_len(_: &[u8], _: u32, _: u32, _: f32) -> usize { + 0 + } + pub(super) const fn decode_rgba_len(_: &[u8]) -> usize { + 0 + } +} + +const LOSSY_BANDS: [u8; 3] = [50, 75, 90]; +const CORPUS_WORKERS: usize = 4; +// Each image gets one deterministic warm-up per implementation followed by three AB/BA pairs (six timed samples per implementation). +const TIMING_PAIRS: u32 = 3; +const TIMING_WARMUPS_PER_IMPLEMENTATION: u32 = 1; +#[cfg(target_os = "windows")] +const WINDOWS_PEAK_OBSERVER: &str = r#" +$ErrorActionPreference = 'Stop' +$exePath = $env:WEBPKIT_SURPASS_WORKER_EXE +$p = Start-Process -FilePath $exePath -ArgumentList 'surpass-worker-env' -WindowStyle Hidden -PassThru +$peak = 0L +while (-not $p.HasExited) { + try { + $p.Refresh() + $peak = [Math]::Max($peak, $p.PeakWorkingSet64) + } catch {} + Start-Sleep -Milliseconds 1 +} +$p.WaitForExit() +Write-Output ("{0} {1}" -f $p.ExitCode, $peak) +exit $p.ExitCode +"#; + +fn ensure_scalar_product_features() -> Result<()> { + ensure!( + !webpkit::INTERNAL_RAYON_ENABLED && !webpkit::INTERNAL_WORK_COUNT_ENABLED, + "formal surpass measurement rejects resolved webpkit rayon and work-count features" + ); + Ok(()) +} + +fn normalized_metadata(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + +#[cfg(target_os = "windows")] +fn cpu_model() -> String { + std::env::var("PROCESSOR_IDENTIFIER").map_or_else( + |_| "unavailable".to_owned(), + |value| normalized_metadata(&value), + ) +} + +#[cfg(target_os = "linux")] +fn cpu_model() -> String { + std::fs::read_to_string("/proc/cpuinfo") + .ok() + .and_then(|text| { + text.lines().find_map(|line| { + let (key, value) = line.split_once(':')?; + matches!(key.trim(), "model name" | "Hardware").then(|| normalized_metadata(value)) + }) + }) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "unavailable".to_owned()) +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn cpu_model() -> String { + std::env::consts::ARCH.to_owned() +} + +fn rust_toolchain() -> Result { + let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + let output = Command::new(rustc) + .arg("--version") + .output() + .context("querying rustc version")?; + ensure!(output.status.success(), "rustc --version failed"); + let version = String::from_utf8(output.stdout).context("rustc version was not UTF-8")?; + let version = normalized_metadata(&version); + ensure!(!version.is_empty(), "rustc --version returned no version"); + Ok(version) +} + +fn source_metadata(root: &Path) -> Result<(String, bool)> { + let revision = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(root) + .output() + .context("querying source revision")?; + ensure!(revision.status.success(), "git rev-parse HEAD failed"); + let revision = + normalized_metadata(&String::from_utf8(revision.stdout).context("revision was not UTF-8")?); + ensure!(!revision.is_empty(), "git returned an empty revision"); + + let status = Command::new("git") + .args(["status", "--porcelain=v1", "--untracked-files=normal"]) + .current_dir(root) + .output() + .context("querying source state")?; + ensure!(status.status.success(), "git status failed"); + Ok((revision, !status.stdout.is_empty())) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Category { + FlatIcon, + Transparent, + Graphic, + Photo, +} + +impl Category { + const ALL: [Self; 4] = [ + Self::FlatIcon, + Self::Transparent, + Self::Graphic, + Self::Photo, + ]; + + const fn label(self) -> &'static str { + match self { + Self::FlatIcon => "flat-icon", + Self::Transparent => "transparent", + Self::Graphic => "graphic", + Self::Photo => "photo", + } + } +} + +fn classify(rgba: &[u8]) -> Category { + let transparent = rgba.as_chunks::<4>().0.iter().any(|pixel| pixel[3] < 255); + let cap = if transparent { 4096 } else { 8192 }; + let mut colors = BTreeSet::new(); + let mut over = false; + for pixel in rgba.as_chunks::<4>().0 { + colors.insert([pixel[0], pixel[1], pixel[2]]); + if colors.len() > cap { + over = true; + break; + } + } + match (transparent, over) { + (true, false) => Category::FlatIcon, + (true, true) => Category::Transparent, + (false, false) => Category::Graphic, + (false, true) => Category::Photo, + } +} + +fn rgba_to_rgb(rgba: &[u8]) -> Vec { + let mut rgb = Vec::with_capacity(rgba.len() / 4 * 3); + for pixel in rgba.as_chunks::<4>().0 { + rgb.extend_from_slice(&pixel[..3]); + } + rgb +} + +fn opaque_rgba(rgba: &[u8]) -> Vec { + let mut opaque = rgba.to_vec(); + for pixel in opaque.as_chunks_mut::<4>().0.iter_mut() { + pixel[3] = 255; + } + opaque +} + +fn wrap_vp8(payload: &[u8]) -> Vec { + let pad = payload.len() & 1; + let chunk_len = u32::try_from(payload.len()).unwrap_or(u32::MAX); + let riff_len = u32::try_from(4 + 8 + payload.len() + pad).unwrap_or(u32::MAX); + let mut out = Vec::with_capacity(8 + usize::try_from(riff_len).unwrap_or(0)); + out.extend_from_slice(b"RIFF"); + out.extend_from_slice(&riff_len.to_le_bytes()); + out.extend_from_slice(b"WEBPVP8 "); + out.extend_from_slice(&chunk_len.to_le_bytes()); + out.extend_from_slice(payload); + if pad != 0 { + out.push(0); + } + out +} + +fn cwebp_lossy_reference( + cwebp: &str, + rgb: &[u8], + width: u32, + height: u32, + quality: u8, +) -> Result> { + let directory = tempfile::tempdir().context("creating anonymous lossy reference directory")?; + let input = directory.path().join("input.png"); + let output = directory.path().join("output.webp"); + let mut rgba = Vec::with_capacity(rgb.len() / 3 * 4); + for pixel in rgb.as_chunks::<3>().0 { + rgba.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]); + } + write_png(&input, width, height, &rgba).context("writing anonymous lossy reference input")?; + let quality = quality.to_string(); + let result = Command::new(cwebp) + .args(["-quiet", "-q", &quality, "-noalpha"]) + .arg(&input) + .args(["-o"]) + .arg(&output) + .output() + .context("running pinned cwebp lossy reference")?; + ensure!( + result.status.success(), + "pinned cwebp lossy reference failed" + ); + std::fs::read(output).context("reading anonymous cwebp lossy reference") +} + +fn prepare_cwebp_lossy_references(cwebp: &str, image: &RealImage) -> Result>> { + let rgb = rgba_to_rgb(&image.rgba); + LOSSY_BANDS + .into_iter() + .map(|quality| { + cwebp_lossy_reference( + cwebp, + &rgb, + image.dims.width(), + image.dims.height(), + quality, + ) + }) + .collect() +} + +fn elapsed_ns(mut operation: impl FnMut() -> Result) -> Result { + let start = Instant::now(); + std::hint::black_box(operation()?); + Ok(u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX)) +} + +/// Time paired A/B operations in alternating order and return each median. +fn paired_medians( + mut ours: impl FnMut() -> Result, + mut reference: impl FnMut() -> Result, +) -> Result<(u64, u64)> { + for _ in 0..TIMING_WARMUPS_PER_IMPLEMENTATION { + std::hint::black_box(ours()?); + std::hint::black_box(reference()?); + } + let mut ours_ns = Vec::with_capacity((TIMING_PAIRS * 2) as usize); + let mut reference_ns = Vec::with_capacity((TIMING_PAIRS * 2) as usize); + for _ in 0..TIMING_PAIRS { + ours_ns.push(elapsed_ns(&mut ours)?); + reference_ns.push(elapsed_ns(&mut reference)?); + reference_ns.push(elapsed_ns(&mut reference)?); + ours_ns.push(elapsed_ns(&mut ours)?); + } + ours_ns.sort_unstable(); + reference_ns.sort_unstable(); + Ok(( + ours_ns[ours_ns.len() / 2], + reference_ns[reference_ns.len() / 2], + )) +} + +fn ratio_permille(ours: u64, reference: u64) -> u32 { + if reference == 0 { + return u32::MAX; + } + let numerator = u128::from(ours) * 1000; + let rounded_up = numerator.div_ceil(u128::from(reference)); + u32::try_from(rounded_up).unwrap_or(u32::MAX) +} + +fn median(mut values: Vec) -> Option { + if values.is_empty() { + return None; + } + values.sort_unstable(); + Some(values[values.len() / 2]) +} + +#[derive(Clone)] +struct LosslessMeasurement { + ours_bytes: u64, + reference_bytes: u64, + ours_payload: Vec, + encode_ratio_permille: Option, + decode_ratio_permille: Option, + ours_encode_peak_bytes: Option, + ours_decode_peak_bytes: Option, + reference_encode_peak_bytes: Option, + reference_decode_peak_bytes: Option, +} + +#[derive(Clone)] +struct LossyMeasurement { + quality: u8, + selected_quality: Option, + ours_bytes: Option, + reference_bytes: u64, + selected_hash: Option, + encode_ratio_permille: Option, + decode_ratio_permille: Option, + ours_encode_peak_bytes: Option, + ours_decode_peak_bytes: Option, + reference_encode_peak_bytes: Option, + reference_decode_peak_bytes: Option, +} + +#[derive(Clone)] +struct ImageMeasurement { + lossless: LosslessMeasurement, + lossy: Vec, +} + +#[derive(Deserialize, Serialize)] +struct WorkerRequest { + kind: String, + input: PathBuf, + width: u32, + height: u32, + quality: u8, +} + +impl WorkerRequest { + fn new(kind: &str, input: &Path, width: u32, height: u32, quality: u8) -> Self { + Self { + kind: kind.to_owned(), + input: input.to_owned(), + width, + height, + quality, + } + } + + #[cfg(target_os = "linux")] + fn command_args(&self) -> Vec { + vec![ + "surpass-worker".to_owned(), + "--kind".to_owned(), + self.kind.clone(), + "--input".to_owned(), + self.input.as_os_str().to_string_lossy().into_owned(), + "--width".to_owned(), + self.width.to_string(), + "--height".to_owned(), + self.height.to_string(), + "--quality".to_owned(), + self.quality.to_string(), + ] + } +} + +#[cfg(target_os = "windows")] +fn subprocess_peak_bytes(request: &WorkerRequest) -> Result { + let exe = std::env::current_exe().context("resolving current xtask executable")?; + // Poll the OS-maintained peak while the worker is alive. + // The worker retains itself briefly after the measured operation, so even a fast decode has an observation window. + // Both implementations use this exact executable and wrapper, making process baseline and sampling basis identical. + let output = Command::new("powershell.exe") + .args([ + "-NoProfile", + "-NonInteractive", + "-Command", + WINDOWS_PEAK_OBSERVER, + ]) + .env("WEBPKIT_SURPASS_WORKER_EXE", exe) + .env( + "WEBPKIT_SURPASS_WORKER_ARGS", + serde_json::to_string(request).context("serializing worker request")?, + ) + .output() + .context("launching Windows peak-working-set observer")?; + ensure!( + output.status.success(), + "surpass worker failed under peak-working-set observer: stdout=`{}` stderr=`{}`", + String::from_utf8_lossy(&output.stdout).trim(), + String::from_utf8_lossy(&output.stderr).trim(), + ); + let text = String::from_utf8(output.stdout).context("worker observer emitted non-UTF-8")?; + let peak = text + .split_whitespace() + .last() + .context("worker observer emitted no peak")? + .parse::() + .context("worker observer emitted an invalid peak")?; + ensure!(peak > 0, "worker observer reported a zero peak"); + Ok(peak) +} + +#[cfg(target_os = "linux")] +fn subprocess_peak_bytes(request: &WorkerRequest) -> Result { + let exe = std::env::current_exe().context("resolving current xtask executable")?; + let metrics = tempfile::NamedTempFile::new().context("creating peak-RSS output")?; + let status = Command::new("/usr/bin/time") + .args(["-f", "%x %M", "-o"]) + .arg(metrics.path()) + .arg(exe) + .args(request.command_args()) + .status() + .context("launching Linux peak-RSS observer")?; + ensure!( + status.success(), + "surpass worker failed under peak-RSS observer" + ); + let text = std::fs::read_to_string(metrics.path()).context("reading peak-RSS output")?; + let kib = text + .split_whitespace() + .last() + .context("peak-RSS observer emitted no peak")? + .parse::() + .context("peak-RSS observer emitted an invalid peak")?; + ensure!(kib > 0, "peak-RSS observer reported a zero peak"); + Ok(kib.saturating_mul(1024)) +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +fn subprocess_peak_bytes(_: &WorkerRequest) -> Result { + bail!("surpass OS peak memory is currently supported on Windows and Linux") +} + +fn measure_worker_peak( + kind: &str, + input: &Path, + width: u32, + height: u32, + quality: u8, +) -> Result { + subprocess_peak_bytes(&WorkerRequest::new(kind, input, width, height, quality)) +} + +/// Execute a Windows peak-observer request transported as JSON in the environment. +/// Only a fixed, space-free subcommand crosses `Start-Process`'s lossy `-ArgumentList` boundary, so paths containing spaces remain one value. +#[cfg(target_os = "windows")] +pub(crate) fn worker_from_environment() -> Result<()> { + let json = std::env::var("WEBPKIT_SURPASS_WORKER_ARGS") + .context("WEBPKIT_SURPASS_WORKER_ARGS is missing")?; + let request: WorkerRequest = + serde_json::from_str(&json).context("deserializing worker request")?; + worker( + &request.kind, + &request.input, + request.width, + request.height, + request.quality, + ) +} + +/// Execute one isolated operation for the parent `surpass` memory observer. +/// +/// Both the safe-Rust and libwebp variants run in this same executable. +/// The brief hold after the operation leaves enough time for the parent to read the OS-maintained peak even for a very fast decode. +pub(crate) fn worker(kind: &str, input: &Path, width: u32, height: u32, quality: u8) -> Result<()> { + ensure!( + lw::AVAILABLE, + "surpass worker requires the scoreboard-libwebp feature" + ); + let bytes = std::fs::read(input).context("reading anonymous worker input")?; + let measured = match kind { + "webpkit-lossless-encode" => { + let dims = webpkit::lossless::Dimensions::new(width, height)?; + let image = webpkit::lossless::ImageRef::new( + dims, + webpkit::lossless::PixelLayout::Rgba8, + &bytes, + )?; + let config = webpkit::lossless::EncoderConfig::new() + .with_effort(webpkit::lossless::Effort::level(9)); + webpkit::lossless::encode(image, &config)?.len() + }, + "libwebp-lossless-encode" => lw::encode_lossless_best_len(&bytes, width, height), + "webpkit-lossy-encode" => { + let dims = webpkit::lossy::Dimensions::new(width, height)?; + let image = + webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, &bytes)?; + let config = webpkit::lossy::LossyConfig::new() + .with_quality(quality) + .with_effort(webpkit::lossy::Effort::level(9)); + webpkit::lossy::encode_vp8(image, &config)?.1.len() + }, + "libwebp-lossy-encode" => { + lw::encode_lossy_rgba_len(&bytes, width, height, f32::from(quality)) + }, + "webpkit-lossless-decode" => webpkit::lossless::decode_rgba(&bytes)?.1.len(), + "webpkit-lossy-decode" => webpkit::decode(&bytes)?.as_bytes().len(), + "libwebp-decode" => lw::decode_rgba_len(&bytes), + _ => bail!("unknown surpass worker operation"), + }; + ensure!(measured > 0, "surpass worker operation failed"); + std::hint::black_box(measured); + std::thread::sleep(Duration::from_millis(25)); + Ok(()) +} + +fn measure_lossless(image: &RealImage) -> Result { + let (width, height) = (image.dims.width(), image.dims.height()); + let input = webpkit::lossless::ImageRef::new( + image.dims, + webpkit::lossless::PixelLayout::Rgba8, + &image.rgba, + )?; + let config = + webpkit::lossless::EncoderConfig::new().with_effort(webpkit::lossless::Effort::level(9)); + + let ours = webpkit::lossless::encode(input, &config)?; + let reference_len = lw::encode_lossless_best_len(&image.rgba, width, height); + ensure!( + reference_len > 0, + "libwebp lossless reference encode failed" + ); + ensure!( + u64::try_from(reference_len).unwrap_or(u64::MAX) == image.cwebp_size, + "libwebp in-process and cwebp lossless settings disagree" + ); + + let decoded = webpkit::lossless::decode_rgba(&ours)?; + std::hint::black_box(decoded.1.len()); + + Ok(LosslessMeasurement { + ours_bytes: u64::try_from(ours.len()).unwrap_or(u64::MAX), + reference_bytes: image.cwebp_size, + ours_payload: ours, + encode_ratio_permille: None, + decode_ratio_permille: None, + ours_encode_peak_bytes: None, + ours_decode_peak_bytes: None, + reference_encode_peak_bytes: None, + reference_decode_peak_bytes: None, + }) +} + +fn populate_lossless_resources( + image: &RealImage, + measurement: &mut LosslessMeasurement, +) -> Result<()> { + let (width, height) = (image.dims.width(), image.dims.height()); + let config = + webpkit::lossless::EncoderConfig::new().with_effort(webpkit::lossless::Effort::level(9)); + let ours = &measurement.ours_payload; + let memory_dir = tempfile::tempdir().context("creating anonymous memory worker directory")?; + let raw_path = memory_dir.path().join("input.rgba"); + let webp_path = memory_dir.path().join("image.webp"); + std::fs::write(&raw_path, &image.rgba).context("writing anonymous RGBA worker input")?; + std::fs::write(&webp_path, ours).context("writing anonymous WebP worker input")?; + let ours_encode_peak_bytes = + measure_worker_peak("webpkit-lossless-encode", &raw_path, width, height, 0)?; + let reference_encode_peak_bytes = + measure_worker_peak("libwebp-lossless-encode", &raw_path, width, height, 0)?; + let ours_decode_peak_bytes = + measure_worker_peak("webpkit-lossless-decode", &webp_path, width, height, 0)?; + let reference_decode_peak_bytes = + measure_worker_peak("libwebp-decode", &webp_path, width, height, 0)?; + + let (ours_encode_ns, reference_encode_ns) = paired_medians( + || { + let input = webpkit::lossless::ImageRef::new( + image.dims, + webpkit::lossless::PixelLayout::Rgba8, + &image.rgba, + ) + .expect("validated real image"); + Ok(webpkit::lossless::encode(input, &config)?.len()) + }, + || { + let len = lw::encode_lossless_best_len(&image.rgba, width, height); + ensure!(len > 0, "timed libwebp lossless encode failed"); + Ok(len) + }, + )?; + let (ours_decode_ns, reference_decode_ns) = paired_medians( + || Ok(webpkit::lossless::decode_rgba(ours)?.1.len()), + || { + let len = lw::decode_rgba_len(ours); + ensure!(len > 0, "timed libwebp lossless decode failed"); + Ok(len) + }, + )?; + + measurement.encode_ratio_permille = Some(ratio_permille(ours_encode_ns, reference_encode_ns)); + measurement.decode_ratio_permille = Some(ratio_permille(ours_decode_ns, reference_decode_ns)); + measurement.ours_encode_peak_bytes = Some(ours_encode_peak_bytes); + measurement.ours_decode_peak_bytes = Some(ours_decode_peak_bytes); + measurement.reference_encode_peak_bytes = Some(reference_encode_peak_bytes); + measurement.reference_decode_peak_bytes = Some(reference_decode_peak_bytes); + Ok(()) +} + +#[allow( + clippy::too_many_lines, + reason = "one RD transaction builds the complete curve and applies every matched-fidelity \ + reference band to it without relabelling quality" +)] +fn measure_lossy(image: &RealImage, cwebp_references: &[Vec]) -> Result> { + #[derive(Clone, Copy)] + struct RdPoint { + quality: u8, + bytes: usize, + hash: u64, + psnr: f64, + ssim: f64, + } + + let (width, height) = (image.dims.width(), image.dims.height()); + let (w, h) = ( + usize::try_from(width).unwrap_or(0), + usize::try_from(height).unwrap_or(0), + ); + let dims = webpkit::lossy::Dimensions::new(width, height)?; + let rgb = rgba_to_rgb(&image.rgba); + let opaque = opaque_rgba(&image.rgba); + let mut bands = Vec::with_capacity(LOSSY_BANDS.len()); + + // Build the complete webpkit RD curve once, before consulting any reference band. + // The three q50/q75/q90 comparisons then select from the same points; + // quality labels are never compared directly. + let mut curve = Vec::with_capacity(101); + for quality in 0..=100u8 { + let config = webpkit::lossy::LossyConfig::new() + .with_quality(quality) + .with_effort(webpkit::lossy::Effort::level(9)); + let input = + webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, &opaque)?; + let (_, payload) = webpkit::lossy::encode_vp8(input, &config)?; + let decoded = webpkit::lossy::decode(&payload)?; + curve.push(RdPoint { + quality, + bytes: payload.len(), + hash: fnv1a64(&payload), + psnr: psnr_rgb(&image.rgba, decoded.as_bytes()), + ssim: ssim_rgb(&image.rgba, decoded.as_bytes(), w, h), + }); + } + + ensure!( + cwebp_references.len() == LOSSY_BANDS.len(), + "incomplete anonymous cwebp lossy reference set" + ); + for (quality, reference_file) in LOSSY_BANDS.into_iter().zip(cwebp_references) { + let in_process_reference = lw::encode_lossy_bytes(&rgb, width, height, f32::from(quality)); + ensure!( + !reference_file.is_empty() && !in_process_reference.is_empty(), + "libwebp lossy reference encode failed" + ); + let reference_payload = extract_vp8_chunk(reference_file)?; + let in_process_payload = extract_vp8_chunk(&in_process_reference)?; + ensure!( + reference_payload == in_process_payload, + "pinned cwebp and in-process libwebp lossy settings disagree" + ); + let reference_decoded = webpkit::lossy::decode(reference_payload)?; + let reference_psnr = psnr_rgb(&image.rgba, reference_decoded.as_bytes()); + let reference_ssim = ssim_rgb(&image.rgba, reference_decoded.as_bytes(), w, h); + + let selected = curve + .iter() + .copied() + .filter(|point| point.psnr >= reference_psnr && point.ssim >= reference_ssim) + .min_by_key(|point| (point.bytes, point.quality)); + let Some(selected) = selected else { + bands.push(LossyMeasurement { + quality, + selected_quality: None, + ours_bytes: None, + reference_bytes: u64::try_from(reference_payload.len()).unwrap_or(u64::MAX), + selected_hash: None, + encode_ratio_permille: None, + decode_ratio_permille: None, + ours_encode_peak_bytes: None, + ours_decode_peak_bytes: None, + reference_encode_peak_bytes: None, + reference_decode_peak_bytes: None, + }); + continue; + }; + + let selected_quality = selected.quality; + bands.push(LossyMeasurement { + quality, + selected_quality: Some(selected_quality), + ours_bytes: Some(u64::try_from(selected.bytes).unwrap_or(u64::MAX)), + reference_bytes: u64::try_from(reference_payload.len()).unwrap_or(u64::MAX), + selected_hash: Some(selected.hash), + encode_ratio_permille: None, + decode_ratio_permille: None, + ours_encode_peak_bytes: None, + ours_decode_peak_bytes: None, + reference_encode_peak_bytes: None, + reference_decode_peak_bytes: None, + }); + } + Ok(bands) +} + +fn populate_lossy_resources(image: &RealImage, bands: &mut [LossyMeasurement]) -> Result<()> { + let (width, height) = (image.dims.width(), image.dims.height()); + let dims = webpkit::lossy::Dimensions::new(width, height)?; + let opaque = opaque_rgba(&image.rgba); + let memory_dir = tempfile::tempdir().context("creating anonymous memory worker directory")?; + let raw_path = memory_dir.path().join("input.rgba"); + let webp_path = memory_dir.path().join("image.webp"); + std::fs::write(&raw_path, &opaque).context("writing anonymous opaque RGBA worker input")?; + + for band in bands { + let (Some(selected_quality), Some(expected_bytes), Some(selected_hash)) = + (band.selected_quality, band.ours_bytes, band.selected_hash) + else { + continue; + }; + let config = webpkit::lossy::LossyConfig::new() + .with_quality(selected_quality) + .with_effort(webpkit::lossy::Effort::level(9)); + let input = + webpkit::lossy::ImageRef::new(dims, webpkit::lossy::PixelLayout::Rgba8, &opaque)?; + let (_, payload) = webpkit::lossy::encode_vp8(input, &config)?; + ensure!( + u64::try_from(payload.len()).unwrap_or(u64::MAX) == expected_bytes + && fnv1a64(&payload) == selected_hash, + "lossy selected encode is not byte-deterministic", + ); + let decoded = webpkit::lossy::decode(&payload)?; + std::hint::black_box(decoded.as_bytes().len()); + + let container = wrap_vp8(&payload); + std::fs::write(&webp_path, &container).context("writing anonymous WebP worker input")?; + let ours_encode_peak_bytes = measure_worker_peak( + "webpkit-lossy-encode", + &raw_path, + width, + height, + selected_quality, + )?; + let reference_encode_peak_bytes = measure_worker_peak( + "libwebp-lossy-encode", + &raw_path, + width, + height, + band.quality, + )?; + let ours_decode_peak_bytes = + measure_worker_peak("webpkit-lossy-decode", &webp_path, width, height, 0)?; + let reference_decode_peak_bytes = + measure_worker_peak("libwebp-decode", &webp_path, width, height, 0)?; + + let (ours_encode_ns, reference_encode_ns) = paired_medians( + || { + let input = webpkit::lossy::ImageRef::new( + dims, + webpkit::lossy::PixelLayout::Rgba8, + &opaque, + ) + .expect("validated real image"); + Ok(webpkit::lossy::encode_vp8(input, &config)?.1.len()) + }, + || { + let len = + lw::encode_lossy_rgba_len(&opaque, width, height, f32::from(band.quality)); + ensure!(len > 0, "timed libwebp lossy encode failed"); + Ok(len) + }, + )?; + let (ours_decode_ns, reference_decode_ns) = paired_medians( + || Ok(webpkit::decode(&container)?.as_bytes().len()), + || { + let len = lw::decode_rgba_len(&container); + ensure!(len > 0, "timed libwebp lossy decode failed"); + Ok(len) + }, + )?; + + band.encode_ratio_permille = Some(ratio_permille(ours_encode_ns, reference_encode_ns)); + band.decode_ratio_permille = Some(ratio_permille(ours_decode_ns, reference_decode_ns)); + band.ours_encode_peak_bytes = Some(ours_encode_peak_bytes); + band.ours_decode_peak_bytes = Some(ours_decode_peak_bytes); + band.reference_encode_peak_bytes = Some(reference_encode_peak_bytes); + band.reference_decode_peak_bytes = Some(reference_decode_peak_bytes); + } + Ok(()) +} + +fn measure_image( + category: Category, + image: &RealImage, + cwebp_references: &[Vec], +) -> Result { + let lossless = measure_lossless(image) + .with_context(|| format!("lossless {} category measurement", category.label()))?; + let lossy = measure_lossy(image, cwebp_references) + .with_context(|| format!("lossy {} category measurement", category.label()))?; + Ok(ImageMeasurement { lossless, lossy }) +} + +fn populate_image_resources( + category: Category, + image: &RealImage, + measurement: &mut ImageMeasurement, +) -> Result<()> { + populate_lossless_resources(image, &mut measurement.lossless) + .with_context(|| format!("lossless {} category resources", category.label()))?; + populate_lossy_resources(image, &mut measurement.lossy) + .with_context(|| format!("lossy {} category resources", category.label())) +} + +#[derive(Serialize)] +struct Environment { + os: &'static str, + architecture: &'static str, + cpu_model: String, + rust_toolchain: String, + source_revision: String, + source_dirty: bool, + debug_assertions: bool, + logical_cpus: usize, + corpus_workers: usize, + timing_pairs: u32, + timing_warmups_per_implementation: u32, + timing_samples_per_implementation: u32, + enabled_product_features: &'static str, + webpkit_path: &'static str, +} + +#[derive(Serialize)] +struct LosslessSummary { + images: usize, + resource_samples: usize, + ratio_permille_median: Option, + at_or_below_reference: usize, + ours_bytes: u64, + reference_bytes: u64, + encode_time_ratio_permille_median: Option, + decode_time_ratio_permille_median: Option, + ours_encode_peak_bytes_max: Option, + ours_decode_peak_bytes_max: Option, + reference_encode_peak_bytes_max: Option, + reference_decode_peak_bytes_max: Option, + encode_peak_ratio_permille_max: Option, + decode_peak_ratio_permille_max: Option, +} + +#[derive(Serialize)] +struct LossySummary { + reference_quality: u8, + matched: usize, + unmatched: usize, + resource_samples: usize, + ratio_permille_median: Option, + ours_bytes: u64, + reference_bytes: u64, + selected_quality_median: Option, + encode_time_ratio_permille_median: Option, + decode_time_ratio_permille_median: Option, + ours_encode_peak_bytes_max: Option, + ours_decode_peak_bytes_max: Option, + reference_encode_peak_bytes_max: Option, + reference_decode_peak_bytes_max: Option, + encode_peak_ratio_permille_max: Option, + decode_peak_ratio_permille_max: Option, +} + +#[derive(Serialize)] +struct CategorySummary { + category: &'static str, + images: usize, + lossless: LosslessSummary, + lossy: Vec, +} + +#[allow( + clippy::too_many_lines, + reason = "the serialized category schema is assembled in one place so every aggregate \ + is visibly derived from anonymous rows only" +)] +fn summarize(category: &'static str, rows: &[&ImageMeasurement]) -> CategorySummary { + let lossless = LosslessSummary { + images: rows.len(), + resource_samples: rows + .iter() + .filter(|row| row.lossless.encode_ratio_permille.is_some()) + .count(), + ratio_permille_median: median( + rows.iter() + .map(|row| ratio_permille(row.lossless.ours_bytes, row.lossless.reference_bytes)) + .collect(), + ), + at_or_below_reference: rows + .iter() + .filter(|row| row.lossless.ours_bytes <= row.lossless.reference_bytes) + .count(), + ours_bytes: rows.iter().map(|row| row.lossless.ours_bytes).sum(), + reference_bytes: rows.iter().map(|row| row.lossless.reference_bytes).sum(), + encode_time_ratio_permille_median: median( + rows.iter() + .filter_map(|row| row.lossless.encode_ratio_permille) + .collect(), + ), + decode_time_ratio_permille_median: median( + rows.iter() + .filter_map(|row| row.lossless.decode_ratio_permille) + .collect(), + ), + ours_encode_peak_bytes_max: rows + .iter() + .filter_map(|row| row.lossless.ours_encode_peak_bytes) + .max(), + ours_decode_peak_bytes_max: rows + .iter() + .filter_map(|row| row.lossless.ours_decode_peak_bytes) + .max(), + reference_encode_peak_bytes_max: rows + .iter() + .filter_map(|row| row.lossless.reference_encode_peak_bytes) + .max(), + reference_decode_peak_bytes_max: rows + .iter() + .filter_map(|row| row.lossless.reference_decode_peak_bytes) + .max(), + encode_peak_ratio_permille_max: rows + .iter() + .filter_map(|row| { + row.lossless + .ours_encode_peak_bytes + .zip(row.lossless.reference_encode_peak_bytes) + .map(|(ours, reference)| ratio_permille(ours, reference)) + }) + .max(), + decode_peak_ratio_permille_max: rows + .iter() + .filter_map(|row| { + row.lossless + .ours_decode_peak_bytes + .zip(row.lossless.reference_decode_peak_bytes) + .map(|(ours, reference)| ratio_permille(ours, reference)) + }) + .max(), + }; + + let lossy = LOSSY_BANDS + .into_iter() + .map(|quality| { + let measurements: Vec<&LossyMeasurement> = rows + .iter() + .filter_map(|row| row.lossy.iter().find(|band| band.quality == quality)) + .collect(); + let matched: Vec<&LossyMeasurement> = measurements + .iter() + .copied() + .filter(|band| band.ours_bytes.is_some()) + .collect(); + LossySummary { + reference_quality: quality, + matched: matched.len(), + unmatched: measurements.len() - matched.len(), + resource_samples: matched + .iter() + .filter(|band| band.encode_ratio_permille.is_some()) + .count(), + ratio_permille_median: median( + matched + .iter() + .filter_map(|band| { + band.ours_bytes + .map(|ours| ratio_permille(ours, band.reference_bytes)) + }) + .collect(), + ), + ours_bytes: matched.iter().filter_map(|band| band.ours_bytes).sum(), + reference_bytes: matched.iter().map(|band| band.reference_bytes).sum(), + selected_quality_median: median( + matched + .iter() + .filter_map(|band| band.selected_quality.map(u32::from)) + .collect(), + ), + encode_time_ratio_permille_median: median( + matched + .iter() + .filter_map(|band| band.encode_ratio_permille) + .collect(), + ), + decode_time_ratio_permille_median: median( + matched + .iter() + .filter_map(|band| band.decode_ratio_permille) + .collect(), + ), + ours_encode_peak_bytes_max: matched + .iter() + .filter_map(|band| band.ours_encode_peak_bytes) + .max(), + ours_decode_peak_bytes_max: matched + .iter() + .filter_map(|band| band.ours_decode_peak_bytes) + .max(), + reference_encode_peak_bytes_max: matched + .iter() + .filter_map(|band| band.reference_encode_peak_bytes) + .max(), + reference_decode_peak_bytes_max: matched + .iter() + .filter_map(|band| band.reference_decode_peak_bytes) + .max(), + encode_peak_ratio_permille_max: matched + .iter() + .filter_map(|band| { + band.ours_encode_peak_bytes + .zip(band.reference_encode_peak_bytes) + .map(|(ours, reference)| ratio_permille(ours, reference)) + }) + .max(), + decode_peak_ratio_permille_max: matched + .iter() + .filter_map(|band| { + band.ours_decode_peak_bytes + .zip(band.reference_decode_peak_bytes) + .map(|(ours, reference)| ratio_permille(ours, reference)) + }) + .max(), + } + }) + .collect(); + CategorySummary { + category, + images: rows.len(), + lossless, + lossy, + } +} + +#[derive(Serialize)] +#[allow( + clippy::struct_excessive_bools, + reason = "these are independent externally reported campaign gate outcomes, not states" +)] +struct Gates { + lossless_size: bool, + lossy_size_and_fidelity: bool, + encode_speed: bool, + decode_speed: bool, + same_executable_peak_memory: Option, + measurement_complete: bool, +} + +const fn measurement_complete(skipped: usize, gates: [bool; 5]) -> bool { + skipped == 0 && gates[0] && gates[1] && gates[2] && gates[3] && gates[4] +} + +const fn sample_set_complete(images: usize, resource_samples: usize) -> bool { + resource_samples == images +} + +const fn lossy_sample_set_complete( + images: usize, + matched: usize, + unmatched: usize, + resource_samples: usize, +) -> bool { + unmatched == 0 && matched == images && resource_samples == images +} + +fn resource_coverage_complete(overall: &CategorySummary, categories: &[CategorySummary]) -> bool { + sample_set_complete(overall.lossless.images, overall.lossless.resource_samples) + && overall.lossy.iter().all(|band| { + lossy_sample_set_complete( + overall.images, + band.matched, + band.unmatched, + band.resource_samples, + ) + }) + && categories.iter().all(|category| { + sample_set_complete(category.lossless.images, category.lossless.resource_samples) + && category.lossy.iter().all(|band| { + lossy_sample_set_complete( + category.images, + band.matched, + band.unmatched, + band.resource_samples, + ) + }) + }) +} + +#[derive(Serialize)] +struct Report { + schema: u32, + libwebp_version: &'static str, + reference_lossless: &'static str, + reference_lossy: &'static str, + fidelity: &'static str, + privacy: &'static str, + max_edge: u32, + images: usize, + skipped_entries: usize, + resource_sampling: &'static str, + resource_samples: usize, + lossy_input_layout: &'static str, + environment: Environment, + categories: Vec, + overall: CategorySummary, + gates: Gates, +} + +/// Run the anonymous, matched-fidelity real-image comparison and save its latest local-only report under `target/surpass/latest.json`. +#[allow( + clippy::too_many_lines, + reason = "campaign orchestration keeps reference validation, anonymous aggregation, and all \ + completion gates in one auditable transaction" +)] +pub(crate) fn surpass(real: &Path, max_edge: u32) -> Result<()> { + ensure!( + max_edge == 1024, + "formal surpass measurement requires --max-edge 1024" + ); + ensure!( + !cfg!(debug_assertions), + "formal surpass measurement requires a --release build" + ); + ensure_scalar_product_features()?; + let root = workspace_root()?; + let path = root.join("target/surpass/latest.json"); + let parent = path.parent().context("surpass report has no parent")?; + std::fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?; + match std::fs::remove_file(&path) { + Ok(()) => {}, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}, + Err(error) => { + return Err(error).with_context(|| format!("invalidating {}", path.display())); + }, + } + if !lw::AVAILABLE { + bail!( + "surpass requires the pinned in-process libwebp reference; rerun with \ + `--features scoreboard-libwebp`" + ); + } + let cwebp = cwebp_bin(); + check_version(&cwebp, "cwebp", "WEBPKIT_CWEBP")?; + verify_cwebp_image_hint(&cwebp)?; + ensure!( + lw::decoder_version() == REQUIRED_LIBWEBP, + "linked libwebp reports {}, required {REQUIRED_LIBWEBP}", + lw::decoder_version() + ); + let rust_toolchain = rust_toolchain()?; + let (source_revision, source_dirty) = source_metadata(&root)?; + + let prepared = prepare_real_images_max_edge(&cwebp, real, max_edge) + .map_err(|_| anyhow!("surpass failed to prepare the anonymous input corpus"))?; + let mut images: Vec<(Category, RealImage)> = Vec::new(); + let mut skipped = 0usize; + for item in prepared { + let RealPrep::Ready(image) = item else { + skipped += 1; + continue; + }; + images.push((classify(&image.rgba), image)); + } + ensure!(!images.is_empty(), "surpass found no decodable images"); + + // Validate every external reference before starting any expensive 101-point RD curve. + // A missing/failed cwebp transaction therefore fails early instead of discarding hours of otherwise valid anonymous corpus work. + let mut cwebp_references = Vec::with_capacity(images.len()); + for (_, image) in &images { + cwebp_references.push( + prepare_cwebp_lossy_references(&cwebp, image) + .map_err(|_| anyhow!("surpass failed to validate anonymous cwebp references"))?, + ); + } + + // Independent image size/RD transactions run in a fixed four-worker wave. + // Each codec invocation remains portable scalar; this only shortens corpus wall time and cannot affect bytes, fidelity selection, or row ordering. + let workers = images.len().min(CORPUS_WORKERS); + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::scope(|scope| { + for worker in 0..workers { + let sender = sender.clone(); + let images = &images; + let cwebp_references = &cwebp_references; + scope.spawn(move || { + for index in (worker..images.len()).step_by(workers) { + let (category, image) = &images[index]; + if sender + .send(( + index, + measure_image(*category, image, &cwebp_references[index]), + )) + .is_err() + { + break; + } + } + }); + } + }); + drop(sender); + + let mut measured = Vec::with_capacity(images.len()); + measured.resize_with(images.len(), || None); + for _ in 0..images.len() { + let (index, measurement) = receiver + .recv() + .context("receiving anonymous corpus measurement")?; + measured[index] = Some(measurement?); + } + + // Populate timing and process peaks for every image without rebuilding its 101-point RD curve. + // These transactions are serial so one sample cannot perturb another sample's paired A/B timing or OS peak observation. + for index in 0..images.len() { + let (category, image) = &images[index]; + let measurement = measured[index] + .as_mut() + .expect("every anonymous image completed size/RD measurement"); + populate_image_resources(*category, image, measurement)?; + } + let rows: Vec<(Category, ImageMeasurement)> = images + .iter() + .zip(measured) + .map(|((category, _), measurement)| { + ( + *category, + measurement.expect("every anonymous image is measured"), + ) + }) + .collect(); + + let categories: Vec = Category::ALL + .into_iter() + .filter_map(|category| { + let selected: Vec<&ImageMeasurement> = rows + .iter() + .filter_map(|(row_category, row)| (*row_category == category).then_some(row)) + .collect(); + (!selected.is_empty()).then(|| summarize(category.label(), &selected)) + }) + .collect(); + let all: Vec<&ImageMeasurement> = rows.iter().map(|(_, row)| row).collect(); + let overall = summarize("overall", &all); + + let lossless_size = overall + .lossless + .ratio_permille_median + .is_some_and(|v| v <= 1005) + && overall.lossless.at_or_below_reference * 2 >= overall.lossless.images; + let lossy_overall = overall + .lossy + .iter() + .all(|band| band.unmatched == 0 && band.ratio_permille_median.is_some_and(|v| v <= 1050)); + let lossy_photo = categories + .iter() + .find(|category| category.category == "photo") + .is_some_and(|photo| { + photo.lossy.iter().all(|band| { + band.unmatched == 0 && band.ratio_permille_median.is_some_and(|v| v <= 1030) + }) + }); + let resource_complete = resource_coverage_complete(&overall, &categories); + let encode_speed = resource_complete + && overall + .lossless + .encode_time_ratio_permille_median + .is_some_and(|v| v <= 2000) + && overall.lossy.iter().all(|band| { + band.encode_time_ratio_permille_median + .is_some_and(|v| v <= 2000) + }); + let decode_speed = resource_complete + && overall + .lossless + .decode_time_ratio_permille_median + .is_some_and(|v| v <= 1500) + && overall.lossy.iter().all(|band| { + band.decode_time_ratio_permille_median + .is_some_and(|v| v <= 1500) + }); + let peak_memory = resource_complete + && overall + .lossless + .encode_peak_ratio_permille_max + .is_some_and(|ratio| ratio <= 1500) + && overall + .lossless + .decode_peak_ratio_permille_max + .is_some_and(|ratio| ratio <= 1500) + && overall.lossy.iter().all(|band| { + band.encode_peak_ratio_permille_max + .is_some_and(|ratio| ratio <= 1500) + && band + .decode_peak_ratio_permille_max + .is_some_and(|ratio| ratio <= 1500) + }); + let lossy_size_and_fidelity = lossy_overall && lossy_photo; + let complete = measurement_complete( + skipped, + [ + lossless_size, + lossy_size_and_fidelity, + encode_speed, + decode_speed, + peak_memory, + ], + ); + let gates = Gates { + lossless_size, + lossy_size_and_fidelity, + encode_speed, + decode_speed, + same_executable_peak_memory: Some(peak_memory), + measurement_complete: complete, + }; + let report = Report { + schema: 4, + libwebp_version: REQUIRED_LIBWEBP, + reference_lossless: "cwebp -lossless -exact -m 6 -q 100 (single-threaded: -mt omitted)", + reference_lossy: "cwebp -q {50,75,90} -noalpha (single-threaded: -mt omitted)", + fidelity: "minimum webpkit bytes with RGB-PSNR and SSIM both >= libwebp", + privacy: "category aggregates only; no path, file name, or per-image result", + max_edge, + images: rows.len(), + skipped_entries: skipped, + resource_sampling: "every recognized image, serially after the fixed four-worker size/RD phase", + resource_samples: rows.len(), + lossy_input_layout: "opaque RGBA8 for both encoders; alpha excluded", + environment: Environment { + os: std::env::consts::OS, + architecture: std::env::consts::ARCH, + cpu_model: cpu_model(), + rust_toolchain, + source_revision, + source_dirty, + debug_assertions: cfg!(debug_assertions), + logical_cpus: std::thread::available_parallelism().map_or(1, usize::from), + corpus_workers: workers, + timing_pairs: TIMING_PAIRS, + timing_warmups_per_implementation: TIMING_WARMUPS_PER_IMPLEMENTATION, + timing_samples_per_implementation: TIMING_PAIRS * 2, + enabled_product_features: "none (rayon and work-count rejected)", + webpkit_path: "safe Rust portable scalar (rayon disabled)", + }, + categories, + overall, + gates, + }; + + let json = format!("{}\n", serde_json::to_string_pretty(&report)?); + let mut temporary = + tempfile::NamedTempFile::new_in(parent).context("creating temporary surpass report")?; + temporary + .write_all(json.as_bytes()) + .context("writing temporary surpass report")?; + temporary + .as_file() + .sync_all() + .context("syncing temporary surpass report")?; + temporary + .persist(&path) + .map_err(|error| error.error) + .with_context(|| format!("publishing {}", path.display()))?; + + println!( + "surpass: {} image(s), {} skipped; anonymous report -> {}", + rows.len(), + skipped, + path.display() + ); + println!( + " gates: lossless-size={} lossy-matched={} encode-speed={} decode-speed={} memory={} measurement-complete={}", + report.gates.lossless_size, + report.gates.lossy_size_and_fidelity, + report.gates.encode_speed, + report.gates.decode_speed, + report.gates.same_executable_peak_memory.unwrap_or(false), + report.gates.measurement_complete, + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::path::Path; + + use anyhow::anyhow; + + use super::{ + Category, WorkerRequest, classify, ensure_scalar_product_features, + lossy_sample_set_complete, measurement_complete, median, opaque_rgba, paired_medians, + ratio_permille, sample_set_complete, wrap_vp8, + }; + + #[test] + fn anonymous_helpers_are_deterministic() { + assert_eq!(median(vec![9, 1, 5]), Some(5)); + assert_eq!(ratio_permille(1005, 1000), 1005); + assert_eq!(ratio_permille(10_059, 10_000), 1006); + assert_eq!(ratio_permille(10_050, 10_000), 1005); + let rgba = [1, 2, 3, 255].repeat(4); + assert!(matches!(classify(&rgba), Category::Graphic)); + let mut many_colors = Vec::with_capacity(8193 * 4); + for value in 0..8193u32 { + let bytes = value.to_le_bytes(); + many_colors.extend_from_slice(&[bytes[0], bytes[1], bytes[2], 255]); + } + assert!(matches!(classify(&many_colors), Category::Photo)); + many_colors[3] = 0; + assert!(matches!(classify(&many_colors), Category::Transparent)); + assert!(matches!( + classify(&[1, 2, 3, 0].repeat(4)), + Category::FlatIcon + )); + let wrapped = wrap_vp8(&[1, 2, 3]); + assert_eq!(&wrapped[..4], b"RIFF"); + assert_eq!(&wrapped[12..16], b"VP8 "); + assert_eq!(wrapped.len() & 1, 0); + } + + #[test] + fn complete_requires_every_measurement_gate_and_no_skips() { + assert!(measurement_complete(0, [true; 5])); + assert!(!measurement_complete(1, [true; 5])); + for failed in 0..5 { + let mut gates = [true; 5]; + gates[failed] = false; + assert!(!measurement_complete(0, gates)); + } + } + + #[test] + fn resource_coverage_requires_every_image_and_lossy_match() { + assert!(sample_set_complete(39, 39)); + assert!(!sample_set_complete(39, 38)); + assert!(lossy_sample_set_complete(39, 39, 0, 39)); + assert!(!lossy_sample_set_complete(39, 38, 1, 38)); + assert!(!lossy_sample_set_complete(39, 39, 0, 38)); + } + + #[test] + fn opaque_rgba_discards_alpha_only() { + assert_eq!( + opaque_rgba(&[1, 2, 3, 0, 4, 5, 6, 127]), + [1, 2, 3, 255, 4, 5, 6, 255] + ); + } + + #[test] + fn paired_timing_warms_once_and_collects_six_samples_each() { + let mut ours_calls = 0; + let mut reference_calls = 0; + let trace = RefCell::new(Vec::new()); + paired_medians( + || { + ours_calls += 1; + trace.borrow_mut().push('A'); + Ok(ours_calls) + }, + || { + reference_calls += 1; + trace.borrow_mut().push('B'); + Ok(reference_calls) + }, + ) + .expect("paired timing"); + assert_eq!(ours_calls, 7); + assert_eq!(reference_calls, 7); + assert_eq!( + trace.into_inner().into_iter().collect::(), + "ABABBAABBAABBA" + ); + + let error = paired_medians(|| Err(anyhow!("warm-up failed")), || Ok(1)) + .expect_err("operation failure must propagate"); + assert_eq!(error.to_string(), "warm-up failed"); + } + + #[test] + fn worker_request_preserves_a_path_containing_spaces() { + let request = WorkerRequest::new( + "webpkit-lossless-encode", + Path::new(r"C:\temporary files\anonymous input.rgba"), + 17, + 19, + 75, + ); + let json = serde_json::to_string(&request).unwrap(); + let decoded: WorkerRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded.input, request.input); + assert_eq!(decoded.kind, request.kind); + assert_eq!(decoded.width, 17); + assert_eq!(decoded.height, 19); + assert_eq!(decoded.quality, 75); + } + + #[test] + fn scalar_gate_observes_dependency_side_feature_unification() { + assert_eq!( + ensure_scalar_product_features().is_ok(), + !webpkit::INTERNAL_RAYON_ENABLED && !webpkit::INTERNAL_WORK_COUNT_ENABLED + ); + } +} diff --git a/xtask/src/work.rs b/xtask/src/work.rs index 65daeed..3988ae3 100644 --- a/xtask/src/work.rs +++ b/xtask/src/work.rs @@ -1,7 +1,8 @@ //! Work-cost ledger (`corpus/work.json`): committed per-(codec, sample, method) -//! deterministic algorithmic-work counters — the toolchain-independent third -//! measurement plane. Available only with the `work-count` feature; a stub bails -//! otherwise so the ordinary `metrics` job stays free of counter code. +//! deterministic encode/decode algorithmic-work counters — the +//! toolchain-independent third measurement plane. Available only with the +//! `work-count` feature; a stub bails otherwise so the ordinary `metrics` job +//! stays free of counter code. #[cfg(feature = "work-count")] use anyhow::Context; @@ -17,7 +18,7 @@ use crate::common::{ use crate::ledger::{FieldDelta, load_ledger, print_field_deltas}; /// One committed work-cost row: the integer algorithmic-work counters recorded -/// while encoding one synthetic sample with one codec+method. +/// while encoding or decoding one synthetic sample. /// /// The `counts` map is keyed by [`webpkit::work_count::field_names`], so it renders /// as a deterministic (BTreeMap-sorted) JSON object and auto-tracks the counter @@ -30,13 +31,14 @@ use crate::ledger::{FieldDelta, load_ledger, print_field_deltas}; struct WorkCase { /// Stable sort key: `"{codec}/{content}_{edge:03}/{method}"`. id: String, - /// Codec label (`"lossless"` = webpkit-lossless | `"lossy"` = webpkit-lossy). + /// Codec/operation label (`"lossless"`, `"lossless-decode"`, `"lossy"`, + /// or `"lossy-decode"`). codec: String, /// Content archetype name (e.g. `"photo"`, `"gradient"`). content: String, /// Square edge length in pixels. edge: u32, - /// Encoder method label (`"fast"` | `"balanced"` | `"best"`). + /// Encoder method label, or `"decode"` for a decoder row. method: String, /// Per-hot-path work counts, keyed by [`webpkit::work_count::field_names`]. counts: std::collections::BTreeMap, @@ -52,6 +54,14 @@ struct WorkLedger { cases: Vec, } +/// Schema-3 shape over the fixed six-archetype × three-edge matrix. +#[cfg(feature = "work-count")] +const WORK_SCHEMA_VERSION: u32 = 3; +#[cfg(feature = "work-count")] +const WORK_EXPECTED_CASES: usize = 132; +#[cfg(feature = "work-count")] +const WORK_EXPECTED_DECODE_CASES_PER_CODEC: usize = 18; + /// Describe the first case where a fresh work run diverges from the committed /// ledger (mirrors `first_metrics_divergence`). #[cfg(feature = "work-count")] @@ -112,7 +122,7 @@ fn explain_work(committed: &WorkLedger, fresh: &WorkLedger) { /// Turn a raw counter snapshot into a name-keyed map for the ledger. #[cfg(feature = "work-count")] -fn counts_map(snapshot: [u64; webpkit::work_count::N]) -> std::collections::BTreeMap { +fn counts_map(snapshot: &[u64; webpkit::work_count::N]) -> std::collections::BTreeMap { webpkit::work_count::field_names() .iter() .zip(snapshot.iter()) @@ -129,7 +139,7 @@ fn counts_map(snapshot: [u64; webpkit::work_count::N]) -> std::collections::BTre fn measure_work_lossless( sample: &webpkit_samples::Sample, method: webpkit::lossless::Effort, -) -> Result { +) -> Result<(WorkCase, Vec)> { let dims = webpkit::lossless::Dimensions::new(sample.edge, sample.edge)?; let image = webpkit::lossless::ImageRef::new( dims, @@ -139,21 +149,24 @@ fn measure_work_lossless( let config = webpkit::lossless::EncoderConfig::new().with_effort(method); webpkit::work_count::reset(); - let _webp = webpkit::lossless::encode(image, &config)?; - let counts = counts_map(webpkit::work_count::snapshot()); + let webp = webpkit::lossless::encode(image, &config)?; + let counts = counts_map(&webpkit::work_count::snapshot()); - Ok(WorkCase { - id: format!( - "lossless/{}/{}", - webpkit_samples::sample_id(sample.content, sample.edge), - method_name(method) - ), - codec: "lossless".to_owned(), - content: sample.content.name().to_owned(), - edge: sample.edge, - method: method_name(method).to_owned(), - counts, - }) + Ok(( + WorkCase { + id: format!( + "lossless/{}/{}", + webpkit_samples::sample_id(sample.content, sample.edge), + method_name(method) + ), + codec: "lossless".to_owned(), + content: sample.content.name().to_owned(), + edge: sample.edge, + method: method_name(method).to_owned(), + counts, + }, + webp, + )) } /// Encode one sample lossily (default quality) with the counters reset, and @@ -171,7 +184,7 @@ fn measure_work_lossy( webpkit::work_count::reset(); let _ = webpkit::lossy::encode_vp8(image, &cfg)?; - let counts = counts_map(webpkit::work_count::snapshot()); + let counts = counts_map(&webpkit::work_count::snapshot()); Ok(WorkCase { id: format!( @@ -215,7 +228,7 @@ fn measure_work_lossy_decode(sample: &webpkit_samples::Sample) -> Result Result Result { + webpkit::work_count::reset(); + let _ = webpkit::lossless::decode(webp)?; + let counts = counts_map(&webpkit::work_count::snapshot()); + + Ok(WorkCase { + id: format!( + "lossless-decode/{}/decode", + webpkit_samples::sample_id(sample.content, sample.edge), + ), + codec: "lossless-decode".to_owned(), + content: sample.content.name().to_owned(), + edge: sample.edge, + method: "decode".to_owned(), + counts, + }) +} + /// The counter increments make the `Best` search several times slower than in a /// normal build, and its `Best@512` cases (lossless meta-Huffman clustering, /// lossy trellis + i4x4) dominate the run. `Best` is therefore capped to @@ -246,12 +280,23 @@ const WORK_BEST_MAX_EDGE: u32 = 256; fn compute_work() -> Result { let mut cases = Vec::new(); for sample in webpkit_samples::matrix() { + let mut lossless_decode_input = None; for &method in &ALL_METHODS { if method == webpkit::lossless::Effort::level(9) && sample.edge > WORK_BEST_MAX_EDGE { continue; } - cases.push(measure_work_lossless(&sample, method)?); + let (case, webp) = measure_work_lossless(&sample, method)?; + if method == webpkit::lossless::Effort::AUTO { + lossless_decode_input = Some(webp); + } + cases.push(case); } + let lossless_decode_input = + lossless_decode_input.context("ALL_METHODS must contain lossless Effort::AUTO")?; + cases.push(measure_work_lossless_decode( + &sample, + &lossless_decode_input, + )?); for &method in &LOSSY_METHODS { if method == webpkit::lossy::Effort::level(9) && sample.edge > WORK_BEST_MAX_EDGE { continue; @@ -262,7 +307,33 @@ fn compute_work() -> Result { cases.push(measure_work_lossy_decode(&sample)?); } cases.sort_by(|a, b| a.id.cmp(&b.id)); - Ok(WorkLedger { version: 2, cases }) + let lossless_decode_cases = cases + .iter() + .filter(|case| case.codec == "lossless-decode") + .count(); + let lossy_decode_cases = cases + .iter() + .filter(|case| case.codec == "lossy-decode") + .count(); + if cases.len() != WORK_EXPECTED_CASES + || lossless_decode_cases != WORK_EXPECTED_DECODE_CASES_PER_CODEC + || lossy_decode_cases != WORK_EXPECTED_DECODE_CASES_PER_CODEC + { + bail!( + "internal work-ledger shape drift: {} total, {} lossless-decode, {} lossy-decode \ + (expected {}, {}, {})", + cases.len(), + lossless_decode_cases, + lossy_decode_cases, + WORK_EXPECTED_CASES, + WORK_EXPECTED_DECODE_CASES_PER_CODEC, + WORK_EXPECTED_DECODE_CASES_PER_CODEC, + ); + } + Ok(WorkLedger { + version: WORK_SCHEMA_VERSION, + cases, + }) } /// Gate (or `--bless`) the committed `corpus/work.json` deterministic work-cost @@ -339,3 +410,23 @@ pub(crate) fn work(_bless: bool, _explain: bool) -> Result<()> { `--features work-count` (or run `just work` / `just work-bless`)" ) } + +#[cfg(all(test, feature = "work-count"))] +mod tests { + use super::{ + WORK_EXPECTED_CASES, WORK_EXPECTED_DECODE_CASES_PER_CODEC, WORK_SCHEMA_VERSION, counts_map, + }; + + #[test] + fn schema_three_shape_and_counter_vocabulary_are_pinned() { + assert_eq!(WORK_SCHEMA_VERSION, 3); + assert_eq!(WORK_EXPECTED_CASES, 132); + assert_eq!(WORK_EXPECTED_DECODE_CASES_PER_CODEC, 18); + + let counts = counts_map(&[0; webpkit::work_count::N]); + assert_eq!(webpkit::work_count::N, 43); + assert_eq!(counts.len(), 43); + assert!(counts.contains_key("vp8l_bit_refill")); + assert!(counts.contains_key("vp8l_pack_pixel")); + } +}