Skip to main content

blazediff_png/
lib.rs

1//! Full-spec PNG codec with byte-exact decode parity to libspng's
2//! `SPNG_FMT_RGBA8` path (the blazediff reference decoder configuration:
3//! tRNS applied, adler32 ignored, CRCs unverified) and round-trip-verified
4//! encoding.
5//!
6//! - **Decode**: every format spng accepts — bit depths 1/2/4/8/16, all five
7//!   color types, palette + tRNS, gray/RGB tRNS color-keying, Adam7
8//!   interlacing — decoded to RGBA8 byte-identically to spng, with the same
9//!   accept/reject behavior on malformed inputs. One deliberate exception: a
10//!   gray tRNS key is masked to the bit depth as the PNG spec requires, which
11//!   spng does not do (see the README).
12//! - **Decode formats**: [`decode_with`] targets any spng `SPNG_FMT_*`
13//!   (`RGBA8`/`RGBA16`/`RGB8`/`GA8`/`GA16`/`G8`/native `PNG`/`RAW`) with
14//!   optional tRNS / gamma / sBIT transforms, each byte-identical to
15//!   `spng_decode_image` — see [`DecodeFormat`] / [`DecodeOptions`].
16//! - **Encode**: all PNG color type / bit depth combinations, optional
17//!   Adam7, real deflate levels (libdeflate) plus a stored-block level 0;
18//!   verified by `decode(encode(x)) == x` and spng cross-decode.
19//! - **Fast**: single-threaded and SIMD-first — whole-buffer libdeflate
20//!   inflate, in-place sequential defiltering, autovectorizable row
21//!   expansion, and NEON-accelerated encode-filter kernels.
22//! - **Metadata**: [`decode_with_metadata`] / [`encode_with_metadata`] capture
23//!   and emit every ancillary chunk spng exposes (text, color management,
24//!   timing, physical dimensions, EXIF, suggested palettes, explicit
25//!   palette/transparency) plus unknown-chunk passthrough — verified field by
26//!   field against `spng_get_*`. The plain [`decode`] stays metadata-free for
27//!   the pixel hot path.
28//!
29//! ```no_run
30//! let bytes = std::fs::read("image.png").unwrap();
31//! let image = blazediff_png::decode(&bytes).unwrap();
32//! let png = blazediff_png::encode(&image, &blazediff_png::EncodeOptions::default()).unwrap();
33//! ```
34
35#[cfg(not(any(feature = "zlib-backend", feature = "rust-backend")))]
36compile_error!(
37    "blazediff_png needs a deflate backend: enable `zlib-backend` (default, \
38     spng parity) or `rust-backend` (pure-Rust, C-free)."
39);
40
41mod backend;
42#[cfg(any(feature = "napi", feature = "wasm", test))]
43mod binding;
44mod chunks;
45mod container;
46mod convert;
47mod decode;
48mod defilter;
49mod encode;
50mod error;
51mod expand;
52mod format;
53mod interlace;
54mod meta;
55#[cfg(feature = "napi")]
56mod napi;
57#[cfg(feature = "parallel")]
58mod parallel;
59#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
60mod wasm;
61
62pub use encode::{ColorMode, EncodeOptions, Filter, FilterSet};
63pub use error::PngError;
64pub use format::{DecodeFormat, DecodeOptions, Decoded};
65pub use meta::{
66    Bkgd, Chrm, Iccp, Location, Metadata, Offs, Palette, Phys, Sbit, Splt, SpltEntry, Text,
67    TextKind, Time, Trns, UnknownChunk,
68};
69
70/// Decoded image: RGBA8, 4 bytes per pixel, row-major.
71pub struct Image {
72    pub data: Vec<u8>,
73    pub width: u32,
74    pub height: u32,
75}
76
77/// Borrowed RGBA8 view, the input for the streaming encode entry points
78/// ([`encode_ref`] / [`encode_to`]). Lets a caller encode straight from an
79/// existing buffer without handing ownership to an [`Image`].
80#[derive(Clone, Copy)]
81pub struct ImageRef<'a> {
82    pub data: &'a [u8],
83    pub width: u32,
84    pub height: u32,
85}
86
87impl<'a> From<&'a Image> for ImageRef<'a> {
88    fn from(image: &'a Image) -> Self {
89        ImageRef {
90            data: &image.data,
91            width: image.width,
92            height: image.height,
93        }
94    }
95}
96
97/// True 16-bit image: RGBA16 host-order, 4 `u16` channels per pixel,
98/// row-major. The input type for [`encode16`], and the natural pairing with
99/// [`decode_with`] at [`DecodeFormat::Rgba16`].
100pub struct Image16 {
101    pub data: Vec<u16>,
102    pub width: u32,
103    pub height: u32,
104}
105
106/// An image plus all of its non-pixel chunk data, from
107/// [`decode_with_metadata`].
108pub struct DecodedPng {
109    pub image: Image,
110    pub meta: Metadata,
111}
112
113/// Decode a PNG to RGBA8, byte-identical to libspng's FMT_RGBA8 + tRNS
114/// output for every input spng accepts, rejecting the inputs spng rejects.
115/// The exception is a gray tRNS key with bits above the bit depth, which is
116/// masked per the PNG spec.
117pub fn decode(data: &[u8]) -> Result<Image, PngError> {
118    decode::decode(data)
119}
120
121/// Fuzzing-only: strict-window reference decode used by the differential
122/// harness to classify streams with no deterministic behavioral contract
123/// (classic zlib reads uninitialized window memory for them).
124#[cfg(feature = "fuzzing")]
125#[doc(hidden)]
126pub fn decode_strict_window(data: &[u8]) -> Result<Image, PngError> {
127    decode::decode_strict_window(data)
128}
129
130/// Decode a PNG to an arbitrary [`DecodeFormat`] with optional tRNS / gamma /
131/// sBIT transforms, byte-identical to libspng's `spng_decode_image(fmt,
132/// flags)`. [`decode`] is the `Rgba8` + tRNS special case.
133///
134/// ```no_run
135/// use blazediff_png::{decode_with, DecodeFormat, DecodeOptions};
136/// let bytes = std::fs::read("image.png").unwrap();
137/// let out = decode_with(&bytes, &DecodeOptions { format: DecodeFormat::Rgb8, ..Default::default() }).unwrap();
138/// ```
139pub fn decode_with(data: &[u8], options: &DecodeOptions) -> Result<Decoded, PngError> {
140    decode::decode_with(data, options)
141}
142
143/// Encode an RGBA8 image to PNG. The requested color mode must represent
144/// the image losslessly (`decode(encode(x)) == x` always holds);
145/// [`EncodeOptions::default`] auto-picks the smallest lossless mode.
146pub fn encode(image: &Image, options: &EncodeOptions) -> Result<Vec<u8>, PngError> {
147    encode::encode(image.into(), options, &Metadata::default())
148}
149
150/// [`encode`] with the deflate stream compressed in fixed bands on up to
151/// `threads` workers (feature `parallel`). The bytes depend only on `image`
152/// and `options`, never on `threads`. They differ from [`encode`]'s
153/// (zlib-rs instead of libdeflate), but decode to the same pixels. Level 0
154/// produces exactly [`encode`]'s output.
155#[cfg(feature = "parallel")]
156pub fn encode_parallel(
157    image: &Image,
158    options: &EncodeOptions,
159    threads: std::num::NonZeroUsize,
160) -> Result<Vec<u8>, PngError> {
161    encode::encode_parallel(image.into(), options, &Metadata::default(), threads)
162}
163
164/// [`encode`] from a borrowed [`ImageRef`] — same output, no [`Image`]
165/// ownership required. The hot path (`Rgba8` + `Filter::None` + level 0,
166/// non-interlaced, no metadata) is written straight from the borrowed rows
167/// with no intermediate raw or zlib buffer.
168pub fn encode_ref(image: ImageRef, options: &EncodeOptions) -> Result<Vec<u8>, PngError> {
169    encode::encode(image, options, &Metadata::default())
170}
171
172/// Stream a PNG encode straight into `out` instead of returning a `Vec`. For
173/// the stored RGBA8 hot path this keeps peak memory at roughly the input size:
174/// the deflate stored blocks are written directly to the sink with no full-size
175/// intermediate buffer. Other modes build a `Vec` and write it through. Wrap a
176/// `File` in a `BufWriter` — the stored path issues many small writes.
177pub fn encode_to<W: std::io::Write>(
178    image: ImageRef,
179    options: &EncodeOptions,
180    out: &mut W,
181) -> Result<(), PngError> {
182    encode::encode_to(image, options, &Metadata::default(), out)
183}
184
185/// Encode a true 16-bit [`Image16`] to PNG, carrying full 16-bit precision
186/// (unlike [`encode`], which byte-replicates an 8-bit source). The color mode
187/// must be a 16-bit one — `Gray16`/`GrayAlpha16`/`Rgb16`/`Rgba16` or `Auto`;
188/// [`EncodeOptions::default`] auto-picks the smallest lossless 16-bit mode.
189pub fn encode16(image: &Image16, options: &EncodeOptions) -> Result<Vec<u8>, PngError> {
190    encode::encode16(image, options, &Metadata::default())
191}
192
193/// [`encode16`] with caller-supplied ancillary chunks, mirroring
194/// [`encode_with_metadata`].
195pub fn encode16_with_metadata(
196    image: &Image16,
197    options: &EncodeOptions,
198    meta: &Metadata,
199) -> Result<Vec<u8>, PngError> {
200    encode::encode16(image, options, meta)
201}
202
203/// Decode a PNG to RGBA8 *and* capture every ancillary chunk spng exposes
204/// (text, color management, timing, physical dimensions, EXIF, suggested
205/// palettes, and unknown chunks), with the same accept/reject behavior as
206/// [`decode`]. Heavier than [`decode`] — use that when only pixels are
207/// needed.
208pub fn decode_with_metadata(data: &[u8]) -> Result<DecodedPng, PngError> {
209    decode::decode_with_metadata(data)
210}
211
212/// Encode an RGBA8 image to PNG, emitting the supplied metadata chunks in
213/// spec-valid positions. When `meta.palette` is set and the resolved color
214/// mode is indexed, that palette (and `meta.transparency`) is used verbatim
215/// instead of the auto-derived one.
216pub fn encode_with_metadata(
217    image: &Image,
218    options: &EncodeOptions,
219    meta: &Metadata,
220) -> Result<Vec<u8>, PngError> {
221    encode::encode(image.into(), options, meta)
222}