use crate::error::{JpxError, Result};
use crate::{DecodeLimits, JpxWarning};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ContainerKind {
Jp2,
RawCodestream,
}
fn jp2_signature() -> [u8; 12] {
[0, 0, 0, 12, 106, 80, 32, 32, 13, 10, 135, 10]
}
fn soc_siz_prefix() -> [u8; 4] {
[255, 79, 255, 81]
}
pub(crate) fn sniff(data: &[u8]) -> Result<ContainerKind> {
if data.len() >= 12 && data[..12] == jp2_signature() {
return Ok(ContainerKind::Jp2);
}
if data.len() >= 4 && data[..4] == soc_siz_prefix() {
return Ok(ContainerKind::RawCodestream);
}
Err(JpxError::NotJpeg2000)
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) enum ColorSpec {
Enumerated(u32),
Icc {
profile: Vec<u8>,
},
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) struct Palette {
pub entries: u16,
pub created_channels: u8,
pub channel_depths: Vec<u8>,
pub values: Vec<i32>,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct ComponentMapping {
pub component: u16,
pub mapping_type: u8,
pub palette_column: u8,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct ChannelDefinition {
pub channel: u16,
pub kind: u16,
pub association: u16,
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub(crate) struct Jp2Header {
pub height: u32,
pub width: u32,
pub num_components: u16,
pub bit_depth: u8,
pub component_depths: Vec<u8>,
pub color: ColorSpec,
pub palette: Option<Palette>,
pub component_mapping: Vec<ComponentMapping>,
pub channel_definitions: Vec<ChannelDefinition>,
}
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct Container<'a> {
pub header: Option<Jp2Header>,
pub codestream: &'a [u8],
pub warnings: Vec<JpxWarning>,
}
pub(crate) fn scan<'a>(data: &'a [u8], limits: &DecodeLimits) -> Result<Container<'a>> {
match sniff(data)? {
ContainerKind::RawCodestream => Ok(Container {
header: None,
codestream: data,
warnings: Vec::new(),
}),
ContainerKind::Jp2 => scan_jp2(data, limits),
}
}
const TYPE_FTYP: [u8; 4] = *b"ftyp";
const TYPE_JP2H: [u8; 4] = *b"jp2h";
const TYPE_IHDR: [u8; 4] = *b"ihdr";
const TYPE_BPCC: [u8; 4] = *b"bpcc";
const TYPE_COLR: [u8; 4] = *b"colr";
const TYPE_PCLR: [u8; 4] = *b"pclr";
const TYPE_CMAP: [u8; 4] = *b"cmap";
const TYPE_CDEF: [u8; 4] = *b"cdef";
const TYPE_RES: [u8; 4] = *b"res ";
const TYPE_JP2C: [u8; 4] = *b"jp2c";
const TYPE_RREQ: [u8; 4] = *b"rreq";
const BRAND_JP2: [u8; 4] = *b"jp2 ";
const BRAND_JPX: [u8; 4] = *b"jpx ";
const BRAND_JPXB: [u8; 4] = *b"jpxb";
fn malformed(detail: impl Into<String>) -> JpxError {
JpxError::Malformed(detail.into())
}
fn type_name(kind: [u8; 4]) -> String {
if kind.iter().all(|byte| (32..=126).contains(byte)) {
kind.iter().map(|&byte| byte as char).collect()
} else {
format!("{kind:?}")
}
}
fn be16(bytes: &[u8]) -> u16 {
u16::from_be_bytes([bytes[0], bytes[1]])
}
fn be32(bytes: &[u8]) -> u32 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
struct RawBox<'a> {
kind: [u8; 4],
payload: &'a [u8],
end: usize,
}
fn read_box(data: &[u8], offset: usize) -> Result<RawBox<'_>> {
let rest = &data[offset..];
if rest.len() < 8 {
return Err(malformed(format!(
"box header truncated: {} bytes left, 8 needed",
rest.len()
)));
}
let lbox = be32(rest);
let kind = [rest[4], rest[5], rest[6], rest[7]];
let (header_len, total): (usize, u64) = match lbox {
0 => (8, rest.len() as u64),
1 => {
if rest.len() < 16 {
return Err(malformed(format!(
"box '{}' truncated inside its XLBox field",
type_name(kind)
)));
}
let xlbox = u64::from_be_bytes([
rest[8], rest[9], rest[10], rest[11], rest[12], rest[13], rest[14], rest[15],
]);
if xlbox < 16 {
return Err(malformed(format!(
"box '{}' XLBox = {xlbox} is shorter than its own header",
type_name(kind)
)));
}
(16, xlbox)
}
2..=7 => {
return Err(malformed(format!(
"box '{}' uses reserved length code {lbox}",
type_name(kind)
)));
}
length => (8, u64::from(length)),
};
if total > rest.len() as u64 {
return Err(malformed(format!(
"box '{}' claims {total} bytes but only {} remain",
type_name(kind),
rest.len()
)));
}
let total = total as usize;
Ok(RawBox {
kind,
payload: &rest[header_len..total],
end: offset + total,
})
}
fn jp2c_payload_to_eof(data: &[u8], offset: usize) -> Option<&[u8]> {
let rest = data.get(offset..)?;
if rest.len() < 8 || rest[4..8] != TYPE_JP2C {
return None;
}
let header_len = match be32(rest) {
0 | 2..=7 => return None,
1 => {
if rest.len() < 16 {
return None;
}
let xlbox = u64::from_be_bytes([
rest[8], rest[9], rest[10], rest[11], rest[12], rest[13], rest[14], rest[15],
]);
if xlbox < 16 || xlbox <= rest.len() as u64 {
return None;
}
16
}
length => {
if u64::from(length) <= rest.len() as u64 {
return None;
}
8
}
};
rest.get(header_len..)
}
fn scan_jp2<'a>(data: &'a [u8], limits: &DecodeLimits) -> Result<Container<'a>> {
let mut warnings = Vec::new();
let ftyp = read_box(data, 12)?;
if ftyp.kind != TYPE_FTYP {
return Err(malformed(format!(
"expected the ftyp box after the signature, found '{}'",
type_name(ftyp.kind)
)));
}
check_ftyp(ftyp.payload, &mut warnings)?;
let mut header: Option<Jp2Header> = None;
let mut codestream: Option<&[u8]> = None;
let mut offset = ftyp.end;
while offset < data.len() {
let next = match read_box(data, offset) {
Ok(next) => next,
Err(err) => {
if codestream.is_none() {
if let Some(payload) = jp2c_payload_to_eof(data, offset) {
warnings.push(JpxWarning::loss(
"jp2c box length overruns the file; codestream truncated to EOF",
));
codestream = Some(payload);
break;
}
}
if header.is_some() && codestream.is_some() {
warnings.push(JpxWarning::note(format!(
"trailing garbage after the codestream: {err}"
)));
break;
}
return Err(err);
}
};
match next.kind {
TYPE_JP2H if header.is_none() => {
header = Some(parse_jp2h(next.payload, limits, &mut warnings)?);
}
TYPE_JP2H => {
warnings.push(JpxWarning::note("duplicate jp2h box skipped"));
}
TYPE_JP2C if codestream.is_none() => {
if header.is_none() {
warnings.push(JpxWarning::note("jp2c box appears before the jp2h box"));
}
codestream = Some(next.payload);
}
TYPE_JP2C => {
warnings.push(JpxWarning::note("extra jp2c box ignored"));
}
TYPE_RREQ => {
warnings.push(JpxWarning::note("reader-requirements (rreq) box skipped"));
}
other => {
warnings.push(JpxWarning::note(format!(
"unknown box '{}' skipped",
type_name(other)
)));
}
}
offset = next.end;
}
let header = header.ok_or_else(|| malformed("missing jp2h box"))?;
let codestream = codestream.ok_or_else(|| malformed("missing jp2c box"))?;
Ok(Container {
header: Some(header),
codestream,
warnings,
})
}
fn check_ftyp(payload: &[u8], warnings: &mut Vec<JpxWarning>) -> Result<()> {
if payload.len() < 8 {
return Err(malformed(
"ftyp box too short for its brand and minor version",
));
}
let brand = [payload[0], payload[1], payload[2], payload[3]];
if be32(&payload[4..]) != 0 {
warnings.push(JpxWarning::note("ftyp minor version is not zero"));
}
let (entries, remainder) = payload[8..].as_chunks::<4>();
let mut compatible = false;
let mut jpx_compatible = false;
for entry in entries {
let code = *entry;
compatible = compatible || code == BRAND_JP2;
jpx_compatible = jpx_compatible || code == BRAND_JPX || code == BRAND_JPXB;
}
if !remainder.is_empty() {
warnings.push(JpxWarning::note(format!(
"ftyp compatibility list has {} trailing bytes",
remainder.len()
)));
}
if brand == BRAND_JP2 {
return Ok(());
}
if brand == BRAND_JPX || brand == BRAND_JPXB {
warnings.push(JpxWarning::note(format!(
"JPX brand '{}': reading the JP2-compatible subset",
type_name(brand)
)));
return Ok(());
}
if compatible || jpx_compatible {
warnings.push(JpxWarning::note(format!(
"unknown brand '{}' with a JP2-compatible entry: reading as JP2",
type_name(brand)
)));
return Ok(());
}
Err(malformed(format!(
"ftyp brand '{}' with no JP2-compatible entry",
type_name(brand)
)))
}
fn parse_jp2h(
payload: &[u8],
limits: &DecodeLimits,
warnings: &mut Vec<JpxWarning>,
) -> Result<Jp2Header> {
let ihdr = read_box(payload, 0)?;
if ihdr.kind != TYPE_IHDR {
return Err(malformed(format!(
"jp2h must start with ihdr, found '{}'",
type_name(ihdr.kind)
)));
}
let (height, width, num_components, bit_depth) = parse_ihdr(ihdr.payload, warnings)?;
let mut component_depths: Vec<u8> = Vec::new();
let mut color: Option<ColorSpec> = None;
let mut palette: Option<Palette> = None;
let mut component_mapping: Vec<ComponentMapping> = Vec::new();
let mut channel_definitions: Vec<ChannelDefinition> = Vec::new();
let mut offset = ihdr.end;
while offset < payload.len() {
let next = read_box(payload, offset)?;
match next.kind {
TYPE_BPCC if component_depths.is_empty() => {
component_depths = parse_bpcc(next.payload, num_components)?;
}
TYPE_COLR if color.is_none() => {
color = parse_colr(next.payload, limits, warnings)?;
}
TYPE_COLR => {
}
TYPE_PCLR if palette.is_none() => {
palette = Some(parse_pclr(next.payload)?);
}
TYPE_CMAP if component_mapping.is_empty() => {
component_mapping = parse_cmap(next.payload, num_components, warnings)?;
}
TYPE_CDEF if channel_definitions.is_empty() => {
channel_definitions = parse_cdef(next.payload)?;
}
TYPE_BPCC | TYPE_PCLR | TYPE_CMAP | TYPE_CDEF => {
warnings.push(JpxWarning::note(format!(
"duplicate {} box skipped",
type_name(next.kind)
)));
}
TYPE_IHDR => {
warnings.push(JpxWarning::note("extra ihdr box skipped"));
}
TYPE_RES => {
}
other => {
warnings.push(JpxWarning::note(format!(
"unknown box '{}' inside jp2h skipped",
type_name(other)
)));
}
}
offset = next.end;
}
if bit_depth == 255 && component_depths.is_empty() {
return Err(malformed("ihdr BPC = 255 but no bpcc box present"));
}
if bit_depth != 255 && !component_depths.is_empty() {
warnings.push(JpxWarning::note(
"bpcc box present although ihdr BPC is uniform",
));
component_depths.clear();
}
let color = color.ok_or_else(|| malformed("jp2h has no usable colr box"))?;
if let Some(palette) = &palette {
if component_mapping.is_empty() {
return Err(malformed("pclr box without a cmap box"));
}
for entry in &component_mapping {
if entry.mapping_type == 1
&& u16::from(entry.palette_column) >= u16::from(palette.created_channels)
{
return Err(malformed(format!(
"cmap references palette column {} of {}",
entry.palette_column, palette.created_channels
)));
}
}
} else if !component_mapping.is_empty() {
if component_mapping
.iter()
.any(|entry| entry.mapping_type == 1)
{
return Err(malformed("cmap palette mapping without a pclr box"));
}
warnings.push(JpxWarning::note("cmap box present without a pclr box"));
}
Ok(Jp2Header {
height,
width,
num_components,
bit_depth,
component_depths,
color,
palette,
component_mapping,
channel_definitions,
})
}
fn parse_ihdr(payload: &[u8], warnings: &mut Vec<JpxWarning>) -> Result<(u32, u32, u16, u8)> {
if payload.len() != 14 {
return Err(malformed(format!(
"ihdr payload is {} bytes, must be 14",
payload.len()
)));
}
let height = be32(payload);
let width = be32(&payload[4..]);
let num_components = be16(&payload[8..]);
let bit_depth = payload[10];
let compression = payload[11];
if height == 0 || width == 0 {
return Err(malformed("ihdr image size is zero"));
}
if num_components == 0 || num_components > 16384 {
return Err(malformed(format!(
"ihdr NC = {num_components} outside 1..=16384"
)));
}
if compression != 7 {
return Err(malformed(format!(
"ihdr compression type {compression}, only 7 is defined"
)));
}
check_depth_byte(bit_depth, true, "ihdr BPC")?;
if payload[12] > 1 {
warnings.push(JpxWarning::note(format!(
"ihdr UnkC = {} is reserved",
payload[12]
)));
}
if payload[13] > 1 {
warnings.push(JpxWarning::note(format!(
"ihdr IPR = {} is reserved",
payload[13]
)));
}
Ok((height, width, num_components, bit_depth))
}
fn check_depth_byte(raw: u8, allow_varies: bool, what: &str) -> Result<()> {
if raw == 255 && allow_varies {
return Ok(());
}
if raw & 127 > 37 {
return Err(malformed(format!(
"{what} depth {} exceeds the 38-bit maximum",
(raw & 127) as u32 + 1
)));
}
Ok(())
}
fn parse_bpcc(payload: &[u8], num_components: u16) -> Result<Vec<u8>> {
if payload.len() != usize::from(num_components) {
return Err(malformed(format!(
"bpcc holds {} depth bytes for {num_components} components",
payload.len()
)));
}
for &raw in payload {
check_depth_byte(raw, false, "bpcc component")?;
}
Ok(payload.to_vec())
}
fn parse_colr(
payload: &[u8],
limits: &DecodeLimits,
warnings: &mut Vec<JpxWarning>,
) -> Result<Option<ColorSpec>> {
if payload.len() < 3 {
return Err(malformed("colr box shorter than METH/PREC/APPROX"));
}
match payload[0] {
1 => {
if payload.len() < 7 {
return Err(malformed("colr METH 1 is missing its EnumCS field"));
}
if payload.len() > 7 {
warnings.push(JpxWarning::note(
"colr METH 1 has trailing bytes after EnumCS",
));
}
Ok(Some(ColorSpec::Enumerated(be32(&payload[3..]))))
}
2 => {
let profile = &payload[3..];
if profile.len() as u64 > limits.max_decoded_bytes {
warnings.push(JpxWarning::note(format!(
"colr ICC profile of {} bytes exceeds max_decoded_bytes; \
profile not carried",
profile.len()
)));
return Ok(Some(ColorSpec::Icc {
profile: Vec::new(),
}));
}
Ok(Some(ColorSpec::Icc {
profile: profile.to_vec(),
}))
}
other => {
warnings.push(JpxWarning::note(format!(
"colr METH {other} is reserved; box ignored"
)));
Ok(None)
}
}
}
fn parse_pclr(payload: &[u8]) -> Result<Palette> {
if payload.len() < 3 {
return Err(malformed("pclr box shorter than NE and NPC"));
}
let entries = be16(payload);
let created_channels = payload[2];
if entries == 0 || entries > 1024 {
return Err(malformed(format!("pclr NE = {entries} outside 1..=1024")));
}
if created_channels == 0 {
return Err(malformed("pclr NPC = 0"));
}
let depths_end = 3 + usize::from(created_channels);
let channel_depths = payload
.get(3..depths_end)
.ok_or_else(|| malformed("pclr truncated inside its depth bytes"))?
.to_vec();
let mut widths = Vec::with_capacity(channel_depths.len());
for &raw in &channel_depths {
check_depth_byte(raw, false, "pclr column")?;
let depth = u32::from(raw & 127) + 1;
if depth > 32 {
return Err(JpxError::Unsupported("palette column wider than 32 bits"));
}
widths.push((depth as usize).div_ceil(8));
}
let mut values = Vec::with_capacity(usize::from(entries) * usize::from(created_channels));
let mut offset = depths_end;
for _ in 0..entries {
for (&raw, &width) in channel_depths.iter().zip(&widths) {
let bytes = payload
.get(offset..offset + width)
.ok_or_else(|| malformed("pclr truncated inside its palette values"))?;
offset += width;
let mut value: u64 = 0;
for &byte in bytes {
value = value << 8 | u64::from(byte);
}
let depth = u32::from(raw & 127) + 1;
value &= (1u64 << depth) - 1;
let signed = raw & 128 != 0;
let extended: i64 = if signed && value >> (depth - 1) & 1 == 1 {
value as i64 - (1i64 << depth)
} else {
value as i64
};
let value = i32::try_from(extended).map_err(|_| {
JpxError::Unsupported("palette value outside the 32-bit signed range")
})?;
values.push(value);
}
}
if offset != payload.len() {
return Err(malformed(format!(
"pclr has {} bytes after its last palette value",
payload.len() - offset
)));
}
Ok(Palette {
entries,
created_channels,
channel_depths,
values,
})
}
fn parse_cmap(
payload: &[u8],
num_components: u16,
warnings: &mut Vec<JpxWarning>,
) -> Result<Vec<ComponentMapping>> {
if payload.is_empty() || !payload.len().is_multiple_of(4) {
return Err(malformed(format!(
"cmap payload of {} bytes is not a run of 4-byte entries",
payload.len()
)));
}
let mut entries = Vec::with_capacity(payload.len() / 4);
for chunk in payload.as_chunks::<4>().0 {
let component = be16(chunk);
let mapping_type = chunk[2];
let palette_column = chunk[3];
if component >= num_components {
return Err(malformed(format!(
"cmap references component {component} of {num_components}"
)));
}
if mapping_type > 1 {
return Err(malformed(format!("cmap MTYP {mapping_type} is reserved")));
}
if mapping_type == 0 && palette_column != 0 {
warnings.push(JpxWarning::note(format!(
"cmap direct-use entry carries PCOL = {palette_column}"
)));
}
entries.push(ComponentMapping {
component,
mapping_type,
palette_column,
});
}
Ok(entries)
}
fn parse_cdef(payload: &[u8]) -> Result<Vec<ChannelDefinition>> {
if payload.len() < 2 {
return Err(malformed("cdef box shorter than its channel count"));
}
let count = be16(payload);
if count == 0 {
return Err(malformed("cdef declares zero channel descriptions"));
}
if payload.len() != 2 + usize::from(count) * 6 {
return Err(malformed(format!(
"cdef declares {count} descriptions in a {}-byte payload",
payload.len()
)));
}
Ok(payload[2..]
.as_chunks::<6>()
.0
.iter()
.map(|chunk| ChannelDefinition {
channel: be16(chunk),
kind: be16(&chunk[2..]),
association: be16(&chunk[4..]),
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sniff_recognizes_the_jp2_signature_box() {
let mut data = vec![0, 0, 0, 12, 106, 80, 32, 32, 13, 10, 135, 10];
data.extend([0, 0, 0, 20]); assert_eq!(sniff(&data).unwrap(), ContainerKind::Jp2);
}
#[test]
fn sniff_recognizes_a_raw_codestream() {
let data = [255, 79, 255, 81, 0, 41];
assert_eq!(sniff(&data).unwrap(), ContainerKind::RawCodestream);
}
#[test]
fn sniff_rejects_everything_else() {
for bad in [
b"%PDF-1.7 not an image".as_slice(),
b"".as_slice(),
&[255, 79, 255, 144],
&[0, 0, 0, 12, 106, 80, 32, 32],
] {
assert!(matches!(sniff(bad), Err(JpxError::NotJpeg2000)));
}
}
#[test]
fn scan_propagates_the_sniff_error() {
assert!(matches!(scan(b"junk"), Err(JpxError::NotJpeg2000)));
}
fn scan(data: &[u8]) -> Result<Container<'_>> {
super::scan(data, &DecodeLimits::default())
}
fn fixture(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name);
std::fs::read(&path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()))
}
fn boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + payload.len());
out.extend_from_slice(&u32::to_be_bytes(8 + payload.len() as u32));
out.extend_from_slice(kind);
out.extend_from_slice(payload);
out
}
fn xl_boxed(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(16 + payload.len());
out.extend_from_slice(&u32::to_be_bytes(1));
out.extend_from_slice(kind);
out.extend_from_slice(&u64::to_be_bytes(16 + payload.len() as u64));
out.extend_from_slice(payload);
out
}
fn jp2_ftyp() -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(b"jp2 ");
payload.extend_from_slice(&u32::to_be_bytes(0));
payload.extend_from_slice(b"jp2 ");
boxed(b"ftyp", &payload)
}
fn ihdr_payload(height: u32, width: u32, nc: u16, bpc: u8) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&height.to_be_bytes());
payload.extend_from_slice(&width.to_be_bytes());
payload.extend_from_slice(&nc.to_be_bytes());
payload.push(bpc);
payload.push(7); payload.push(0); payload.push(0); payload
}
fn enumerated_colr(enum_cs: u32) -> Vec<u8> {
let mut payload = vec![1, 0, 0];
payload.extend_from_slice(&enum_cs.to_be_bytes());
payload
}
fn build_jp2(jp2h_children: &[Vec<u8>], codestream: &[u8]) -> Vec<u8> {
let mut file = jp2_signature().to_vec();
file.extend_from_slice(&jp2_ftyp());
let jp2h: Vec<u8> = jp2h_children.iter().flatten().copied().collect();
file.extend_from_slice(&boxed(b"jp2h", &jp2h));
file.extend_from_slice(&boxed(b"jp2c", codestream));
file
}
fn minimal_jp2h() -> Vec<Vec<u8>> {
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
]
}
fn soc_siz() -> Vec<u8> {
soc_siz_prefix().to_vec()
}
#[test]
fn raw_codestream_passes_through_whole_input() {
for name in ["gray-53-raw.j2k", "rgb-97-raw.j2k"] {
let data = fixture(name);
let container = scan(&data).unwrap();
assert!(container.header.is_none(), "{name}");
assert_eq!(container.codestream.len(), data.len(), "{name}");
assert!(container.warnings.is_empty(), "{name}");
}
}
#[test]
fn zoo_fixture_headers_match_the_manifest() {
let expected: &[(&str, u32, u32, u16, u8, u32)] = &[
("gray-53-jp2.jp2", 97, 61, 1, 7, 17),
("gray-97-jp2.jp2", 97, 61, 1, 7, 17),
("gray16-53-jp2.jp2", 80, 50, 1, 15, 17),
("rgb-53-jp2.jp2", 130, 83, 3, 7, 16),
("rgb-97-jp2.jp2", 130, 83, 3, 7, 16),
("rgb-tiled.jp2", 523, 311, 3, 7, 16),
("rgb-layers.jp2", 523, 311, 3, 7, 16),
("rgb-res3.jp2", 130, 83, 3, 7, 16),
("rgb-cb16.jp2", 130, 83, 3, 7, 16),
("rgb-precinct.jp2", 523, 311, 3, 7, 16),
("rgb-prog-lrcp.jp2", 130, 83, 3, 7, 16),
("rgb-prog-rlcp.jp2", 130, 83, 3, 7, 16),
("rgb-prog-rpcl.jp2", 130, 83, 3, 7, 16),
("rgb-prog-pcrl.jp2", 130, 83, 3, 7, 16),
("rgb-prog-cprl.jp2", 130, 83, 3, 7, 16),
("rgba-53-jp2.jp2", 64, 64, 4, 7, 16),
];
for &(name, width, height, nc, bpc, enum_cs) in expected {
let data = fixture(name);
let container = scan(&data).unwrap();
let header = container.header.as_ref().unwrap_or_else(|| {
panic!("{name}: JP2 file without header");
});
assert_eq!(header.width, width, "{name} width");
assert_eq!(header.height, height, "{name} height");
assert_eq!(header.num_components, nc, "{name} nc");
assert_eq!(header.bit_depth, bpc, "{name} bpc");
assert!(header.component_depths.is_empty(), "{name} bpcc");
match header.color {
ColorSpec::Enumerated(value) => {
assert_eq!(value, enum_cs, "{name} EnumCS");
}
ColorSpec::Icc { .. } => panic!("{name}: unexpected ICC colr"),
}
assert!(header.palette.is_none(), "{name} palette");
assert!(header.component_mapping.is_empty(), "{name} cmap");
assert!(
container.codestream.len() >= 4 && container.codestream[..4] == soc_siz_prefix(),
"{name}: codestream does not start with SOC + SIZ",
);
}
}
#[test]
fn rgb_fixture_codestream_is_the_jp2c_payload() {
let data = fixture("rgb-53-jp2.jp2");
assert_eq!(data.len(), 414);
let container = scan(&data).unwrap();
assert_eq!(container.codestream.len(), 329);
assert_eq!(container.codestream, &data[85..414]);
assert!(container.warnings.is_empty());
}
#[test]
fn rgba_fixture_reports_the_cdef_channels() {
let data = fixture("rgba-53-jp2.jp2");
let container = scan(&data).unwrap();
let header = container.header.unwrap();
let expected = [(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 1, 0)];
assert_eq!(header.channel_definitions.len(), expected.len());
for (definition, &(channel, kind, association)) in
header.channel_definitions.iter().zip(&expected)
{
assert_eq!(definition.channel, channel);
assert_eq!(definition.kind, kind);
assert_eq!(definition.association, association);
}
}
#[test]
fn full_feature_container_parses_every_header_box() {
let mut file = jp2_signature().to_vec();
let mut ftyp = Vec::new();
ftyp.extend_from_slice(b"jpx ");
ftyp.extend_from_slice(&u32::to_be_bytes(0));
ftyp.extend_from_slice(b"jpxb");
ftyp.extend_from_slice(b"jp2 ");
file.extend_from_slice(&boxed(b"ftyp", &ftyp));
file.extend_from_slice(&boxed(b"rreq", &[5, 1, 2, 3, 4, 5]));
let mut children: Vec<Vec<u8>> = Vec::new();
children.push(boxed(b"ihdr", &ihdr_payload(5, 3, 2, 255)));
children.push(boxed(b"bpcc", &[7, 137]));
let mut icc = vec![2, 0, 0];
icc.extend_from_slice(&[9; 20]);
children.push(boxed(b"colr", &icc));
children.push(boxed(b"colr", &enumerated_colr(16)));
let mut pclr = Vec::new();
pclr.extend_from_slice(&u16::to_be_bytes(2)); pclr.push(3); pclr.extend_from_slice(&[7, 9, 135]); pclr.extend_from_slice(&[200, 2, 3, 255]); pclr.extend_from_slice(&[1, 3, 255, 127]); children.push(boxed(b"pclr", &pclr));
let mut cmap = Vec::new();
for column in 0..3u8 {
cmap.extend_from_slice(&u16::to_be_bytes(0)); cmap.push(1); cmap.push(column); }
cmap.extend_from_slice(&u16::to_be_bytes(1));
cmap.push(0); cmap.push(0); children.push(boxed(b"cmap", &cmap));
let mut cdef = Vec::new();
cdef.extend_from_slice(&u16::to_be_bytes(1)); cdef.extend_from_slice(&u16::to_be_bytes(3)); cdef.extend_from_slice(&u16::to_be_bytes(1)); cdef.extend_from_slice(&u16::to_be_bytes(0)); children.push(boxed(b"cdef", &cdef));
let resc = boxed(b"resc", &[0, 1, 0, 1, 0, 1, 0, 1, 0, 0]);
children.push(boxed(b"res ", &resc));
children.push(boxed(b"blah", &[1, 2, 3]));
let jp2h: Vec<u8> = children.iter().flatten().copied().collect();
file.extend_from_slice(&boxed(b"jp2h", &jp2h));
let mut codestream = soc_siz();
codestream.extend_from_slice(&[0, 41]);
file.extend_from_slice(&xl_boxed(b"jp2c", &codestream));
let mut trailer = Vec::new();
trailer.extend_from_slice(&u32::to_be_bytes(0));
trailer.extend_from_slice(b"blah");
trailer.extend_from_slice(&[7; 5]);
file.extend_from_slice(&trailer);
let container = scan(&file).unwrap();
assert_eq!(container.codestream, codestream.as_slice());
let header = container.header.unwrap();
assert_eq!(header.height, 5);
assert_eq!(header.width, 3);
assert_eq!(header.num_components, 2);
assert_eq!(header.bit_depth, 255);
assert_eq!(header.component_depths, vec![7, 137]);
match header.color {
ColorSpec::Icc { profile } => assert_eq!(profile, vec![9; 20]),
ColorSpec::Enumerated(value) => panic!("expected ICC, got EnumCS {value}"),
}
let palette = header.palette.unwrap();
assert_eq!(palette.entries, 2);
assert_eq!(palette.created_channels, 3);
assert_eq!(palette.channel_depths, vec![7, 9, 135]);
assert_eq!(palette.values, vec![200, 515, -1, 1, 1023, 127]);
assert_eq!(header.component_mapping.len(), 4);
for (column, entry) in header.component_mapping[..3].iter().enumerate() {
assert_eq!(entry.component, 0);
assert_eq!(entry.mapping_type, 1);
assert_eq!(entry.palette_column, column as u8);
}
assert_eq!(header.component_mapping[3].component, 1);
assert_eq!(header.component_mapping[3].mapping_type, 0);
assert_eq!(header.channel_definitions.len(), 1);
assert_eq!(header.channel_definitions[0].channel, 3);
assert_eq!(header.channel_definitions[0].kind, 1);
assert_eq!(header.channel_definitions[0].association, 0);
assert!(container.warnings.len() >= 3, "{:?}", container.warnings);
}
#[test]
fn palette_fields_are_masked_to_their_declared_depth() {
let mut pclr = Vec::new();
pclr.extend_from_slice(&u16::to_be_bytes(1)); pclr.push(2); pclr.extend_from_slice(&[9, 137]); pclr.extend_from_slice(&[255, 3]); pclr.extend_from_slice(&[3, 255]); let mut cmap = Vec::new();
for column in 0..2u8 {
cmap.extend_from_slice(&u16::to_be_bytes(0));
cmap.push(1);
cmap.push(column);
}
let children = vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"pclr", &pclr),
boxed(b"cmap", &cmap),
];
let file = build_jp2(&children, &soc_siz());
let header = scan(&file).unwrap().header.unwrap();
let palette = header.palette.unwrap();
assert_eq!(palette.values, vec![771, -1]);
}
#[test]
fn oversize_icc_profile_is_dropped_before_allocation() {
let mut colr = vec![2, 0, 0];
colr.extend_from_slice(&[9; 64]);
let children = vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &colr),
];
let file = build_jp2(&children, &soc_siz());
let tight = DecodeLimits {
max_decoded_bytes: 16,
..DecodeLimits::default()
};
let container = super::scan(&file, &tight).unwrap();
match container.header.unwrap().color {
ColorSpec::Icc { profile } => assert!(profile.is_empty()),
ColorSpec::Enumerated(value) => panic!("expected ICC, got EnumCS {value}"),
}
assert!(container
.warnings
.iter()
.any(|w| !w.data_loss && w.message.contains("profile not carried")));
let container = scan(&file).unwrap();
match container.header.unwrap().color {
ColorSpec::Icc { profile } => assert_eq!(profile, vec![9; 64]),
ColorSpec::Enumerated(value) => panic!("expected ICC, got EnumCS {value}"),
}
}
#[test]
fn length_zero_jp2c_extends_to_end_of_file() {
let mut file = jp2_signature().to_vec();
file.extend_from_slice(&jp2_ftyp());
let jp2h: Vec<u8> = minimal_jp2h().iter().flatten().copied().collect();
file.extend_from_slice(&boxed(b"jp2h", &jp2h));
let mut codestream = soc_siz();
codestream.extend_from_slice(&[0, 41, 8, 9, 10]);
file.extend_from_slice(&u32::to_be_bytes(0));
file.extend_from_slice(b"jp2c");
file.extend_from_slice(&codestream);
let container = scan(&file).unwrap();
assert_eq!(container.codestream, codestream.as_slice());
}
#[test]
fn jp2c_before_jp2h_is_tolerated_with_a_warning() {
let mut file = jp2_signature().to_vec();
file.extend_from_slice(&jp2_ftyp());
let codestream = soc_siz();
file.extend_from_slice(&boxed(b"jp2c", &codestream));
let jp2h: Vec<u8> = minimal_jp2h().iter().flatten().copied().collect();
file.extend_from_slice(&boxed(b"jp2h", &jp2h));
let container = scan(&file).unwrap();
assert_eq!(container.codestream, codestream.as_slice());
assert!(container.header.is_some());
assert!(!container.warnings.is_empty());
}
#[test]
fn every_truncated_prefix_errors_cleanly_or_degrades_to_eof() {
let data = fixture("rgb-53-jp2.jp2");
assert_eq!(&data[81..85], b"jp2c");
let payload_start = 85;
for len in 0..data.len() {
let result = scan(&data[..len]);
if len < payload_start {
assert!(result.is_err(), "prefix of {len} bytes unexpectedly parsed");
} else {
let container =
result.unwrap_or_else(|e| panic!("prefix of {len} bytes failed hard: {e}"));
assert!(
container
.warnings
.iter()
.any(|warning| warning.message.contains("truncated to EOF")),
"prefix of {len} bytes lacks the truncation note: {:?}",
container.warnings
);
}
}
let container = scan(&data).unwrap();
assert!(container.warnings.is_empty(), "{:?}", container.warnings);
}
#[test]
fn oversized_and_reserved_box_lengths_error_cleanly() {
let mut oversized = jp2_signature().to_vec();
oversized.extend_from_slice(&jp2_ftyp());
oversized.extend_from_slice(&u32::to_be_bytes(1000));
oversized.extend_from_slice(b"jp2h");
oversized.extend_from_slice(&[0; 16]);
assert!(matches!(scan(&oversized), Err(JpxError::Malformed(_))));
let mut reserved = jp2_signature().to_vec();
reserved.extend_from_slice(&jp2_ftyp());
reserved.extend_from_slice(&u32::to_be_bytes(5));
reserved.extend_from_slice(b"jp2h");
reserved.extend_from_slice(&[0; 16]);
assert!(matches!(scan(&reserved), Err(JpxError::Malformed(_))));
for xlbox in [10u64, u64::MAX] {
let mut bad = jp2_signature().to_vec();
bad.extend_from_slice(&jp2_ftyp());
bad.extend_from_slice(&u32::to_be_bytes(1));
bad.extend_from_slice(b"jp2h");
bad.extend_from_slice(&xlbox.to_be_bytes());
bad.extend_from_slice(&[0; 32]);
assert!(
matches!(scan(&bad), Err(JpxError::Malformed(_))),
"XLBox {xlbox}"
);
}
}
#[test]
fn structural_requirements_are_enforced() {
let mut no_ftyp = jp2_signature().to_vec();
let jp2h: Vec<u8> = minimal_jp2h().iter().flatten().copied().collect();
no_ftyp.extend_from_slice(&boxed(b"jp2h", &jp2h));
no_ftyp.extend_from_slice(&boxed(b"jp2c", &soc_siz()));
assert!(matches!(scan(&no_ftyp), Err(JpxError::Malformed(_))));
let mut alien = jp2_signature().to_vec();
let mut ftyp = Vec::new();
ftyp.extend_from_slice(b"abcd");
ftyp.extend_from_slice(&u32::to_be_bytes(0));
ftyp.extend_from_slice(b"efgh");
alien.extend_from_slice(&boxed(b"ftyp", &ftyp));
alien.extend_from_slice(&boxed(b"jp2h", &jp2h));
alien.extend_from_slice(&boxed(b"jp2c", &soc_siz()));
assert!(matches!(scan(&alien), Err(JpxError::Malformed(_))));
let mut no_header = jp2_signature().to_vec();
no_header.extend_from_slice(&jp2_ftyp());
no_header.extend_from_slice(&boxed(b"jp2c", &soc_siz()));
assert!(matches!(scan(&no_header), Err(JpxError::Malformed(_))));
let mut no_codestream = jp2_signature().to_vec();
no_codestream.extend_from_slice(&jp2_ftyp());
no_codestream.extend_from_slice(&boxed(b"jp2h", &jp2h));
assert!(matches!(scan(&no_codestream), Err(JpxError::Malformed(_))));
}
#[test]
fn jp2h_content_rules_are_enforced() {
let checks: &[(&str, Vec<Vec<u8>>)] = &[
(
"colr before ihdr",
vec![
boxed(b"colr", &enumerated_colr(17)),
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
],
),
(
"short ihdr",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)[..13]),
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"bad compression type",
vec![
{
let mut payload = ihdr_payload(5, 3, 1, 7);
payload[11] = 8;
boxed(b"ihdr", &payload)
},
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"zero width",
vec![
boxed(b"ihdr", &ihdr_payload(5, 0, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"zero components",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 0, 7)),
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"bpc 255 without bpcc",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 2, 255)),
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"bpcc count mismatch",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 2, 255)),
boxed(b"bpcc", &[7]),
boxed(b"colr", &enumerated_colr(17)),
],
),
(
"missing colr",
vec![boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7))],
),
(
"truncated colr",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &[1, 0, 0, 0]),
],
),
(
"pclr without cmap",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"pclr", &[0, 1, 1, 7, 42]),
],
),
(
"cmap palette column out of range",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"pclr", &[0, 1, 1, 7, 42]),
boxed(b"cmap", &[0, 0, 1, 3]),
],
),
(
"cmap component out of range",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"cmap", &[0, 9, 0, 0]),
],
),
(
"palette entry count out of range",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"pclr", &[7, 208, 1, 7, 42]),
boxed(b"cmap", &[0, 0, 1, 0]),
],
),
(
"palette values truncated",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"pclr", &[0, 2, 1, 7, 42]),
boxed(b"cmap", &[0, 0, 1, 0]),
],
),
(
"cdef length mismatch",
vec![
boxed(b"ihdr", &ihdr_payload(5, 3, 1, 7)),
boxed(b"colr", &enumerated_colr(17)),
boxed(b"cdef", &[0, 2, 0, 0, 0, 1, 0, 0]),
],
),
];
for (what, children) in checks {
let file = build_jp2(children, &soc_siz());
assert!(
matches!(scan(&file), Err(JpxError::Malformed(_))),
"{what}: expected Malformed",
);
}
}
}