Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 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
21602ed
feat(png): carry the C2PA manifest store in the caBX chunk
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
7f60deb
Merge remote-tracking branch 'origin/feat/224-png-encoder-efficiency'…
justin13888 Sep 9, 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
5ee185b
Merge remote-tracking branch 'origin/feat/224-png-encoder-efficiency'…
justin13888 Sep 9, 2026
cb30ea8
fix(png): end the C2PA store walk with the datastream
justin13888 Sep 9, 2026
0342473
feat(png): fill a reserved C2PA store in place
justin13888 Sep 9, 2026
704fa25
fix(png): never read a caBX after IDAT as the manifest store
justin13888 Sep 9, 2026
a4f9e7f
refactor(png): name what a metadata walk returns
justin13888 Sep 9, 2026
35957e0
docs(png): separate C2PA carriage from what a decode admits
justin13888 Sep 9, 2026
60aec64
test(png): pin the exact fit in the C2PA fill's bounds check
justin13888 Sep 9, 2026
eca33fa
fix(png): reject a C2PA span the chunk's own length contradicts
justin13888 Sep 9, 2026
c49be4c
fix(png): count every ignored C2PA store, and say what is not counted
justin13888 Sep 9, 2026
e1dddb7
refactor(png): read the C2PA chunk header as bytes, not offsets
justin13888 Sep 9, 2026
0a7e665
feat(png): preserve metadata across a re-encode, and refuse the chunk…
justin13888 Sep 10, 2026
d8c2ffd
test(png): pin Latin-1 text, the refused chunk pairs, and what a re-e…
justin13888 Sep 10, 2026
d7868bc
fix(png)!: implement §11.3.3's text clauses and stop refusing the col…
justin13888 Sep 10, 2026
47b42c3
feat(cli): say what metadata a conversion could not carry
justin13888 Sep 10, 2026
0a78c0f
docs(png): record the clauses metadata preservation implements
justin13888 Sep 10, 2026
0314208
test(png): pin what end_carry separates and what a dropped payload is…
justin13888 Sep 10, 2026
cc52efd
fix(png)!: carry an XMP packet's framing, and report what §11.3.3 onl…
justin13888 Sep 10, 2026
5d71425
docs(png): correct what preservation carries and what it only reports
justin13888 Sep 10, 2026
beda774
test(png): pin both readings of an XMP chunk's iTXt framing
justin13888 Sep 10, 2026
64a61f4
docs(png): say whose job the colour-chunk ranking is
justin13888 Sep 10, 2026
9243d2e
docs(png): cite the sections the vendored spec actually numbers
justin13888 Sep 10, 2026
cbf3e02
refactor(png): give the trailing-opaque tRNS trim one owner
justin13888 Sep 10, 2026
1f3ef91
feat(png): clean a caller-supplied palette before writing it
justin13888 Sep 10, 2026
a7737fc
docs(png): record what cleaning a caller's palette buys
justin13888 Sep 10, 2026
7b1cc00
fix(png): trim the trailing opaque alphas without a loop that can hang
justin13888 Sep 10, 2026
87f3429
fix(png): keep the palette entry a colour-form background names
justin13888 Sep 10, 2026
48cdb2b
docs(png): say which background forms cleaning keeps, and name OPAQUE…
justin13888 Sep 10, 2026
ac3aba9
refactor(png): renumber the background unconditionally
justin13888 Sep 10, 2026
bfb5b26
docs(png): rewrap the bKGD paragraph to the file's column
justin13888 Sep 10, 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
50 changes: 49 additions & 1 deletion crates/gamut-cli/src/commands/convert.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! `gamut convert` — decode an image and re-encode it with a gamut codec.

use std::path::PathBuf;
use std::path::{Path, PathBuf};

use clap::{Args, ValueEnum};
use gamut::avif::AvifEncoder;
Expand Down Expand Up @@ -86,6 +86,16 @@ pub(crate) struct ConvertArgs {
/// for other output formats.
#[arg(long)]
jxl_container: bool,
/// Drop the input's metadata instead of carrying it into the output. By default a PNG input
/// re-encoded to PNG keeps its EXIF, ICC profile, XMP packet, text annotations and colour
/// chunks; a stripped file is smaller, an unstripped one is colour-accurate, so the default
/// is the one that loses nothing. Anything that cannot be carried — the C2PA manifest store,
/// signed over the bytes of the file it was made for — and anything carried in a shape the
/// PNG specification does not endorse is reported on stderr rather than passed over in
/// silence. Currently applies only to the PNG output path with a PNG input; every other pair
/// drops metadata regardless.
#[arg(long)]
strip_metadata: bool,
}

/// Output container/codec for `gamut convert`.
Expand Down Expand Up @@ -242,6 +252,34 @@ pub(crate) fn run(args: &ConvertArgs) -> Result<(), CliError> {
if let Some(effort) = args.png_effort {
encoder = encoder.with_effort(effort);
}
// Carry the input's metadata rather than dropping it (issue #483). `png_metadata`
// reads the file from disk a second time; the *walk* is cheap (it skips IDAT by
// length and never inflates a pixel), the second read is not, and it is what the
// convenience of taking a path rather than the already-loaded bytes costs. It yields
// nothing for an input that is not a PNG.
let metadata = (!args.strip_metadata)
.then(|| png_metadata(&args.input))
.flatten();
if let Some(metadata) = &metadata {
tracing::info!(
texts = metadata.texts.len(),
exif = metadata.exif.is_some(),
icc = metadata.icc_profile.is_some(),
xmp = metadata.xmp.is_some(),
"carrying input metadata"
);
encoder = encoder.with_metadata(metadata);
// Say what could not come along, and what came along with a caveat. Silent loss
// is the defect this path exists to remove, and a payload the spec forbids
// carrying is still a payload the caller had.
for notice in encoder.metadata_notices() {
if notice.carried() {
tracing::warn!("input metadata carried with a caveat — {notice}");
} else {
tracing::warn!("input metadata not carried — {notice}");
}
}
}
encoder.encode_image(ImageRef::<Rgba8>::new(&rgba, dims)?, &mut out)?;
(rgba.len(), dims)
}
Expand Down Expand Up @@ -324,6 +362,16 @@ pub(crate) fn run(args: &ConvertArgs) -> Result<(), CliError> {
Ok(())
}

/// The metadata `path` carries, or `None` when it is not a PNG or cannot be read.
///
/// Deliberately total: the input has already been decoded successfully by the time this is
/// called, so an error here means the file is simply not a PNG — a JPEG or WebP input has
/// metadata of its own, but mapping that into PNG chunks is a cross-format job this command does
/// not do yet. Failing to *read* metadata must never fail a conversion whose pixels are fine.
fn png_metadata(path: &Path) -> Option<gamut::png::PngMetadata> {
gamut::png::metadata(&std::fs::read(path).ok()?).ok()
}

/// Picks the output format from `--format`, falling back to the output file's extension.
fn resolve_format(args: &ConvertArgs) -> Result<OutputFormat, CliError> {
if let Some(format) = args.format {
Expand Down
Loading
Loading