use crate::error::{MjpegError as Error, Result};
use super::markers;
use super::parser::{parse_dri, parse_sof, MarkerWalker};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SofKind {
Baseline,
ExtendedSequential,
Progressive,
Lossless,
ExtendedSequentialArith,
ProgressiveArith,
LosslessArith,
HierarchicalDct,
HierarchicalArith,
}
impl SofKind {
fn from_marker(b: u8) -> Option<Self> {
match b {
0xC0 => Some(Self::Baseline),
0xC1 => Some(Self::ExtendedSequential),
0xC2 => Some(Self::Progressive),
0xC3 => Some(Self::Lossless),
0xC5..=0xC7 => Some(Self::HierarchicalDct),
0xC9 => Some(Self::ExtendedSequentialArith),
0xCA => Some(Self::ProgressiveArith),
0xCB => Some(Self::LosslessArith),
0xCD..=0xCF => Some(Self::HierarchicalArith),
_ => None,
}
}
pub fn is_supported_by_decoder(self) -> bool {
matches!(
self,
Self::Baseline
| Self::ExtendedSequential
| Self::Progressive
| Self::Lossless
| Self::ExtendedSequentialArith
| Self::ProgressiveArith
| Self::LosslessArith
)
}
pub fn is_dct(self) -> bool {
!matches!(self, Self::Lossless | Self::LosslessArith)
}
pub fn is_arithmetic(self) -> bool {
matches!(
self,
Self::ExtendedSequentialArith
| Self::ProgressiveArith
| Self::LosslessArith
| Self::HierarchicalArith
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChromaSubsampling {
GrayscaleOnly,
Yuv444,
Yuv422,
Yuv420,
Yuv411,
Custom,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorHint {
Unspecified,
JfifYCbCr,
AdobeUntransformed,
AdobeYCbCr,
AdobeYcck,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InspectedComponent {
pub id: u8,
pub h_sampling: u8,
pub v_sampling: u8,
pub quant_table: u8,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JfifUnits {
AspectRatio,
DotsPerInch,
DotsPerCm,
}
impl JfifUnits {
pub fn as_byte(self) -> u8 {
match self {
Self::AspectRatio => 0x00,
Self::DotsPerInch => 0x01,
Self::DotsPerCm => 0x02,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct JfifApp0 {
pub version_major: u8,
pub version_minor: u8,
pub units: JfifUnits,
pub h_density: u16,
pub v_density: u16,
pub thumbnail_width: u8,
pub thumbnail_height: u8,
}
impl JfifApp0 {
pub fn has_thumbnail(self) -> bool {
self.thumbnail_width != 0 && self.thumbnail_height != 0
}
pub fn thumbnail_payload_len(self) -> usize {
(self.thumbnail_width as usize) * (self.thumbnail_height as usize) * 3
}
pub fn version(self) -> (u8, u8) {
(self.version_major, self.version_minor)
}
pub fn h_density_dpi(self) -> Option<u32> {
match self.units {
JfifUnits::AspectRatio => None,
JfifUnits::DotsPerInch => Some(self.h_density as u32),
JfifUnits::DotsPerCm => Some(((self.h_density as u32).saturating_mul(254) + 50) / 100),
}
}
pub fn v_density_dpi(self) -> Option<u32> {
match self.units {
JfifUnits::AspectRatio => None,
JfifUnits::DotsPerInch => Some(self.v_density as u32),
JfifUnits::DotsPerCm => Some(((self.v_density as u32).saturating_mul(254) + 50) / 100),
}
}
pub fn pixel_aspect_ratio(self) -> (u16, u16) {
(self.v_density, self.h_density)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JfxxThumbnail {
JpegEncoded {
jpeg_len: usize,
},
PaletteRgb {
width: u8,
height: u8,
},
Rgb24 {
width: u8,
height: u8,
},
}
impl JfxxThumbnail {
pub fn extension_code(self) -> u8 {
match self {
Self::JpegEncoded { .. } => 0x10,
Self::PaletteRgb { .. } => 0x11,
Self::Rgb24 { .. } => 0x13,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct JfxxApp0 {
pub thumbnail: JfxxThumbnail,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AdobeColorTransform {
Unknown,
YCbCr,
Ycck,
}
impl AdobeColorTransform {
pub fn as_byte(self) -> u8 {
match self {
Self::Unknown => 0x00,
Self::YCbCr => 0x01,
Self::Ycck => 0x02,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdobeApp14 {
pub dct_encode_version: u16,
pub flags_0: u16,
pub flags_1: u16,
pub transform: AdobeColorTransform,
}
impl AdobeApp14 {
pub fn is_standard_version(self) -> bool {
self.dct_encode_version == 100
}
pub fn as_color_hint(self) -> ColorHint {
match self.transform {
AdobeColorTransform::Unknown => ColorHint::AdobeUntransformed,
AdobeColorTransform::YCbCr => ColorHint::AdobeYCbCr,
AdobeColorTransform::Ycck => ColorHint::AdobeYcck,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct IccProfileApp2Chunk<'a> {
pub seq_no: u8,
pub total: u8,
pub profile_bytes: &'a [u8],
}
const ICC_PROFILE_MAGIC: &[u8; 12] = b"ICC_PROFILE\0";
pub fn parse_icc_profile_app2(payload: &[u8]) -> Result<IccProfileApp2Chunk<'_>> {
if payload.len() < 14 {
return Err(Error::invalid("parse_icc_profile_app2: payload too short"));
}
if &payload[..12] != ICC_PROFILE_MAGIC {
return Err(Error::invalid(
"parse_icc_profile_app2: identifier != ICC_PROFILE\\0",
));
}
let seq_no = payload[12];
let total = payload[13];
if total == 0 {
return Err(Error::invalid("parse_icc_profile_app2: total = 0"));
}
if seq_no == 0 || seq_no > total {
return Err(Error::invalid(
"parse_icc_profile_app2: seq_no outside 1..=total",
));
}
Ok(IccProfileApp2Chunk {
seq_no,
total,
profile_bytes: &payload[14..],
})
}
#[derive(Clone, Debug)]
pub struct IccProfileChunks {
pub total: u8,
pub total_payload_len: usize,
pub chunks: Vec<(u8, usize)>,
}
impl IccProfileChunks {
pub fn is_complete(&self) -> bool {
if self.total == 0 {
return false;
}
if self.chunks.len() != self.total as usize {
return false;
}
let mut seen = [false; 256];
for (seq, _) in &self.chunks {
let idx = *seq as usize;
if idx == 0 || idx > self.total as usize || seen[idx] {
return false;
}
seen[idx] = true;
}
true
}
}
#[derive(Clone, Debug)]
pub struct JpegInfo {
pub sof_kind: SofKind,
pub precision: u8,
pub width: u16,
pub height: u16,
pub components: Vec<InspectedComponent>,
pub subsampling: ChromaSubsampling,
pub color_hint: ColorHint,
pub restart_interval: u16,
pub jfif: Option<JfifApp0>,
pub jfxx: Option<JfxxApp0>,
pub adobe: Option<AdobeApp14>,
pub icc_profile: Option<IccProfileChunks>,
}
impl JpegInfo {
pub fn num_components(&self) -> usize {
self.components.len()
}
}
const JFIF_MAGIC: &[u8; 5] = b"JFIF\0";
const JFXX_MAGIC: &[u8; 5] = b"JFXX\0";
const ADOBE_MAGIC: &[u8; 5] = b"Adobe";
pub fn parse_jfif_app0(payload: &[u8]) -> Result<JfifApp0> {
if payload.len() < 14 {
return Err(Error::invalid("parse_jfif_app0: payload too short"));
}
if &payload[..5] != JFIF_MAGIC {
return Err(Error::invalid("parse_jfif_app0: identifier != JFIF\\0"));
}
let version_major = payload[5];
let version_minor = payload[6];
let units = match payload[7] {
0x00 => JfifUnits::AspectRatio,
0x01 => JfifUnits::DotsPerInch,
0x02 => JfifUnits::DotsPerCm,
_ => return Err(Error::invalid("parse_jfif_app0: illegal units byte")),
};
let h_density = u16::from_be_bytes([payload[8], payload[9]]);
let v_density = u16::from_be_bytes([payload[10], payload[11]]);
if h_density == 0 || v_density == 0 {
return Err(Error::invalid("parse_jfif_app0: zero density"));
}
let thumbnail_width = payload[12];
let thumbnail_height = payload[13];
let thumb_bytes = (thumbnail_width as usize) * (thumbnail_height as usize) * 3;
if payload.len() < 14 + thumb_bytes {
return Err(Error::invalid(
"parse_jfif_app0: declared thumbnail overflows payload",
));
}
Ok(JfifApp0 {
version_major,
version_minor,
units,
h_density,
v_density,
thumbnail_width,
thumbnail_height,
})
}
pub fn parse_jfxx_app0(payload: &[u8]) -> Result<JfxxApp0> {
if payload.len() < 6 {
return Err(Error::invalid("parse_jfxx_app0: payload too short"));
}
if &payload[..5] != JFXX_MAGIC {
return Err(Error::invalid("parse_jfxx_app0: identifier != JFXX\\0"));
}
let data = &payload[6..];
let thumbnail = match payload[5] {
0x10 => JfxxThumbnail::JpegEncoded {
jpeg_len: data.len(),
},
0x11 => {
if data.len() < 2 {
return Err(Error::invalid(
"parse_jfxx_app0: palette thumbnail missing dimensions",
));
}
let width = data[0];
let height = data[1];
if width == 0 || height == 0 {
return Err(Error::invalid(
"parse_jfxx_app0: palette thumbnail has zero dimension",
));
}
let need = 2usize + 768 + (width as usize) * (height as usize);
if data.len() < need {
return Err(Error::invalid(
"parse_jfxx_app0: palette thumbnail overflows payload",
));
}
JfxxThumbnail::PaletteRgb { width, height }
}
0x13 => {
if data.len() < 2 {
return Err(Error::invalid(
"parse_jfxx_app0: rgb thumbnail missing dimensions",
));
}
let width = data[0];
let height = data[1];
if width == 0 || height == 0 {
return Err(Error::invalid(
"parse_jfxx_app0: rgb thumbnail has zero dimension",
));
}
let need = 2usize + 3 * (width as usize) * (height as usize);
if data.len() < need {
return Err(Error::invalid(
"parse_jfxx_app0: rgb thumbnail overflows payload",
));
}
JfxxThumbnail::Rgb24 { width, height }
}
_ => {
return Err(Error::invalid(
"parse_jfxx_app0: reserved extension_code byte",
));
}
};
Ok(JfxxApp0 { thumbnail })
}
pub fn parse_adobe_app14(payload: &[u8]) -> Result<AdobeApp14> {
if payload.len() < 12 {
return Err(Error::invalid("parse_adobe_app14: payload too short"));
}
if &payload[..5] != ADOBE_MAGIC {
return Err(Error::invalid("parse_adobe_app14: identifier != Adobe"));
}
let dct_encode_version = u16::from_be_bytes([payload[5], payload[6]]);
let flags_0 = u16::from_be_bytes([payload[7], payload[8]]);
let flags_1 = u16::from_be_bytes([payload[9], payload[10]]);
let transform = match payload[11] {
0x00 => AdobeColorTransform::Unknown,
0x01 => AdobeColorTransform::YCbCr,
0x02 => AdobeColorTransform::Ycck,
_ => return Err(Error::invalid("parse_adobe_app14: reserved transform byte")),
};
Ok(AdobeApp14 {
dct_encode_version,
flags_0,
flags_1,
transform,
})
}
pub fn inspect_jpeg(buf: &[u8]) -> Result<JpegInfo> {
if buf.len() < 2 || buf[0] != 0xFF || buf[1] != markers::SOI {
return Err(Error::invalid("inspect: missing SOI"));
}
let mut walker = MarkerWalker::new(buf);
walker.pos = 2;
let mut sof_kind: Option<SofKind> = None;
let mut precision: u8 = 0;
let mut width: u16 = 0;
let mut height: u16 = 0;
let mut components: Vec<InspectedComponent> = Vec::new();
let mut color_hint = ColorHint::Unspecified;
let mut restart_interval: u16 = 0;
let mut jfif: Option<JfifApp0> = None;
let mut jfxx: Option<JfxxApp0> = None;
let mut adobe: Option<AdobeApp14> = None;
let mut icc_total: Option<u8> = None;
let mut icc_payload_len: usize = 0;
let mut icc_chunks: Vec<(u8, usize)> = Vec::new();
loop {
let Some(marker) = walker.next_marker()? else {
return Err(Error::invalid("inspect: EOF before SOS"));
};
if marker == markers::SOI || markers::is_rst(marker) || marker == markers::EOI {
if marker == markers::EOI {
return Err(Error::invalid("inspect: EOI before SOS"));
}
continue;
}
if marker == markers::SOS {
break;
}
let payload = walker.read_segment_payload()?;
if markers::is_sof(marker) {
if sof_kind.is_none() {
let kind = SofKind::from_marker(marker)
.ok_or_else(|| Error::invalid("inspect: SOF marker not classifiable"))?;
let sof = parse_sof(payload)?;
sof_kind = Some(kind);
precision = sof.precision;
width = sof.width;
height = sof.height;
components = sof
.components
.iter()
.map(|c| InspectedComponent {
id: c.id,
h_sampling: c.h_factor,
v_sampling: c.v_factor,
quant_table: c.qt_id,
})
.collect();
}
continue;
}
if marker == markers::DRI {
restart_interval = parse_dri(payload)?;
continue;
}
if markers::is_app(marker) {
if marker == markers::APP2 && payload.len() >= 12 && &payload[..12] == ICC_PROFILE_MAGIC
{
if let Ok(chunk) = parse_icc_profile_app2(payload) {
let accept = match icc_total {
None => {
icc_total = Some(chunk.total);
true
}
Some(t) => t == chunk.total,
};
if accept {
icc_chunks.push((chunk.seq_no, chunk.profile_bytes.len()));
icc_payload_len = icc_payload_len.saturating_add(chunk.profile_bytes.len());
}
}
continue;
}
if marker == markers::APP0 && payload.len() >= 5 && &payload[..5] == JFIF_MAGIC {
if color_hint == ColorHint::Unspecified {
color_hint = ColorHint::JfifYCbCr;
}
if jfif.is_none() {
if let Ok(parsed) = parse_jfif_app0(payload) {
jfif = Some(parsed);
}
}
continue;
}
if marker == markers::APP0 && payload.len() >= 5 && &payload[..5] == JFXX_MAGIC {
if jfxx.is_none() {
if let Ok(parsed) = parse_jfxx_app0(payload) {
jfxx = Some(parsed);
}
}
continue;
}
if marker == markers::APP14 && payload.len() >= 12 && &payload[..5] == ADOBE_MAGIC {
let transform = payload[11];
color_hint = match transform {
0 => ColorHint::AdobeUntransformed,
1 => ColorHint::AdobeYCbCr,
2 => ColorHint::AdobeYcck,
_ => {
if color_hint == ColorHint::Unspecified {
ColorHint::AdobeUntransformed
} else {
color_hint
}
}
};
if adobe.is_none() {
if let Ok(parsed) = parse_adobe_app14(payload) {
adobe = Some(parsed);
}
}
continue;
}
continue;
}
}
let sof_kind = sof_kind.ok_or_else(|| Error::invalid("inspect: SOS before SOF"))?;
let subsampling = classify_subsampling(&components);
let icc_profile = icc_total.map(|total| IccProfileChunks {
total,
total_payload_len: icc_payload_len,
chunks: icc_chunks,
});
Ok(JpegInfo {
sof_kind,
precision,
width,
height,
components,
subsampling,
color_hint,
restart_interval,
jfif,
jfxx,
adobe,
icc_profile,
})
}
fn classify_subsampling(comps: &[InspectedComponent]) -> ChromaSubsampling {
match comps.len() {
1 => ChromaSubsampling::GrayscaleOnly,
3 => {
let y = comps[0];
let cb = comps[1];
let cr = comps[2];
if cb.h_sampling != cr.h_sampling || cb.v_sampling != cr.v_sampling {
return ChromaSubsampling::Custom;
}
if y.h_sampling < cb.h_sampling || y.v_sampling < cb.v_sampling {
return ChromaSubsampling::Custom;
}
if cb.h_sampling == 0 || cb.v_sampling == 0 {
return ChromaSubsampling::Custom;
}
let hr = y.h_sampling / cb.h_sampling;
let vr = y.v_sampling / cb.v_sampling;
if y.h_sampling % cb.h_sampling != 0 || y.v_sampling % cb.v_sampling != 0 {
return ChromaSubsampling::Custom;
}
match (hr, vr) {
(1, 1) => ChromaSubsampling::Yuv444,
(2, 1) => ChromaSubsampling::Yuv422,
(2, 2) => ChromaSubsampling::Yuv420,
(4, 1) => ChromaSubsampling::Yuv411,
_ => ChromaSubsampling::Custom,
}
}
_ => ChromaSubsampling::Custom,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_prefix(
sof_marker: u8,
precision: u8,
width: u16,
height: u16,
components: &[(u8, u8, u8, u8)],
extras: &[(u8, &[u8])],
) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0xFF, markers::SOI]);
for (marker, payload) in extras {
out.push(0xFF);
out.push(*marker);
let len = (payload.len() + 2) as u16;
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(payload);
}
let nf = components.len() as u8;
let payload_len = 6 + (nf as usize) * 3;
out.push(0xFF);
out.push(sof_marker);
let seg_len = (payload_len + 2) as u16;
out.extend_from_slice(&seg_len.to_be_bytes());
out.push(precision);
out.extend_from_slice(&height.to_be_bytes());
out.extend_from_slice(&width.to_be_bytes());
out.push(nf);
for (id, h, v, tq) in components {
out.push(*id);
out.push((h << 4) | (v & 0x0F));
out.push(*tq);
}
out.push(0xFF);
out.push(markers::SOS);
out
}
#[test]
fn rejects_missing_soi() {
let data = [0x00, 0x01, 0x02, 0x03];
assert!(inspect_jpeg(&data).is_err());
}
#[test]
fn rejects_empty_buffer() {
assert!(inspect_jpeg(&[]).is_err());
}
#[test]
fn rejects_eof_before_sos() {
let buf = [0xFF, markers::SOI];
assert!(inspect_jpeg(&buf).is_err());
}
#[test]
fn rejects_eoi_before_sof() {
let buf = [0xFF, markers::SOI, 0xFF, markers::EOI];
assert!(inspect_jpeg(&buf).is_err());
}
#[test]
fn baseline_yuv420_jfif() {
let extras = [(
markers::APP0,
&b"JFIF\0\x01\x02\x00\x00\x01\x00\x01\x00\x00"[..],
)];
let buf = build_prefix(
0xC0,
8,
640,
480,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect baseline 4:2:0");
assert_eq!(info.sof_kind, SofKind::Baseline);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(info.sof_kind.is_dct());
assert!(!info.sof_kind.is_arithmetic());
assert_eq!(info.precision, 8);
assert_eq!(info.width, 640);
assert_eq!(info.height, 480);
assert_eq!(info.num_components(), 3);
assert_eq!(info.subsampling, ChromaSubsampling::Yuv420);
assert_eq!(info.color_hint, ColorHint::JfifYCbCr);
assert_eq!(info.restart_interval, 0);
let jfif = info.jfif.expect("baseline JFIF view populated");
assert_eq!(jfif.version(), (1, 2));
assert_eq!(jfif.units, JfifUnits::AspectRatio);
assert_eq!(jfif.h_density, 1);
assert_eq!(jfif.v_density, 1);
assert_eq!(jfif.thumbnail_width, 0);
assert_eq!(jfif.thumbnail_height, 0);
assert!(!jfif.has_thumbnail());
assert_eq!(jfif.thumbnail_payload_len(), 0);
assert_eq!(jfif.h_density_dpi(), None);
assert_eq!(jfif.v_density_dpi(), None);
assert_eq!(jfif.pixel_aspect_ratio(), (1, 1));
assert!(info.adobe.is_none());
}
#[test]
fn baseline_yuv422() {
let buf = build_prefix(
0xC0,
8,
320,
240,
&[(1, 2, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect baseline 4:2:2");
assert_eq!(info.subsampling, ChromaSubsampling::Yuv422);
assert_eq!(info.color_hint, ColorHint::Unspecified);
assert!(info.jfif.is_none());
}
#[test]
fn baseline_yuv444() {
let buf = build_prefix(
0xC0,
8,
8,
8,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect baseline 4:4:4");
assert_eq!(info.subsampling, ChromaSubsampling::Yuv444);
}
#[test]
fn baseline_yuv411() {
let buf = build_prefix(
0xC0,
8,
64,
64,
&[(1, 4, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect baseline 4:1:1");
assert_eq!(info.subsampling, ChromaSubsampling::Yuv411);
}
#[test]
fn baseline_grayscale() {
let buf = build_prefix(0xC0, 8, 16, 16, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect baseline grayscale");
assert_eq!(info.num_components(), 1);
assert_eq!(info.subsampling, ChromaSubsampling::GrayscaleOnly);
}
#[test]
fn baseline_cmyk_is_custom_subsampling() {
let buf = build_prefix(
0xC0,
8,
32,
32,
&[(1, 1, 1, 0), (2, 1, 1, 0), (3, 1, 1, 0), (4, 1, 1, 0)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect 4-comp");
assert_eq!(info.num_components(), 4);
assert_eq!(info.subsampling, ChromaSubsampling::Custom);
}
#[test]
fn asymmetric_chroma_is_custom() {
let buf = build_prefix(
0xC0,
8,
32,
32,
&[(1, 2, 2, 0), (2, 2, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect asymmetric");
assert_eq!(info.subsampling, ChromaSubsampling::Custom);
}
#[test]
fn progressive_kind() {
let buf = build_prefix(
0xC2,
8,
16,
16,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect progressive");
assert_eq!(info.sof_kind, SofKind::Progressive);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(info.sof_kind.is_dct());
assert!(!info.sof_kind.is_arithmetic());
}
#[test]
fn lossless_kind() {
let buf = build_prefix(0xC3, 12, 100, 100, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect lossless");
assert_eq!(info.sof_kind, SofKind::Lossless);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(!info.sof_kind.is_dct());
assert!(!info.sof_kind.is_arithmetic());
assert_eq!(info.precision, 12);
}
#[test]
fn arith_kind() {
let buf = build_prefix(
0xC9,
8,
16,
16,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&[],
);
let info = inspect_jpeg(&buf).expect("inspect SOF9");
assert_eq!(info.sof_kind, SofKind::ExtendedSequentialArith);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(info.sof_kind.is_dct());
assert!(info.sof_kind.is_arithmetic());
}
#[test]
fn hierarchical_dct_kind_not_supported() {
let buf = build_prefix(0xC5, 8, 16, 16, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect SOF5");
assert_eq!(info.sof_kind, SofKind::HierarchicalDct);
assert!(!info.sof_kind.is_supported_by_decoder());
}
#[test]
fn progressive_arith_kind() {
let buf = build_prefix(0xCA, 8, 16, 16, &[(1, 2, 2, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect SOF10");
assert_eq!(info.sof_kind, SofKind::ProgressiveArith);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(info.sof_kind.is_dct());
assert!(info.sof_kind.is_arithmetic());
}
#[test]
fn lossless_arith_kind() {
let buf = build_prefix(0xCB, 8, 16, 16, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect SOF11");
assert_eq!(info.sof_kind, SofKind::LosslessArith);
assert!(info.sof_kind.is_supported_by_decoder());
assert!(!info.sof_kind.is_dct());
assert!(info.sof_kind.is_arithmetic());
}
#[test]
fn dri_before_sof_reported() {
let dri_payload = [0x00, 0x10]; let extras = [(markers::DRI, &dri_payload[..])];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect DRI");
assert_eq!(info.restart_interval, 16);
}
#[test]
fn dri_after_sof_also_reported() {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&[0xFF, markers::SOI]);
buf.extend_from_slice(&[0xFF, 0xC0]);
buf.extend_from_slice(&11u16.to_be_bytes());
buf.push(8);
buf.extend_from_slice(&8u16.to_be_bytes()); buf.extend_from_slice(&8u16.to_be_bytes()); buf.push(1);
buf.extend_from_slice(&[1, 0x11, 0]);
buf.extend_from_slice(&[0xFF, markers::DRI, 0x00, 0x04, 0x00, 0x20]);
buf.extend_from_slice(&[0xFF, markers::SOS]);
let info = inspect_jpeg(&buf).expect("inspect DRI-after-SOF");
assert_eq!(info.restart_interval, 32);
}
#[test]
fn adobe_app14_yccc_color_hint() {
let mut adobe = Vec::new();
adobe.extend_from_slice(b"Adobe");
adobe.extend_from_slice(&[0x00, 0x65]); adobe.extend_from_slice(&[0x00, 0x00]); adobe.extend_from_slice(&[0x00, 0x00]); adobe.push(2); let extras = [(markers::APP14, adobe.as_slice())];
let buf = build_prefix(
0xC0,
8,
32,
32,
&[(1, 1, 1, 0), (2, 1, 1, 0), (3, 1, 1, 0), (4, 1, 1, 0)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect Adobe YCCK");
assert_eq!(info.color_hint, ColorHint::AdobeYcck);
assert_eq!(info.num_components(), 4);
let adobe = info.adobe.expect("typed Adobe view");
assert_eq!(adobe.dct_encode_version, 0x0065);
assert_eq!(adobe.flags_0, 0);
assert_eq!(adobe.flags_1, 0);
assert_eq!(adobe.transform, AdobeColorTransform::Ycck);
assert_eq!(adobe.transform.as_byte(), 0x02);
assert_eq!(adobe.as_color_hint(), ColorHint::AdobeYcck);
assert!(!adobe.is_standard_version()); }
#[test]
fn adobe_app14_untransformed_color_hint() {
let mut adobe = Vec::new();
adobe.extend_from_slice(b"Adobe");
adobe.extend_from_slice(&[0x00, 0x65]);
adobe.extend_from_slice(&[0x00, 0x00]);
adobe.extend_from_slice(&[0x00, 0x00]);
adobe.push(0);
let extras = [(markers::APP14, adobe.as_slice())];
let buf = build_prefix(
0xC0,
8,
32,
32,
&[(1, 1, 1, 0), (2, 1, 1, 0), (3, 1, 1, 0)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect Adobe untransformed");
assert_eq!(info.color_hint, ColorHint::AdobeUntransformed);
let adobe = info.adobe.expect("typed Adobe view");
assert_eq!(adobe.transform, AdobeColorTransform::Unknown);
assert_eq!(adobe.transform.as_byte(), 0x00);
assert_eq!(adobe.as_color_hint(), ColorHint::AdobeUntransformed);
}
#[test]
fn unknown_app_segment_skipped_no_color_hint() {
let exif = b"Exif\0\0junk";
let extras = [(0xE1u8, &exif[..])];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect EXIF prefix");
assert_eq!(info.color_hint, ColorHint::Unspecified);
}
#[test]
fn malformed_sof_returns_err() {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&[0xFF, markers::SOI]);
buf.extend_from_slice(&[0xFF, 0xC0]);
buf.extend_from_slice(&6u16.to_be_bytes());
buf.extend_from_slice(&[0, 0, 0, 0]);
buf.extend_from_slice(&[0xFF, markers::SOS]);
assert!(inspect_jpeg(&buf).is_err());
}
#[test]
fn jfif_dots_per_inch_parsed() {
let payload = b"JFIF\0\x01\x01\x01\x00\x48\x00\x48\x00\x00";
let extras = [(markers::APP0, &payload[..])];
let buf = build_prefix(
0xC0,
8,
32,
32,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect JFIF dpi");
let jfif = info.jfif.expect("JFIF view present");
assert_eq!(jfif.units, JfifUnits::DotsPerInch);
assert_eq!(jfif.h_density, 72);
assert_eq!(jfif.v_density, 72);
assert_eq!(jfif.version(), (1, 1));
assert_eq!(jfif.h_density_dpi(), Some(72));
assert_eq!(jfif.v_density_dpi(), Some(72));
assert_eq!(jfif.units.as_byte(), 0x01);
}
#[test]
fn jfif_dots_per_cm_converted_to_dpi() {
let payload = b"JFIF\0\x01\x02\x02\x00\x64\x00\x27\x00\x00";
let parsed = parse_jfif_app0(payload).expect("dpcm");
assert_eq!(parsed.units, JfifUnits::DotsPerCm);
assert_eq!(parsed.h_density, 100);
assert_eq!(parsed.v_density, 39);
assert_eq!(parsed.h_density_dpi(), Some(254));
assert_eq!(parsed.v_density_dpi(), Some(99));
assert_eq!(parsed.units.as_byte(), 0x02);
}
#[test]
fn jfif_rejects_illegal_units() {
let payload = b"JFIF\0\x01\x02\x05\x00\x48\x00\x48\x00\x00"; assert!(parse_jfif_app0(payload).is_err());
}
#[test]
fn jfif_rejects_zero_density() {
let payload = b"JFIF\0\x01\x02\x01\x00\x00\x00\x48\x00\x00"; assert!(parse_jfif_app0(payload).is_err());
let payload = b"JFIF\0\x01\x02\x01\x00\x48\x00\x00\x00\x00"; assert!(parse_jfif_app0(payload).is_err());
}
#[test]
fn jfif_rejects_truncated_header() {
let payload = b"JFIF\0\x01\x02\x01\x00\x48\x00\x48\x00";
assert!(parse_jfif_app0(payload).is_err());
}
#[test]
fn jfif_rejects_bad_identifier() {
let payload = b"JFXX\0\x01\x02\x01\x00\x48\x00\x48\x00\x00";
assert!(parse_jfif_app0(payload).is_err());
}
#[test]
fn jfif_with_2x2_thumbnail() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFIF\0");
payload.push(0x01);
payload.push(0x02);
payload.push(0x01); payload.extend_from_slice(&96u16.to_be_bytes());
payload.extend_from_slice(&96u16.to_be_bytes());
payload.push(2); payload.push(2); for i in 0..12 {
payload.push(i as u8);
}
let parsed = parse_jfif_app0(&payload).expect("parse 2x2 thumb");
assert!(parsed.has_thumbnail());
assert_eq!(parsed.thumbnail_width, 2);
assert_eq!(parsed.thumbnail_height, 2);
assert_eq!(parsed.thumbnail_payload_len(), 12);
assert_eq!(parsed.h_density_dpi(), Some(96));
}
#[test]
fn jfxx_jpeg_encoded_thumbnail() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFXX\0");
payload.push(0x10);
payload.extend_from_slice(&[0xFF, 0xD8, 0xFF, 0xD9]);
let parsed = parse_jfxx_app0(&payload).expect("parse JFXX jpeg thumb");
assert_eq!(parsed.thumbnail, JfxxThumbnail::JpegEncoded { jpeg_len: 4 });
assert_eq!(parsed.thumbnail.extension_code(), 0x10);
}
#[test]
fn jfxx_palette_rgb_thumbnail() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFXX\0");
payload.push(0x11);
payload.push(4); payload.push(3); payload.extend_from_slice(&[0u8; 768]); payload.extend_from_slice(&[0u8; 12]); let parsed = parse_jfxx_app0(&payload).expect("parse JFXX palette thumb");
assert_eq!(
parsed.thumbnail,
JfxxThumbnail::PaletteRgb {
width: 4,
height: 3
}
);
assert_eq!(parsed.thumbnail.extension_code(), 0x11);
}
#[test]
fn jfxx_rgb24_thumbnail() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFXX\0");
payload.push(0x13);
payload.push(2); payload.push(2); payload.extend_from_slice(&[0u8; 12]); let parsed = parse_jfxx_app0(&payload).expect("parse JFXX rgb thumb");
assert_eq!(
parsed.thumbnail,
JfxxThumbnail::Rgb24 {
width: 2,
height: 2
}
);
assert_eq!(parsed.thumbnail.extension_code(), 0x13);
}
#[test]
fn jfxx_rejects_short_payload() {
assert!(parse_jfxx_app0(b"JFXX\0").is_err());
}
#[test]
fn jfxx_rejects_bad_identifier() {
let payload = b"JFIF\0\x10";
assert!(parse_jfxx_app0(payload).is_err());
}
#[test]
fn jfxx_rejects_reserved_extension_code() {
let payload = b"JFXX\0\x12\x02\x02";
assert!(parse_jfxx_app0(payload).is_err());
}
#[test]
fn jfxx_rejects_zero_dimension() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFXX\0");
payload.push(0x13);
payload.push(0); payload.push(4);
assert!(parse_jfxx_app0(&payload).is_err());
}
#[test]
fn jfxx_rejects_thumbnail_overflowing_payload() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFXX\0");
payload.push(0x13);
payload.push(2);
payload.push(2);
payload.extend_from_slice(&[0, 0, 0, 0, 0]);
assert!(parse_jfxx_app0(&payload).is_err());
}
#[test]
fn inspect_jfif_plus_jfxx_dual_app0() {
let jfif: Vec<u8> = b"JFIF\0\x01\x02\x01\x00\x48\x00\x48\x00\x00".to_vec();
let mut jfxx: Vec<u8> = Vec::new();
jfxx.extend_from_slice(b"JFXX\0");
jfxx.push(0x13);
jfxx.push(2);
jfxx.push(2);
jfxx.extend_from_slice(&[0u8; 12]);
let extras = [
(markers::APP0, jfif.as_slice()),
(markers::APP0, jfxx.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect JFIF+JFXX");
let jfif_view = info.jfif.expect("jfif view");
assert!(!jfif_view.has_thumbnail());
let jfxx_view = info.jfxx.expect("jfxx view");
assert_eq!(
jfxx_view.thumbnail,
JfxxThumbnail::Rgb24 {
width: 2,
height: 2
}
);
assert_eq!(info.color_hint, ColorHint::JfifYCbCr);
}
#[test]
fn inspect_no_jfxx_when_absent() {
let buf = build_prefix(0xC0, 8, 8, 8, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect plain grayscale");
assert!(info.jfxx.is_none());
}
#[test]
fn jfif_rejects_thumbnail_overflowing_payload() {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(b"JFIF\0");
payload.push(0x01);
payload.push(0x02);
payload.push(0x01);
payload.extend_from_slice(&72u16.to_be_bytes());
payload.extend_from_slice(&72u16.to_be_bytes());
payload.push(2);
payload.push(2);
payload.extend_from_slice(&[0, 0, 0, 0, 0]);
assert!(parse_jfif_app0(&payload).is_err());
}
#[test]
fn jfif_view_disjoint_from_adobe_when_only_adobe_present() {
let mut adobe = Vec::new();
adobe.extend_from_slice(b"Adobe");
adobe.extend_from_slice(&[0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 1]);
let extras = [(markers::APP14, adobe.as_slice())];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect Adobe-only");
assert_eq!(info.color_hint, ColorHint::AdobeYCbCr);
assert!(info.jfif.is_none());
let adobe = info.adobe.expect("Adobe typed view");
assert_eq!(adobe.transform, AdobeColorTransform::YCbCr);
}
#[test]
fn jfif_malformed_segment_still_sets_color_hint() {
let payload = b"JFIF\0\x01\x02\x01"; let extras = [(markers::APP0, &payload[..])];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect truncated JFIF");
assert_eq!(info.color_hint, ColorHint::JfifYCbCr);
assert!(info.jfif.is_none());
}
#[test]
fn jfif_only_first_segment_wins() {
let payload1 = &b"JFIF\0\x01\x02\x01\x00\x48\x00\x48\x00\x00"[..]; let payload2 = &b"JFIF\0\x01\x02\x01\x01\x90\x01\x90\x00\x00"[..]; let extras = [(markers::APP0, payload1), (markers::APP0, payload2)];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect dup JFIF");
let jfif = info.jfif.expect("typed view");
assert_eq!(jfif.h_density, 72);
assert_eq!(jfif.v_density, 72);
}
#[test]
fn parse_adobe_app14_standard_version() {
let payload = b"Adobe\x00\x64\x00\x00\x00\x00\x01";
let parsed = parse_adobe_app14(payload).expect("parse standard Adobe");
assert!(parsed.is_standard_version());
assert_eq!(parsed.dct_encode_version, 100);
assert_eq!(parsed.flags_0, 0);
assert_eq!(parsed.flags_1, 0);
assert_eq!(parsed.transform, AdobeColorTransform::YCbCr);
assert_eq!(parsed.transform.as_byte(), 0x01);
assert_eq!(parsed.as_color_hint(), ColorHint::AdobeYCbCr);
}
#[test]
fn parse_adobe_app14_with_flags() {
let payload = b"Adobe\x00\x64\xC0\x00\x00\x00\x00";
let parsed = parse_adobe_app14(payload).expect("parse flags");
assert_eq!(parsed.flags_0, 0xC000);
assert_eq!(parsed.transform, AdobeColorTransform::Unknown);
assert_eq!(parsed.as_color_hint(), ColorHint::AdobeUntransformed);
}
#[test]
fn parse_adobe_app14_rejects_too_short() {
let payload = b"Adobe\x00\x64\x00\x00\x00\x00";
assert!(parse_adobe_app14(payload).is_err());
}
#[test]
fn parse_adobe_app14_rejects_bad_identifier() {
let payload = b"Other\x00\x64\x00\x00\x00\x00\x01";
assert!(parse_adobe_app14(payload).is_err());
}
#[test]
fn parse_adobe_app14_rejects_reserved_transform() {
let payload = b"Adobe\x00\x64\x00\x00\x00\x00\x05"; assert!(parse_adobe_app14(payload).is_err());
}
#[test]
fn inspect_adobe_with_reserved_transform_byte_keeps_color_hint() {
let mut adobe = Vec::new();
adobe.extend_from_slice(b"Adobe");
adobe.extend_from_slice(&[0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x05]);
let extras = [(markers::APP14, adobe.as_slice())];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect reserved transform");
assert_eq!(info.color_hint, ColorHint::AdobeUntransformed);
assert!(info.adobe.is_none());
}
#[test]
fn inspect_jfif_and_adobe_both_populated() {
let jfif_payload = &b"JFIF\0\x01\x02\x01\x00\x48\x00\x48\x00\x00"[..];
let mut adobe = Vec::new();
adobe.extend_from_slice(b"Adobe");
adobe.extend_from_slice(&[0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x01]);
let extras = [
(markers::APP0, jfif_payload),
(markers::APP14, adobe.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect dual");
let jfif = info.jfif.expect("typed JFIF");
let adobe_view = info.adobe.expect("typed Adobe");
assert_eq!(jfif.h_density, 72);
assert!(adobe_view.is_standard_version());
assert_eq!(adobe_view.transform, AdobeColorTransform::YCbCr);
assert_eq!(info.color_hint, ColorHint::AdobeYCbCr);
}
#[test]
fn inspect_adobe_first_segment_wins() {
let mut a1 = Vec::new();
a1.extend_from_slice(b"Adobe");
a1.extend_from_slice(&[0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x01]);
let mut a2 = Vec::new();
a2.extend_from_slice(b"Adobe");
a2.extend_from_slice(&[0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x02]);
let extras = [
(markers::APP14, a1.as_slice()),
(markers::APP14, a2.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect dup Adobe");
let adobe = info.adobe.expect("typed Adobe view");
assert_eq!(adobe.transform, AdobeColorTransform::YCbCr);
}
fn icc_payload(seq_no: u8, total: u8, body: &[u8]) -> Vec<u8> {
let mut p = Vec::with_capacity(14 + body.len());
p.extend_from_slice(ICC_PROFILE_MAGIC);
p.push(seq_no);
p.push(total);
p.extend_from_slice(body);
p
}
#[test]
fn parse_icc_profile_app2_minimal() {
let payload = icc_payload(1, 1, &[]);
let chunk = parse_icc_profile_app2(&payload).expect("parse minimal ICC");
assert_eq!(chunk.seq_no, 1);
assert_eq!(chunk.total, 1);
assert_eq!(chunk.profile_bytes.len(), 0);
}
#[test]
fn parse_icc_profile_app2_with_body() {
let body: Vec<u8> = (0..64u8).collect();
let payload = icc_payload(1, 1, &body);
let chunk = parse_icc_profile_app2(&payload).expect("parse body ICC");
assert_eq!(chunk.seq_no, 1);
assert_eq!(chunk.total, 1);
assert_eq!(chunk.profile_bytes, body.as_slice());
}
#[test]
fn parse_icc_profile_app2_rejects_too_short() {
let mut payload = Vec::new();
payload.extend_from_slice(ICC_PROFILE_MAGIC);
payload.push(1);
assert!(parse_icc_profile_app2(&payload).is_err());
}
#[test]
fn parse_icc_profile_app2_rejects_bad_identifier() {
let mut payload = Vec::new();
payload.extend_from_slice(b"OtherProfile"); payload.push(1);
payload.push(1);
assert!(parse_icc_profile_app2(&payload).is_err());
}
#[test]
fn parse_icc_profile_app2_rejects_zero_total() {
let payload = icc_payload(1, 0, &[]);
assert!(parse_icc_profile_app2(&payload).is_err());
}
#[test]
fn parse_icc_profile_app2_rejects_zero_seq_no() {
let payload = icc_payload(0, 1, &[]);
assert!(parse_icc_profile_app2(&payload).is_err());
}
#[test]
fn parse_icc_profile_app2_rejects_seq_no_above_total() {
let payload = icc_payload(3, 2, &[]);
assert!(parse_icc_profile_app2(&payload).is_err());
}
#[test]
fn inspect_single_icc_chunk_populates_summary() {
let body: Vec<u8> = (0..128u8).collect();
let payload = icc_payload(1, 1, &body);
let extras = [(markers::APP2, payload.as_slice())];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 2, 2, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect single ICC");
let icc = info.icc_profile.expect("typed ICC summary");
assert_eq!(icc.total, 1);
assert_eq!(icc.total_payload_len, body.len());
assert_eq!(icc.chunks.len(), 1);
assert_eq!(icc.chunks[0], (1, body.len()));
assert!(icc.is_complete());
assert_eq!(info.color_hint, ColorHint::Unspecified);
}
#[test]
fn inspect_multi_chunk_icc_concatenates_lengths() {
let p1 = icc_payload(1, 3, &[0xAA; 100]);
let p2 = icc_payload(2, 3, &[0xBB; 100]);
let p3 = icc_payload(3, 3, &[0xCC; 56]);
let extras = [
(markers::APP2, p1.as_slice()),
(markers::APP2, p2.as_slice()),
(markers::APP2, p3.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect multi-chunk ICC");
let icc = info.icc_profile.expect("typed ICC summary");
assert_eq!(icc.total, 3);
assert_eq!(icc.total_payload_len, 256);
assert_eq!(icc.chunks, vec![(1u8, 100usize), (2, 100), (3, 56)]);
assert!(icc.is_complete());
}
#[test]
fn inspect_missing_icc_chunk_marks_incomplete() {
let p1 = icc_payload(1, 3, &[0x11; 10]);
let p3 = icc_payload(3, 3, &[0x33; 30]);
let extras = [
(markers::APP2, p1.as_slice()),
(markers::APP2, p3.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect partial ICC");
let icc = info.icc_profile.expect("partial ICC summary");
assert_eq!(icc.total, 3);
assert_eq!(icc.chunks.len(), 2);
assert!(!icc.is_complete());
}
#[test]
fn inspect_duplicate_icc_seq_marks_incomplete() {
let p1 = icc_payload(1, 2, &[0xAA; 10]);
let p1_dup = icc_payload(1, 2, &[0xBB; 10]);
let extras = [
(markers::APP2, p1.as_slice()),
(markers::APP2, p1_dup.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect dup ICC");
let icc = info.icc_profile.expect("typed ICC summary");
assert_eq!(icc.total, 2);
assert_eq!(icc.chunks.len(), 2);
assert!(!icc.is_complete());
}
#[test]
fn inspect_mismatched_icc_totals_drops_second() {
let p1 = icc_payload(1, 2, &[0xAA; 10]);
let p_bad = icc_payload(1, 5, &[0xCC; 20]);
let extras = [
(markers::APP2, p1.as_slice()),
(markers::APP2, p_bad.as_slice()),
];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect mismatched totals");
let icc = info.icc_profile.expect("ICC summary");
assert_eq!(icc.total, 2);
assert_eq!(icc.total_payload_len, 10);
assert_eq!(icc.chunks.len(), 1);
assert_eq!(icc.chunks[0], (1, 10));
assert!(!icc.is_complete()); }
#[test]
fn inspect_app2_without_icc_magic_is_ignored() {
let mut payload = Vec::new();
payload.extend_from_slice(b"FPXR\0extra-bytes-go-here");
let extras = [(markers::APP2, payload.as_slice())];
let buf = build_prefix(
0xC0,
8,
16,
16,
&[(1, 1, 1, 0), (2, 1, 1, 1), (3, 1, 1, 1)],
&extras,
);
let info = inspect_jpeg(&buf).expect("inspect non-ICC APP2");
assert!(info.icc_profile.is_none());
}
#[test]
fn inspect_no_icc_segment_leaves_field_none() {
let buf = build_prefix(0xC0, 8, 8, 8, &[(1, 1, 1, 0)], &[]);
let info = inspect_jpeg(&buf).expect("inspect no-ICC");
assert!(info.icc_profile.is_none());
}
#[test]
fn second_sof_does_not_overwrite() {
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&[0xFF, markers::SOI]);
buf.extend_from_slice(&[0xFF, 0xC0]);
buf.extend_from_slice(&11u16.to_be_bytes());
buf.push(8);
buf.extend_from_slice(&8u16.to_be_bytes());
buf.extend_from_slice(&8u16.to_be_bytes());
buf.push(1);
buf.extend_from_slice(&[1, 0x11, 0]);
buf.extend_from_slice(&[0xFF, 0xC2]);
buf.extend_from_slice(&11u16.to_be_bytes());
buf.push(8);
buf.extend_from_slice(&64u16.to_be_bytes());
buf.extend_from_slice(&64u16.to_be_bytes());
buf.push(1);
buf.extend_from_slice(&[1, 0x11, 0]);
buf.extend_from_slice(&[0xFF, markers::SOS]);
let info = inspect_jpeg(&buf).expect("inspect dup SOF");
assert_eq!(info.sof_kind, SofKind::Baseline);
assert_eq!(info.width, 8);
assert_eq!(info.height, 8);
}
}