Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
fc9e589
feat(png): account every byte of a PNG with deconstruct
justin13888 Sep 1, 2026
27359a2
refactor(png): expose the encoder stages behind test-support
justin13888 Sep 1, 2026
92a1474
chore(png): benchmark encode size, bpp and per-stage throughput
justin13888 Sep 1, 2026
78466a1
test(png): pin the output size against libpng at maximum compression
justin13888 Sep 1, 2026
ded5128
feat(png): opt-in cleanup of invisible pixel colour
justin13888 Sep 1, 2026
6b31ab9
fix(png): keep the palette only when it is actually smaller
justin13888 Sep 1, 2026
85beb2f
perf(png): accelerate CRC-32 and the scanline filter loops
justin13888 Sep 1, 2026
fddc749
docs: record the gamut-png efficiency baseline and the benchmarking c…
justin13888 Sep 1, 2026
1cc51fd
test(png): close the mutation gaps in the new efficiency code
justin13888 Sep 1, 2026
f360e51
feat(cli): inspect PNG byte accounting
justin13888 Sep 1, 2026
54eb160
feat(png): reduce binary alpha to a tRNS colour key
justin13888 Sep 1, 2026
1529ab0
docs(png): record the colour key in the axis scorecard
justin13888 Sep 1, 2026
364efa9
feat(png): order the palette, and close the colour-key mutation gaps
justin13888 Sep 1, 2026
cb1c377
docs(png): refresh the efficiency tables after palette ordering
justin13888 Sep 1, 2026
4fa858b
feat(png): entropy and bigram filter heuristics
justin13888 Sep 1, 2026
319c345
fix(png): emit a filtered row when every filter candidate ties
justin13888 Sep 1, 2026
ea4a9e2
refactor(png): read the bigram index as one big-endian pair
justin13888 Sep 1, 2026
73e9c0b
refactor(png): make the colour-key arms total instead of unreachable
justin13888 Sep 1, 2026
0c27e29
test(png): separate palette ordering from discovery order
justin13888 Sep 1, 2026
934a76f
perf(png): index the chunk tally by type
justin13888 Sep 1, 2026
25b1a14
feat(png): clean invisible colour on the 16-bit paths too
justin13888 Sep 1, 2026
dbb0d80
docs: settle the counter rule, the png authority row and two counts
justin13888 Sep 1, 2026
82220cc
fix(png): budget the quantity the decoder budgets
justin13888 Sep 1, 2026
c16ac22
test(png): cover the greyscale colour key
justin13888 Sep 1, 2026
ef73616
test(png): pin the size race a colour key can lose
justin13888 Sep 1, 2026
589261f
test(png): re-pin sub-byte indexed auto-reduce
justin13888 Sep 1, 2026
8dcac02
fix(png-cli): say why the filter scan was skipped
justin13888 Sep 1, 2026
0d680a8
docs(cli): state what inspect's exit code means per format
justin13888 Sep 1, 2026
e858818
refactor(png): delete choose_min_sum_abs
justin13888 Sep 1, 2026
9ca0f19
test(png): derive every size budget from its measurement
justin13888 Sep 1, 2026
8e9f038
fix(png): race the cleaned encoding instead of assuming it wins
justin13888 Sep 1, 2026
448c3f6
docs(png): say which efficiency tables are gated and which only report
justin13888 Sep 2, 2026
c831147
style(png): drop a stray blank line in the size contract
justin13888 Sep 2, 2026
c04b8d4
chore: merge origin/master into feat/224-png-encoder-efficiency
justin13888 Sep 2, 2026
e2c38fb
test(png): pin the cleanup tie-break and the entropy weighting
justin13888 Sep 2, 2026
7593fe5
feat(png): bound the deconstruct walk and name what it actually read
justin13888 Sep 2, 2026
a0bde8e
test(png): pin the walk's ceilings, its saturation and the unread ver…
justin13888 Sep 2, 2026
cd70f78
fix(cli): gate inspect on what it read, and bound the lists it prints
justin13888 Sep 2, 2026
b20f9d4
feat(png): seal FilterStrategy, and clear only the bigram words a row…
justin13888 Sep 2, 2026
5a363fc
refactor(png): drop a palette sort key that cannot change the order
justin13888 Sep 2, 2026
bec1e5b
test(png): pin the greyscale colour key the race declines
justin13888 Sep 2, 2026
423a0e6
docs(png): correct the cost model and the size claim against the encoder
justin13888 Sep 2, 2026
332af8d
docs: record the crc32fast approval for gamut-png
justin13888 Sep 2, 2026
97567f5
test(png): kill the five mutants the new walk code left alive
justin13888 Sep 2, 2026
9d7f770
chore: merge origin/master into feat/224-png-encoder-efficiency
justin13888 Sep 5, 2026
49189a6
fix(png): emit bKGD and sBIT for the colour type actually written
justin13888 Sep 5, 2026
5e2807c
fix(png): bound the filter scan's inflation by the stream that claims it
justin13888 Sep 6, 2026
1851bb0
fix(png): count chunks, not segments, against max_chunks
justin13888 Sep 6, 2026
90ff376
test(png): pin the chunk tally's constant-time lookup structurally
justin13888 Sep 6, 2026
4517679
docs(png): record what the races cost and how the chunks follow them
justin13888 Sep 6, 2026
2c08480
docs(cli): state why inspect's verification gate is PNG-only
justin13888 Sep 6, 2026
eabb0bd
docs(png)!: record FilterStrategy as non-exhaustive
justin13888 Sep 6, 2026
5f8e71b
test(png): a grey sBIT needs all three channels to agree, not two
justin13888 Sep 6, 2026
589df4f
fix(png): resolve a background against the written palette's alpha an…
justin13888 Sep 6, 2026
e1e39b3
test(png): count the tally's lookup probes instead of trusting its shape
justin13888 Sep 6, 2026
fac39dc
docs(png): qualify the bKGD/sBIT contract by axis and state what a bo…
justin13888 Sep 6, 2026
deb28c7
fix(png): race the chunk-free reduction the raw estimate eliminated
justin13888 Sep 6, 2026
97cb438
fix(png): keep the byte-exact encoding when cleanup ties on size
justin13888 Sep 6, 2026
e943666
fix(png): charge the tally's probe counter per entry examined
justin13888 Sep 6, 2026
e8441df
test(png): shrink the distinct-type fixture to the count it needs
justin13888 Sep 6, 2026
cda73e3
feat(png): name the inflation-ratio refusal apart from the byte budget
justin13888 Sep 6, 2026
8a0fc3c
fix(png): count IHDR against the walk's chunk ceiling
justin13888 Sep 6, 2026
57fa923
fix(cli): restore the truncated-list notice for TIFF and DNG
justin13888 Sep 6, 2026
0595ee8
docs(cli): record the gamut inspect exit-code contract in docs/
justin13888 Sep 6, 2026
c59df6f
style(png): rustfmt the chunk-ceiling closure and its boundary test
justin13888 Sep 6, 2026
8373af0
docs(png): say what moving the chunk ceiling into push actually fixed
justin13888 Sep 9, 2026
cce9939
docs(png): state the tie-break rule once, without appealing to encode…
justin13888 Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ Dependency edges (a crate depends on those to its right):
hostile input, ancillary metadata surfaced as raw `MetadataBlock`-ready payloads
(eXIf/iCCP/XMP/text) plus parsed gAMA/cHRM/sRGB/cICP. APNG out of scope (decodes as the
default image). Differential oracle both directions: libpng, which also *generates* the
decoder's conformance fixtures. ← core, deflate (+ `miniz_oxide` for inflate).
decoder's conformance fixtures. ← core, deflate (+ `miniz_oxide` for inflate, and
**maintainer-approved `crc32fast`** for the chunk CRC that every encode pays on its critical
path — hardware CRC-32 on x86-64/aarch64, table fallback elsewhere including wasm32, and it
keeps its `unsafe` to itself, so gamut-png stays `#![deny(unsafe_code)]`).
- **gamut-ifd** — TIFF/IFD container core (byte order, field types, IFD read/write); a
low-level container primitive (sibling to bitstream), shared by `gamut-tiff` and EXIF
metadata. ← core. Optional `bigtiff` feature adds 64-bit BigTIFF. Per-format metadata
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ cargo test --workspace
| `mise run lint-fix` | Lint and auto-fix |
| `mise run check-commits` | Check commits are Conventional Commits |
| `mise run coverage` | Run tests with coverage (min 80%) |
| `mise run bench` | Run performance benchmarks (Divan; see [docs/benchmarking.md](docs/benchmarking.md)) |
| `mise run bench-test` | Run every bench once to prove it still executes (no timings) |
| `mise run check-cross <triple>` | Cross-compile-check the libs for a target (extended CI; master/manual) |
| `mise run check-msrv` | Check the libs compile on the documented MSRV (extended CI; master/manual) |
| `mise run versions` | List every crate's version |
Expand Down
318 changes: 311 additions & 7 deletions crates/gamut-cli/src/commands/inspect.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion crates/gamut-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ struct Cli {
enum Command {
/// Decode an image (PNG/JPEG/PPM/WebP/JXL) and re-encode it as AVIF/WebP/TIFF/PNG/JXL/JPEG.
Convert(commands::convert::ConvertArgs),
/// Strictly deconstruct a TIFF or DNG: account every byte and flag unknowns (gamut-tiff/gamut-dng).
/// Strictly deconstruct a TIFF, DNG or PNG: account every byte, flag unknowns, and for PNG report where the bytes went (gamut-tiff/gamut-dng/gamut-png).
Inspect(commands::inspect::InspectArgs),
/// Extract and inspect the embedded ICC colour profile of an image (gamut-icc).
Icc(commands::icc::IccArgs),
Expand Down
22 changes: 22 additions & 0 deletions crates/gamut-png/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ categories.workspace = true
[lints]
workspace = true

[features]
# Re-exports the encoder's pipeline stages (`src/stages.rs`) so `benches/encode.rs` -- an external
# crate, which can only see `pub` -- can time them one at a time (issue #224). Additive,
# `doc(hidden)`, no SemVer guarantee, and never enabled by the `gamut` umbrella, so the shipped
# surface and `mise run check-ffi-features` are unaffected. The module is re-exports only, so it
# adds no coverage regions and no mutants.
test-support = []

[dependencies]
gamut-core.workspace = true
# The shared codestream-backend seam (issue #272): the `repr(C)` vtable + fallback contract the
Expand All @@ -26,10 +34,24 @@ gamut-deflate.workspace = true
# is deliberately encoder-only, and its docs bless miniz_oxide as the decode-side inflate (the same
# choice gamut-dng made); revisiting an in-house inflater is tracked by issue #196.
miniz_oxide = "0.8"
# CRC-32 (ISO-HDLC) for every chunk, IDAT included, so it is on the critical path of every encode.
# Hardware-accelerated (x86-64 PCLMULQDQ/AVX-512, aarch64 `crc32`) with a table fallback elsewhere,
# including wasm32. MIT/Apache-2.0, pure Rust, and it keeps its `unsafe` to itself -- gamut-png
# stays 100% safe Rust. Replaces a hand-written byte-at-a-time table loop; see `src/crc32.rs`.
crc32fast = "1.5"

[dev-dependencies]
# Differential cross-check oracle: a vendored, statically-linked libpng (built from the
# `third_party/libpng` submodule). Dev-only and excluded from the workspace. libpng proves both
# directions: it decodes the gamut encoder's output, and it generates the fixture corpus (and the
# reference pixels) the gamut decoder is differentially checked against.
libpng-oracle = { path = "../../tooling/libpng-oracle" }
# Benchmark harness (issue #149) plus this crate's own `test-support` feature, which the bench
# target needs to reach the pipeline stages. A self dev-dependency is the standard way to enable an
# own feature for tests and benches only; `mise run check-release-deps` skips self-edges.
divan.workspace = true
gamut-png = { path = ".", features = ["test-support"] }

[[bench]]
name = "encode"
harness = false
7 changes: 6 additions & 1 deletion crates/gamut-png/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@ A differential oracle (`tooling/libpng-oracle`, a vendored static libpng) proves
libpng decodes the encoder's output pixel-exact, and a libpng *reference encoder* generates the
decoder's conformance fixtures (interlaced, sub-byte, forced-filter, metadata-laden) which both
decoders must read identically — no vendored image corpus. A hand-crafted malformed-input corpus
pins the rejection policy, and output size is benchmarked against libpng at maximum compression.
pins the rejection policy. Output size is measured against libpng at zlib level 9 by
`cargo bench -p gamut-png`, and **enforced** by `tests/size_contract.rs`, whose per-case budgets
each carry a written justification — a regression in the crate's reason to exist fails the build.
`STATUS.md` records the measured table; gamut is smaller than libpng-9 on every corpus entry, by
28-85% wherever a reduction or a filter choice applies and by 0.2% on the incompressible noise row,
where there is nothing for either encoder to find.

## License

Expand Down
Loading
Loading