#![deny(unreachable_pub)]
mod cabac;
mod color;
mod dct;
mod deblock;
mod error;
mod fmt;
mod hevc;
mod hevc_transform;
mod intra;
mod isobmff;
mod metadata;
mod yuv;
pub use color::{ColorEncoding, ColorMetadata, MatrixCoefficients, Primaries, TransferFunction};
pub use error::EncodeError;
pub use fmt::{BitDepth, ChromaFormat};
pub use metadata::{ContentLightLevel, Metadata, Orientation};
pub use yuv::Yuv;
const MIN_DIM: u32 = 1;
const MAX_DIM: u32 = 16_384;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PixelLayout {
Gray,
GrayAlpha,
Rgb,
Rgba,
}
impl PixelLayout {
pub fn channels(self) -> usize {
match self {
PixelLayout::Gray => 1,
PixelLayout::GrayAlpha => 2,
PixelLayout::Rgb => 3,
PixelLayout::Rgba => 4,
}
}
pub fn has_alpha(self) -> bool {
matches!(self, PixelLayout::GrayAlpha | PixelLayout::Rgba)
}
pub fn is_gray(self) -> bool {
matches!(self, PixelLayout::Gray | PixelLayout::GrayAlpha)
}
}
#[derive(Clone, Debug)]
pub struct EncodeConfig {
pub quality: u8,
pub chroma: ChromaFormat,
pub bit_depth: BitDepth,
pub color: ColorMetadata,
pub metadata: Metadata,
}
impl Default for EncodeConfig {
fn default() -> Self {
EncodeConfig {
quality: 90,
chroma: ChromaFormat::Yuv420,
bit_depth: BitDepth::Eight,
color: ColorMetadata::default(), metadata: Metadata::default(),
}
}
}
impl EncodeConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_quality(mut self, quality: u8) -> Self {
self.quality = quality;
self
}
pub fn with_chroma(mut self, chroma: ChromaFormat) -> Self {
self.chroma = chroma;
self
}
pub fn with_bit_depth(mut self, bit_depth: BitDepth) -> Self {
self.bit_depth = bit_depth;
self
}
pub fn with_color(mut self, color: ColorMetadata) -> Self {
self.color = color;
self
}
pub fn with_icc_profile(mut self, icc: Vec<u8>) -> Self {
self.color = ColorMetadata::Icc(icc);
self
}
pub fn with_cicp(mut self, enc: ColorEncoding) -> Self {
self.color = ColorMetadata::Cicp(enc);
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_orientation(mut self, o: Orientation) -> Self {
self.metadata.orientation = o;
self
}
pub fn with_content_light_level(mut self, cll: ContentLightLevel) -> Self {
self.metadata.content_light_level = Some(cll);
self
}
pub fn with_exif(mut self, exif: Vec<u8>) -> Self {
self.metadata.exif = Some(exif);
self
}
pub(crate) fn validate(&self) -> Result<(), EncodeError> {
validate_quality(self.quality)?;
Ok(())
}
}
pub fn encode(
pixels: &[u16],
width: u32,
height: u32,
layout: PixelLayout,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
cfg.validate()?;
validate_pixel_buffer(pixels, width, height, layout)?;
if layout == PixelLayout::Gray && !cfg.chroma.is_monochrome() {
return Err(EncodeError::InvalidInput);
}
if layout == PixelLayout::GrayAlpha && !cfg.chroma.is_monochrome() {
return Err(EncodeError::InvalidInput);
}
let rgb_owned: Vec<u16>;
let rgb: &[u16] = match layout {
PixelLayout::Rgb | PixelLayout::Gray => pixels,
PixelLayout::Rgba => {
rgb_owned = pixels
.chunks_exact(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect();
&rgb_owned
}
PixelLayout::GrayAlpha => {
rgb_owned = pixels.chunks_exact(2).map(|px| px[0]).collect();
&rgb_owned
}
};
if layout.is_gray() && !cfg.chroma.is_monochrome() {
return Err(EncodeError::InvalidInput);
}
let (enc_w, enc_h) = encoded_dims(width, height, cfg.chroma);
let mut yuv = if enc_w != width || enc_h != height {
let padded = pad_rgb_to_even(rgb, width, height, enc_w, enc_h);
yuv::rgb_to_yuv(&padded, enc_w, enc_h, cfg.chroma, cfg.bit_depth)
} else {
yuv::rgb_to_yuv(rgb, width, height, cfg.chroma, cfg.bit_depth)
};
yuv = yuv.with_display(width, height);
encode_yuv(&yuv, cfg)
}
pub fn encode_with_alpha(
pixels: &[u16],
width: u32,
height: u32,
layout: PixelLayout,
cfg: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
cfg.validate()?;
validate_pixel_buffer(pixels, width, height, layout)?;
if !layout.has_alpha() {
return Err(EncodeError::InvalidInput);
}
let (enc_w, enc_h) = encoded_dims(width, height, cfg.chroma);
let (w, h) = (width as usize, height as usize);
let (nw, nh) = (enc_w as usize, enc_h as usize);
let ch = layout.channels();
let mut alpha_plane = vec![0u16; nw * nh];
let (_, color_stream) = match layout {
PixelLayout::Rgba => {
let mut colour_buf = vec![0u16; nw * nh * 3];
for (dst_row_idx, (col_row, alp_row)) in colour_buf
.chunks_exact_mut(nw * 3)
.zip(alpha_plane.chunks_exact_mut(nw))
.enumerate()
{
let sr = dst_row_idx.min(h - 1);
let src_row = &pixels[sr * w * ch..(sr * w + w) * ch];
for (dst_col_idx, (col_px, alp_px)) in col_row
.chunks_exact_mut(3)
.zip(alp_row.iter_mut())
.enumerate()
{
let sc = dst_col_idx.min(w - 1);
let src = &src_row[sc * 4..sc * 4 + 4];
col_px.copy_from_slice(&src[..3]);
*alp_px = src[3];
}
}
let yuv = yuv::rgb_to_yuv(&colour_buf, enc_w, enc_h, cfg.chroma, cfg.bit_depth);
let stream = hevc::encode_intra(&yuv, enc_w, enc_h, cfg.quality)?;
(yuv, stream)
}
PixelLayout::GrayAlpha => {
let mut luma = vec![0u16; nw * nh];
for (dst_row_idx, (luma_row, alp_row)) in luma
.chunks_exact_mut(nw)
.zip(alpha_plane.chunks_exact_mut(nw))
.enumerate()
{
let sr = dst_row_idx.min(h - 1);
let src_row = &pixels[sr * w * 2..(sr * w + w) * 2];
for (dst_col_idx, (luma_px, alp_px)) in
luma_row.iter_mut().zip(alp_row.iter_mut()).enumerate()
{
let sc = dst_col_idx.min(w - 1);
*luma_px = src_row[sc * 2];
*alp_px = src_row[sc * 2 + 1];
}
}
let yuv = Yuv {
y: luma,
cb: Vec::new(),
cr: Vec::new(),
width: enc_w,
height: enc_h,
display_w: width,
display_h: height,
chroma: ChromaFormat::Monochrome,
bit_depth: cfg.bit_depth,
};
let stream = hevc::encode_intra(&yuv, enc_w, enc_h, cfg.quality)?;
(yuv, stream)
}
_ => unreachable!(), };
let alpha_yuv = Yuv {
y: alpha_plane,
cb: Vec::new(),
cr: Vec::new(),
width: enc_w,
height: enc_h,
display_w: width,
display_h: height,
chroma: ChromaFormat::Monochrome,
bit_depth: cfg.bit_depth,
};
let alpha_stream = hevc::encode_intra(&alpha_yuv, enc_w, enc_h, cfg.quality)?;
isobmff::wrap_hevc_image_with_alpha(
&color_stream,
&alpha_stream,
width,
height,
cfg.bit_depth,
&cfg.color,
&cfg.metadata,
)
}
pub fn encode_yuv(yuv: &Yuv, cfg: &EncodeConfig) -> Result<Vec<u8>, EncodeError> {
yuv.validate()?;
cfg.validate()?;
let enc_w = yuv.width;
let enc_h = yuv.height;
let nalu_stream = hevc::encode_intra(yuv, enc_w, enc_h, cfg.quality)?;
isobmff::wrap_hevc_image(
&nalu_stream,
yuv.display_w,
yuv.display_h,
yuv.bit_depth,
&cfg.color,
&cfg.metadata,
)
}
pub fn encode_heic(
rgb: &[u8],
width: u32,
height: u32,
quality: u8,
) -> Result<Vec<u8>, EncodeError> {
encode_heic_fmt(rgb, width, height, quality, ChromaFormat::Yuv420)
}
pub fn encode_heic_fmt(
rgb: &[u8],
width: u32,
height: u32,
quality: u8,
chroma: ChromaFormat,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer_u8(rgb, width, height, PixelLayout::Rgb)?;
let wide: Vec<u16> = rgb.iter().map(|&b| b as u16).collect();
let cfg = EncodeConfig::new()
.with_quality(quality)
.with_chroma(chroma)
.with_bit_depth(BitDepth::Eight);
encode(&wide, width, height, PixelLayout::Rgb, &cfg)
}
pub fn encode_heic_fmt_bd(
rgb: &[u16],
width: u32,
height: u32,
quality: u8,
chroma: ChromaFormat,
bit_depth: BitDepth,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer(rgb, width, height, PixelLayout::Rgb)?;
let cfg = EncodeConfig::new()
.with_quality(quality)
.with_chroma(chroma)
.with_bit_depth(bit_depth);
encode(rgb, width, height, PixelLayout::Rgb, &cfg)
}
pub fn encode_heic_gray_alpha(
ya: &[u8],
width: u32,
height: u32,
quality: u8,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer_u8(ya, width, height, PixelLayout::GrayAlpha)?;
let wide: Vec<u16> = ya.iter().map(|&b| b as u16).collect();
let cfg = EncodeConfig::new()
.with_quality(quality)
.with_chroma(ChromaFormat::Monochrome);
encode_with_alpha(&wide, width, height, PixelLayout::GrayAlpha, &cfg)
}
pub fn encode_heic_gray_alpha_bd(
ya: &[u16],
width: u32,
height: u32,
quality: u8,
bit_depth: BitDepth,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer(ya, width, height, PixelLayout::GrayAlpha)?;
let cfg = EncodeConfig::new()
.with_quality(quality)
.with_chroma(ChromaFormat::Monochrome)
.with_bit_depth(bit_depth);
encode_with_alpha(ya, width, height, PixelLayout::GrayAlpha, &cfg)
}
pub fn encode_heic_with_alpha(
rgba: &[u8],
width: u32,
height: u32,
quality: u8,
chroma: ChromaFormat,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer_u8(rgba, width, height, PixelLayout::Rgba)?;
let wide: Vec<u16> = rgba.iter().map(|&b| b as u16).collect();
encode_heic_with_alpha_bd(&wide, width, height, quality, chroma, BitDepth::Eight)
}
pub fn encode_heic_with_alpha_bd(
rgba: &[u16],
width: u32,
height: u32,
quality: u8,
chroma: ChromaFormat,
bit_depth: BitDepth,
) -> Result<Vec<u8>, EncodeError> {
validate_dims(width, height)?;
validate_quality(quality)?;
validate_pixel_buffer(rgba, width, height, PixelLayout::Rgba)?;
let cfg = EncodeConfig::new()
.with_quality(quality)
.with_chroma(chroma)
.with_bit_depth(bit_depth);
encode_with_alpha(rgba, width, height, PixelLayout::Rgba, &cfg)
}
pub(crate) fn validate_dims(width: u32, height: u32) -> Result<(), EncodeError> {
if width < MIN_DIM || height < MIN_DIM || width > MAX_DIM || height > MAX_DIM {
return Err(EncodeError::InvalidDimensions { width, height });
}
Ok(())
}
fn validate_quality(quality: u8) -> Result<(), EncodeError> {
if quality == 0 || quality > 100 {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn validate_pixel_buffer(
buf: &[u16],
width: u32,
height: u32,
layout: PixelLayout,
) -> Result<(), EncodeError> {
let needed = checked_buffer_size::<u16>(width as usize, height as usize, layout.channels())?;
if buf.len() != needed {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
fn validate_pixel_buffer_u8(
buf: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
) -> Result<(), EncodeError> {
let needed = checked_buffer_size::<u8>(width as usize, height as usize, layout.channels())?;
if buf.len() < needed {
return Err(EncodeError::InvalidInput);
}
Ok(())
}
pub(crate) fn checked_buffer_size<T>(
width: usize,
height: usize,
channels: usize,
) -> Result<usize, EncodeError> {
let pixel_size = size_of::<T>();
let byte_total = width
.checked_mul(height)
.and_then(|v| v.checked_mul(channels))
.and_then(|v| v.checked_mul(pixel_size))
.and_then(|v| isize::try_from(v).ok())
.ok_or(EncodeError::DimensionTooLarge { width, height })?;
width
.checked_mul(height)
.and_then(|v| v.checked_mul(channels))
.filter(|_| byte_total >= 0) .ok_or(EncodeError::DimensionTooLarge { width, height })
}
fn encoded_dims(width: u32, height: u32, chroma: ChromaFormat) -> (u32, u32) {
let sw = chroma.sub_w() as u32;
let sh = chroma.sub_h() as u32;
(width.div_ceil(sw) * sw, height.div_ceil(sh) * sh)
}
fn pad_rgb_to_even(rgb: &[u16], w: u32, h: u32, nw: u32, nh: u32) -> Vec<u16> {
let (w, h, nw, nh) = (w as usize, h as usize, nw as usize, nh as usize);
let channels = rgb.len() / (w * h); let row_stride = w * channels;
let dst_row_stride = nw * channels;
let mut out = vec![0u16; nw * nh * channels];
for (dst_row_idx, dst_row) in out.chunks_exact_mut(dst_row_stride).enumerate() {
let src_row_idx = dst_row_idx.min(h - 1);
let src_row = &rgb[src_row_idx * row_stride..(src_row_idx + 1) * row_stride];
let (dst_real, dst_pad) = dst_row.split_at_mut(w * channels);
dst_real.copy_from_slice(src_row);
if !dst_pad.is_empty() {
let last_px = &src_row[src_row.len() - channels..];
for px in dst_pad.chunks_exact_mut(channels) {
px.copy_from_slice(last_px);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_builder_chains() {
let cfg = EncodeConfig::new()
.with_quality(80)
.with_chroma(ChromaFormat::Yuv444)
.with_bit_depth(BitDepth::Ten)
.with_cicp(ColorEncoding::bt709());
assert_eq!(cfg.quality, 80);
assert_eq!(cfg.chroma, ChromaFormat::Yuv444);
assert_eq!(cfg.bit_depth, BitDepth::Ten);
assert!(matches!(cfg.color, ColorMetadata::Cicp(_)));
}
#[test]
fn config_validate_rejects_quality_zero() {
let cfg = EncodeConfig::new().with_quality(0);
assert!(cfg.validate().is_err());
}
#[test]
fn config_validate_rejects_quality_over_100() {
let cfg = EncodeConfig::new().with_quality(101);
assert!(cfg.validate().is_err());
}
#[test]
fn pixel_layout_channels() {
assert_eq!(PixelLayout::Gray.channels(), 1);
assert_eq!(PixelLayout::Rgb.channels(), 3);
assert_eq!(PixelLayout::Rgba.channels(), 4);
}
#[test]
fn validate_dims_rejects_zero_width() {
assert!(validate_dims(0, 1).is_err());
}
#[test]
fn validate_dims_rejects_zero_height() {
assert!(validate_dims(1, 0).is_err());
}
#[test]
fn validate_dims_rejects_oversized() {
assert!(validate_dims(MAX_DIM + 1, 1).is_err());
assert!(validate_dims(1, MAX_DIM + 1).is_err());
}
#[test]
fn validate_dims_accepts_boundary() {
assert!(validate_dims(MIN_DIM, MIN_DIM).is_ok());
assert!(validate_dims(MAX_DIM, MAX_DIM).is_ok());
}
#[test]
fn pixel_buffer_rejects_short_rgb() {
assert!(validate_pixel_buffer(&vec![0u16; 47], 4, 4, PixelLayout::Rgb).is_err());
}
#[test]
fn pixel_buffer_accepts_exact_rgb() {
assert!(validate_pixel_buffer(&vec![0u16; 48], 4, 4, PixelLayout::Rgb).is_ok());
}
#[test]
fn pixel_buffer_rejects_short_rgba() {
assert!(validate_pixel_buffer(&vec![0u16; 63], 4, 4, PixelLayout::Rgba).is_err());
}
#[test]
fn from_planes_validates_sizes() {
let ok = Yuv::from_planes(
vec![0u16; 16],
vec![0u16; 4],
vec![0u16; 4],
4,
4,
ChromaFormat::Yuv420,
BitDepth::Eight,
);
assert!(ok.is_ok());
let bad = Yuv::from_planes(
vec![0u16; 15],
vec![0u16; 4],
vec![0u16; 4],
4,
4,
ChromaFormat::Yuv420,
BitDepth::Eight,
);
assert!(bad.is_err());
let mono_bad = Yuv::from_planes(
vec![0u16; 16],
vec![0u16; 4],
vec![],
4,
4,
ChromaFormat::Monochrome,
BitDepth::Eight,
);
assert!(mono_bad.is_err());
}
#[test]
fn encode_yuv_roundtrips() {
let rgb = vec![128u16; 16 * 16 * 3];
let yuv = yuv::rgb_to_yuv(&rgb, 16, 16, ChromaFormat::Yuv420, BitDepth::Eight);
let cfg = EncodeConfig::new();
let out = encode_yuv(&yuv, &cfg).unwrap();
assert!(out.len() > 100);
assert_eq!(&out[4..8], b"ftyp");
}
#[test]
fn encode_rgb_even_dims() {
let rgb = vec![100u16; 16 * 16 * 3];
let cfg = EncodeConfig::new();
let out = encode(&rgb, 16, 16, PixelLayout::Rgb, &cfg).unwrap();
assert!(out.len() > 100);
assert_eq!(&out[4..8], b"ftyp");
}
#[test]
fn encode_rgba_strips_alpha() {
let rgba = vec![100u16; 16 * 16 * 4];
let cfg = EncodeConfig::new();
let out = encode(&rgba, 16, 16, PixelLayout::Rgba, &cfg).unwrap();
assert!(out.len() > 100);
}
#[test]
fn encode_mono_requires_monochrome_config() {
let mono = vec![128u16; 8 * 8];
let cfg_ok = EncodeConfig::new().with_chroma(ChromaFormat::Monochrome);
let out = encode(&mono, 8, 8, PixelLayout::Gray, &cfg_ok).unwrap();
assert!(out.len() > 100);
}
#[test]
fn encode_rejects_short_buffer() {
let rgb = vec![0u16; 10]; let cfg = EncodeConfig::new();
assert!(encode(&rgb, 16, 16, PixelLayout::Rgb, &cfg).is_err());
}
#[test]
fn encode_with_alpha_rejects_rgb_layout() {
let rgb = vec![128u16; 16 * 16 * 3];
let cfg = EncodeConfig::new();
assert!(encode_with_alpha(&rgb, 16, 16, PixelLayout::Rgb, &cfg).is_err());
}
#[test]
fn encode_with_alpha_accepts_rgba() {
let rgba = vec![200u16; 16 * 16 * 4];
let cfg = EncodeConfig::new();
let out = encode_with_alpha(&rgba, 16, 16, PixelLayout::Rgba, &cfg).unwrap();
assert!(out.len() > 100);
assert_eq!(&out[4..8], b"ftyp");
}
#[test]
fn odd_dimensions_reported_in_ispe() {
let rgb = vec![100u16; 281 * 181 * 3];
let cfg = EncodeConfig::new().with_chroma(ChromaFormat::Yuv420);
let out = encode(&rgb, 281, 181, PixelLayout::Rgb, &cfg).unwrap();
let ispe = out
.windows(4)
.position(|w| w == b"ispe")
.expect("ispe present");
let wpos = ispe + 4 + 4;
let w = u32::from_be_bytes(out[wpos..wpos + 4].try_into().unwrap());
let h = u32::from_be_bytes(out[wpos + 4..wpos + 8].try_into().unwrap());
assert_eq!((w, h), (281, 181), "ispe must report the true odd size");
}
#[test]
fn checked_buffer_size_normal() {
assert_eq!(checked_buffer_size::<u16>(4, 4, 3).unwrap(), 48);
}
#[test]
fn checked_buffer_size_overflow() {
assert!(checked_buffer_size::<u16>(usize::MAX, usize::MAX, 3).is_err());
}
#[test]
fn pad_rgb_replicates_last_column() {
let rgb = vec![10u16, 20, 30]; let out = pad_rgb_to_even(&rgb, 1, 1, 2, 1);
assert_eq!(out, vec![10, 20, 30, 10, 20, 30]);
}
#[test]
fn pad_rgb_replicates_last_row() {
let rgb = vec![10u16, 20, 30];
let out = pad_rgb_to_even(&rgb, 1, 1, 1, 2);
assert_eq!(out, vec![10, 20, 30, 10, 20, 30]);
}
#[test]
fn pad_rgb_noop_when_already_even() {
let rgb: Vec<u16> = (0..12).collect(); let out = pad_rgb_to_even(&rgb, 2, 2, 2, 2);
assert_eq!(out, rgb);
}
#[test]
fn gray_alpha_layout_channels() {
assert_eq!(PixelLayout::GrayAlpha.channels(), 2);
assert!(PixelLayout::GrayAlpha.has_alpha());
assert!(PixelLayout::GrayAlpha.is_gray());
assert!(!PixelLayout::Rgb.is_gray());
assert!(!PixelLayout::Rgba.is_gray());
}
#[test]
fn encode_gray_alpha_rejects_non_mono_config() {
let ya = vec![128u16; 8 * 8 * 2];
let cfg = EncodeConfig::new().with_chroma(ChromaFormat::Yuv420);
assert!(encode(&ya, 8, 8, PixelLayout::GrayAlpha, &cfg).is_err());
}
#[test]
fn encode_gray_alpha_strips_alpha() {
let ya = vec![200u16; 8 * 8 * 2];
let cfg = EncodeConfig::new().with_chroma(ChromaFormat::Monochrome);
let out = encode(&ya, 8, 8, PixelLayout::GrayAlpha, &cfg).unwrap();
assert!(out.len() > 100);
}
#[test]
fn encode_with_alpha_gray_alpha_roundtrip() {
let ya = vec![180u16; 16 * 16 * 2];
let cfg = EncodeConfig::new().with_chroma(ChromaFormat::Monochrome);
let out = encode_with_alpha(&ya, 16, 16, PixelLayout::GrayAlpha, &cfg).unwrap();
assert!(out.len() > 100);
assert_eq!(&out[4..8], b"ftyp");
}
#[test]
fn encode_with_alpha_rejects_mono_layout() {
let mono = vec![128u16; 8 * 8];
let cfg = EncodeConfig::new().with_chroma(ChromaFormat::Monochrome);
assert!(encode_with_alpha(&mono, 8, 8, PixelLayout::Gray, &cfg).is_err());
}
}