oxideav_pixfmt/lib.rs
1#![cfg_attr(feature = "nightly", feature(portable_simd))]
2//! Pure-Rust pixel-format conversions for the oxideav framework.
3//!
4//! This crate extends [`oxideav_core::PixelFormat`] with the converters
5//! that the rest of the codec/container ecosystem depends on — RGB/BGR
6//! swizzles, YUV↔RGB (BT.601 and BT.709, limited and full range), chroma
7//! subsampling changes (4:2:0 ↔ 4:2:2 ↔ 4:4:4), NV12/NV21 ↔ Yuv420P,
8//! grayscale expansion, 16-bit/8-bit bit-depth changes, and palette
9//! generation + Pal8 encode/decode with optional dithering.
10//!
11//! # Entry points
12//!
13//! - [`convert`] — the single conversion function; dispatches on
14//! `(src_info.format, dst_format)` (the caller provides the source
15//! format / dimensions via [`FrameInfo`]) and returns a freshly
16//! allocated [`VideoFrame`].
17//! - [`generate_palette`] — build a [`Palette`] from one or more source
18//! frames, honouring the selected [`PaletteStrategy`].
19//! - [`convert_in_place_if_same`] — trivial passthrough helper for the
20//! "no conversion needed" case so callers don't duplicate the check.
21//!
22//! # Colour science
23//!
24//! The YUV↔RGB paths are written as scalar integer pipelines against
25//! BT.601 and BT.709 weights. The "limited" variants use the studio
26//! range (Y in 16..=235, chroma in 16..=240); "full" variants use the
27//! full 0..=255 range, matching JPEG / "J" YUV. Every converter clamps
28//! to `[0, 255]` after reconstruction. See [`yuv`] for the exact matrix
29//! coefficients.
30//!
31//! Range is a property of the pixel format (`YuvJ*` full, every other
32//! YUV layout limited); [`ConvertOptions::color_space`] picks the
33//! matrix weights. A colour signal overrides both: the source frame's
34//! [`oxideav_core::VideoFrame::color_signal`] record (e.g. a HEIF
35//! `nclx` box declaring a `Yuva420P` or `Yuv420P10Le` payload full range
36//! BT.709), or a caller override in [`ConvertContext::signal`] passed to
37//! [`convert_with`]. Signalled H.273 matrices 1 (BT.709), 4 (FCC),
38//! 5 / 6 (BT.601), 7 (SMPTE ST 240) and 9 (BT.2020 NCL) are applied
39//! directly; 0 (identity) routes the planes as G, B, R through the
40//! planar GBR family; an unsignalled frame behaves exactly as before.
41//!
42//! # Odd dimensions
43//!
44//! Every subsampled planar / semi-planar layout accepts odd widths and
45//! heights: the chroma planes hold `ceil(w / wsub)` × `ceil(h / hsub)`
46//! samples (ITU-T T.81 A.1.1 component dimensions; the core
47//! `PixelFormat::plane_dimensions` contract), the trailing odd luma
48//! column / row decodes against the last chroma sample, and encoding /
49//! chroma downsampling replicate the last column / row into a truncated
50//! block. Even dimensions are unaffected. `Yuv411P` still needs a
51//! multiple-of-4 width and packed `Yuyv422` / `Uyvy422` an even width.
52//! See [`convert_with`] for the full statement.
53//!
54//! # Speed and threading
55//!
56//! The 8-bit planar YUV(A) ↔ `Rgb24` / `Rgba` rows share one row-band
57//! engine: tight planes are borrowed rather than copied, RGBA is
58//! interleaved in the same pass as the decode, and the picture is cut
59//! into bands of whole chroma rows. [`convert`] runs serial (the
60//! oxideav-core threading contract's default); [`convert_with`] with a
61//! [`ConvertContext`] whose `execution` budget is above one converts the
62//! bands on scoped threads. The output is byte-identical at every
63//! budget, and identical to the scalar Q15 reference on every SIMD path
64//! (AVX2 on x86_64, NEON on aarch64, both directions).
65//!
66//! # Feature coverage
67//!
68//! Not every pair in the Cartesian product of [`PixelFormat`] variants
69//! is supported; the first-tier matrix is:
70//!
71//! - RGB family (Rgb24/Bgr24/Rgba/Bgra/Argb/Abgr) all-to-all.
72//! - Yuv420P/422P/444P ↔ Rgb24 / Rgba under BT.601 / BT.709 / BT.2020
73//! (limited range), plus the full-range YuvJ420P/422P/444P families
74//! directly ↔ Rgb24 / Rgba (the format pins the matrix range; the
75//! `ColorSpace` option picks the primaries).
76//! - Gray8 ↔ every YUV family (luma extraction with range rescale /
77//! neutral-chroma synthesis; no colour matrix), and Rgb24 / Rgba →
78//! Gray8 full-range luminance projection.
79//! - Yuv420P/422P/444P all-to-all direct (chroma resample only — luma
80//! copied byte-for-byte, no RGB hop), plus the same six pairs on the
81//! full-range `YuvJ*` family AND on the 16-bit `Yuv*P16Le` family
82//! (full 16-bit precision chroma resample, identical rounding).
83//! - Yuv411P ↔ Yuv420P/422P/444P (chroma resample only — luma copied),
84//! plus Yuv411P ↔ Rgb24/Rgba under BT.601/709/2020. The 4:1:1 layout
85//! has chroma horizontally subsampled by 4 (NTSC DV-25 / JPEG
86//! `-sample 4x1`); RGB encode and decode stage through a 4:4:4
87//! chroma intermediate so no new colour math is introduced.
88//! - YuvJ420P/422P/444P ↔ Yuv* equivalents — plane copy with range
89//! rescale.
90//! - Nv12/Nv21 ↔ Yuv420P, plus direct ↔ Rgb24/Rgba via a fused path
91//! that runs the planar 4:2:0 encoder/decoder under the hood.
92//! - Yuyv422/Uyvy422 packed 4:2:2 ↔ Yuv422P, plus direct ↔ Rgb24/Rgba
93//! under BT.601/709/2020 (limited or full range). Yuyv ↔ Uyvy is a
94//! zero-math byte swap.
95//! - Gray8 ↔ Rgb24/Rgba broadcast.
96//! - Ya8 ↔ Gray8/Rgb24/Rgba (luma broadcast; alpha is carried through to
97//! Rgba, dropped on the way to Rgb24, and synthesised as opaque 255 on
98//! the return paths from Gray8/Rgb24).
99//! - Ya16Le (packed 16-bit grey + alpha, full-scale LE16 words) ↔ Ya8
100//! (exact ×257 widen / high-byte narrow on both words), Gray16Le
101//! (luma word verbatim), Gray8, Rgba64Le (bit-exact broadcast +
102//! alpha carry out; rounded-mean luma derivation back, so
103//! grey-on-alpha content round-trips exactly) and Rgb24 / Rgba.
104//! - Rgb48Le ↔ Rgb24, Rgba64Le ↔ Rgba (bit-shift), and
105//! Rgb48Le ↔ Rgba64Le (colour words verbatim; opaque 65535 alpha
106//! synthesised / dropped).
107//! - The full ten-member planar GBR(A) ladder — `Gbrp8` / `Gbrap8`
108//! (byte planes)
109//! through the 10/12/14-bit members to the full-width `Gbrp16Le` /
110//! `Gbrap16Le` — ↔ **both** deep packed formats (Rgb48Le / Rgba64Le,
111//! alpha synthesised opaque full-scale or dropped when the shapes
112//! differ) and the 8-bit packed ones (Rgb24 / Rgba), plus Gray8
113//! interop: full-range luminance projection on the way out (same
114//! kernel as the packed RGB → Gray8 rows, alpha dropped) and an
115//! MSB-replicated `r = g = b` broadcast on the way in — gray content
116//! round-trips exactly.
117//! - The full bit-depth ladder: planar YUV 8 ↔ 10 ↔ 12 ↔ 16 bit (same
118//! subsampling; MSB-replicated widen / truncating narrow, exact
119//! round-trips), and Gray8 ↔ Gray10Le ↔ Gray12Le ↔ Gray16Le. On the
120//! `Yuv*P16Le` trio every bit of the LE word is significant
121//! (full-scale 65535), so the 8 → 16 widen is the exact ×257 mapping.
122//! - Gray16Le ↔ Gray8.
123//! - MonoBlack/MonoWhite ↔ Gray8.
124//! - The full planar YUV + alpha family — Yuva420P/422P/444P plus the
125//! deep 10/12/16-bit members at all three chroma sitings
126//! (Yuva420P/422P/444P × 10/12/16Le): promote/drop vs
127//! the alpha-less siblings, alpha-preserving chroma resample and
128//! depth moves inside the family (luma + alpha bit-exact at same
129//! depth, MSB-widened / truncated across depths), Rgb24/Rgba interop
130//! (alpha carried / dropped / synthesised opaque), and Gray8 luma
131//! extraction. Every ordered pair inside the uniform planar family
132//! ({Yuv,Yuva} × {420,422,444} × {8,10,12,16}) is a **direct**
133//! single-step conversion generated by a computed dispatch tier that
134//! fuses the depth move, the chroma resample (performed at the deeper
135//! of the two depths) and the alpha handling.
136//! - Pal8 → Rgb24/Rgba uses the frame's attached palette side-channel
137//! ([`oxideav_core::VideoFrame::palette`]) when present, falling back
138//! to `opts.palette`; with neither, `Error::Invalid`.
139//! - Rgb24/Rgba → Pal8 requires `opts.palette`; dithering per
140//! `opts.dither`. The output frame carries the table it was quantised
141//! against as its palette side-channel, so it is self-describing.
142//! - Cmyk and CmykInverted ↔ Rgb24/Rgba — uncalibrated device-CMYK
143//! approximation in both ink conventions (see [`cmyk`] for the
144//! formulas and the ICC caveats), plus Cmyk ↔ CmykInverted as the
145//! exact per-byte complement.
146//! - **4:4:0 planar** (`Yuv440P` / `Yuv440P10Le` / `Yuv440P12Le` /
147//! `Yuv440P16Le`, full-width half-height chroma): full members of
148//! the computed planar family (direct to every planar YUV(A)
149//! member at every siting and depth, `Rgb24` / `Rgba` / `Gray8`),
150//! explicit `Yuv411P` rows, and `Yuv440P16Le` ↔ `Rgb48Le` on the
151//! deep matrix. The only new arithmetic is a vertical pair-average
152//! (4:4:4 → 4:4:0) and a row broadcast (4:4:0 → 4:4:4); every other
153//! siting move composes those with the existing kernels through a
154//! 4:4:4 intermediate, bit-identical to a fused kernel. Odd widths
155//! and heights are legal (see the odd-dimension rule below).
156//! - **Scene-referred float** (`GrayF32Le` / `RgbF32Le` / `RgbaF32Le`
157//! / `GbrpF32Le` / `GbrapF32Le`): every ordered pair inside the
158//! family is direct and never clamps (gray broadcast, linear-light
159//! luminance projection under the selected primaries, alpha carry /
160//! drop / synthesise `1.0`, packed ↔ planar reorder); direct hops to
161//! and from eighteen integer shapes (`Gray8/10/12/16Le`, `Rgb24` /
162//! `Rgba` / `Rgb48Le` / `Rgba64Le`, the ten-member GBR(A) ladder)
163//! by pure normalisation in and saturating round-to-nearest
164//! quantisation out; and direct hops to and from the 16-bit planar
165//! YUV(A) tier through the deep matrix. See [`float`] for the exact
166//! value rules.
167//! - **Full-precision deep matrix**: the 16-bit planar tier
168//! ({Yuv,Yuva} × {420,422,444,440} `P16Le`) ↔ Rgb48Le / Rgba64Le runs
169//! the k-coefficient construction in Q30 over 16-bit samples — no
170//! 8-bit narrowing anywhere on the path; chroma is resampled at
171//! 16-bit precision and the Yuva alpha plane rides verbatim in the
172//! packed alpha word. The 10/12-bit family reaches these rows
173//! losslessly through the exact widen to the 16-bit tier.
174//!
175//! Pairs without a direct entry (table row or computed planar-family
176//! op) are resolved automatically through a
177//! **single-pivot staged conversion** (one intermediate format, chosen
178//! in a fidelity-aware order: YUV pivots for YUV → YUV moves so no
179//! colour matrix enters the path — with the 16-bit `Yuv(a)*P16Le` tier
180//! preferred whenever a deep endpoint is YUV carriage, so chroma is
181//! resampled and the matrix applied at full precision — and
182//! alpha-capable / deep RGB pivots
183//! preferred where the endpoints call for them). [`supports`] /
184//! [`supports_direct`] report per-pair availability. The matrix is
185//! fully closed: **every ordered pair** of the 70 `PixelFormat`
186//! variants resolves — directly (1820 pairs) or through one staged
187//! pivot (all 4830).
188//!
189//! # Bit-depth precision policy
190//!
191//! Every depth change in the crate (8 ↔ 10 ↔ 12 ↔ 16 on planar YUV,
192//! the deep grayscale ladder, and the GBR(A) widen/narrow steps) uses
193//! one deterministic rule and **no dithering**:
194//!
195//! - **Widening** places the value in the top bits and replicates its
196//! MSBs into the freed low bits. Zero maps to zero, full-scale maps
197//! to full-scale, the mapping is strictly monotonic, and it tracks
198//! the ideal `v · (2^dst − 1) / (2^src − 1)` rescale within one
199//! output code (exactly, for 8 → 16, where the ratio is the integer
200//! 257).
201//! - **Narrowing** truncates the low bits (keeps the top ones) — the
202//! exact inverse of the replication fill, so widen → narrow and
203//! narrow-of-widen round-trips are lossless.
204//!
205//! Dithered quantisation is deliberately excluded from depth moves:
206//! codec pipelines feeding this crate (lossless intermediates,
207//! reference comparisons, bit-exact round-trip tests) need
208//! deterministic, invertible mappings. `Dither` applies only to the
209//! palette quantisation path (`Rgb24`/`Rgba` → `Pal8`).
210//!
211//! # Integer ↔ float
212//!
213//! The integer formats state no transfer characteristic in the core
214//! format definitions, so the hop to and from the float family is a
215//! **pure normalisation**: `f = code / (2^bits − 1)` in, `code =
216//! round(clamp(f, 0, 1) · (2^bits − 1))` out (NaN → 0). No OETF / EOTF
217//! is applied in either direction — apply one from [`transfer`] on the
218//! float samples when the pipeline calls for it. Values outside
219//! `[0, 1]` are saturated on the way to an integer format (the only
220//! lossy step); float → float moves preserve them.
221//!
222//! # Per-plane significant bits
223//!
224//! Source frames may carry the per-plane **significant-bits
225//! side-channel** defined by [`oxideav_core::VideoFrame`] — one
226//! LSB-anchored byte per image plane naming that plane's true depth,
227//! e.g. `[12, 10, 10]` for a wavelet codec's 12-bit luma + 10-bit
228//! chroma on a `Yuv444P12Le` / `Yuv444P16Le` surface. [`convert`]
229//! honours the record (marked planes convert at their recorded depth),
230//! rejects invalid records (`0` or above the surface's nominal depth)
231//! with `Error::Invalid`, ignores records on `Pal8` (indices are not
232//! magnitudes) and on the float surfaces (binary32 samples are
233//! magnitudes already), and always emits nominal-depth output with no record
234//! attached — see the policy section on [`convert`] for the details.
235
236pub mod alpha;
237pub mod cmyk;
238pub mod convert;
239pub mod dither;
240pub mod float;
241pub mod format_info;
242pub mod gray;
243pub mod pal8;
244pub mod palette;
245mod planar8;
246pub mod rgb;
247mod simd_dispatch;
248pub mod transfer;
249pub mod yuv;
250mod yuv_simd;
251
252pub use alpha::{
253 blit_alpha_mask, modulate_alpha, over_buffer, over_premul, over_straight, premultiply,
254 unpremultiply,
255};
256pub use convert::{
257 convert, convert_in_place_if_same, convert_with, staged_pivot, supports, supports_direct,
258 ColorSpace, ConvertContext, ConvertOptions, Dither, FrameInfo,
259};
260pub use format_info::{ChromaSubsampling, FormatInfo};
261pub use palette::{generate_palette, Palette, PaletteGenOptions, PaletteStrategy};