mod boxes;
mod color;
mod dequant;
mod dwt;
mod error;
mod geometry;
mod markers;
mod mq;
mod packet;
mod t1;
mod tagtree;
pub use error::{JpxError, Result};
#[derive(Clone, Copy, Debug)]
pub struct DecodeLimits {
pub max_pixels: u64,
pub max_components: u16,
pub max_tiles: u32,
pub max_decoded_bytes: u64,
}
impl Default for DecodeLimits {
fn default() -> Self {
DecodeLimits {
max_pixels: 1 << 27,
max_components: 16,
max_tiles: 65_535,
max_decoded_bytes: 1 << 30,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ColorKind {
Gray,
Rgb,
Cmyk,
IccGuess {
components: u8,
},
Other {
enumeration: u32,
components: u8,
},
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct JpxWarning {
pub message: String,
pub data_loss: bool,
}
impl JpxWarning {
pub(crate) fn note(message: impl Into<String>) -> JpxWarning {
JpxWarning {
message: message.into(),
data_loss: false,
}
}
pub(crate) fn loss(message: impl Into<String>) -> JpxWarning {
JpxWarning {
message: message.into(),
data_loss: true,
}
}
}
#[derive(Clone, Debug)]
pub struct DecodedImage {
pub width: u32,
pub height: u32,
pub components: u8,
pub samples: Vec<u8>,
pub component_depths: Vec<u8>,
pub color: ColorKind,
pub icc_profile: Option<Vec<u8>>,
pub alpha_index: Option<u8>,
pub warnings: Vec<JpxWarning>,
}
pub fn decode(data: &[u8], limits: &DecodeLimits) -> Result<DecodedImage> {
let container = boxes::scan(data, limits)?;
let cs = markers::parse_codestream(container.codestream, limits)?;
let siz = &cs.main.siz;
validate_limits(siz, limits)?;
let mut warnings = container.warnings;
warnings.extend(cs.warnings.iter().cloned());
let (tiles_wide, tiles_high) = geometry::tile_grid(siz)?;
let tile_total = u64::from(tiles_wide) * u64::from(tiles_high);
let mut tiles: Vec<Vec<(usize, &markers::TilePart<'_>)>> =
(0..tile_total).map(|_| Vec::new()).collect();
for (pos, part) in cs.tile_parts.iter().enumerate() {
let index = u64::from(part.sot.tile_index);
if index >= tile_total {
warnings.push(JpxWarning::loss(format!(
"tile-part for out-of-range tile {index} skipped"
)));
continue;
}
tiles[index as usize].push((pos, part));
}
if let Some(warning) = tnsot_compatibility_warning(&tiles) {
warnings.push(JpxWarning::note(warning));
}
let missing_tiles = tiles.iter().filter(|parts| parts.is_empty()).count();
if missing_tiles > 0 {
warnings.push(JpxWarning::loss(format!(
"{missing_tiles} tile(s) have no tile-parts; rendered as background"
)));
}
let ppm_blobs = if cs.main.ppm.is_empty() {
None
} else {
Some(markers::split_packed_headers(
&cs.main.ppm,
cs.tile_parts.len(),
)?)
};
let mut assembler = color::ImageAssembler::new(siz, container.header.as_ref(), limits)?;
let mut image_packets_decoded = false;
let mut short_quant_components = 0u64;
for (tile_index, parts) in tiles.iter().enumerate() {
if parts.is_empty() {
continue;
}
if parts
.windows(2)
.any(|pair| pair[0].1.sot.tile_part_index > pair[1].1.sot.tile_part_index)
{
warnings.push(JpxWarning::note(format!(
"tile {tile_index}: tile-parts out of TPsot order; using appearance order"
)));
}
let part_refs: Vec<&markers::TilePart<'_>> = parts.iter().map(|(_, part)| *part).collect();
let overrides = markers::merge_tile_overrides(&part_refs)?;
let tile_coding = markers::resolve_tile_coding(&cs.main, &overrides)?;
let p = tile_index as u32 % tiles_wide;
let q = tile_index as u32 / tiles_wide;
let tile_rect = geometry::tile_rect(siz, p, q);
if tile_rect.is_empty() {
continue;
}
let mut codings = Vec::with_capacity(siz.components.len());
let mut metadata_cost = 0u64;
for (index, component) in siz.components.iter().enumerate() {
let coding = markers::resolve_component_coding(&cs.main, &overrides, index as u16)?;
metadata_cost = metadata_cost.saturating_add(geometry::partition_metadata_cost(
tile_rect,
component,
&coding.style,
)?);
if coding.quant.short_for(coding.style.decomposition_levels) {
short_quant_components += 1;
}
codings.push(coding);
}
if metadata_cost > limits.max_decoded_bytes {
return Err(JpxError::LimitExceeded {
what: "max_decoded_bytes",
actual: metadata_cost,
limit: limits.max_decoded_bytes,
});
}
let mut components = Vec::with_capacity(siz.components.len());
for (component, coding) in siz.components.iter().zip(codings) {
let geometry = geometry::tile_component_geometry(tile_rect, component, &coding.style)?;
components.push(packet::ComponentContext {
geometry,
coding,
xrsiz: component.xrsiz,
yrsiz: component.yrsiz,
});
}
let mct_wavelet = if components.len() >= 3 {
let wavelet = components[0].coding.style.wavelet;
components[1..3]
.iter()
.all(|component| component.coding.style.wavelet == wavelet)
.then_some(wavelet)
} else {
None
};
let bitstream: Vec<u8> = parts
.iter()
.flat_map(|(_, part)| part.body.iter().copied())
.collect();
let packed_headers = packed_headers_for_tile(parts, &overrides, ppm_blobs.as_deref());
let ctx = packet::TileDecodeContext {
components,
tile_rect,
progression: tile_coding.progression,
layers: tile_coding.layers,
poc: tile_coding.poc.clone(),
sop_markers: tile_coding.sop_markers,
eph_markers: tile_coding.eph_markers,
bitstream: &bitstream,
packed_headers: packed_headers.as_deref(),
};
let mut packets = packet::read_tile_packets(&ctx, limits, image_packets_decoded)?;
image_packets_decoded |= packets.packets_decoded > 0;
warnings.extend(packets.warnings.drain(..).map(|warning| JpxWarning {
message: format!("tile {tile_index}: {}", warning.message),
data_loss: warning.data_loss,
}));
let mut canvases = Vec::with_capacity(ctx.components.len());
for (index, context) in ctx.components.iter().enumerate() {
let component_packets = packets
.components
.get(index)
.ok_or_else(|| JpxError::Malformed("tier-2 produced too few components".into()))?;
let mut bands = Vec::with_capacity(component_packets.bands.len());
for band in &component_packets.bands {
let mut blocks = Vec::with_capacity(band.blocks.len());
for block in &band.blocks {
let coefficients = t1::decode_code_block(block, &bitstream)?;
if coefficients.corrupt {
warnings.push(JpxWarning::loss(format!(
"tile {tile_index} component {index}: corrupt code-block \
[{}, {}) x [{}, {}) kept partially decoded",
block.rect.x0, block.rect.x1, block.rect.y0, block.rect.y1,
)));
}
blocks.push(coefficients);
}
bands.push(t1::BandCoefficients {
kind: band.kind,
level: band.level,
rect: band.rect,
blocks,
});
}
let mut canvas = dequant::dequantize_tile_component(
&context.geometry,
&context.coding,
&siz.components[index],
&bands,
limits,
)?;
dwt::inverse(&mut canvas)?;
canvases.push(canvas);
}
assembler.push_tile(tile_rect, tile_coding.mct, mct_wavelet, canvases)?;
}
if short_quant_components > 0 {
warnings.push(JpxWarning::note(format!(
"{short_quant_components} tile-component(s) signal fewer QCD/QCC sub-band \
entries than their decomposition describes; missing step sizes derived \
from the first entry via Equation (E-5)"
)));
}
assembler.finish(warnings)
}
fn tnsot_compatibility_warning(tiles: &[Vec<(usize, &markers::TilePart<'_>)>]) -> Option<String> {
let affected = tiles
.iter()
.filter(|parts| {
parts.iter().any(|(_, part)| {
let declared = part.sot.tile_part_count;
declared != 0
&& (parts.len() > usize::from(declared) || part.sot.tile_part_index >= declared)
})
})
.count();
if affected == 0 {
return None;
}
Some(format!(
"{affected} tile(s) ship more tile-parts than their declared TNsot \
(violates T.800 A.4.2); tolerated for compatibility with \
real-world encoders"
))
}
fn validate_limits(siz: &markers::Siz, limits: &DecodeLimits) -> Result<()> {
let image = geometry::Rect {
x0: siz.xosiz,
y0: siz.yosiz,
x1: siz.xsiz,
y1: siz.ysiz,
};
let pixels = u64::from(image.width()) * u64::from(image.height());
if pixels > limits.max_pixels {
return Err(JpxError::LimitExceeded {
what: "max_pixels",
actual: pixels,
limit: limits.max_pixels,
});
}
let components = siz.components.len() as u64;
if components > u64::from(limits.max_components) {
return Err(JpxError::LimitExceeded {
what: "max_components",
actual: components,
limit: u64::from(limits.max_components),
});
}
let (tiles_wide, tiles_high) = geometry::tile_grid(siz)?;
let tiles = u64::from(tiles_wide) * u64::from(tiles_high);
if tiles > u64::from(limits.max_tiles) {
return Err(JpxError::LimitExceeded {
what: "max_tiles",
actual: tiles,
limit: u64::from(limits.max_tiles),
});
}
Ok(())
}
fn packed_headers_for_tile(
parts: &[(usize, &markers::TilePart<'_>)],
overrides: &markers::TileOverrides,
ppm_blobs: Option<&[Vec<u8>]>,
) -> Option<Vec<u8>> {
if let Some(blobs) = ppm_blobs {
let mut buffer = Vec::new();
for (pos, _) in parts {
if let Some(blob) = blobs.get(*pos) {
buffer.extend_from_slice(blob);
}
}
return Some(buffer);
}
if overrides.ppt.is_empty() {
return None;
}
Some(
overrides
.ppt
.iter()
.flat_map(|segment| segment.data.iter().copied())
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn example_siz() -> markers::Siz {
markers::Siz {
rsiz: 0,
xsiz: 1432,
ysiz: 954,
xosiz: 152,
yosiz: 234,
xtsiz: 396,
ytsiz: 297,
xtosiz: 0,
ytosiz: 0,
components: vec![
markers::SizComponent {
depth: 8,
signed: false,
xrsiz: 1,
yrsiz: 1,
},
markers::SizComponent {
depth: 8,
signed: false,
xrsiz: 2,
yrsiz: 2,
},
],
}
}
#[test]
fn limits_default_to_the_documented_bounds() {
let limits = DecodeLimits::default();
assert_eq!(limits.max_pixels, 134_217_728); assert_eq!(limits.max_components, 16);
assert_eq!(limits.max_tiles, 65_535);
assert_eq!(limits.max_decoded_bytes, 1_073_741_824); }
#[test]
fn validate_limits_accepts_the_b4_example_under_defaults() {
validate_limits(&example_siz(), &DecodeLimits::default()).unwrap();
}
#[test]
fn validate_limits_measures_the_image_region() {
let limits = DecodeLimits {
max_pixels: 921_599,
..DecodeLimits::default()
};
match validate_limits(&example_siz(), &limits) {
Err(JpxError::LimitExceeded {
what,
actual,
limit,
}) => {
assert_eq!(what, "max_pixels");
assert_eq!(actual, 921_600);
assert_eq!(limit, 921_599);
}
other => panic!("expected max_pixels breach, got {other:?}"),
}
}
#[test]
fn validate_limits_counts_components_and_tiles() {
let limits = DecodeLimits {
max_components: 1,
..DecodeLimits::default()
};
assert!(matches!(
validate_limits(&example_siz(), &limits),
Err(JpxError::LimitExceeded {
what: "max_components",
actual: 2,
..
})
));
let limits = DecodeLimits {
max_tiles: 15,
..DecodeLimits::default()
};
assert!(matches!(
validate_limits(&example_siz(), &limits),
Err(JpxError::LimitExceeded {
what: "max_tiles",
actual: 16,
..
})
));
}
fn part(tile_part_index: u8, tile_part_count: u8) -> markers::TilePart<'static> {
markers::TilePart {
sot: markers::Sot {
tile_index: 0,
tile_part_length: 0,
tile_part_index,
tile_part_count,
},
overrides: markers::TileOverrides::default(),
body: &[],
}
}
fn grouped<'a>(
tiles: &'a [Vec<markers::TilePart<'a>>],
) -> Vec<Vec<(usize, &'a markers::TilePart<'a>)>> {
tiles
.iter()
.map(|parts| parts.iter().enumerate().collect())
.collect()
}
#[test]
fn tnsot_summary_is_silent_when_the_declared_counts_hold() {
let tiles = vec![
vec![part(0, 2), part(1, 2)],
vec![part(0, 0), part(1, 0)],
vec![],
];
assert_eq!(tnsot_compatibility_warning(&grouped(&tiles)), None);
}
#[test]
fn tnsot_summary_counts_affected_tiles_once_per_codestream() {
let tiles = vec![
vec![part(0, 2), part(1, 2), part(2, 2)],
vec![part(5, 2)],
vec![part(0, 1)],
];
assert_eq!(
tnsot_compatibility_warning(&grouped(&tiles)).as_deref(),
Some(
"2 tile(s) ship more tile-parts than their declared TNsot \
(violates T.800 A.4.2); tolerated for compatibility with \
real-world encoders"
)
);
}
}