use crate::error::{MjpegError as Error, Result};
#[cfg(feature = "registry")]
use oxideav_core::{PixelFormat, VideoFrame};
#[cfg(not(feature = "registry"))]
use crate::image::{MjpegFrame as VideoFrame, MjpegPixelFormat as PixelFormat};
#[cfg(feature = "registry")]
pub use crate::registry::{make_encoder, MjpegEncoder};
use crate::jpeg::dct::fdct8x8;
use crate::jpeg::huffman::{
DefaultHuffman, HuffTable, STD_AC_CHROMA_BITS, STD_AC_CHROMA_VALS, STD_AC_LUMA_BITS,
STD_AC_LUMA_VALS, STD_DC_CHROMA_BITS, STD_DC_CHROMA_VALS, STD_DC_LUMA_BITS, STD_DC_LUMA_VALS,
};
use crate::jpeg::markers;
use crate::jpeg::quant::{scale_for_quality, DEFAULT_CHROMA_Q50, DEFAULT_LUMA_Q50};
use crate::jpeg::zigzag::ZIGZAG;
pub const DEFAULT_QUALITY: u8 = 75;
pub fn encode_jpeg(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_with_opts(frame, width, height, pix, quality, 0)
}
pub fn extract_app_segments(jpeg: &[u8]) -> Vec<u8> {
if jpeg.len() < 2 || jpeg[0] != 0xFF || jpeg[1] != markers::SOI {
return Vec::new();
}
let mut out = Vec::new();
let mut pos = 2usize;
while pos + 3 < jpeg.len() {
if jpeg[pos] != 0xFF {
break;
}
let mut p = pos + 1;
while p < jpeg.len() && jpeg[p] == 0xFF {
p += 1;
}
if p >= jpeg.len() {
break;
}
let marker = jpeg[p];
p += 1; if marker == markers::SOI || marker == markers::EOI || markers::is_rst(marker) {
pos = p;
continue;
}
if p + 2 > jpeg.len() {
break;
}
let len = u16::from_be_bytes([jpeg[p], jpeg[p + 1]]) as usize;
if len < 2 || p + len > jpeg.len() {
break;
}
if markers::is_app(marker) || marker == markers::COM {
out.push(0xFF);
out.push(marker);
out.extend_from_slice(&jpeg[p..p + len]);
}
if marker == markers::DQT
|| markers::is_sof(marker)
|| marker == markers::SOS
|| marker == markers::DHT
{
break;
}
pos = p + len;
}
out
}
pub fn encode_jpeg_with_opts(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
restart_interval: u16,
) -> Result<Vec<u8>> {
encode_jpeg_with_meta(frame, width, height, pix, quality, restart_interval, &[])
}
pub fn encode_jpeg_with_meta(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
restart_interval: u16,
meta: &[u8],
) -> Result<Vec<u8>> {
let width = width as usize;
let height = height as usize;
let (h_factor, v_factor) = match pix {
PixelFormat::Yuv444P => (1u8, 1u8),
PixelFormat::Yuv422P => (2, 1),
PixelFormat::Yuv420P => (2, 2),
_ => {
return Err(Error::unsupported(
"MJPEG encoder: unsupported pixel format",
))
}
};
if frame.planes.len() != 3 {
return Err(Error::invalid("MJPEG encoder: expected 3 planes"));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
if meta.is_empty() {
write_jfif_app0(&mut out);
} else {
out.extend_from_slice(meta);
}
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof0(&mut out, width as u16, height as u16, h_factor, v_factor);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos(&mut out);
write_scan(
&mut out,
frame,
pix,
width,
height,
h_factor,
v_factor,
&luma_q,
&chroma_q,
&huff,
restart_interval,
)?;
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
pub fn encode_jpeg_progressive(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_progressive_with_meta(frame, width, height, pix, quality, &[])
}
pub fn encode_jpeg_progressive_with_meta(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
meta: &[u8],
) -> Result<Vec<u8>> {
encode_jpeg_progressive_inner(frame, width, height, pix, quality, meta, false)
}
pub fn encode_jpeg_progressive_sa(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_progressive_sa_with_meta(frame, width, height, pix, quality, &[])
}
pub fn encode_jpeg_progressive_sa_with_meta(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
meta: &[u8],
) -> Result<Vec<u8>> {
encode_jpeg_progressive_inner(frame, width, height, pix, quality, meta, true)
}
#[allow(clippy::too_many_arguments)]
fn encode_jpeg_progressive_inner(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
meta: &[u8],
use_sa: bool,
) -> Result<Vec<u8>> {
let width = width as usize;
let height = height as usize;
let (h_factor, v_factor) = match pix {
PixelFormat::Yuv444P => (1u8, 1u8),
PixelFormat::Yuv422P => (2, 1),
PixelFormat::Yuv420P => (2, 2),
_ => {
return Err(Error::unsupported(
"MJPEG progressive encoder: unsupported pixel format",
))
}
};
if frame.planes.len() != 3 {
return Err(Error::invalid(
"MJPEG progressive encoder: expected 3 planes",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mcu_w_px = 8 * h_factor as usize;
let mcu_h_px = 8 * v_factor as usize;
let mcus_x = width.div_ceil(mcu_w_px);
let mcus_y = height.div_ceil(mcu_h_px);
let luma_blocks_x = mcus_x * h_factor as usize;
let luma_blocks_y = mcus_y * v_factor as usize;
let mut y_coefs = vec![[0i32; 64]; luma_blocks_x * luma_blocks_y];
fill_coef_grid(
&mut y_coefs,
&frame.planes[0].data,
frame.planes[0].stride,
width,
height,
luma_blocks_x,
luma_blocks_y,
&luma_q,
);
let (c_w, c_h) = match pix {
PixelFormat::Yuv444P => (width, height),
PixelFormat::Yuv422P => (width.div_ceil(2), height),
PixelFormat::Yuv420P => (width.div_ceil(2), height.div_ceil(2)),
_ => unreachable!(),
};
let chroma_blocks_x = mcus_x;
let chroma_blocks_y = mcus_y;
let mut cb_coefs = vec![[0i32; 64]; chroma_blocks_x * chroma_blocks_y];
let mut cr_coefs = vec![[0i32; 64]; chroma_blocks_x * chroma_blocks_y];
fill_coef_grid(
&mut cb_coefs,
&frame.planes[1].data,
frame.planes[1].stride,
c_w,
c_h,
chroma_blocks_x,
chroma_blocks_y,
&chroma_q,
);
fill_coef_grid(
&mut cr_coefs,
&frame.planes[2].data,
frame.planes[2].stride,
c_w,
c_h,
chroma_blocks_x,
chroma_blocks_y,
&chroma_q,
);
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
if meta.is_empty() {
write_jfif_app0(&mut out);
} else {
out.extend_from_slice(meta);
}
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof2(&mut out, width as u16, height as u16, h_factor, v_factor);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
if use_sa {
write_sos_progressive_dc_sa(&mut out, 0, 1);
write_dc_scan_interleaved_sa(
&mut out,
&y_coefs,
luma_blocks_x,
h_factor as usize,
v_factor as usize,
mcus_x,
mcus_y,
&cb_coefs,
&cr_coefs,
chroma_blocks_x,
&huff,
1, );
for &(ss, se) in &[(1u8, 5u8), (6u8, 63u8)] {
write_sos_progressive_ac_sa(&mut out, 1, 0, ss, se, 0, 1);
write_ac_scan_sa(
&mut out,
&y_coefs,
luma_blocks_x * luma_blocks_y,
&huff.luma_ac,
ss as usize,
se as usize,
1,
);
write_sos_progressive_ac_sa(&mut out, 2, 1, ss, se, 0, 1);
write_ac_scan_sa(
&mut out,
&cb_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
1,
);
write_sos_progressive_ac_sa(&mut out, 3, 1, ss, se, 0, 1);
write_ac_scan_sa(
&mut out,
&cr_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
1,
);
}
write_sos_progressive_dc_sa(&mut out, 1, 0);
write_dc_refine_scan_interleaved(
&mut out,
&y_coefs,
luma_blocks_x,
h_factor as usize,
v_factor as usize,
mcus_x,
mcus_y,
&cb_coefs,
&cr_coefs,
chroma_blocks_x,
);
for &(ss, se) in &[(1u8, 5u8), (6u8, 63u8)] {
write_sos_progressive_ac_sa(&mut out, 1, 0, ss, se, 1, 0);
write_ac_refine_scan(
&mut out,
&y_coefs,
luma_blocks_x * luma_blocks_y,
&huff.luma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac_sa(&mut out, 2, 1, ss, se, 1, 0);
write_ac_refine_scan(
&mut out,
&cb_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac_sa(&mut out, 3, 1, ss, se, 1, 0);
write_ac_refine_scan(
&mut out,
&cr_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
}
} else {
write_sos_progressive_dc_interleaved(&mut out);
write_dc_scan_interleaved(
&mut out,
&y_coefs,
luma_blocks_x,
h_factor as usize,
v_factor as usize,
mcus_x,
mcus_y,
&cb_coefs,
&cr_coefs,
chroma_blocks_x,
&huff,
);
for &(ss, se) in &[(1u8, 5u8), (6u8, 63u8)] {
write_sos_progressive_ac(&mut out, 1, 0, ss, se);
write_ac_scan(
&mut out,
&y_coefs,
luma_blocks_x * luma_blocks_y,
&huff.luma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac(&mut out, 2, 1, ss, se);
write_ac_scan(
&mut out,
&cb_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac(&mut out, 3, 1, ss, se);
write_ac_scan(
&mut out,
&cr_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
}
}
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn fill_coef_grid(
coefs: &mut [[i32; 64]],
plane: &[u8],
stride: usize,
w: usize,
h: usize,
blocks_x: usize,
blocks_y: usize,
quant: &[u16; 64],
) {
for by in 0..blocks_y {
for bx in 0..blocks_x {
let mut block = [0.0f32; 64];
fill_block(&mut block, plane, stride, w, h, bx * 8, by * 8);
fdct8x8(&mut block);
let mut q = [0i32; 64];
for k in 0..64 {
let v = block[k] / quant[k] as f32;
q[k] = if v >= 0.0 {
(v + 0.5) as i32
} else {
-((-v + 0.5) as i32)
};
}
coefs[by * blocks_x + bx] = q;
}
}
}
fn write_sof2(out: &mut Vec<u8>, width: u16, height: u16, h: u8, v: u8) {
let mut payload = Vec::with_capacity(8 + 9);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(3); payload.push(1);
payload.push((h << 4) | v);
payload.push(0);
payload.push(2);
payload.push(0x11);
payload.push(1);
payload.push(3);
payload.push(0x11);
payload.push(1);
write_length_prefix(out, markers::SOF2, &payload);
}
fn write_sos_progressive_dc_interleaved(out: &mut Vec<u8>) {
let payload: [u8; 10] = [3, 1, 0x00, 2, 0x10, 3, 0x10, 0, 0, 0x00];
write_length_prefix(out, markers::SOS, &payload);
}
fn write_sos_progressive_ac(out: &mut Vec<u8>, comp_id: u8, ac_table: u8, ss: u8, se: u8) {
let payload: [u8; 6] = [
1, comp_id, ac_table & 0x0F, ss, se, 0x00, ];
write_length_prefix(out, markers::SOS, &payload);
}
fn write_sos_progressive_dc_sa(out: &mut Vec<u8>, ah: u8, al: u8) {
let payload: [u8; 10] = [3, 1, 0x00, 2, 0x10, 3, 0x10, 0, 0, (ah << 4) | (al & 0x0F)];
write_length_prefix(out, markers::SOS, &payload);
}
fn write_sos_progressive_ac_sa(
out: &mut Vec<u8>,
comp_id: u8,
ac_table: u8,
ss: u8,
se: u8,
ah: u8,
al: u8,
) {
let payload: [u8; 6] = [1, comp_id, ac_table & 0x0F, ss, se, (ah << 4) | (al & 0x0F)];
write_length_prefix(out, markers::SOS, &payload);
}
#[allow(clippy::too_many_arguments)]
fn write_dc_scan_interleaved(
out: &mut Vec<u8>,
y_coefs: &[[i32; 64]],
luma_blocks_x: usize,
h_factor: usize,
v_factor: usize,
mcus_x: usize,
mcus_y: usize,
cb_coefs: &[[i32; 64]],
cr_coefs: &[[i32; 64]],
chroma_blocks_x: usize,
huff: &DefaultHuffman,
) {
let mut bw = BitWriter::new(out);
let mut prev_dc_y: i32 = 0;
let mut prev_dc_cb: i32 = 0;
let mut prev_dc_cr: i32 = 0;
for my in 0..mcus_y {
for mx in 0..mcus_x {
for by in 0..v_factor {
for bx in 0..h_factor {
let bi_x = mx * h_factor + bx;
let bi_y = my * v_factor + by;
let bi = bi_y * luma_blocks_x + bi_x;
encode_dc(&mut bw, y_coefs[bi][0], &mut prev_dc_y, &huff.luma_dc);
}
}
let cbi = my * chroma_blocks_x + mx;
encode_dc(&mut bw, cb_coefs[cbi][0], &mut prev_dc_cb, &huff.chroma_dc);
encode_dc(&mut bw, cr_coefs[cbi][0], &mut prev_dc_cr, &huff.chroma_dc);
}
}
bw.finish();
}
fn encode_dc(bw: &mut BitWriter<'_>, dc: i32, prev_dc: &mut i32, dc_huff: &HuffTable) {
let dc_diff = dc - *prev_dc;
*prev_dc = dc;
let (size, bits) = category(dc_diff);
let hc = dc_huff.encode[size as usize];
bw.write_bits(hc.code as u32, hc.len as u32);
if size > 0 {
bw.write_bits(bits, size as u32);
}
}
fn write_ac_scan(
out: &mut Vec<u8>,
coefs: &[[i32; 64]],
block_count: usize,
ac_huff: &HuffTable,
ss: usize,
se: usize,
) {
let mut bw = BitWriter::new(out);
for bi in 0..block_count {
let block = &coefs[bi];
let mut run: u32 = 0;
for k in ss..=se {
let v = block[ZIGZAG[k]];
if v == 0 {
run += 1;
} else {
while run >= 16 {
let zc = ac_huff.encode[0xF0];
bw.write_bits(zc.code as u32, zc.len as u32);
run -= 16;
}
let (sz, bv) = category(v);
let rs = ((run as u8) << 4) | sz;
let ac = ac_huff.encode[rs as usize];
bw.write_bits(ac.code as u32, ac.len as u32);
if sz > 0 {
bw.write_bits(bv, sz as u32);
}
run = 0;
}
}
if run > 0 {
let eob = ac_huff.encode[0x00];
bw.write_bits(eob.code as u32, eob.len as u32);
}
}
bw.finish();
}
#[allow(clippy::too_many_arguments)]
fn write_dc_scan_interleaved_sa(
out: &mut Vec<u8>,
y_coefs: &[[i32; 64]],
luma_blocks_x: usize,
h_factor: usize,
v_factor: usize,
mcus_x: usize,
mcus_y: usize,
cb_coefs: &[[i32; 64]],
cr_coefs: &[[i32; 64]],
chroma_blocks_x: usize,
huff: &DefaultHuffman,
al: u8,
) {
let mut bw = BitWriter::new(out);
let mut prev_dc_y: i32 = 0;
let mut prev_dc_cb: i32 = 0;
let mut prev_dc_cr: i32 = 0;
for my in 0..mcus_y {
for mx in 0..mcus_x {
for by in 0..v_factor {
for bx in 0..h_factor {
let bi_x = mx * h_factor + bx;
let bi_y = my * v_factor + by;
let bi = bi_y * luma_blocks_x + bi_x;
encode_dc_sa(&mut bw, y_coefs[bi][0], &mut prev_dc_y, &huff.luma_dc, al);
}
}
let cbi = my * chroma_blocks_x + mx;
encode_dc_sa(
&mut bw,
cb_coefs[cbi][0],
&mut prev_dc_cb,
&huff.chroma_dc,
al,
);
encode_dc_sa(
&mut bw,
cr_coefs[cbi][0],
&mut prev_dc_cr,
&huff.chroma_dc,
al,
);
}
}
bw.finish();
}
fn encode_dc_sa(bw: &mut BitWriter<'_>, dc: i32, prev_dc: &mut i32, dc_huff: &HuffTable, al: u8) {
let dc_shifted = dc >> al;
let dc_diff = dc_shifted - *prev_dc;
*prev_dc = dc_shifted;
let (size, bits) = category(dc_diff);
let hc = dc_huff.encode[size as usize];
bw.write_bits(hc.code as u32, hc.len as u32);
if size > 0 {
bw.write_bits(bits, size as u32);
}
}
#[allow(clippy::too_many_arguments)]
fn write_dc_refine_scan_interleaved(
out: &mut Vec<u8>,
y_coefs: &[[i32; 64]],
luma_blocks_x: usize,
h_factor: usize,
v_factor: usize,
mcus_x: usize,
mcus_y: usize,
cb_coefs: &[[i32; 64]],
cr_coefs: &[[i32; 64]],
chroma_blocks_x: usize,
) {
let mut bw = BitWriter::new(out);
for my in 0..mcus_y {
for mx in 0..mcus_x {
for by in 0..v_factor {
for bx in 0..h_factor {
let bi_x = mx * h_factor + bx;
let bi_y = my * v_factor + by;
let bi = bi_y * luma_blocks_x + bi_x;
let dc = y_coefs[bi][0];
bw.write_bits(dc.unsigned_abs() & 1, 1);
}
}
let cbi = my * chroma_blocks_x + mx;
let dc_cb = cb_coefs[cbi][0];
bw.write_bits(dc_cb.unsigned_abs() & 1, 1);
let dc_cr = cr_coefs[cbi][0];
bw.write_bits(dc_cr.unsigned_abs() & 1, 1);
}
}
bw.finish();
}
fn write_ac_scan_sa(
out: &mut Vec<u8>,
coefs: &[[i32; 64]],
block_count: usize,
ac_huff: &HuffTable,
ss: usize,
se: usize,
al: u8,
) {
let mut bw = BitWriter::new(out);
for bi in 0..block_count {
let block = &coefs[bi];
let mut run: u32 = 0;
for k in ss..=se {
let v = block[ZIGZAG[k]] >> al; if v == 0 {
run += 1;
} else {
while run >= 16 {
let zc = ac_huff.encode[0xF0];
bw.write_bits(zc.code as u32, zc.len as u32);
run -= 16;
}
let (sz, bv) = category(v);
let rs = ((run as u8) << 4) | sz;
let ac = ac_huff.encode[rs as usize];
bw.write_bits(ac.code as u32, ac.len as u32);
if sz > 0 {
bw.write_bits(bv, sz as u32);
}
run = 0;
}
}
if run > 0 {
let eob = ac_huff.encode[0x00];
bw.write_bits(eob.code as u32, eob.len as u32);
}
}
bw.finish();
}
fn write_ac_refine_scan(
out: &mut Vec<u8>,
coefs: &[[i32; 64]],
block_count: usize,
ac_huff: &HuffTable,
ss: usize,
se: usize,
) {
let mut bw = BitWriter::new(out);
for bi in 0..block_count {
emit_ac_refine_block(&mut bw, &coefs[bi], ss, se, ac_huff);
}
bw.finish();
}
fn emit_ac_refine_block(
bw: &mut BitWriter<'_>,
block: &[i32; 64],
ss: usize,
se: usize,
ac_huff: &HuffTable,
) {
let has_new = (ss..=se).any(|k| {
let v = block[ZIGZAG[k]];
(v >> 1) == 0 && v != 0
});
if !has_new {
let eob = ac_huff.encode[0x00];
bw.write_bits(eob.code as u32, eob.len as u32);
for k in ss..=se {
let v = block[ZIGZAG[k]];
if (v >> 1) != 0 {
bw.write_bits(v.unsigned_abs() & 1, 1);
}
}
return;
}
let mut new_nz: Vec<(usize, u32)> = Vec::new(); for k in ss..=se {
let v = block[ZIGZAG[k]];
if (v >> 1) == 0 && v != 0 {
let sign = if v > 0 { 1u32 } else { 0u32 };
new_nz.push((k, sign));
}
}
let mut decoder_nonzero = [false; 64]; for k in ss..=se {
let v = block[ZIGZAG[k]];
if (v >> 1) != 0 {
decoder_nonzero[k] = true;
}
}
let mut k = ss;
for (event_k, sign) in &new_nz {
let mut run: u32 = 0;
let mut pos = k;
while pos < *event_k {
if !decoder_nonzero[pos] {
run += 1;
}
pos += 1;
}
while run >= 16 {
let zrl = ac_huff.encode[0xF0];
bw.write_bits(zrl.code as u32, zrl.len as u32);
let mut zrl_run = 0u32;
while zrl_run < 16 {
if decoder_nonzero[k] {
let v = block[ZIGZAG[k]];
bw.write_bits(v.unsigned_abs() & 1, 1);
} else {
zrl_run += 1;
}
k += 1;
}
run -= 16;
}
let rs = ((run as u8) << 4) | 1;
let hc = ac_huff.encode[rs as usize];
bw.write_bits(hc.code as u32, hc.len as u32);
bw.write_bits(*sign, 1);
while k < *event_k {
if decoder_nonzero[k] {
let v = block[ZIGZAG[k]];
bw.write_bits(v.unsigned_abs() & 1, 1);
}
k += 1;
}
decoder_nonzero[*event_k] = true;
k = event_k + 1;
}
while k <= se {
if decoder_nonzero[k] {
let v = block[ZIGZAG[k]];
if (v >> 1) != 0 {
bw.write_bits(v.unsigned_abs() & 1, 1);
} else {
bw.write_bits(0, 1);
}
}
k += 1;
}
}
fn write_length_prefix(out: &mut Vec<u8>, marker: u8, payload: &[u8]) {
let len = (payload.len() + 2) as u16;
out.push(0xFF);
out.push(marker);
out.push((len >> 8) as u8);
out.push(len as u8);
out.extend_from_slice(payload);
}
fn write_jfif_app0(out: &mut Vec<u8>) {
let payload = [
b'J', b'F', b'I', b'F', 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, ];
write_length_prefix(out, markers::APP0, &payload);
}
fn write_dqt(out: &mut Vec<u8>, table_id: u8, nat_order: &[u16; 64]) {
let mut payload = Vec::with_capacity(1 + 64);
payload.push(table_id & 0x0F); for k in 0..64 {
payload.push(nat_order[ZIGZAG[k]].min(255) as u8);
}
write_length_prefix(out, markers::DQT, &payload);
}
fn write_sof0(out: &mut Vec<u8>, width: u16, height: u16, h: u8, v: u8) {
let mut payload = Vec::with_capacity(8 + 9);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(3); payload.push(1);
payload.push((h << 4) | v);
payload.push(0);
payload.push(2);
payload.push(0x11);
payload.push(1);
payload.push(3);
payload.push(0x11);
payload.push(1);
write_length_prefix(out, markers::SOF0, &payload);
}
fn write_dht(out: &mut Vec<u8>, class: u8, id: u8, bits: &[u8; 16], values: &[u8]) {
let mut payload = Vec::with_capacity(1 + 16 + values.len());
payload.push(((class & 0x01) << 4) | (id & 0x0F));
payload.extend_from_slice(bits);
payload.extend_from_slice(values);
write_length_prefix(out, markers::DHT, &payload);
}
fn write_dri(out: &mut Vec<u8>, restart_interval: u16) {
out.push(0xFF);
out.push(markers::DRI);
out.push(0x00);
out.push(0x04);
out.extend_from_slice(&restart_interval.to_be_bytes());
}
fn write_sos(out: &mut Vec<u8>) {
let payload: [u8; 10] = [
3, 1, 0x00, 2, 0x11, 3, 0x11, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[allow(clippy::too_many_arguments)]
fn write_scan(
out: &mut Vec<u8>,
frame: &VideoFrame,
pix: PixelFormat,
width: usize,
height: usize,
h_factor: u8,
v_factor: u8,
luma_q: &[u16; 64],
chroma_q: &[u16; 64],
huff: &DefaultHuffman,
restart_interval: u16,
) -> Result<()> {
let mcu_w_px = 8 * h_factor as usize;
let mcu_h_px = 8 * v_factor as usize;
let mcus_x = width.div_ceil(mcu_w_px);
let mcus_y = height.div_ceil(mcu_h_px);
let total_mcus = mcus_x.saturating_mul(mcus_y);
let y_plane = &frame.planes[0];
let cb_plane = &frame.planes[1];
let cr_plane = &frame.planes[2];
let (c_w, c_h) = match pix {
PixelFormat::Yuv444P => (width, height),
PixelFormat::Yuv422P => (width.div_ceil(2), height),
PixelFormat::Yuv420P => (width.div_ceil(2), height.div_ceil(2)),
_ => unreachable!(),
};
let mut prev_dc_y: i32 = 0;
let mut prev_dc_cb: i32 = 0;
let mut prev_dc_cr: i32 = 0;
let ri = restart_interval as usize;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: usize = 0;
let mut mcu_index: usize = 0;
let mut bw = BitWriter::new(out);
for my in 0..mcus_y {
for mx in 0..mcus_x {
for by in 0..v_factor as usize {
for bx in 0..h_factor as usize {
let x0 = mx * mcu_w_px + bx * 8;
let y0 = my * mcu_h_px + by * 8;
let mut blk = [0.0f32; 64];
fill_block(
&mut blk,
&y_plane.data,
y_plane.stride,
width,
height,
x0,
y0,
);
encode_block(
&mut bw,
&mut blk,
luma_q,
&mut prev_dc_y,
&huff.luma_dc,
&huff.luma_ac,
);
}
}
let cb_x0 = mx * 8;
let cb_y0 = my * 8;
let mut blk_cb = [0.0f32; 64];
fill_block(
&mut blk_cb,
&cb_plane.data,
cb_plane.stride,
c_w,
c_h,
cb_x0,
cb_y0,
);
encode_block(
&mut bw,
&mut blk_cb,
chroma_q,
&mut prev_dc_cb,
&huff.chroma_dc,
&huff.chroma_ac,
);
let mut blk_cr = [0.0f32; 64];
fill_block(
&mut blk_cr,
&cr_plane.data,
cr_plane.stride,
c_w,
c_h,
cb_x0,
cb_y0,
);
encode_block(
&mut bw,
&mut blk_cr,
chroma_q,
&mut prev_dc_cr,
&huff.chroma_dc,
&huff.chroma_ac,
);
mcu_index += 1;
mcus_since_restart += 1;
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
prev_dc_y = 0;
prev_dc_cb = 0;
prev_dc_cr = 0;
mcus_since_restart = 0;
}
}
}
bw.finish();
Ok(())
}
fn fill_block(
dst: &mut [f32; 64],
plane: &[u8],
stride: usize,
w: usize,
h: usize,
x0: usize,
y0: usize,
) {
for j in 0..8 {
let y = (y0 + j).min(h.saturating_sub(1));
for i in 0..8 {
let x = (x0 + i).min(w.saturating_sub(1));
let v = plane[y * stride + x] as i32;
dst[j * 8 + i] = (v - 128) as f32;
}
}
}
fn encode_block(
bw: &mut BitWriter<'_>,
block: &mut [f32; 64],
quant: &[u16; 64],
prev_dc: &mut i32,
dc_huff: &HuffTable,
ac_huff: &HuffTable,
) {
fdct8x8(block);
let mut q = [0i32; 64];
for k in 0..64 {
let v = block[k] / quant[k] as f32;
q[k] = if v >= 0.0 {
(v + 0.5) as i32
} else {
-((-v + 0.5) as i32)
};
}
let dc_diff = q[0] - *prev_dc;
*prev_dc = q[0];
let (size, bits) = category(dc_diff);
let hc = dc_huff.encode[size as usize];
bw.write_bits(hc.code as u32, hc.len as u32);
if size > 0 {
bw.write_bits(bits, size as u32);
}
let mut run: u32 = 0;
for k in 1..64 {
let v = q[ZIGZAG[k]];
if v == 0 {
run += 1;
} else {
while run >= 16 {
let zc = ac_huff.encode[0xF0];
bw.write_bits(zc.code as u32, zc.len as u32);
run -= 16;
}
let (sz, bv) = category(v);
let rs = ((run as u8) << 4) | sz;
let ac = ac_huff.encode[rs as usize];
bw.write_bits(ac.code as u32, ac.len as u32);
if sz > 0 {
bw.write_bits(bv, sz as u32);
}
run = 0;
}
}
if run > 0 {
let eob = ac_huff.encode[0x00];
bw.write_bits(eob.code as u32, eob.len as u32);
}
}
fn category(v: i32) -> (u8, u32) {
if v == 0 {
return (0, 0);
}
let abs = v.unsigned_abs();
let size = 32 - abs.leading_zeros();
debug_assert!(size <= 16);
let bits = if v > 0 {
abs
} else {
(1u32 << size).wrapping_sub(1).wrapping_add(v as u32)
};
(size as u8, bits)
}
struct BitWriter<'a> {
out: &'a mut Vec<u8>,
buf: u32,
nbits: u32,
}
impl<'a> BitWriter<'a> {
fn new(out: &'a mut Vec<u8>) -> Self {
Self {
out,
buf: 0,
nbits: 0,
}
}
fn write_bits(&mut self, value: u32, len: u32) {
if len == 0 {
return;
}
let v = value & ((1u32 << len) - 1);
self.buf = (self.buf << len) | v;
self.nbits += len;
while self.nbits >= 8 {
self.nbits -= 8;
let b = ((self.buf >> self.nbits) & 0xFF) as u8;
self.out.push(b);
if b == 0xFF {
self.out.push(0x00);
}
}
}
fn finish(&mut self) {
self.flush_to_byte();
}
fn flush_to_byte(&mut self) {
if self.nbits > 0 {
let pad = 8 - self.nbits;
self.buf = (self.buf << pad) | ((1u32 << pad) - 1);
self.nbits = 0;
let b = (self.buf & 0xFF) as u8;
self.out.push(b);
if b == 0xFF {
self.out.push(0x00);
}
}
}
fn emit_raw_marker(&mut self, marker: u8) {
debug_assert_eq!(self.nbits, 0);
self.out.push(0xFF);
self.out.push(marker);
}
}
#[cfg(test)]
pub(crate) fn encode_jpeg_non_interleaved(
frame: &VideoFrame,
width: u32,
height: u32,
pix: PixelFormat,
quality: u8,
) -> Result<Vec<u8>> {
let width = width as usize;
let height = height as usize;
let (h_factor, v_factor) = match pix {
PixelFormat::Yuv444P => (1u8, 1u8),
PixelFormat::Yuv422P => (2, 1),
PixelFormat::Yuv420P => (2, 2),
_ => {
return Err(Error::unsupported(
"non-interleaved helper: unsupported format",
))
}
};
if frame.planes.len() != 3 {
return Err(Error::invalid("non-interleaved helper: expected 3 planes"));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof0(&mut out, width as u16, height as u16, h_factor, v_factor);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
let mcu_w_px = 8 * h_factor as usize;
let mcu_h_px = 8 * v_factor as usize;
let mcus_x = width.div_ceil(mcu_w_px);
let mcus_y = height.div_ceil(mcu_h_px);
let (c_w, c_h) = match pix {
PixelFormat::Yuv444P => (width, height),
PixelFormat::Yuv422P => (width.div_ceil(2), height),
PixelFormat::Yuv420P => (width.div_ceil(2), height.div_ceil(2)),
_ => unreachable!(),
};
write_non_interleaved_sos(&mut out, 1, 0, 0);
write_component_scan(
&mut out,
&frame.planes[0].data,
frame.planes[0].stride,
width,
height,
mcus_x * h_factor as usize,
mcus_y * v_factor as usize,
&luma_q,
&huff.luma_dc,
&huff.luma_ac,
);
write_non_interleaved_sos(&mut out, 2, 1, 1);
write_component_scan(
&mut out,
&frame.planes[1].data,
frame.planes[1].stride,
c_w,
c_h,
mcus_x,
mcus_y,
&chroma_q,
&huff.chroma_dc,
&huff.chroma_ac,
);
write_non_interleaved_sos(&mut out, 3, 1, 1);
write_component_scan(
&mut out,
&frame.planes[2].data,
frame.planes[2].stride,
c_w,
c_h,
mcus_x,
mcus_y,
&chroma_q,
&huff.chroma_dc,
&huff.chroma_ac,
);
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
#[cfg(test)]
fn write_non_interleaved_sos(out: &mut Vec<u8>, comp_id: u8, dc_table: u8, ac_table: u8) {
let payload = [
1, comp_id,
((dc_table & 0x0F) << 4) | (ac_table & 0x0F),
0,
63,
0, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
fn write_component_scan(
out: &mut Vec<u8>,
plane: &[u8],
plane_stride: usize,
plane_w: usize,
plane_h: usize,
blocks_x: usize,
blocks_y: usize,
quant: &[u16; 64],
dc_huff: &HuffTable,
ac_huff: &HuffTable,
) {
let mut bw = BitWriter::new(out);
let mut prev_dc: i32 = 0;
for by in 0..blocks_y {
for bx in 0..blocks_x {
let mut blk = [0.0f32; 64];
fill_block(
&mut blk,
plane,
plane_stride,
plane_w,
plane_h,
bx * 8,
by * 8,
);
encode_block(&mut bw, &mut blk, quant, &mut prev_dc, dc_huff, ac_huff);
}
}
bw.finish();
}
#[allow(clippy::too_many_arguments)]
pub fn encode_jpeg_cmyk_1111(
width: u32,
height: u32,
planes: &[&[u8]; 4],
plane_strides: &[usize; 4],
quality: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
for (i, p) in planes.iter().enumerate() {
if p.len() < plane_strides[i] * h {
return Err(Error::invalid(
"cmyk helper: plane shorter than height*stride",
));
}
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let needs_invert_all = matches!(adobe_transform, Some(0));
let needs_invert_k_only = matches!(adobe_transform, Some(2));
let maybe_invert = |plane_idx: usize, samples: &[u8]| -> Vec<u8> {
let must = needs_invert_all || (needs_invert_k_only && plane_idx == 3);
if !must {
return samples.to_vec();
}
samples.iter().map(|&b| 255 - b).collect()
};
let owned: [Vec<u8>; 4] = [
maybe_invert(0, planes[0]),
maybe_invert(1, planes[1]),
maybe_invert(2, planes[2]),
maybe_invert(3, planes[3]),
];
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
if let Some(tx) = adobe_transform {
write_adobe_app14(&mut out, tx);
}
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof0_4comp(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
write_sos_4comp(&mut out);
let mcus_x = w.div_ceil(8);
let mcus_y = h.div_ceil(8);
let mut bw = BitWriter::new(&mut out);
let mut prev_dc = [0i32; 4];
for my in 0..mcus_y {
for mx in 0..mcus_x {
for ci in 0..4 {
let mut blk = [0.0f32; 64];
fill_block(
&mut blk,
&owned[ci],
plane_strides[ci],
w,
h,
mx * 8,
my * 8,
);
let (qt, dc_t, ac_t) = if ci == 0 {
(&luma_q, &huff.luma_dc, &huff.luma_ac)
} else {
(&chroma_q, &huff.chroma_dc, &huff.chroma_ac)
};
encode_block(&mut bw, &mut blk, qt, &mut prev_dc[ci], dc_t, ac_t);
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn write_adobe_app14(out: &mut Vec<u8>, transform: u8) {
let payload = [
b'A', b'd', b'o', b'b', b'e', 0, 100, 0, 0, 0, 0, transform,
];
write_length_prefix(out, 0xEE, &payload);
}
fn write_sof0_4comp(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 12);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(4); for (id, qt) in [(1, 0u8), (2, 1), (3, 1), (4, 1)] {
payload.push(id);
payload.push(0x11); payload.push(qt);
}
write_length_prefix(out, markers::SOF0, &payload);
}
fn write_sos_4comp(out: &mut Vec<u8>) {
let payload: [u8; 12] = [
4, 1, 0x00, 2, 0x11, 3, 0x11, 4, 0x11, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
fn write_sof2_4comp(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 12);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(4); for (id, qt) in [(1, 0u8), (2, 1), (3, 1), (4, 1)] {
payload.push(id);
payload.push(0x11); payload.push(qt);
}
write_length_prefix(out, markers::SOF2, &payload);
}
fn write_sos_progressive_dc_4comp_interleaved(out: &mut Vec<u8>) {
let payload: [u8; 12] = [
4, 1, 0x00, 2, 0x10, 3, 0x10, 4, 0x10, 0, 0, 0x00, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[allow(clippy::too_many_arguments)]
pub fn encode_jpeg_progressive_cmyk_1111(
width: u32,
height: u32,
planes: &[&[u8]; 4],
plane_strides: &[usize; 4],
quality: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
for (i, p) in planes.iter().enumerate() {
if p.len() < plane_strides[i] * h {
return Err(Error::invalid(
"cmyk progressive helper: plane shorter than height*stride",
));
}
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let needs_invert_all = matches!(adobe_transform, Some(0));
let needs_invert_k_only = matches!(adobe_transform, Some(2));
let maybe_invert = |plane_idx: usize, samples: &[u8]| -> Vec<u8> {
let must = needs_invert_all || (needs_invert_k_only && plane_idx == 3);
if !must {
return samples.to_vec();
}
samples.iter().map(|&b| 255 - b).collect()
};
let owned: [Vec<u8>; 4] = [
maybe_invert(0, planes[0]),
maybe_invert(1, planes[1]),
maybe_invert(2, planes[2]),
maybe_invert(3, planes[3]),
];
let mcus_x = w.div_ceil(8);
let mcus_y = h.div_ceil(8);
let blocks = mcus_x * mcus_y;
let qts: [&[u16; 64]; 4] = [&luma_q, &chroma_q, &chroma_q, &chroma_q];
let mut comp_coefs: [Vec<[i32; 64]>; 4] = [
vec![[0i32; 64]; blocks],
vec![[0i32; 64]; blocks],
vec![[0i32; 64]; blocks],
vec![[0i32; 64]; blocks],
];
for ci in 0..4 {
fill_coef_grid(
&mut comp_coefs[ci],
&owned[ci],
plane_strides[ci],
w,
h,
mcus_x,
mcus_y,
qts[ci],
);
}
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
if let Some(tx) = adobe_transform {
write_adobe_app14(&mut out, tx);
}
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof2_4comp(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
write_sos_progressive_dc_4comp_interleaved(&mut out);
{
let mut bw = BitWriter::new(&mut out);
let mut prev_dc = [0i32; 4];
for my in 0..mcus_y {
for mx in 0..mcus_x {
let bi = my * mcus_x + mx;
for ci in 0..4 {
let dc_t = if ci == 0 {
&huff.luma_dc
} else {
&huff.chroma_dc
};
encode_dc(&mut bw, comp_coefs[ci][bi][0], &mut prev_dc[ci], dc_t);
}
}
}
bw.finish();
}
for &(ss, se) in &[(1u8, 5u8), (6u8, 63u8)] {
for ci in 0..4 {
let (comp_id, ac_table, ac_huff) = if ci == 0 {
(1u8, 0u8, &huff.luma_ac)
} else {
((ci as u8) + 1, 1u8, &huff.chroma_ac)
};
write_sos_progressive_ac(&mut out, comp_id, ac_table, ss, se);
write_ac_scan(
&mut out,
&comp_coefs[ci],
blocks,
ac_huff,
ss as usize,
se as usize,
);
}
}
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn unpack_cmyk(
width: u32,
height: u32,
packed: &[u8],
packed_stride: usize,
) -> Result<[Vec<u8>; 4]> {
let w = width as usize;
let h = height as usize;
if packed_stride < w * 4 {
return Err(Error::invalid(
"encode_jpeg_cmyk: packed stride must be at least width * 4",
));
}
if packed.len() < packed_stride * h {
return Err(Error::invalid(
"encode_jpeg_cmyk: packed buffer shorter than height * stride",
));
}
let mut c = vec![0u8; w * h];
let mut m = vec![0u8; w * h];
let mut y = vec![0u8; w * h];
let mut k = vec![0u8; w * h];
for j in 0..h {
let row = &packed[j * packed_stride..j * packed_stride + w * 4];
for i in 0..w {
let o = i * 4;
c[j * w + i] = row[o];
m[j * w + i] = row[o + 1];
y[j * w + i] = row[o + 2];
k[j * w + i] = row[o + 3];
}
}
Ok([c, m, y, k])
}
pub fn encode_jpeg_cmyk(
width: u32,
height: u32,
packed: &[u8],
stride: usize,
quality: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
if let Some(t) = adobe_transform {
if t != 0 && t != 2 {
return Err(Error::invalid(
"encode_jpeg_cmyk: adobe_transform must be 0 (CMYK) or 2 (YCCK)",
));
}
}
let planes = unpack_cmyk(width, height, packed, stride)?;
let refs: [&[u8]; 4] = [&planes[0], &planes[1], &planes[2], &planes[3]];
let w = width as usize;
let strides = [w; 4];
encode_jpeg_cmyk_1111(width, height, &refs, &strides, quality, adobe_transform)
}
pub fn encode_jpeg_cmyk_progressive(
width: u32,
height: u32,
packed: &[u8],
stride: usize,
quality: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
if let Some(t) = adobe_transform {
if t != 0 && t != 2 {
return Err(Error::invalid(
"encode_jpeg_cmyk_progressive: adobe_transform must be 0 (CMYK) or 2 (YCCK)",
));
}
}
let planes = unpack_cmyk(width, height, packed, stride)?;
let refs: [&[u8]; 4] = [&planes[0], &planes[1], &planes[2], &planes[3]];
let w = width as usize;
let strides = [w; 4];
encode_jpeg_progressive_cmyk_1111(width, height, &refs, &strides, quality, adobe_transform)
}
pub fn encode_jpeg_grayscale(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_grayscale_with_meta(width, height, samples, stride, quality, 0, &[])
}
pub fn encode_jpeg_grayscale_with_opts(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
restart_interval: u16,
) -> Result<Vec<u8>> {
encode_jpeg_grayscale_with_meta(
width,
height,
samples,
stride,
quality,
restart_interval,
&[],
)
}
pub fn encode_jpeg_grayscale_with_meta(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
restart_interval: u16,
meta: &[u8],
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("grayscale encoder: zero-size image"));
}
if stride < w {
return Err(Error::invalid(
"grayscale encoder: stride smaller than width",
));
}
if samples.len() < stride * h {
return Err(Error::invalid(
"grayscale encoder: samples shorter than stride*h",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(8_192);
out.push(0xFF);
out.push(markers::SOI);
if meta.is_empty() {
write_jfif_app0(&mut out);
} else {
out.extend_from_slice(meta);
}
write_dqt(&mut out, 0, &luma_q);
write_sof0_grayscale_8bit(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_grayscale_8bit(&mut out);
let mcus_x = w.div_ceil(8);
let mcus_y = h.div_ceil(8);
let total_mcus = mcus_x.saturating_mul(mcus_y);
let ri = restart_interval as usize;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: usize = 0;
let mut mcu_index: usize = 0;
let mut bw = BitWriter::new(&mut out);
let mut prev_dc: i32 = 0;
for my in 0..mcus_y {
for mx in 0..mcus_x {
let mut blk = [0.0f32; 64];
fill_block(&mut blk, samples, stride, w, h, mx * 8, my * 8);
encode_block(
&mut bw,
&mut blk,
&luma_q,
&mut prev_dc,
&huff.luma_dc,
&huff.luma_ac,
);
mcu_index += 1;
mcus_since_restart += 1;
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
prev_dc = 0;
mcus_since_restart = 0;
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn write_sof0_grayscale_8bit(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 3);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(1); payload.push(1); payload.push(0x11); payload.push(0); write_length_prefix(out, markers::SOF0, &payload);
}
fn write_sos_grayscale_8bit(out: &mut Vec<u8>) {
let payload: [u8; 6] = [
1, 1, 0x00, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
pub fn encode_jpeg_progressive_grayscale(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_progressive_grayscale_with_meta(width, height, samples, stride, quality, &[])
}
pub fn encode_jpeg_progressive_grayscale_with_meta(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
meta: &[u8],
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid(
"progressive grayscale encoder: zero-size image",
));
}
if stride < w {
return Err(Error::invalid(
"progressive grayscale encoder: stride smaller than width",
));
}
if samples.len() < stride * h {
return Err(Error::invalid(
"progressive grayscale encoder: samples shorter than stride*h",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let blocks_x = w.div_ceil(8);
let blocks_y = h.div_ceil(8);
let mut coefs = vec![[0i32; 64]; blocks_x * blocks_y];
fill_coef_grid(
&mut coefs, samples, stride, w, h, blocks_x, blocks_y, &luma_q,
);
let mut out: Vec<u8> = Vec::with_capacity(8_192);
out.push(0xFF);
out.push(markers::SOI);
if meta.is_empty() {
write_jfif_app0(&mut out);
} else {
out.extend_from_slice(meta);
}
write_dqt(&mut out, 0, &luma_q);
write_sof2_grayscale_8bit(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_sos_progressive_ac(&mut out, 1, 0, 0, 0);
{
let mut bw = BitWriter::new(&mut out);
let mut prev_dc: i32 = 0;
for bi in 0..coefs.len() {
encode_dc(&mut bw, coefs[bi][0], &mut prev_dc, &huff.luma_dc);
}
bw.finish();
}
write_sos_progressive_ac(&mut out, 1, 0, 1, 5);
write_ac_scan(&mut out, &coefs, coefs.len(), &huff.luma_ac, 1, 5);
write_sos_progressive_ac(&mut out, 1, 0, 6, 63);
write_ac_scan(&mut out, &coefs, coefs.len(), &huff.luma_ac, 6, 63);
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn write_sof2_grayscale_8bit(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 3);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(1); payload.push(1); payload.push(0x11); payload.push(0); write_length_prefix(out, markers::SOF2, &payload);
}
pub fn encode_jpeg_rgb24(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
) -> Result<Vec<u8>> {
encode_jpeg_rgb24_with_meta(width, height, samples, stride, quality, 0, &[])
}
pub fn encode_jpeg_rgb24_with_opts(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
restart_interval: u16,
) -> Result<Vec<u8>> {
encode_jpeg_rgb24_with_meta(
width,
height,
samples,
stride,
quality,
restart_interval,
&[],
)
}
pub fn encode_jpeg_rgb24_with_meta(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
quality: u8,
restart_interval: u16,
meta: &[u8],
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("RGB24 encoder: zero-size image"));
}
let min_stride = w.checked_mul(3).ok_or_else(|| {
Error::invalid("RGB24 encoder: width * 3 overflow when computing min stride")
})?;
if stride < min_stride {
return Err(Error::invalid(
"RGB24 encoder: stride smaller than width * 3",
));
}
if samples.len() < stride.saturating_mul(h) {
return Err(Error::invalid(
"RGB24 encoder: samples shorter than stride*h",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
if meta.is_empty() {
write_jfif_app0(&mut out);
write_adobe_app14(&mut out, 0);
} else {
out.extend_from_slice(meta);
}
write_dqt(&mut out, 0, &luma_q);
write_sof0_rgb24_8bit(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_rgb24_8bit(&mut out);
let mcus_x = w.div_ceil(8);
let mcus_y = h.div_ceil(8);
let total_mcus = mcus_x.saturating_mul(mcus_y);
let ri = restart_interval as usize;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: usize = 0;
let mut mcu_index: usize = 0;
let mut bw = BitWriter::new(&mut out);
let mut prev_dc = [0i32; 3];
for my in 0..mcus_y {
for mx in 0..mcus_x {
for ch in 0..3 {
let mut blk = [0.0f32; 64];
fill_block_packed_channel(&mut blk, samples, stride, w, h, mx * 8, my * 8, ch);
encode_block(
&mut bw,
&mut blk,
&luma_q,
&mut prev_dc[ch],
&huff.luma_dc,
&huff.luma_ac,
);
}
mcu_index += 1;
mcus_since_restart += 1;
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
for d in prev_dc.iter_mut() {
*d = 0;
}
mcus_since_restart = 0;
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn write_sof0_rgb24_8bit(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 9);
payload.push(8); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(3); payload.push(b'R');
payload.push(0x11); payload.push(0); payload.push(b'G');
payload.push(0x11);
payload.push(0);
payload.push(b'B');
payload.push(0x11);
payload.push(0);
write_length_prefix(out, markers::SOF0, &payload);
}
fn write_sos_rgb24_8bit(out: &mut Vec<u8>) {
let payload: [u8; 10] = [
3, b'R', 0x00, b'G', 0x00, b'B', 0x00, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[allow(clippy::too_many_arguments)]
fn fill_block_packed_channel(
dst: &mut [f32; 64],
plane: &[u8],
stride: usize,
w: usize,
h: usize,
x0: usize,
y0: usize,
channel: usize,
) {
for j in 0..8 {
let y = (y0 + j).min(h.saturating_sub(1));
for i in 0..8 {
let x = (x0 + i).min(w.saturating_sub(1));
let v = plane[y * stride + x * 3 + channel] as i32;
dst[j * 8 + i] = (v - 128) as f32;
}
}
}
#[cfg(test)]
pub(crate) fn encode_grayscale_jpeg_12bit(
width: u32,
height: u32,
samples: &[u16],
stride: usize,
quality: u8,
) -> Result<Vec<u8>> {
let w = width as usize;
let h = height as usize;
if samples.len() < stride * h {
return Err(Error::invalid(
"12-bit gray helper: samples buffer too short",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(16_384);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_dqt(&mut out, 0, &luma_q);
write_sof_grayscale_12bit(&mut out, w as u16, h as u16);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_sos_grayscale(&mut out);
let mcus_x = w.div_ceil(8);
let mcus_y = h.div_ceil(8);
let mut bw = BitWriter::new(&mut out);
let mut prev_dc: i32 = 0;
for my in 0..mcus_y {
for mx in 0..mcus_x {
let mut blk = [0.0f32; 64];
fill_block_u16_levelshift_2048(&mut blk, samples, stride, w, h, mx * 8, my * 8);
encode_block(
&mut bw,
&mut blk,
&luma_q,
&mut prev_dc,
&huff.luma_dc,
&huff.luma_ac,
);
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
#[cfg(test)]
fn fill_block_u16_levelshift_2048(
dst: &mut [f32; 64],
plane: &[u16],
stride: usize,
w: usize,
h: usize,
x0: usize,
y0: usize,
) {
for j in 0..8 {
let y = (y0 + j).min(h.saturating_sub(1));
for i in 0..8 {
let x = (x0 + i).min(w.saturating_sub(1));
let v = plane[y * stride + x] as i32;
dst[j * 8 + i] = (v - 2048) as f32;
}
}
}
#[cfg(test)]
fn write_sof_grayscale_12bit(out: &mut Vec<u8>, width: u16, height: u16) {
let mut payload = Vec::with_capacity(8 + 3);
payload.push(12); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(1); payload.push(1); payload.push(0x11); payload.push(0); write_length_prefix(out, markers::SOF0, &payload);
}
#[cfg(test)]
fn write_sos_grayscale(out: &mut Vec<u8>) {
let payload: [u8; 6] = [
1, 1, 0x00, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_yuv_jpeg_12bit(
width: u32,
height: u32,
y_samples: &[u16],
cb_samples: &[u16],
cr_samples: &[u16],
h_factor: u8,
v_factor: u8,
quality: u8,
) -> Result<Vec<u8>> {
if !matches!((h_factor, v_factor), (1, 1) | (2, 1) | (2, 2) | (4, 1)) {
return Err(Error::invalid(
"12-bit YUV helper: unsupported sampling factor",
));
}
let w = width as usize;
let h = height as usize;
let c_w = w.div_ceil(h_factor as usize);
let c_h = h.div_ceil(v_factor as usize);
if y_samples.len() < w * h || cb_samples.len() < c_w * c_h || cr_samples.len() < c_w * c_h {
return Err(Error::invalid(
"12-bit YUV helper: samples buffer too short",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mut out: Vec<u8> = Vec::with_capacity(32_768);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof1_yuv_12bit(&mut out, w as u16, h as u16, h_factor, v_factor);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
write_sos_yuv(&mut out);
let mcu_w_px = 8 * h_factor as usize;
let mcu_h_px = 8 * v_factor as usize;
let mcus_x = w.div_ceil(mcu_w_px);
let mcus_y = h.div_ceil(mcu_h_px);
let mut bw = BitWriter::new(&mut out);
let mut prev_dc_y: i32 = 0;
let mut prev_dc_cb: i32 = 0;
let mut prev_dc_cr: i32 = 0;
for my in 0..mcus_y {
for mx in 0..mcus_x {
for jy in 0..v_factor as usize {
for ix in 0..h_factor as usize {
let x0 = mx * mcu_w_px + ix * 8;
let y0 = my * mcu_h_px + jy * 8;
let mut blk = [0.0f32; 64];
fill_block_u16_levelshift_2048(&mut blk, y_samples, w, w, h, x0, y0);
encode_block(
&mut bw,
&mut blk,
&luma_q,
&mut prev_dc_y,
&huff.luma_dc,
&huff.luma_ac,
);
}
}
let cx0 = mx * 8;
let cy0 = my * 8;
let mut cb_blk = [0.0f32; 64];
fill_block_u16_levelshift_2048(&mut cb_blk, cb_samples, c_w, c_w, c_h, cx0, cy0);
encode_block(
&mut bw,
&mut cb_blk,
&chroma_q,
&mut prev_dc_cb,
&huff.chroma_dc,
&huff.chroma_ac,
);
let mut cr_blk = [0.0f32; 64];
fill_block_u16_levelshift_2048(&mut cr_blk, cr_samples, c_w, c_w, c_h, cx0, cy0);
encode_block(
&mut bw,
&mut cr_blk,
&chroma_q,
&mut prev_dc_cr,
&huff.chroma_dc,
&huff.chroma_ac,
);
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
#[cfg(test)]
fn write_sof1_yuv_12bit(out: &mut Vec<u8>, width: u16, height: u16, h_factor: u8, v_factor: u8) {
let mut payload = Vec::with_capacity(8 + 9);
payload.push(12); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(3); payload.push(1);
payload.push((h_factor << 4) | v_factor);
payload.push(0); payload.push(2);
payload.push(0x11);
payload.push(1); payload.push(3);
payload.push(0x11);
payload.push(1);
write_length_prefix(out, markers::SOF1, &payload);
}
#[cfg(test)]
fn write_sos_yuv(out: &mut Vec<u8>) {
let payload: [u8; 10] = [
3, 1, 0x00, 2, 0x11, 3, 0x11, 0, 63, 0, ];
write_length_prefix(out, markers::SOS, &payload);
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
fn fill_coef_grid_u16_2048(
coefs: &mut [[i32; 64]],
plane: &[u16],
stride: usize,
w: usize,
h: usize,
blocks_x: usize,
blocks_y: usize,
quant: &[u16; 64],
) {
for by in 0..blocks_y {
for bx in 0..blocks_x {
let mut block = [0.0f32; 64];
fill_block_u16_levelshift_2048(&mut block, plane, stride, w, h, bx * 8, by * 8);
fdct8x8(&mut block);
let mut q = [0i32; 64];
for k in 0..64 {
let v = block[k] / quant[k] as f32;
q[k] = if v >= 0.0 {
(v + 0.5) as i32
} else {
-((-v + 0.5) as i32)
};
}
coefs[by * blocks_x + bx] = q;
}
}
}
#[cfg(test)]
fn write_sof2_yuv_12bit(out: &mut Vec<u8>, width: u16, height: u16, h: u8, v: u8) {
let mut payload = Vec::with_capacity(8 + 9);
payload.push(12); payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(3); payload.push(1);
payload.push((h << 4) | v);
payload.push(0); payload.push(2);
payload.push(0x11);
payload.push(1); payload.push(3);
payload.push(0x11);
payload.push(1);
write_length_prefix(out, markers::SOF2, &payload);
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn encode_yuv_jpeg_progressive_12bit(
width: u32,
height: u32,
y_samples: &[u16],
cb_samples: &[u16],
cr_samples: &[u16],
h_factor: u8,
v_factor: u8,
quality: u8,
) -> Result<Vec<u8>> {
if !matches!((h_factor, v_factor), (1, 1) | (2, 1) | (2, 2)) {
return Err(Error::invalid(
"12-bit progressive YUV helper: unsupported sampling factor",
));
}
let w = width as usize;
let h = height as usize;
let c_w = w.div_ceil(h_factor as usize);
let c_h = h.div_ceil(v_factor as usize);
if y_samples.len() < w * h || cb_samples.len() < c_w * c_h || cr_samples.len() < c_w * c_h {
return Err(Error::invalid(
"12-bit progressive YUV helper: samples buffer too short",
));
}
let luma_q = scale_for_quality(&DEFAULT_LUMA_Q50, quality);
let chroma_q = scale_for_quality(&DEFAULT_CHROMA_Q50, quality);
let huff = DefaultHuffman::build()?;
let mcu_w_px = 8 * h_factor as usize;
let mcu_h_px = 8 * v_factor as usize;
let mcus_x = w.div_ceil(mcu_w_px);
let mcus_y = h.div_ceil(mcu_h_px);
let luma_blocks_x = mcus_x * h_factor as usize;
let luma_blocks_y = mcus_y * v_factor as usize;
let mut y_coefs = vec![[0i32; 64]; luma_blocks_x * luma_blocks_y];
fill_coef_grid_u16_2048(
&mut y_coefs,
y_samples,
w,
w,
h,
luma_blocks_x,
luma_blocks_y,
&luma_q,
);
let chroma_blocks_x = mcus_x;
let chroma_blocks_y = mcus_y;
let mut cb_coefs = vec![[0i32; 64]; chroma_blocks_x * chroma_blocks_y];
let mut cr_coefs = vec![[0i32; 64]; chroma_blocks_x * chroma_blocks_y];
fill_coef_grid_u16_2048(
&mut cb_coefs,
cb_samples,
c_w,
c_w,
c_h,
chroma_blocks_x,
chroma_blocks_y,
&chroma_q,
);
fill_coef_grid_u16_2048(
&mut cr_coefs,
cr_samples,
c_w,
c_w,
c_h,
chroma_blocks_x,
chroma_blocks_y,
&chroma_q,
);
let mut out: Vec<u8> = Vec::with_capacity(32_768);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_dqt(&mut out, 0, &luma_q);
write_dqt(&mut out, 1, &chroma_q);
write_sof2_yuv_12bit(&mut out, w as u16, h as u16, h_factor, v_factor);
write_dht(&mut out, 0, 0, &STD_DC_LUMA_BITS, &STD_DC_LUMA_VALS);
write_dht(&mut out, 1, 0, &STD_AC_LUMA_BITS, &STD_AC_LUMA_VALS);
write_dht(&mut out, 0, 1, &STD_DC_CHROMA_BITS, &STD_DC_CHROMA_VALS);
write_dht(&mut out, 1, 1, &STD_AC_CHROMA_BITS, &STD_AC_CHROMA_VALS);
write_sos_progressive_dc_interleaved(&mut out);
write_dc_scan_interleaved(
&mut out,
&y_coefs,
luma_blocks_x,
h_factor as usize,
v_factor as usize,
mcus_x,
mcus_y,
&cb_coefs,
&cr_coefs,
chroma_blocks_x,
&huff,
);
for &(ss, se) in &[(1u8, 5u8), (6u8, 63u8)] {
write_sos_progressive_ac(&mut out, 1, 0, ss, se);
write_ac_scan(
&mut out,
&y_coefs,
luma_blocks_x * luma_blocks_y,
&huff.luma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac(&mut out, 2, 1, ss, se);
write_ac_scan(
&mut out,
&cb_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
write_sos_progressive_ac(&mut out, 3, 1, ss, se);
write_ac_scan(
&mut out,
&cr_coefs,
chroma_blocks_x * chroma_blocks_y,
&huff.chroma_ac,
ss as usize,
se as usize,
);
}
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
const STD_DC_LOSSLESS_BITS: [u8; 16] = [0, 0, 0, 15, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const STD_DC_LOSSLESS_VALS: [u8; 17] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
fn category_lossless(diff: i32) -> (u8, u32) {
let diff16 = (diff as i64) & 0xFFFF;
if diff16 == 0x8000 {
return (16, 0);
}
let signed: i32 = if diff16 >= 0x8000 {
(diff16 - 0x1_0000) as i32
} else {
diff16 as i32
};
category(signed)
}
pub fn encode_lossless_jpeg_grayscale(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
precision: u8,
predictor: u8,
) -> Result<Vec<u8>> {
encode_lossless_jpeg_grayscale_with_opts(
width, height, samples, stride, precision, predictor, 0, 0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_jpeg_grayscale_with_opts(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
precision: u8,
predictor: u8,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
if !(2..=16).contains(&precision) {
return Err(Error::unsupported(format!(
"lossless encoder: precision {precision} out of range 2..=16"
)));
}
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= precision {
return Err(Error::invalid(format!(
"lossless encoder: point_transform {point_transform} must be < precision {precision}"
)));
}
if point_transform > 15 {
return Err(Error::invalid(format!(
"lossless encoder: point_transform {point_transform} > 15 (SOS Al is 4 bits)"
)));
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("lossless encoder: zero-size image"));
}
let bytes_per_sample = if precision <= 8 { 1 } else { 2 };
if stride < w * bytes_per_sample {
return Err(Error::invalid(
"lossless encoder: stride smaller than width*bytes_per_sample",
));
}
if samples.len() < stride * h {
return Err(Error::invalid(
"lossless encoder: samples shorter than stride*h",
));
}
let dc_huff = HuffTable::build(&STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS)?;
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 2 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_sof_lossless(&mut out, w as u16, h as u16, precision);
write_dht(&mut out, 0, 0, &STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless(&mut out, predictor, point_transform);
let pt = point_transform as u32;
let mut src = vec![0u16; w * h];
if precision <= 8 {
for y in 0..h {
for x in 0..w {
src[y * w + x] = (samples[y * stride + x] as u16) >> pt;
}
}
} else {
for y in 0..h {
for x in 0..w {
let lo = samples[y * stride + x * 2] as u16;
let hi = samples[y * stride + x * 2 + 1] as u16;
src[y * w + x] = (lo | (hi << 8)) >> pt;
}
}
}
let max_sample: u32 = (1u32 << precision) - 1;
if precision <= 8 {
for y in 0..h {
for x in 0..w {
let v = samples[y * stride + x] as u32;
if v > max_sample {
return Err(Error::invalid(format!(
"lossless encoder: sample {v} exceeds precision-{precision} max {max_sample}"
)));
}
}
}
} else {
for y in 0..h {
for x in 0..w {
let lo = samples[y * stride + x * 2] as u32;
let hi = samples[y * stride + x * 2 + 1] as u32;
let v = lo | (hi << 8);
if v > max_sample {
return Err(Error::invalid(format!(
"lossless encoder: sample {v} exceeds precision-{precision} max {max_sample}"
)));
}
}
}
}
let sample_bits = precision as u32 - pt;
let origin: i32 = 1i32 << (sample_bits - 1);
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut samples_since_restart: u32 = 0;
let total_samples: u64 = w as u64 * h as u64;
let mut sample_index: u64 = 0;
let mut reset_pred = true;
let mut bw = BitWriter::new(&mut out);
for y in 0..h {
for x in 0..w {
let actual = src[y * w + x] as i32;
let pred: i32 = if reset_pred {
origin
} else if y == 0 {
src[y * w + x - 1] as i32
} else if x == 0 {
src[(y - 1) * w + x] as i32
} else {
let ra = src[y * w + x - 1] as i32;
let rb = src[(y - 1) * w + x] as i32;
let rc = src[(y - 1) * w + x - 1] as i32;
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra + rb - rc,
5 => ra + ((rb - rc) >> 1),
6 => rb + ((ra - rc) >> 1),
7 => (ra + rb) >> 1,
_ => unreachable!(),
}
};
let diff = actual - pred;
let (s, bits) = category_lossless(diff);
let hc = dc_huff.encode[s as usize];
debug_assert!(
hc.len != 0,
"DC Huffman code for SSSS={s} must be present; \
STD_DC_LOSSLESS_VALS covers 0..=16"
);
bw.write_bits(hc.code as u32, hc.len as u32);
if s != 0 && s != 16 {
bw.write_bits(bits, s as u32);
}
reset_pred = false;
sample_index += 1;
samples_since_restart += 1;
if ri != 0 && samples_since_restart == ri && sample_index < total_samples {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
samples_since_restart = 0;
reset_pred = true;
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
#[cfg(test)]
pub(crate) fn encode_lossless_grayscale_jpeg_8bit(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
) -> Result<Vec<u8>> {
encode_lossless_jpeg_grayscale(width, height, samples, stride, 8, 1)
}
fn write_sof11_lossless(out: &mut Vec<u8>, width: u16, height: u16, precision: u8) {
let mut payload = Vec::with_capacity(8 + 3);
payload.push(precision);
payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(1); payload.push(1); payload.push(0x11); payload.push(0); write_length_prefix(out, markers::SOF11, &payload);
}
fn write_sof11_lossless_multi(out: &mut Vec<u8>, width: u16, height: u16, precision: u8, nf: u8) {
debug_assert!((1..=4).contains(&nf));
let mut payload = Vec::with_capacity(8 + 3 * nf as usize);
payload.push(precision);
payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(nf);
for ci in 1..=nf {
payload.push(ci); payload.push(0x11); payload.push(0); }
write_length_prefix(out, markers::SOF11, &payload);
}
pub fn encode_lossless_arith_jpeg_grayscale(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
precision: u8,
predictor: u8,
) -> Result<Vec<u8>> {
encode_lossless_arith_jpeg_grayscale_with_opts(
width, height, samples, stride, precision, predictor, 0, 0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_arith_jpeg_grayscale_with_opts(
width: u32,
height: u32,
samples: &[u8],
stride: usize,
precision: u8,
predictor: u8,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
use crate::jpeg::arith::{encode_lossless_diff, ArithEncoder, LosslessStats};
if !(2..=16).contains(&precision) {
return Err(Error::unsupported(format!(
"lossless-arith encoder: precision {precision} out of range 2..=16"
)));
}
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless-arith encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= precision {
return Err(Error::invalid(format!(
"lossless-arith encoder: point_transform {point_transform} must be < precision {precision}"
)));
}
if point_transform > 15 {
return Err(Error::invalid(format!(
"lossless-arith encoder: point_transform {point_transform} > 15 (SOS Al is 4 bits)"
)));
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("lossless-arith encoder: zero-size image"));
}
let bytes_per_sample = if precision <= 8 { 1 } else { 2 };
if stride < w * bytes_per_sample {
return Err(Error::invalid(
"lossless-arith encoder: stride smaller than width*bytes_per_sample",
));
}
if samples.len() < stride * h {
return Err(Error::invalid(
"lossless-arith encoder: samples shorter than stride*h",
));
}
let pt = point_transform as u32;
let max_sample: u32 = (1u32 << precision) - 1;
let mut src = vec![0u32; w * h];
if precision <= 8 {
for y in 0..h {
for x in 0..w {
let v = samples[y * stride + x] as u32;
if v > max_sample {
return Err(Error::invalid(format!(
"lossless-arith encoder: sample {v} exceeds precision-{precision} max {max_sample}"
)));
}
src[y * w + x] = v >> pt;
}
}
} else {
for y in 0..h {
for x in 0..w {
let lo = samples[y * stride + x * 2] as u32;
let hi = samples[y * stride + x * 2 + 1] as u32;
let v = lo | (hi << 8);
if v > max_sample {
return Err(Error::invalid(format!(
"lossless-arith encoder: sample {v} exceeds precision-{precision} max {max_sample}"
)));
}
src[y * w + x] = v >> pt;
}
}
}
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 2 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_sof11_lossless(&mut out, w as u16, h as u16, precision);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless(&mut out, predictor, point_transform);
let sample_bits = precision as u32 - pt;
let origin: u32 = 1u32 << (sample_bits - 1);
let mut prev_diff = vec![0i32; w];
let mut cur_diff = vec![0i32; w];
let mut stats = LosslessStats::new();
let mut enc = ArithEncoder::new();
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut samples_since_restart: u32 = 0;
let total_samples: u64 = w as u64 * h as u64;
let mut sample_index: u64 = 0;
let mut reset_pred = true;
let mut first_line_y = 0usize;
for y in 0..h {
for x in 0..w {
if ri != 0 && samples_since_restart == ri && sample_index < total_samples {
out.extend_from_slice(&std::mem::take(&mut enc).finish());
out.push(0xFF);
out.push(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
samples_since_restart = 0;
stats.reset();
prev_diff.fill(0);
cur_diff.fill(0);
reset_pred = true;
first_line_y = y;
}
let actual = src[y * w + x];
let pred: u32 = if reset_pred {
origin
} else if y == first_line_y {
src[y * w + x - 1]
} else if x == 0 {
src[(y - 1) * w + x]
} else {
let ra = src[y * w + x - 1];
let rb = src[(y - 1) * w + x];
let rc = src[(y - 1) * w + x - 1];
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra.wrapping_add(rb).wrapping_sub(rc),
5 => ra.wrapping_add(rb.wrapping_sub(rc) >> 1),
6 => rb.wrapping_add(ra.wrapping_sub(rc) >> 1),
7 => (ra.wrapping_add(rb)) >> 1,
_ => unreachable!(),
}
};
let dm = (actual.wrapping_sub(pred) & 0xFFFF) as i32;
let dm = if dm >= 0x8000 { dm - 0x10000 } else { dm };
let da = if x == 0 { 0 } else { cur_diff[x - 1] };
let db = prev_diff[x];
encode_lossless_diff(&mut enc, &mut stats, da, db, dm)?;
cur_diff[x] = dm;
reset_pred = false;
sample_index += 1;
samples_since_restart += 1;
}
std::mem::swap(&mut prev_diff, &mut cur_diff);
}
out.extend_from_slice(&enc.finish());
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
pub fn encode_lossless_arith_jpeg_rgb(
width: u32,
height: u32,
planes: [&[u8]; 3],
strides: [usize; 3],
precision: u8,
predictor: u8,
) -> Result<Vec<u8>> {
encode_lossless_arith_jpeg_rgb_with_opts(
width, height, planes, strides, precision, predictor, 0, 0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_arith_jpeg_rgb_with_opts(
width: u32,
height: u32,
planes: [&[u8]; 3],
strides: [usize; 3],
precision: u8,
predictor: u8,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
use crate::jpeg::arith::{encode_lossless_diff, ArithEncoder, LosslessStats};
if !(2..=16).contains(&precision) {
return Err(Error::unsupported(format!(
"lossless-arith RGB encoder: precision {precision} out of range 2..=16"
)));
}
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless-arith RGB encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= precision {
return Err(Error::invalid(format!(
"lossless-arith RGB encoder: point_transform {point_transform} must be < precision {precision}"
)));
}
if point_transform > 15 {
return Err(Error::invalid(format!(
"lossless-arith RGB encoder: point_transform {point_transform} > 15 (SOS Al is 4 bits)"
)));
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid(
"lossless-arith RGB encoder: zero-size image",
));
}
let bytes_per_sample = if precision <= 8 { 1 } else { 2 };
for c in 0..3 {
if strides[c] < w * bytes_per_sample {
return Err(Error::invalid(
"lossless-arith RGB encoder: stride smaller than width*bytes_per_sample",
));
}
if planes[c].len() < strides[c] * h {
return Err(Error::invalid(
"lossless-arith RGB encoder: plane shorter than stride*h",
));
}
}
let pt = point_transform as u32;
let max_sample: u32 = (1u32 << precision) - 1;
let mut src: [Vec<u32>; 3] = [vec![0u32; w * h], vec![0u32; w * h], vec![0u32; w * h]];
if precision <= 8 {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
let v = planes[c][y * strides[c] + x] as u32;
if v > max_sample {
return Err(Error::invalid(format!(
"lossless-arith RGB encoder: sample {v} in plane {c} exceeds precision-{precision} max {max_sample}"
)));
}
src[c][y * w + x] = v >> pt;
}
}
}
} else {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
let lo = planes[c][y * strides[c] + x * 2] as u32;
let hi = planes[c][y * strides[c] + x * 2 + 1] as u32;
let v = lo | (hi << 8);
if v > max_sample {
return Err(Error::invalid(format!(
"lossless-arith RGB encoder: sample {v} in plane {c} exceeds precision-{precision} max {max_sample}"
)));
}
src[c][y * w + x] = v >> pt;
}
}
}
}
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 3 * 2 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_sof11_lossless_multi(&mut out, w as u16, h as u16, precision, 3);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless_multi(&mut out, predictor, 3, point_transform);
let sample_bits = precision as u32 - pt;
let origin: u32 = 1u32 << (sample_bits - 1);
let mut prev_diff: [Vec<i32>; 3] = [vec![0i32; w], vec![0i32; w], vec![0i32; w]];
let mut cur_diff: [Vec<i32>; 3] = [vec![0i32; w], vec![0i32; w], vec![0i32; w]];
let mut stats: [LosslessStats; 3] = [
LosslessStats::new(),
LosslessStats::new(),
LosslessStats::new(),
];
let mut enc = ArithEncoder::new();
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: u32 = 0;
let total_mcus: u64 = w as u64 * h as u64;
let mut mcu_index: u64 = 0;
let mut reset_pred = true;
let mut first_line_y = 0usize;
for y in 0..h {
for x in 0..w {
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
out.extend_from_slice(&std::mem::take(&mut enc).finish());
out.push(0xFF);
out.push(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
mcus_since_restart = 0;
for s in stats.iter_mut() {
s.reset();
}
for r in prev_diff.iter_mut() {
r.fill(0);
}
for r in cur_diff.iter_mut() {
r.fill(0);
}
reset_pred = true;
first_line_y = y;
}
for c in 0..3 {
let plane = &src[c];
let actual = plane[y * w + x];
let pred: u32 = if reset_pred {
origin
} else if y == first_line_y {
plane[y * w + x - 1]
} else if x == 0 {
plane[(y - 1) * w + x]
} else {
let ra = plane[y * w + x - 1];
let rb = plane[(y - 1) * w + x];
let rc = plane[(y - 1) * w + x - 1];
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra.wrapping_add(rb).wrapping_sub(rc),
5 => ra.wrapping_add(rb.wrapping_sub(rc) >> 1),
6 => rb.wrapping_add(ra.wrapping_sub(rc) >> 1),
7 => (ra.wrapping_add(rb)) >> 1,
_ => unreachable!(),
}
};
let dm = (actual.wrapping_sub(pred) & 0xFFFF) as i32;
let dm = if dm >= 0x8000 { dm - 0x10000 } else { dm };
let da = if x == 0 { 0 } else { cur_diff[c][x - 1] };
let db = prev_diff[c][x];
encode_lossless_diff(&mut enc, &mut stats[c], da, db, dm)?;
cur_diff[c][x] = dm;
}
reset_pred = false;
mcu_index += 1;
mcus_since_restart += 1;
}
std::mem::swap(&mut prev_diff, &mut cur_diff);
}
out.extend_from_slice(&enc.finish());
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
pub fn encode_lossless_arith_jpeg_cmyk(
width: u32,
height: u32,
planes: [&[u8]; 4],
strides: [usize; 4],
predictor: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
encode_lossless_arith_jpeg_cmyk_with_opts(
width,
height,
planes,
strides,
predictor,
adobe_transform,
0,
0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_arith_jpeg_cmyk_with_opts(
width: u32,
height: u32,
planes: [&[u8]; 4],
strides: [usize; 4],
predictor: u8,
adobe_transform: Option<u8>,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
use crate::jpeg::arith::{encode_lossless_diff, ArithEncoder, LosslessStats};
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless-arith CMYK encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= 8 {
return Err(Error::invalid(format!(
"lossless-arith CMYK encoder: point_transform {point_transform} must be < precision 8"
)));
}
match adobe_transform {
None | Some(0) | Some(2) => {}
Some(other) => {
return Err(Error::invalid(format!(
"lossless-arith CMYK encoder: adobe_transform = {other} (only None / Some(0) / Some(2) are supported)"
)));
}
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid(
"lossless-arith CMYK encoder: zero-size image",
));
}
for c in 0..4 {
if strides[c] < w {
return Err(Error::invalid(
"lossless-arith CMYK encoder: stride smaller than width",
));
}
if planes[c].len() < strides[c] * h {
return Err(Error::invalid(
"lossless-arith CMYK encoder: plane shorter than stride*h",
));
}
}
let invert_all = matches!(adobe_transform, Some(0));
let invert_k_only = matches!(adobe_transform, Some(2));
let pt = point_transform as u32;
let mut src: [Vec<u32>; 4] = [
vec![0u32; w * h],
vec![0u32; w * h],
vec![0u32; w * h],
vec![0u32; w * h],
];
for c in 0..4 {
let invert_this = invert_all || (invert_k_only && c == 3);
for y in 0..h {
for x in 0..w {
let v = planes[c][y * strides[c] + x] as u32;
let v = if invert_this { 255 - v } else { v };
src[c][y * w + x] = v >> pt;
}
}
}
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 4 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
if let Some(tx) = adobe_transform {
write_adobe_app14(&mut out, tx);
}
write_sof11_lossless_multi(&mut out, w as u16, h as u16, 8, 4);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless_multi(&mut out, predictor, 4, point_transform);
let sample_bits = 8u32 - pt;
let origin: u32 = 1u32 << (sample_bits - 1);
let mut prev_diff: [Vec<i32>; 4] = [vec![0i32; w], vec![0i32; w], vec![0i32; w], vec![0i32; w]];
let mut cur_diff: [Vec<i32>; 4] = [vec![0i32; w], vec![0i32; w], vec![0i32; w], vec![0i32; w]];
let mut stats: [LosslessStats; 4] = [
LosslessStats::new(),
LosslessStats::new(),
LosslessStats::new(),
LosslessStats::new(),
];
let mut enc = ArithEncoder::new();
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: u32 = 0;
let total_mcus: u64 = w as u64 * h as u64;
let mut mcu_index: u64 = 0;
let mut reset_pred = true;
let mut first_line_y = 0usize;
for y in 0..h {
for x in 0..w {
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
out.extend_from_slice(&std::mem::take(&mut enc).finish());
out.push(0xFF);
out.push(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
mcus_since_restart = 0;
for s in stats.iter_mut() {
s.reset();
}
for r in prev_diff.iter_mut() {
r.fill(0);
}
for r in cur_diff.iter_mut() {
r.fill(0);
}
reset_pred = true;
first_line_y = y;
}
for c in 0..4 {
let plane = &src[c];
let actual = plane[y * w + x];
let pred: u32 = if reset_pred {
origin
} else if y == first_line_y {
plane[y * w + x - 1]
} else if x == 0 {
plane[(y - 1) * w + x]
} else {
let ra = plane[y * w + x - 1];
let rb = plane[(y - 1) * w + x];
let rc = plane[(y - 1) * w + x - 1];
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra.wrapping_add(rb).wrapping_sub(rc),
5 => ra.wrapping_add(rb.wrapping_sub(rc) >> 1),
6 => rb.wrapping_add(ra.wrapping_sub(rc) >> 1),
7 => (ra.wrapping_add(rb)) >> 1,
_ => unreachable!(),
}
};
let dm = (actual.wrapping_sub(pred) & 0xFFFF) as i32;
let dm = if dm >= 0x8000 { dm - 0x10000 } else { dm };
let da = if x == 0 { 0 } else { cur_diff[c][x - 1] };
let db = prev_diff[c][x];
encode_lossless_diff(&mut enc, &mut stats[c], da, db, dm)?;
cur_diff[c][x] = dm;
}
reset_pred = false;
mcu_index += 1;
mcus_since_restart += 1;
}
std::mem::swap(&mut prev_diff, &mut cur_diff);
}
out.extend_from_slice(&enc.finish());
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
pub fn encode_lossless_jpeg_rgb(
width: u32,
height: u32,
planes: [&[u8]; 3],
strides: [usize; 3],
precision: u8,
predictor: u8,
) -> Result<Vec<u8>> {
encode_lossless_jpeg_rgb_with_opts(width, height, planes, strides, precision, predictor, 0, 0)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_jpeg_rgb_with_opts(
width: u32,
height: u32,
planes: [&[u8]; 3],
strides: [usize; 3],
precision: u8,
predictor: u8,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
if !(2..=16).contains(&precision) {
return Err(Error::unsupported(format!(
"lossless RGB encoder: precision {precision} out of range 2..=16"
)));
}
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless RGB encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= precision {
return Err(Error::invalid(format!(
"lossless RGB encoder: point_transform {point_transform} must be < precision {precision}"
)));
}
if point_transform > 15 {
return Err(Error::invalid(format!(
"lossless RGB encoder: point_transform {point_transform} > 15 (SOS Al is 4 bits)"
)));
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("lossless RGB encoder: zero-size image"));
}
let bytes_per_sample = if precision <= 8 { 1 } else { 2 };
for c in 0..3 {
if strides[c] < w * bytes_per_sample {
return Err(Error::invalid(
"lossless RGB encoder: stride smaller than width*bytes_per_sample",
));
}
if planes[c].len() < strides[c] * h {
return Err(Error::invalid(
"lossless RGB encoder: plane shorter than stride*h",
));
}
}
let dc_huff = HuffTable::build(&STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS)?;
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 3 * 2 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
write_sof_lossless_multi(&mut out, w as u16, h as u16, precision, 3);
write_dht(&mut out, 0, 0, &STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless_multi(&mut out, predictor, 3, point_transform);
let pt = point_transform as u32;
let mut src: [Vec<u16>; 3] = [vec![0u16; w * h], vec![0u16; w * h], vec![0u16; w * h]];
if precision <= 8 {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
src[c][y * w + x] = (planes[c][y * strides[c] + x] as u16) >> pt;
}
}
}
} else {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
let lo = planes[c][y * strides[c] + x * 2] as u16;
let hi = planes[c][y * strides[c] + x * 2 + 1] as u16;
src[c][y * w + x] = (lo | (hi << 8)) >> pt;
}
}
}
}
let max_sample: u32 = (1u32 << precision) - 1;
if precision <= 8 {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
let v = planes[c][y * strides[c] + x] as u32;
if v > max_sample {
return Err(Error::invalid(format!(
"lossless RGB encoder: sample {v} in plane {c} exceeds precision-{precision} max {max_sample}"
)));
}
}
}
}
} else {
for c in 0..3 {
for y in 0..h {
for x in 0..w {
let lo = planes[c][y * strides[c] + x * 2] as u32;
let hi = planes[c][y * strides[c] + x * 2 + 1] as u32;
let v = lo | (hi << 8);
if v > max_sample {
return Err(Error::invalid(format!(
"lossless RGB encoder: sample {v} in plane {c} exceeds precision-{precision} max {max_sample}"
)));
}
}
}
}
}
let sample_bits = precision as u32 - pt;
let origin: i32 = 1i32 << (sample_bits - 1);
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: u32 = 0;
let total_mcus: u64 = w as u64 * h as u64;
let mut mcu_index: u64 = 0;
let mut reset_pred = true;
let mut bw = BitWriter::new(&mut out);
for y in 0..h {
for x in 0..w {
for c in 0..3 {
let plane = &src[c];
let actual = plane[y * w + x] as i32;
let pred: i32 = if reset_pred {
origin
} else if y == 0 {
plane[y * w + x - 1] as i32
} else if x == 0 {
plane[(y - 1) * w + x] as i32
} else {
let ra = plane[y * w + x - 1] as i32;
let rb = plane[(y - 1) * w + x] as i32;
let rc = plane[(y - 1) * w + x - 1] as i32;
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra + rb - rc,
5 => ra + ((rb - rc) >> 1),
6 => rb + ((ra - rc) >> 1),
7 => (ra + rb) >> 1,
_ => unreachable!(),
}
};
let diff = actual - pred;
let (s, bits) = category_lossless(diff);
let hc = dc_huff.encode[s as usize];
debug_assert!(hc.len != 0, "DC Huffman code for SSSS={s} must be present");
bw.write_bits(hc.code as u32, hc.len as u32);
if s != 0 && s != 16 {
bw.write_bits(bits, s as u32);
}
}
reset_pred = false;
mcu_index += 1;
mcus_since_restart += 1;
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
mcus_since_restart = 0;
reset_pred = true;
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
pub fn encode_lossless_jpeg_cmyk(
width: u32,
height: u32,
planes: [&[u8]; 4],
strides: [usize; 4],
predictor: u8,
adobe_transform: Option<u8>,
) -> Result<Vec<u8>> {
encode_lossless_jpeg_cmyk_with_opts(
width,
height,
planes,
strides,
predictor,
adobe_transform,
0,
0,
)
}
#[allow(clippy::too_many_arguments)]
pub fn encode_lossless_jpeg_cmyk_with_opts(
width: u32,
height: u32,
planes: [&[u8]; 4],
strides: [usize; 4],
predictor: u8,
adobe_transform: Option<u8>,
restart_interval: u16,
point_transform: u8,
) -> Result<Vec<u8>> {
if !(1..=7).contains(&predictor) {
return Err(Error::invalid(format!(
"lossless CMYK encoder: predictor {predictor} not in 1..=7"
)));
}
if point_transform >= 8 {
return Err(Error::invalid(format!(
"lossless CMYK encoder: point_transform {point_transform} must be < precision 8"
)));
}
match adobe_transform {
None | Some(0) | Some(2) => {}
Some(other) => {
return Err(Error::invalid(format!(
"lossless CMYK encoder: adobe_transform = {other} (only None / Some(0) / Some(2) are supported)"
)));
}
}
let w = width as usize;
let h = height as usize;
if w == 0 || h == 0 {
return Err(Error::invalid("lossless CMYK encoder: zero-size image"));
}
for c in 0..4 {
if strides[c] < w {
return Err(Error::invalid(
"lossless CMYK encoder: stride smaller than width",
));
}
if planes[c].len() < strides[c] * h {
return Err(Error::invalid(
"lossless CMYK encoder: plane shorter than stride*h",
));
}
}
let invert_all = matches!(adobe_transform, Some(0));
let invert_k_only = matches!(adobe_transform, Some(2));
let dc_huff = HuffTable::build(&STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS)?;
let mut out: Vec<u8> = Vec::with_capacity(16_384 + 4 * w * h);
out.push(0xFF);
out.push(markers::SOI);
write_jfif_app0(&mut out);
if let Some(tx) = adobe_transform {
write_adobe_app14(&mut out, tx);
}
write_sof_lossless_multi(&mut out, w as u16, h as u16, 8, 4);
write_dht(&mut out, 0, 0, &STD_DC_LOSSLESS_BITS, &STD_DC_LOSSLESS_VALS);
if restart_interval != 0 {
write_dri(&mut out, restart_interval);
}
write_sos_lossless_multi(&mut out, predictor, 4, point_transform);
let pt = point_transform as u32;
let mut src: [Vec<u16>; 4] = [
vec![0u16; w * h],
vec![0u16; w * h],
vec![0u16; w * h],
vec![0u16; w * h],
];
for c in 0..4 {
let invert_this = invert_all || (invert_k_only && c == 3);
for y in 0..h {
for x in 0..w {
let v = planes[c][y * strides[c] + x] as u16;
let v = if invert_this { 255 - v } else { v };
src[c][y * w + x] = v >> pt;
}
}
}
let sample_bits = 8u32 - pt;
let origin: i32 = 1i32 << (sample_bits - 1);
let ri = restart_interval as u32;
let mut rst_counter: u8 = 0;
let mut mcus_since_restart: u32 = 0;
let total_mcus: u64 = w as u64 * h as u64;
let mut mcu_index: u64 = 0;
let mut reset_pred = true;
let mut bw = BitWriter::new(&mut out);
for y in 0..h {
for x in 0..w {
for c in 0..4 {
let plane = &src[c];
let actual = plane[y * w + x] as i32;
let pred: i32 = if reset_pred {
origin
} else if y == 0 {
plane[y * w + x - 1] as i32
} else if x == 0 {
plane[(y - 1) * w + x] as i32
} else {
let ra = plane[y * w + x - 1] as i32;
let rb = plane[(y - 1) * w + x] as i32;
let rc = plane[(y - 1) * w + x - 1] as i32;
match predictor {
1 => ra,
2 => rb,
3 => rc,
4 => ra + rb - rc,
5 => ra + ((rb - rc) >> 1),
6 => rb + ((ra - rc) >> 1),
7 => (ra + rb) >> 1,
_ => unreachable!(),
}
};
let diff = actual - pred;
let (s, bits) = category_lossless(diff);
let hc = dc_huff.encode[s as usize];
debug_assert!(hc.len != 0, "DC Huffman code for SSSS={s} must be present");
bw.write_bits(hc.code as u32, hc.len as u32);
if s != 0 && s != 16 {
bw.write_bits(bits, s as u32);
}
}
reset_pred = false;
mcu_index += 1;
mcus_since_restart += 1;
if ri != 0 && mcus_since_restart == ri && mcu_index < total_mcus {
bw.flush_to_byte();
bw.emit_raw_marker(markers::RST0 + (rst_counter & 0x07));
rst_counter = rst_counter.wrapping_add(1);
mcus_since_restart = 0;
reset_pred = true;
}
}
}
bw.finish();
out.push(0xFF);
out.push(markers::EOI);
Ok(out)
}
fn write_sof_lossless(out: &mut Vec<u8>, width: u16, height: u16, precision: u8) {
let mut payload = Vec::with_capacity(8 + 3);
payload.push(precision);
payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(1); payload.push(1); payload.push(0x11); payload.push(0); write_length_prefix(out, 0xC3 , &payload);
}
fn write_sos_lossless(out: &mut Vec<u8>, predictor: u8, point_transform: u8) {
debug_assert!(point_transform <= 0x0F, "Pt must fit in 4 bits");
let payload: [u8; 6] = [
1, 1,
0x00, predictor,
0, point_transform & 0x0F, ];
write_length_prefix(out, markers::SOS, &payload);
}
fn write_sof_lossless_multi(out: &mut Vec<u8>, width: u16, height: u16, precision: u8, nf: u8) {
debug_assert!((1..=4).contains(&nf));
let mut payload = Vec::with_capacity(8 + 3 * nf as usize);
payload.push(precision);
payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.push(nf);
for ci in 1..=nf {
payload.push(ci); payload.push(0x11); payload.push(0); }
write_length_prefix(out, markers::SOF3, &payload);
}
fn write_sos_lossless_multi(out: &mut Vec<u8>, predictor: u8, ns: u8, point_transform: u8) {
debug_assert!((1..=4).contains(&ns));
debug_assert!(point_transform <= 0x0F, "Pt must fit in 4 bits");
let mut payload = Vec::with_capacity(3 + 2 * ns as usize);
payload.push(ns);
for ci in 1..=ns {
payload.push(ci); payload.push(0x00); }
payload.push(predictor); payload.push(0); payload.push(point_transform & 0x0F); write_length_prefix(out, markers::SOS, &payload);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn category_zero() {
assert_eq!(category(0), (0, 0));
}
#[test]
fn category_small() {
assert_eq!(category(1), (1, 1));
assert_eq!(category(-1), (1, 0));
assert_eq!(category(5), (3, 5));
assert_eq!(category(-5), (3, 2));
}
#[test]
fn bit_writer_stuffs_ff() {
let mut buf = Vec::new();
let mut bw = BitWriter::new(&mut buf);
bw.write_bits(0xFF, 8);
bw.finish();
assert_eq!(buf, vec![0xFF, 0x00]);
}
#[test]
fn category_lossless_special_case_half_modulus() {
assert_eq!(category_lossless(32768), (16, 0));
assert_eq!(category_lossless(-32768), (16, 0));
assert_eq!(category_lossless(32767).0, 15);
assert_eq!(category_lossless(-32767).0, 15);
}
#[test]
fn category_lossless_mod_2_to_16_aliases() {
for v in [-65535i32, -65000, -16384, 0, 16384, 65000, 65535] {
let (s1, b1) = category_lossless(v);
let (s2, b2) = category_lossless(v + 0x1_0000);
assert_eq!((s1, b1), (s2, b2), "mod-2^16 alias mismatch at v={v}");
}
}
#[test]
fn baseline_grayscale_emits_well_formed_jpeg() {
let w = 16usize;
let h = 16usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
samples[y * w + x] = ((x + y) * 8) as u8;
}
}
let jpeg =
encode_jpeg_grayscale(w as u32, h as u32, &samples, w, 75).expect("encode grayscale");
assert_eq!(&jpeg[..2], &[0xFF, 0xD8], "expected SOI prefix");
assert_eq!(
&jpeg[jpeg.len() - 2..],
&[0xFF, 0xD9],
"expected EOI suffix"
);
let mut found_sof0 = false;
let mut i = 2;
while i + 3 < jpeg.len() {
assert_eq!(jpeg[i], 0xFF, "expected marker prefix at {i}");
let marker = jpeg[i + 1];
if marker == 0xDA {
break;
}
let len = u16::from_be_bytes([jpeg[i + 2], jpeg[i + 3]]) as usize;
if marker == 0xC0 {
let p = i + 4;
let precision = jpeg[p];
let nf = jpeg[p + 5];
assert_eq!(precision, 8, "SOF0 precision must be 8");
assert_eq!(nf, 1, "SOF0 must declare a single component");
let comp_id = jpeg[p + 6];
let hv = jpeg[p + 7];
let tq = jpeg[p + 8];
assert_eq!(comp_id, 1, "component id");
assert_eq!(hv, 0x11, "H = V = 1");
assert_eq!(tq, 0, "single quant table id");
found_sof0 = true;
}
i += 2 + len;
}
assert!(found_sof0, "expected SOF0 segment in output");
}
#[test]
fn baseline_grayscale_high_quality_roundtrip_is_near_lossless() {
let w = 32usize;
let h = 32usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
samples[y * w + x] = (((x * 7 + y * 11) % 256) as u8).clamp(0, 255);
}
}
let jpeg =
encode_jpeg_grayscale(w as u32, h as u32, &samples, w, 100).expect("encode grayscale");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
assert_eq!(frame.planes.len(), 1, "Gray8 frame has one plane");
let recovered = &frame.planes[0].data;
assert!(recovered.len() >= w * h, "recovered plane too short");
let stride = frame.planes[0].stride;
let mut max_diff: u32 = 0;
for y in 0..h {
for x in 0..w {
let a = samples[y * w + x];
let b = recovered[y * stride + x];
let d = (a as i32 - b as i32).unsigned_abs();
if d > max_diff {
max_diff = d;
}
}
}
assert!(max_diff <= 4, "Q=100 max diff = {max_diff} (expected ≤ 4)");
}
#[test]
fn baseline_grayscale_q75_roundtrip_psnr_above_30db() {
let w = 64usize;
let h = 64usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let r = ((x as i32 - 32).abs() + (y as i32 - 32).abs()) as u32;
samples[y * w + x] = (128 + (r as i32 - 32).clamp(-127, 127)) as u8;
}
}
let jpeg = encode_jpeg_grayscale(w as u32, h as u32, &samples, w, DEFAULT_QUALITY)
.expect("encode grayscale");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
let recovered = &frame.planes[0].data;
let stride = frame.planes[0].stride;
let mut sse: f64 = 0.0;
for y in 0..h {
for x in 0..w {
let a = samples[y * w + x] as f64;
let b = recovered[y * stride + x] as f64;
let d = a - b;
sse += d * d;
}
}
let mse = sse / (w * h) as f64;
let psnr = if mse <= f64::EPSILON {
99.0
} else {
20.0 * (255.0_f64 / mse.sqrt()).log10()
};
assert!(psnr >= 30.0, "Q=75 PSNR = {psnr:.2} dB (expected ≥ 30)");
}
#[test]
fn baseline_grayscale_rejects_short_stride() {
let samples = vec![0u8; 16 * 16];
let err =
encode_jpeg_grayscale(16, 16, &samples, 8, 75).expect_err("stride < width must fail");
assert!(matches!(err, Error::InvalidData(_)), "got {err:?}");
}
#[test]
fn baseline_grayscale_rejects_short_buffer() {
let samples = vec![0u8; 16 * 8];
let err = encode_jpeg_grayscale(16, 16, &samples, 16, 75)
.expect_err("samples shorter than stride*h must fail");
assert!(matches!(err, Error::InvalidData(_)), "got {err:?}");
}
#[test]
fn baseline_grayscale_with_opts_emits_dri_and_restart() {
let w = 32u32;
let h = 32u32;
let mut samples = vec![0u8; (w * h) as usize];
for y in 0..h as usize {
for x in 0..w as usize {
samples[y * w as usize + x] = ((x + y) as u8).wrapping_mul(3);
}
}
let jpeg = encode_jpeg_grayscale_with_opts(w, h, &samples, w as usize, 80, 4)
.expect("encode grayscale with DRI");
let mut found_dri = false;
let mut found_rst = false;
for i in 0..jpeg.len().saturating_sub(1) {
if jpeg[i] == 0xFF && jpeg[i + 1] == 0xDD {
found_dri = true;
}
if jpeg[i] == 0xFF && (0xD0..=0xD7).contains(&jpeg[i + 1]) {
found_rst = true;
}
}
assert!(found_dri, "DRI segment must be present when interval > 0");
assert!(found_rst, "at least one RSTn marker must be present");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode DRI grayscale");
assert_eq!(frame.planes.len(), 1);
}
#[test]
fn baseline_grayscale_with_meta_embeds_app_segments() {
let w = 16u32;
let h = 16u32;
let samples = vec![100u8; (w * h) as usize];
let app1_body = b"FAKEEXIF";
let mut meta = Vec::new();
meta.push(0xFF);
meta.push(0xE1); let len = (2 + app1_body.len()) as u16;
meta.extend_from_slice(&len.to_be_bytes());
meta.extend_from_slice(app1_body);
let jpeg = encode_jpeg_grayscale_with_meta(w, h, &samples, w as usize, 90, 0, &meta)
.expect("encode with meta");
assert_eq!(&jpeg[2..4], &[0xFF, 0xE1], "APP1 must follow SOI");
assert!(
jpeg.windows(app1_body.len()).any(|w| w == app1_body),
"APP1 body not embedded"
);
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode with meta");
assert_eq!(frame.planes.len(), 1);
}
#[test]
fn progressive_grayscale_emits_well_formed_sof2_scan_layout() {
let w = 16usize;
let h = 16usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
samples[y * w + x] = ((x + y) * 8) as u8;
}
}
let jpeg = encode_jpeg_progressive_grayscale(w as u32, h as u32, &samples, w, 75)
.expect("encode progressive grayscale");
assert_eq!(&jpeg[..2], &[0xFF, 0xD8], "expected SOI prefix");
assert_eq!(
&jpeg[jpeg.len() - 2..],
&[0xFF, 0xD9],
"expected EOI suffix"
);
let mut found_sof2 = false;
let mut dqt_count = 0usize;
let mut dht_pairs: Vec<(u8, u8)> = Vec::new();
let mut sos_ssse: Vec<(u8, u8)> = Vec::new();
let mut i = 2;
while i + 3 < jpeg.len() {
assert_eq!(jpeg[i], 0xFF, "expected marker prefix at {i}");
let marker = jpeg[i + 1];
if marker == 0xDA {
let len = u16::from_be_bytes([jpeg[i + 2], jpeg[i + 3]]) as usize;
let payload = &jpeg[i + 4..i + 2 + len];
let ns = payload[0] as usize;
let ss = payload[1 + 2 * ns];
let se = payload[2 + 2 * ns];
sos_ssse.push((ss, se));
let mut j = i + 2 + len;
while j + 1 < jpeg.len() {
if jpeg[j] == 0xFF && jpeg[j + 1] != 0x00 {
let m = jpeg[j + 1];
if (0xD0..=0xD7).contains(&m) {
j += 2;
continue;
}
break;
}
j += 1;
}
i = j;
continue;
}
let len = u16::from_be_bytes([jpeg[i + 2], jpeg[i + 3]]) as usize;
let payload = &jpeg[i + 4..i + 2 + len];
match marker {
0xC2 => {
assert_eq!(payload[0], 8, "SOF2 precision must be 8");
assert_eq!(payload[5], 1, "SOF2 must declare Nf = 1");
assert_eq!(payload[6], 1, "component id");
assert_eq!(payload[7], 0x11, "H = V = 1");
assert_eq!(payload[8], 0, "quant table id");
found_sof2 = true;
}
0xDB => {
dqt_count += 1;
}
0xC4 => {
let class = payload[0] >> 4;
let id = payload[0] & 0x0F;
dht_pairs.push((class, id));
}
_ => {}
}
i += 2 + len;
}
assert!(found_sof2, "expected SOF2 segment in output");
assert_eq!(dqt_count, 1, "exactly one DQT expected (luma only)");
assert_eq!(dht_pairs.len(), 2, "exactly two DHTs expected");
assert!(
dht_pairs.contains(&(0, 0)),
"luma DC DHT (class=0, id=0) missing"
);
assert!(
dht_pairs.contains(&(1, 0)),
"luma AC DHT (class=1, id=0) missing"
);
assert_eq!(
sos_ssse,
vec![(0, 0), (1, 5), (6, 63)],
"SOS scans must be DC / AC-low / AC-high in order",
);
}
#[test]
fn progressive_grayscale_high_quality_roundtrip_is_near_lossless() {
let w = 32usize;
let h = 32usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
samples[y * w + x] = (((x * 7 + y * 11) % 256) as u8).clamp(0, 255);
}
}
let jpeg = encode_jpeg_progressive_grayscale(w as u32, h as u32, &samples, w, 100)
.expect("encode progressive grayscale");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
assert_eq!(frame.planes.len(), 1, "Gray8 frame has one plane");
let recovered = &frame.planes[0].data;
let stride = frame.planes[0].stride;
let mut max_diff: u32 = 0;
for y in 0..h {
for x in 0..w {
let a = samples[y * w + x];
let b = recovered[y * stride + x];
let d = (a as i32 - b as i32).unsigned_abs();
if d > max_diff {
max_diff = d;
}
}
}
assert!(max_diff <= 4, "Q=100 max diff = {max_diff} (expected ≤ 4)");
}
#[test]
fn progressive_grayscale_q75_roundtrip_psnr_above_30db() {
let w = 64usize;
let h = 64usize;
let mut samples = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let r = ((x as i32 - 32).abs() + (y as i32 - 32).abs()) as u32;
samples[y * w + x] = (128 + (r as i32 - 32).clamp(-127, 127)) as u8;
}
}
let jpeg =
encode_jpeg_progressive_grayscale(w as u32, h as u32, &samples, w, DEFAULT_QUALITY)
.expect("encode progressive grayscale");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
let recovered = &frame.planes[0].data;
let stride = frame.planes[0].stride;
let mut sse: f64 = 0.0;
for y in 0..h {
for x in 0..w {
let a = samples[y * w + x] as f64;
let b = recovered[y * stride + x] as f64;
sse += (a - b) * (a - b);
}
}
let mse = sse / ((w * h) as f64);
let psnr = if mse == 0.0 {
f64::INFINITY
} else {
10.0 * (255.0_f64 * 255.0 / mse).log10()
};
assert!(psnr >= 30.0, "Q=75 PSNR = {psnr:.2} dB (expected ≥ 30)");
}
#[test]
fn progressive_grayscale_rejects_short_stride() {
let samples = vec![0u8; 64];
let err = encode_jpeg_progressive_grayscale(16, 4, &samples, 8, 75).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn progressive_grayscale_rejects_short_buffer() {
let samples = vec![0u8; 30];
let err = encode_jpeg_progressive_grayscale(8, 8, &samples, 8, 75).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn progressive_grayscale_with_meta_embeds_app_segments() {
let w = 16u32;
let h = 16u32;
let samples = vec![100u8; (w * h) as usize];
let app1_body = b"FAKEEXIF";
let mut meta = Vec::new();
meta.push(0xFF);
meta.push(0xE1); let len = (2 + app1_body.len()) as u16;
meta.extend_from_slice(&len.to_be_bytes());
meta.extend_from_slice(app1_body);
let jpeg =
encode_jpeg_progressive_grayscale_with_meta(w, h, &samples, w as usize, 90, &meta)
.expect("encode with meta");
assert_eq!(&jpeg[2..4], &[0xFF, 0xE1], "APP1 must follow SOI");
assert!(
jpeg.windows(app1_body.len()).any(|win| win == app1_body),
"APP1 body not embedded"
);
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode with meta");
assert_eq!(frame.planes.len(), 1);
}
fn walk_rgb24_header(jpeg: &[u8]) {
assert_eq!(&jpeg[..2], &[0xFF, 0xD8], "expected SOI prefix");
assert_eq!(
&jpeg[jpeg.len() - 2..],
&[0xFF, 0xD9],
"expected EOI suffix"
);
let mut found_sof0 = false;
let mut found_app14 = false;
let mut dqt_count = 0usize;
let mut dht_pairs: Vec<(u8, u8)> = Vec::new();
let mut i = 2;
while i + 3 < jpeg.len() {
assert_eq!(jpeg[i], 0xFF, "expected marker prefix at {i}");
let marker = jpeg[i + 1];
if marker == 0xDA {
break;
}
let len = u16::from_be_bytes([jpeg[i + 2], jpeg[i + 3]]) as usize;
let payload = &jpeg[i + 4..i + 2 + len];
match marker {
0xC0 => {
assert_eq!(payload[0], 8, "P must be 8");
assert_eq!(payload[5], 3, "Nf must be 3");
let comps = [
(payload[6], payload[7], payload[8]),
(payload[9], payload[10], payload[11]),
(payload[12], payload[13], payload[14]),
];
assert_eq!(comps[0].0, b'R', "component 0 id must be 'R'");
assert_eq!(comps[1].0, b'G', "component 1 id must be 'G'");
assert_eq!(comps[2].0, b'B', "component 2 id must be 'B'");
for c in &comps {
assert_eq!(c.1, 0x11, "H = V = 1 on every component");
assert_eq!(c.2, 0, "qt 0 on every component");
}
found_sof0 = true;
}
0xEE if payload.len() >= 12 && &payload[0..5] == b"Adobe" => {
assert_eq!(payload[11], 0, "Adobe APP14 transform must be 0");
found_app14 = true;
}
0xDB => {
dqt_count += 1;
assert_eq!(payload[0] & 0xF0, 0, "DQT precision must be 0 (8-bit)");
assert_eq!(payload[0] & 0x0F, 0, "DQT table id must be 0");
}
0xC4 => {
let class = payload[0] >> 4;
let id = payload[0] & 0x0F;
dht_pairs.push((class, id));
}
_ => {}
}
i += 2 + len;
}
assert!(found_sof0, "expected SOF0 segment");
assert!(found_app14, "expected Adobe APP14 transform=0 segment");
assert_eq!(dqt_count, 1, "expected exactly one DQT (luma only)");
assert_eq!(
dht_pairs,
vec![(0u8, 0u8), (1u8, 0u8)],
"expected luma DC + luma AC only"
);
}
#[test]
fn baseline_rgb24_header_is_well_formed() {
let w = 16usize;
let h = 16usize;
let mut samples = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let o = (y * w + x) * 3;
samples[o] = ((x * 8) & 0xFF) as u8;
samples[o + 1] = ((y * 8) & 0xFF) as u8;
samples[o + 2] = (((x + y) * 4) & 0xFF) as u8;
}
}
let jpeg =
encode_jpeg_rgb24(w as u32, h as u32, &samples, w * 3, 75).expect("encode rgb24");
walk_rgb24_header(&jpeg);
}
#[test]
fn baseline_rgb24_q100_roundtrip_is_near_lossless() {
let w = 32usize;
let h = 32usize;
let mut samples = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let o = (y * w + x) * 3;
samples[o] = ((x * 7 + y * 11) % 256) as u8;
samples[o + 1] = ((x * 11 + y * 7) % 256) as u8;
samples[o + 2] = ((x * 13 + y * 5) % 256) as u8;
}
}
let jpeg =
encode_jpeg_rgb24(w as u32, h as u32, &samples, w * 3, 100).expect("encode rgb24");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
assert_eq!(frame.planes.len(), 1, "Rgb24 frame has one plane");
let recovered = &frame.planes[0].data;
let stride = frame.planes[0].stride;
assert_eq!(stride, w * 3, "expected packed RGB stride");
let mut max_diff: u32 = 0;
for y in 0..h {
for x in 0..w {
for ch in 0..3 {
let a = samples[(y * w + x) * 3 + ch];
let b = recovered[y * stride + x * 3 + ch];
let d = (a as i32 - b as i32).unsigned_abs();
if d > max_diff {
max_diff = d;
}
}
}
}
assert!(max_diff <= 4, "Q=100 max diff = {max_diff} (expected ≤ 4)");
}
#[test]
fn baseline_rgb24_q75_roundtrip_psnr_above_30db() {
let w = 64usize;
let h = 64usize;
let mut samples = vec![0u8; w * h * 3];
for y in 0..h {
for x in 0..w {
let o = (y * w + x) * 3;
let r = ((x as i32 - 32).abs() + (y as i32 - 32).abs()) as u32;
samples[o] = (128 + (r as i32 - 32).clamp(-127, 127)) as u8;
samples[o + 1] = ((x * 4) & 0xFF) as u8;
samples[o + 2] = ((y * 4) & 0xFF) as u8;
}
}
let jpeg = encode_jpeg_rgb24(w as u32, h as u32, &samples, w * 3, DEFAULT_QUALITY)
.expect("encode rgb24");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode");
let recovered = &frame.planes[0].data;
let stride = frame.planes[0].stride;
let mut sse: f64 = 0.0;
for y in 0..h {
for x in 0..w {
for ch in 0..3 {
let a = samples[(y * w + x) * 3 + ch] as f64;
let b = recovered[y * stride + x * 3 + ch] as f64;
let d = a - b;
sse += d * d;
}
}
}
let mse = sse / (w * h * 3) as f64;
let psnr = if mse <= f64::EPSILON {
99.0
} else {
20.0 * (255.0_f64 / mse.sqrt()).log10()
};
assert!(psnr >= 30.0, "Q=75 PSNR = {psnr:.2} dB (expected ≥ 30)");
}
#[test]
fn baseline_rgb24_rejects_short_stride() {
let samples = vec![0u8; 16 * 16 * 3];
let err =
encode_jpeg_rgb24(16, 16, &samples, 16, 75).expect_err("stride < width * 3 must fail");
assert!(matches!(err, Error::InvalidData(_)), "got {err:?}");
}
#[test]
fn baseline_rgb24_rejects_short_buffer() {
let samples = vec![0u8; 16 * 8 * 3];
let err = encode_jpeg_rgb24(16, 16, &samples, 16 * 3, 75)
.expect_err("samples shorter than stride*h must fail");
assert!(matches!(err, Error::InvalidData(_)), "got {err:?}");
}
#[test]
fn baseline_rgb24_with_opts_emits_dri_and_restart() {
let w = 32u32;
let h = 32u32;
let mut samples = vec![0u8; (w * h * 3) as usize];
for y in 0..h as usize {
for x in 0..w as usize {
let o = (y * w as usize + x) * 3;
samples[o] = ((x + y) as u8).wrapping_mul(3);
samples[o + 1] = ((x + y) as u8).wrapping_mul(5);
samples[o + 2] = ((x + y) as u8).wrapping_mul(7);
}
}
let jpeg = encode_jpeg_rgb24_with_opts(w, h, &samples, (w * 3) as usize, 80, 4)
.expect("encode rgb24 with DRI");
let mut found_dri = false;
let mut found_rst = false;
for i in 0..jpeg.len().saturating_sub(1) {
if jpeg[i] == 0xFF && jpeg[i + 1] == 0xDD {
found_dri = true;
}
if jpeg[i] == 0xFF && (0xD0..=0xD7).contains(&jpeg[i + 1]) {
found_rst = true;
}
}
assert!(found_dri, "DRI segment must be present when interval > 0");
assert!(found_rst, "at least one RSTn marker must be present");
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode DRI rgb24");
assert_eq!(frame.planes.len(), 1);
assert_eq!(frame.planes[0].stride, (w * 3) as usize);
}
#[test]
fn baseline_rgb24_with_meta_embeds_app_segments() {
let w = 16u32;
let h = 16u32;
let samples = vec![100u8; (w * h * 3) as usize];
let app1_body = b"FAKEEXIF";
let mut meta = Vec::new();
meta.push(0xFF);
meta.push(0xE1); let len = (2 + app1_body.len()) as u16;
meta.extend_from_slice(&len.to_be_bytes());
meta.extend_from_slice(app1_body);
let jpeg = encode_jpeg_rgb24_with_meta(w, h, &samples, (w * 3) as usize, 90, 0, &meta)
.expect("encode rgb24 with meta");
assert_eq!(&jpeg[2..4], &[0xFF, 0xE1], "APP1 must follow SOI");
assert!(
jpeg.windows(app1_body.len()).any(|win| win == app1_body),
"APP1 body not embedded"
);
let frame = crate::decoder::decode_jpeg(&jpeg, None).expect("decode with meta");
assert_eq!(frame.planes.len(), 1);
assert_eq!(frame.planes[0].stride, (w * 3) as usize);
}
#[test]
fn lossless_dc_huff_table_kraft_complete() {
let mut kraft_num: u32 = 0; for (i, &n) in STD_DC_LOSSLESS_BITS.iter().enumerate() {
let len = (i + 1) as u32;
kraft_num += (n as u32) * (1u32 << (16 - len));
}
assert_eq!(kraft_num, 1 << 16, "Kraft inequality must equal exactly 1");
assert_eq!(STD_DC_LOSSLESS_VALS.len(), 17);
let mut seen = [false; 17];
for &v in &STD_DC_LOSSLESS_VALS {
assert!((v as usize) < 17, "symbol {v} out of expected 0..=16 range");
seen[v as usize] = true;
}
assert!(seen.iter().all(|&b| b), "every SSSS in 0..=16 must appear");
}
}