Expand description
Pure-Rust pixel-format conversions for the oxideav framework.
This crate extends oxideav_core::PixelFormat with the converters
that the rest of the codec/container ecosystem depends on — RGB/BGR
swizzles, YUV↔RGB (BT.601 and BT.709, limited and full range), chroma
subsampling changes (4:2:0 ↔ 4:2:2 ↔ 4:4:4), NV12/NV21 ↔ Yuv420P,
grayscale expansion, 16-bit/8-bit bit-depth changes, and palette
generation + Pal8 encode/decode with optional dithering.
§Entry points
- [
convert] — the single conversion function; dispatches on(src_info.format, dst_format)(the caller provides the source format / dimensions viaFrameInfo) and returns a freshly allocated [VideoFrame]. generate_palette— build aPalettefrom one or more source frames, honouring the selectedPaletteStrategy.convert_in_place_if_same— trivial passthrough helper for the “no conversion needed” case so callers don’t duplicate the check.
§Colour science
The YUV↔RGB paths are written as scalar integer pipelines against
BT.601 and BT.709 weights. The “limited” variants use the studio
range (Y in 16..=235, chroma in 16..=240); “full” variants use the
full 0..=255 range, matching JPEG / “J” YUV. Every converter clamps
to [0, 255] after reconstruction. See yuv for the exact matrix
coefficients.
Range is a property of the pixel format (YuvJ* full, every other
YUV layout limited); ConvertOptions::color_space picks the
matrix weights. A colour signal overrides both: the source frame’s
oxideav_core::VideoFrame::color_signal record (e.g. a HEIF
nclx box declaring a Yuva420P or Yuv420P10Le payload full range
BT.709), or a caller override in ConvertContext::signal passed to
convert_with. Signalled H.273 matrices 1 (BT.709), 4 (FCC),
5 / 6 (BT.601), 7 (SMPTE ST 240) and 9 (BT.2020 NCL) are applied
directly; 0 (identity) routes the planes as G, B, R through the
planar GBR family; an unsignalled frame behaves exactly as before.
§Odd dimensions
Every subsampled planar / semi-planar layout accepts odd widths and
heights: the chroma planes hold ceil(w / wsub) × ceil(h / hsub)
samples (ITU-T T.81 A.1.1 component dimensions; the core
PixelFormat::plane_dimensions contract), the trailing odd luma
column / row decodes against the last chroma sample, and encoding /
chroma downsampling replicate the last column / row into a truncated
block. Even dimensions are unaffected. Yuv411P still needs a
multiple-of-4 width and packed Yuyv422 / Uyvy422 an even width.
See convert_with for the full statement.
§Speed and threading
The 8-bit planar YUV(A) ↔ Rgb24 / Rgba rows share one row-band
engine: tight planes are borrowed rather than copied, RGBA is
interleaved in the same pass as the decode, and the picture is cut
into bands of whole chroma rows. [convert] runs serial (the
oxideav-core threading contract’s default); convert_with with a
ConvertContext whose execution budget is above one converts the
bands on scoped threads. The output is byte-identical at every
budget, and identical to the scalar Q15 reference on every SIMD path
(AVX2 on x86_64, NEON on aarch64, both directions).
§Feature coverage
Not every pair in the Cartesian product of [PixelFormat] variants
is supported; the first-tier matrix is:
- RGB family (Rgb24/Bgr24/Rgba/Bgra/Argb/Abgr) all-to-all.
- Yuv420P/422P/444P ↔ Rgb24 / Rgba under BT.601 / BT.709 / BT.2020
(limited range), plus the full-range YuvJ420P/422P/444P families
directly ↔ Rgb24 / Rgba (the format pins the matrix range; the
ColorSpaceoption picks the primaries). - Gray8 ↔ every YUV family (luma extraction with range rescale / neutral-chroma synthesis; no colour matrix), and Rgb24 / Rgba → Gray8 full-range luminance projection.
- Yuv420P/422P/444P all-to-all direct (chroma resample only — luma
copied byte-for-byte, no RGB hop), plus the same six pairs on the
full-range
YuvJ*family AND on the 16-bitYuv*P16Lefamily (full 16-bit precision chroma resample, identical rounding). - Yuv411P ↔ Yuv420P/422P/444P (chroma resample only — luma copied),
plus Yuv411P ↔ Rgb24/Rgba under BT.601/709/2020. The 4:1:1 layout
has chroma horizontally subsampled by 4 (NTSC DV-25 / JPEG
-sample 4x1); RGB encode and decode stage through a 4:4:4 chroma intermediate so no new colour math is introduced. - YuvJ420P/422P/444P ↔ Yuv* equivalents — plane copy with range rescale.
- Nv12/Nv21 ↔ Yuv420P, plus direct ↔ Rgb24/Rgba via a fused path that runs the planar 4:2:0 encoder/decoder under the hood.
- Yuyv422/Uyvy422 packed 4:2:2 ↔ Yuv422P, plus direct ↔ Rgb24/Rgba under BT.601/709/2020 (limited or full range). Yuyv ↔ Uyvy is a zero-math byte swap.
- Gray8 ↔ Rgb24/Rgba broadcast.
- Ya8 ↔ Gray8/Rgb24/Rgba (luma broadcast; alpha is carried through to Rgba, dropped on the way to Rgb24, and synthesised as opaque 255 on the return paths from Gray8/Rgb24).
- Ya16Le (packed 16-bit grey + alpha, full-scale LE16 words) ↔ Ya8 (exact ×257 widen / high-byte narrow on both words), Gray16Le (luma word verbatim), Gray8, Rgba64Le (bit-exact broadcast + alpha carry out; rounded-mean luma derivation back, so grey-on-alpha content round-trips exactly) and Rgb24 / Rgba.
- Rgb48Le ↔ Rgb24, Rgba64Le ↔ Rgba (bit-shift), and Rgb48Le ↔ Rgba64Le (colour words verbatim; opaque 65535 alpha synthesised / dropped).
- The full ten-member planar GBR(A) ladder —
Gbrp8/Gbrap8(byte planes) through the 10/12/14-bit members to the full-widthGbrp16Le/Gbrap16Le— ↔ both deep packed formats (Rgb48Le / Rgba64Le, alpha synthesised opaque full-scale or dropped when the shapes differ) and the 8-bit packed ones (Rgb24 / Rgba), plus Gray8 interop: full-range luminance projection on the way out (same kernel as the packed RGB → Gray8 rows, alpha dropped) and an MSB-replicatedr = g = bbroadcast on the way in — gray content round-trips exactly. - The full bit-depth ladder: planar YUV 8 ↔ 10 ↔ 12 ↔ 16 bit (same
subsampling; MSB-replicated widen / truncating narrow, exact
round-trips), and Gray8 ↔ Gray10Le ↔ Gray12Le ↔ Gray16Le. On the
Yuv*P16Letrio every bit of the LE word is significant (full-scale 65535), so the 8 → 16 widen is the exact ×257 mapping. - Gray16Le ↔ Gray8.
- MonoBlack/MonoWhite ↔ Gray8.
- The full planar YUV + alpha family — Yuva420P/422P/444P plus the deep 10/12/16-bit members at all three chroma sitings (Yuva420P/422P/444P × 10/12/16Le): promote/drop vs the alpha-less siblings, alpha-preserving chroma resample and depth moves inside the family (luma + alpha bit-exact at same depth, MSB-widened / truncated across depths), Rgb24/Rgba interop (alpha carried / dropped / synthesised opaque), and Gray8 luma extraction. Every ordered pair inside the uniform planar family ({Yuv,Yuva} × {420,422,444} × {8,10,12,16}) is a direct single-step conversion generated by a computed dispatch tier that fuses the depth move, the chroma resample (performed at the deeper of the two depths) and the alpha handling.
- Pal8 → Rgb24/Rgba uses the frame’s attached palette side-channel
(
oxideav_core::VideoFrame::palette) when present, falling back toopts.palette; with neither,Error::Invalid. - Rgb24/Rgba → Pal8 requires
opts.palette; dithering peropts.dither. The output frame carries the table it was quantised against as its palette side-channel, so it is self-describing. - Cmyk and CmykInverted ↔ Rgb24/Rgba — uncalibrated device-CMYK
approximation in both ink conventions (see
cmykfor the formulas and the ICC caveats), plus Cmyk ↔ CmykInverted as the exact per-byte complement. - 4:4:0 planar (
Yuv440P/Yuv440P10Le/Yuv440P12Le/Yuv440P16Le, full-width half-height chroma): full members of the computed planar family (direct to every planar YUV(A) member at every siting and depth,Rgb24/Rgba/Gray8), explicitYuv411Prows, andYuv440P16Le↔Rgb48Leon the deep matrix. The only new arithmetic is a vertical pair-average (4:4:4 → 4:4:0) and a row broadcast (4:4:0 → 4:4:4); every other siting move composes those with the existing kernels through a 4:4:4 intermediate, bit-identical to a fused kernel. Odd widths and heights are legal (see the odd-dimension rule below). - Scene-referred float (
GrayF32Le/RgbF32Le/RgbaF32Le/GbrpF32Le/GbrapF32Le): every ordered pair inside the family is direct and never clamps (gray broadcast, linear-light luminance projection under the selected primaries, alpha carry / drop / synthesise1.0, packed ↔ planar reorder); direct hops to and from eighteen integer shapes (Gray8/10/12/16Le,Rgb24/Rgba/Rgb48Le/Rgba64Le, the ten-member GBR(A) ladder) by pure normalisation in and saturating round-to-nearest quantisation out; and direct hops to and from the 16-bit planar YUV(A) tier through the deep matrix. Seefloatfor the exact value rules. - Full-precision deep matrix: the 16-bit planar tier
({Yuv,Yuva} × {420,422,444,440}
P16Le) ↔ Rgb48Le / Rgba64Le runs the k-coefficient construction in Q30 over 16-bit samples — no 8-bit narrowing anywhere on the path; chroma is resampled at 16-bit precision and the Yuva alpha plane rides verbatim in the packed alpha word. The 10/12-bit family reaches these rows losslessly through the exact widen to the 16-bit tier.
Pairs without a direct entry (table row or computed planar-family
op) are resolved automatically through a
single-pivot staged conversion (one intermediate format, chosen
in a fidelity-aware order: YUV pivots for YUV → YUV moves so no
colour matrix enters the path — with the 16-bit Yuv(a)*P16Le tier
preferred whenever a deep endpoint is YUV carriage, so chroma is
resampled and the matrix applied at full precision — and
alpha-capable / deep RGB pivots
preferred where the endpoints call for them). supports /
supports_direct report per-pair availability. The matrix is
fully closed: every ordered pair of the 70 PixelFormat
variants resolves — directly (1820 pairs) or through one staged
pivot (all 4830).
§Bit-depth precision policy
Every depth change in the crate (8 ↔ 10 ↔ 12 ↔ 16 on planar YUV, the deep grayscale ladder, and the GBR(A) widen/narrow steps) uses one deterministic rule and no dithering:
- Widening places the value in the top bits and replicates its
MSBs into the freed low bits. Zero maps to zero, full-scale maps
to full-scale, the mapping is strictly monotonic, and it tracks
the ideal
v · (2^dst − 1) / (2^src − 1)rescale within one output code (exactly, for 8 → 16, where the ratio is the integer 257). - Narrowing truncates the low bits (keeps the top ones) — the exact inverse of the replication fill, so widen → narrow and narrow-of-widen round-trips are lossless.
Dithered quantisation is deliberately excluded from depth moves:
codec pipelines feeding this crate (lossless intermediates,
reference comparisons, bit-exact round-trip tests) need
deterministic, invertible mappings. Dither applies only to the
palette quantisation path (Rgb24/Rgba → Pal8).
§Integer ↔ float
The integer formats state no transfer characteristic in the core
format definitions, so the hop to and from the float family is a
pure normalisation: f = code / (2^bits − 1) in, code = round(clamp(f, 0, 1) · (2^bits − 1)) out (NaN → 0). No OETF / EOTF
is applied in either direction — apply one from transfer on the
float samples when the pipeline calls for it. Values outside
[0, 1] are saturated on the way to an integer format (the only
lossy step); float → float moves preserve them.
§Per-plane significant bits
Source frames may carry the per-plane significant-bits
side-channel defined by oxideav_core::VideoFrame — one
LSB-anchored byte per image plane naming that plane’s true depth,
e.g. [12, 10, 10] for a wavelet codec’s 12-bit luma + 10-bit
chroma on a Yuv444P12Le / Yuv444P16Le surface. [convert]
honours the record (marked planes convert at their recorded depth),
rejects invalid records (0 or above the surface’s nominal depth)
with Error::Invalid, ignores records on Pal8 (indices are not
magnitudes) and on the float surfaces (binary32 samples are
magnitudes already), and always emits nominal-depth output with no record
attached — see the policy section on [convert] for the details.
Re-exports§
pub use alpha::blit_alpha_mask;pub use alpha::modulate_alpha;pub use alpha::over_buffer;pub use alpha::over_premul;pub use alpha::over_straight;pub use alpha::premultiply;pub use alpha::unpremultiply;pub use convert::convert;pub use convert::convert_in_place_if_same;pub use convert::convert_with;pub use convert::staged_pivot;pub use convert::supports;pub use convert::supports_direct;pub use convert::ColorSpace;pub use convert::ConvertContext;pub use convert::ConvertOptions;pub use convert::Dither;pub use convert::FrameInfo;pub use format_info::ChromaSubsampling;pub use format_info::FormatInfo;pub use palette::generate_palette;pub use palette::Palette;pub use palette::PaletteGenOptions;pub use palette::PaletteStrategy;
Modules§
- alpha
- Alpha-blending and Porter-Duff composite primitives for RGBA buffers.
- cmyk
- CMYK ↔ RGB conversion.
- convert
- High-level
convert()entry point. - dither
- Dithering helpers used when down-quantising to a palette.
- float
- Scene-referred 32-bit float sample helpers (the
GrayF32Le/RgbF32Le/RgbaF32Le/GbrpF32Le/GbrapF32Lefamily of oxideav-core 0.1.35). - format_
info - Per-format metadata: subsampling, planes, and bit depth.
- gray
- Grayscale / mono conversions.
- pal8
- Pal8 encode / decode.
- palette
- Palette types and quantisation strategies.
- rgb
- RGB / BGR family swizzles, plus bit-depth changes between packed 8-bit and 16-bit representations.
- transfer
- Opto-electronic / electro-optical transfer functions.
- yuv
- YUV ↔ RGB conversions (BT.601 and BT.709, limited and full range), planar chroma resampling (4:2:0 ↔ 4:2:2 ↔ 4:4:4), and NV12/NV21 ↔ Yuv420P bridging.