use crate::error::{Result, TiffError as Error};
use oxideav_core::{frame::VideoFrame, time::TimeBase, CodecId, CodecParameters, Frame, Packet};
#[derive(Debug, Clone)]
pub struct JpegSegment {
pub width: u32,
pub height: u32,
pub planes: Vec<Plane>,
pub pixel_format: JpegPixelFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JpegPixelFormat {
Gray8,
Yuv444P,
Yuv422P,
Yuv420P,
Yuv411P,
Rgb24,
Cmyk8,
}
#[derive(Debug, Clone)]
pub struct Plane {
pub stride: usize,
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
pub fn merge_jpeg_segment(tables: Option<&[u8]>, segment: &[u8]) -> Result<Vec<u8>> {
if segment.len() < 4 || segment[0] != 0xFF || segment[1] != 0xD8 {
return Err(Error::invalid(
"TIFF/JPEG: segment does not begin with SOI (FF D8)",
));
}
if segment[segment.len() - 2..] != [0xFF, 0xD9] {
return Err(Error::invalid(
"TIFF/JPEG: segment does not end with EOI (FF D9)",
));
}
let Some(tab) = tables else {
return Ok(segment.to_vec());
};
if tab.len() < 4 || tab[0] != 0xFF || tab[1] != 0xD8 {
return Err(Error::invalid(
"TIFF/JPEG: JPEGTables does not begin with SOI (FF D8)",
));
}
if tab[tab.len() - 2..] != [0xFF, 0xD9] {
return Err(Error::invalid(
"TIFF/JPEG: JPEGTables does not end with EOI (FF D9)",
));
}
let table_body = &tab[2..tab.len() - 2];
let mut out = Vec::with_capacity(tab.len() + segment.len());
out.extend_from_slice(&segment[..2]); out.extend_from_slice(table_body);
out.extend_from_slice(&segment[2..]); Ok(out)
}
fn decode_one_jpeg(jpeg_bytes: Vec<u8>) -> Result<VideoFrame> {
let params = CodecParameters::video(CodecId::new(oxideav_mjpeg::CODEC_ID_STR));
let mut dec = oxideav_mjpeg::registry::make_decoder(¶ms)
.map_err(|e| Error::invalid(format!("TIFF/JPEG: failed to make mjpeg decoder: {e}")))?;
let pkt = Packet::new(0, TimeBase::new(1, 1), jpeg_bytes);
dec.send_packet(&pkt)
.map_err(|e| Error::invalid(format!("TIFF/JPEG: mjpeg send_packet: {e}")))?;
match dec.receive_frame() {
Ok(Frame::Video(vf)) => Ok(vf),
Ok(other) => Err(Error::invalid(format!(
"TIFF/JPEG: mjpeg returned non-video frame {other:?}"
))),
Err(e) => Err(Error::invalid(format!(
"TIFF/JPEG: mjpeg receive_frame: {e}"
))),
}
}
fn classify(vf: &VideoFrame, seg_w: u32, seg_h: u32, photometric: u16) -> Result<JpegPixelFormat> {
use crate::types::*;
let np = vf.planes.len();
match np {
1 => {
let p = &vf.planes[0];
let w = seg_w as usize;
let h = seg_h as usize;
if p.stride >= w.saturating_mul(4) && p.data.len() >= p.stride * h {
if photometric != PHOTO_CMYK {
return Err(Error::invalid(format!(
"TIFF/JPEG: 1-plane packed-4 JPEG but photometric={photometric} (expected 5/CMYK)"
)));
}
return Ok(JpegPixelFormat::Cmyk8);
}
if photometric != PHOTO_BLACK_IS_ZERO && photometric != PHOTO_WHITE_IS_ZERO {
return Err(Error::invalid(format!(
"TIFF/JPEG: 1-plane JPEG but photometric={photometric} (expected 0 or 1)"
)));
}
if p.stride < w || p.data.len() < p.stride * h {
return Err(Error::invalid(format!(
"TIFF/JPEG: gray plane too small: stride={} data={} expected w={seg_w} h={seg_h}",
p.stride,
p.data.len()
)));
}
Ok(JpegPixelFormat::Gray8)
}
3 => {
let y_w = vf.planes[0].stride.max(seg_w as usize);
let c_stride = vf.planes[1].stride;
if c_stride == y_w {
if photometric == PHOTO_RGB {
Ok(JpegPixelFormat::Rgb24)
} else if photometric == PHOTO_YCBCR {
Ok(JpegPixelFormat::Yuv444P)
} else {
Err(Error::invalid(format!(
"TIFF/JPEG: 3-plane full-res JPEG but photometric={photometric}"
)))
}
} else if c_stride * 2 == y_w {
let c_h = vf.planes[1].data.len() / c_stride.max(1);
let seg_h_us = seg_h as usize;
if c_h >= seg_h_us {
Ok(JpegPixelFormat::Yuv422P)
} else {
Ok(JpegPixelFormat::Yuv420P)
}
} else if c_stride * 4 == y_w {
Ok(JpegPixelFormat::Yuv411P)
} else {
Err(Error::invalid(format!(
"TIFF/JPEG: cannot classify 3-plane JPEG (y_stride≈{y_w} chroma_stride={c_stride})"
)))
}
}
n => Err(Error::invalid(format!(
"TIFF/JPEG: unsupported JPEG plane count {n}"
))),
}
}
pub fn decode_segment(
tables: Option<&[u8]>,
segment: &[u8],
seg_w: u32,
seg_h: u32,
photometric: u16,
) -> Result<JpegSegment> {
let merged = merge_jpeg_segment(tables, segment)?;
let vf = decode_one_jpeg(merged)?;
let pf = classify(&vf, seg_w, seg_h, photometric)?;
let planes = vf
.planes
.into_iter()
.enumerate()
.map(|(i, p)| {
let (pw, ph) = plane_dims(pf, seg_w, seg_h, i);
Plane {
stride: p.stride,
width: pw,
height: ph,
data: p.data,
}
})
.collect();
Ok(JpegSegment {
width: seg_w,
height: seg_h,
planes,
pixel_format: pf,
})
}
fn plane_dims(pf: JpegPixelFormat, seg_w: u32, seg_h: u32, i: usize) -> (u32, u32) {
if i == 0 {
return (seg_w, seg_h);
}
match pf {
JpegPixelFormat::Gray8 => (seg_w, seg_h),
JpegPixelFormat::Yuv444P | JpegPixelFormat::Rgb24 => (seg_w, seg_h),
JpegPixelFormat::Yuv422P => (seg_w.div_ceil(2), seg_h),
JpegPixelFormat::Yuv420P => (seg_w.div_ceil(2), seg_h.div_ceil(2)),
JpegPixelFormat::Yuv411P => (seg_w.div_ceil(4), seg_h),
JpegPixelFormat::Cmyk8 => (seg_w, seg_h),
}
}
#[allow(clippy::too_many_arguments)]
pub fn composite_yuv_to_rgb(
seg: &JpegSegment,
visible_w: u32,
visible_h: u32,
dst: &mut [u8],
dst_row_stride: usize,
dst_x: u32,
dst_y: u32,
) -> Result<()> {
let (sh, sv) = match seg.pixel_format {
JpegPixelFormat::Gray8 => return Err(Error::invalid("composite_yuv_to_rgb on Gray8")),
JpegPixelFormat::Rgb24 => return Err(Error::invalid("composite_yuv_to_rgb on Rgb24")),
JpegPixelFormat::Cmyk8 => return Err(Error::invalid("composite_yuv_to_rgb on Cmyk8")),
JpegPixelFormat::Yuv444P => (1u32, 1u32),
JpegPixelFormat::Yuv422P => (2, 1),
JpegPixelFormat::Yuv420P => (2, 2),
JpegPixelFormat::Yuv411P => (4, 1),
};
let yp = &seg.planes[0];
let cb = &seg.planes[1];
let cr = &seg.planes[2];
for y in 0..visible_h as usize {
let py = y;
let cy = y / sv as usize;
let dy = dst_y as usize + y;
for x in 0..visible_w as usize {
let cx = x / sh as usize;
let y_val = yp.data[py * yp.stride + x] as i32;
let cb_val = cb.data[cy * cb.stride + cx] as i32;
let cr_val = cr.data[cy * cr.stride + cx] as i32;
let (r, g, b) = ycbcr_to_rgb(y_val, cb_val, cr_val);
let dst_off = dy * dst_row_stride + (dst_x as usize + x) * 3;
dst[dst_off] = r;
dst[dst_off + 1] = g;
dst[dst_off + 2] = b;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn composite_gray(
seg: &JpegSegment,
visible_w: u32,
visible_h: u32,
dst: &mut [u8],
dst_row_stride: usize,
dst_x: u32,
dst_y: u32,
invert: bool,
) -> Result<()> {
if seg.pixel_format != JpegPixelFormat::Gray8 {
return Err(Error::invalid(
"composite_gray called with non-Gray8 segment",
));
}
let p = &seg.planes[0];
for y in 0..visible_h as usize {
let dy = dst_y as usize + y;
let src_row = &p.data[y * p.stride..y * p.stride + visible_w as usize];
let dst_row = &mut dst[dy * dst_row_stride + dst_x as usize
..dy * dst_row_stride + dst_x as usize + visible_w as usize];
if invert {
for (d, s) in dst_row.iter_mut().zip(src_row.iter()) {
*d = 255 - *s;
}
} else {
dst_row.copy_from_slice(src_row);
}
}
Ok(())
}
pub fn composite_rgb_planar(
seg: &JpegSegment,
visible_w: u32,
visible_h: u32,
dst: &mut [u8],
dst_row_stride: usize,
dst_x: u32,
dst_y: u32,
) -> Result<()> {
if seg.pixel_format != JpegPixelFormat::Rgb24 && seg.pixel_format != JpegPixelFormat::Yuv444P {
return Err(Error::invalid(
"composite_rgb_planar called with non-3-plane-full-res segment",
));
}
let r = &seg.planes[0];
let g = &seg.planes[1];
let b = &seg.planes[2];
for y in 0..visible_h as usize {
let dy = dst_y as usize + y;
for x in 0..visible_w as usize {
let off = dy * dst_row_stride + (dst_x as usize + x) * 3;
dst[off] = r.data[y * r.stride + x];
dst[off + 1] = g.data[y * g.stride + x];
dst[off + 2] = b.data[y * b.stride + x];
}
}
Ok(())
}
pub fn composite_cmyk_to_rgb(
seg: &JpegSegment,
visible_w: u32,
visible_h: u32,
dst: &mut [u8],
dst_row_stride: usize,
dst_x: u32,
dst_y: u32,
) -> Result<()> {
if seg.pixel_format != JpegPixelFormat::Cmyk8 {
return Err(Error::invalid(
"composite_cmyk_to_rgb called with non-Cmyk8 segment",
));
}
let p = &seg.planes[0];
for y in 0..visible_h as usize {
let dy = dst_y as usize + y;
let src_off = y * p.stride;
for x in 0..visible_w as usize {
let s = src_off + x * 4;
let c = p.data[s] as u32;
let m = p.data[s + 1] as u32;
let yy = p.data[s + 2] as u32;
let k = p.data[s + 3] as u32;
let r = ((255 - c) * (255 - k) / 255) as u8;
let g = ((255 - m) * (255 - k) / 255) as u8;
let b = ((255 - yy) * (255 - k) / 255) as u8;
let off = dy * dst_row_stride + (dst_x as usize + x) * 3;
dst[off] = r;
dst[off + 1] = g;
dst[off + 2] = b;
}
}
Ok(())
}
fn ycbcr_to_rgb(y: i32, cb: i32, cr: i32) -> (u8, u8, u8) {
let cb = cb - 128;
let cr = cr - 128;
let r = y + ((91881 * cr + 32768) >> 16);
let g = y - ((22554 * cb + 46802 * cr + 32768) >> 16);
let b = y + ((116130 * cb + 32768) >> 16);
(clamp_u8(r), clamp_u8(g), clamp_u8(b))
}
fn clamp_u8(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_empty_tables_passes_segment_through() {
let seg = vec![0xFF, 0xD8, 0xAB, 0xCD, 0xFF, 0xD9];
let tables = vec![0xFF, 0xD8, 0xFF, 0xD9];
let merged = merge_jpeg_segment(Some(&tables), &seg).unwrap();
assert_eq!(merged, vec![0xFF, 0xD8, 0xAB, 0xCD, 0xFF, 0xD9]);
}
#[test]
fn merge_with_table_payload() {
let seg = vec![0xFF, 0xD8, 0x11, 0x22, 0xFF, 0xD9];
let tables = vec![0xFF, 0xD8, 0xEE, 0xFF, 0xD9];
let merged = merge_jpeg_segment(Some(&tables), &seg).unwrap();
assert_eq!(merged, vec![0xFF, 0xD8, 0xEE, 0x11, 0x22, 0xFF, 0xD9]);
}
#[test]
fn merge_without_tables_returns_segment_clone() {
let seg = vec![0xFF, 0xD8, 0x42, 0xFF, 0xD9];
let merged = merge_jpeg_segment(None, &seg).unwrap();
assert_eq!(merged, seg);
}
#[test]
fn merge_rejects_segment_without_soi() {
let seg = vec![0xFF, 0xE0, 0xFF, 0xD9];
assert!(merge_jpeg_segment(None, &seg).is_err());
}
#[test]
fn merge_rejects_segment_without_eoi() {
let seg = vec![0xFF, 0xD8, 0x00, 0x00];
assert!(merge_jpeg_segment(None, &seg).is_err());
}
#[test]
fn merge_rejects_tables_without_soi() {
let seg = vec![0xFF, 0xD8, 0xFF, 0xD9];
let bad_tab = vec![0xFF, 0xE0, 0xFF, 0xD9];
assert!(merge_jpeg_segment(Some(&bad_tab), &seg).is_err());
}
#[test]
fn merge_rejects_tables_without_eoi() {
let seg = vec![0xFF, 0xD8, 0xFF, 0xD9];
let bad_tab = vec![0xFF, 0xD8, 0x00, 0x00];
assert!(merge_jpeg_segment(Some(&bad_tab), &seg).is_err());
}
#[test]
fn jpeg_pixel_format_variants_distinct() {
assert_ne!(JpegPixelFormat::Gray8, JpegPixelFormat::Yuv420P);
assert_ne!(JpegPixelFormat::Yuv422P, JpegPixelFormat::Yuv444P);
assert_ne!(JpegPixelFormat::Cmyk8, JpegPixelFormat::Gray8);
assert_ne!(JpegPixelFormat::Cmyk8, JpegPixelFormat::Rgb24);
}
#[test]
fn composite_cmyk_to_rgb_known_values() {
let plane_w = 4u32;
let plane_h = 1u32;
let stride = plane_w as usize * 4 + 3; let mut data = vec![0xAAu8; stride * plane_h as usize];
data[0] = 0;
data[1] = 0;
data[2] = 0;
data[3] = 0;
data[4] = 255;
data[5] = 0;
data[6] = 0;
data[7] = 0;
data[8] = 0;
data[9] = 0;
data[10] = 255;
data[11] = 0;
data[12] = 0;
data[13] = 0;
data[14] = 0;
data[15] = 255;
let seg = JpegSegment {
width: plane_w,
height: plane_h,
planes: vec![Plane {
stride,
width: plane_w,
height: plane_h,
data,
}],
pixel_format: JpegPixelFormat::Cmyk8,
};
let dst_stride = plane_w as usize * 3;
let mut dst = vec![0u8; dst_stride * plane_h as usize];
composite_cmyk_to_rgb(&seg, plane_w, plane_h, &mut dst, dst_stride, 0, 0).unwrap();
assert_eq!(&dst[0..3], &[255, 255, 255], "white pixel");
assert_eq!(&dst[3..6], &[0, 255, 255], "cyan pixel");
assert_eq!(&dst[6..9], &[255, 255, 0], "yellow pixel");
assert_eq!(&dst[9..12], &[0, 0, 0], "pure-K black pixel");
}
#[test]
fn composite_cmyk_rejects_non_cmyk_segment() {
let seg = JpegSegment {
width: 1,
height: 1,
planes: vec![Plane {
stride: 1,
width: 1,
height: 1,
data: vec![0u8],
}],
pixel_format: JpegPixelFormat::Gray8,
};
let mut dst = vec![0u8; 3];
let r = composite_cmyk_to_rgb(&seg, 1, 1, &mut dst, 3, 0, 0);
assert!(r.is_err());
}
}