use crate::boxes::{ChannelDefinition, ColorSpec, Jp2Header, Palette};
use crate::dequant::{CoefficientCanvas, TileComponentCanvas};
use crate::error::{JpxError, Result};
use crate::geometry::Rect;
use crate::markers::{Siz, SizComponent, WaveletKind};
use crate::{ColorKind, DecodeLimits, DecodedImage, JpxWarning};
fn inverse_rct(y0: i64, y1: i64, y2: i64) -> (i64, i64, i64) {
let i1 = y0 - (y2 + y1).div_euclid(4);
(y2 + i1, i1, y1 + i1)
}
fn inverse_rct_f64(y0: f64, y1: f64, y2: f64) -> (f64, f64, f64) {
let i1 = y0 - ((y2 + y1) / 4.0).floor();
(y2 + i1, i1, y1 + i1)
}
fn inverse_ict(y0: f64, y1: f64, y2: f64) -> (f64, f64, f64) {
(
y0 + 1.402 * y2, y0 - 0.34413 * y1 - 0.71414 * y2, y0 + 1.772 * y1, )
}
const SYCC_KR: f64 = 0.299;
const SYCC_KB: f64 = 0.114;
fn usable_depth(depth: u8) -> u32 {
u32::from(depth).clamp(1, 38)
}
fn level_shift_and_clamp(value: i64, depth: u8, signed: bool) -> i64 {
let d = usable_depth(depth);
let half = 1i64 << (d - 1);
if signed {
value.clamp(-half, half - 1)
} else {
(value + half).clamp(0, (1i64 << d) - 1)
}
}
fn normalize_to_u8(value: i64, depth: u8, signed: bool) -> u8 {
let d = usable_depth(depth);
let half = 1i64 << (d - 1);
let max = (1i64 << d) - 1;
let v = if signed { value + half } else { value }.clamp(0, max);
if d > 8 {
let shift = d - 8;
((v + (1i64 << (shift - 1))) >> shift).min(255) as u8
} else if d < 8 {
((v * 255 + max / 2) / max) as u8
} else {
v as u8
}
}
fn clamp_round_u8(value: f64) -> u8 {
value.round().clamp(0.0, 255.0) as u8
}
#[derive(Clone, Copy, Debug)]
enum ChannelSource {
Direct {
component: usize,
},
Palette {
component: usize,
column: usize,
},
Zero,
}
#[derive(Clone, Copy, Debug)]
struct Channel {
source: ChannelSource,
depth: u8,
signed: bool,
}
fn identity_channels(components: &[SizComponent]) -> Vec<Channel> {
components
.iter()
.enumerate()
.map(|(component, spec)| Channel {
source: ChannelSource::Direct { component },
depth: spec.depth,
signed: spec.signed,
})
.collect()
}
fn build_channels(
siz: &Siz,
header: Option<&Jp2Header>,
warnings: &mut Vec<JpxWarning>,
) -> (Vec<Channel>, Option<Palette>) {
let Some(header) = header else {
return (identity_channels(&siz.components), None);
};
if header.component_mapping.is_empty() {
if header.palette.is_some() {
warnings.push(JpxWarning::note(
"pclr without a cmap box ignored (I.5.3.4 requires both)",
));
}
return (identity_channels(&siz.components), None);
}
let palette = header.palette.as_ref().and_then(|palette| {
let columns = usize::from(palette.created_channels);
let consistent = (1..=1024).contains(&palette.entries)
&& columns >= 1
&& palette.channel_depths.len() == columns
&& palette.values.len() == usize::from(palette.entries) * columns;
if consistent {
Some(palette.clone())
} else {
warnings.push(JpxWarning::note(
"pclr box internally inconsistent (Table I.12); palette ignored",
));
None
}
});
let zero_channel = Channel {
source: ChannelSource::Zero,
depth: 8,
signed: false,
};
let mut channels = Vec::with_capacity(header.component_mapping.len());
for (index, mapping) in header.component_mapping.iter().enumerate() {
let component = usize::from(mapping.component);
let Some(spec) = siz.components.get(component) else {
warnings.push(JpxWarning::loss(format!(
"cmap channel {index}: codestream component {component} does not exist; \
channel zero-filled"
)));
channels.push(zero_channel);
continue;
};
let direct = Channel {
source: ChannelSource::Direct { component },
depth: spec.depth,
signed: spec.signed,
};
match (mapping.mapping_type, &palette) {
(0, _) => channels.push(direct),
(1, Some(palette)) => {
let column = usize::from(mapping.palette_column);
match palette.channel_depths.get(column) {
Some(&raw) => channels.push(Channel {
source: ChannelSource::Palette { component, column },
depth: (raw & 127) + 1,
signed: raw & 128 != 0,
}),
None => {
warnings.push(JpxWarning::loss(format!(
"cmap channel {index}: palette column {column} out of range; \
channel zero-filled"
)));
channels.push(zero_channel);
}
}
}
(1, None) => {
warnings.push(JpxWarning::loss(format!(
"cmap channel {index}: palette mapping without a usable pclr box; \
component {component} used directly"
)));
channels.push(direct);
}
(other, _) => {
warnings.push(JpxWarning::note(format!(
"cmap channel {index}: reserved MTYP {other} (Table I.14); \
component {component} used directly"
)));
channels.push(direct);
}
}
}
(channels, palette)
}
fn palette_value(palette: &Palette, index: i64, column: usize) -> i64 {
let last = i64::from(palette.entries).max(1) - 1;
let entry = index.clamp(0, last) as usize;
palette
.values
.get(entry * usize::from(palette.created_channels) + column)
.copied()
.map(i64::from)
.unwrap_or(0)
}
enum WorkCanvas {
Int(Vec<i64>),
Float(Vec<f64>),
}
fn apply_inverse_mct(
work: &mut [(Rect, WorkCanvas)],
wavelet: Option<WaveletKind>,
warnings: &mut Vec<JpxWarning>,
) {
let [first, second, third, ..] = work else {
warnings.push(JpxWarning::loss(
"MCT signalled with fewer than three components; transform skipped (G.2/G.3)",
));
return;
};
if first.0 != second.0 || first.0 != third.0 {
warnings.push(JpxWarning::loss(
"MCT components disagree on their tile-component rects \
(G.2/G.3 demand identical separations); transform skipped",
));
return;
}
let Some(wavelet) = wavelet else {
warnings.push(JpxWarning::loss(
"MCT components disagree on their wavelet filter, so Table A.17 \
pairs no transform with them (G.2.1/G.3.1); transform skipped",
));
return;
};
match (wavelet, &mut first.1, &mut second.1, &mut third.1) {
(WaveletKind::Reversible53, WorkCanvas::Int(a), WorkCanvas::Int(b), WorkCanvas::Int(c)) => {
for ((y0, y1), y2) in a.iter_mut().zip(b.iter_mut()).zip(c.iter_mut()) {
let (i0, i1, i2) = inverse_rct(*y0, *y1, *y2);
*y0 = i0;
*y1 = i1;
*y2 = i2;
}
}
(
WaveletKind::Reversible53,
WorkCanvas::Float(a),
WorkCanvas::Float(b),
WorkCanvas::Float(c),
) => {
for ((y0, y1), y2) in a.iter_mut().zip(b.iter_mut()).zip(c.iter_mut()) {
let (i0, i1, i2) = inverse_rct_f64(*y0, *y1, *y2);
*y0 = i0;
*y1 = i1;
*y2 = i2;
}
}
(
WaveletKind::Irreversible97,
WorkCanvas::Float(a),
WorkCanvas::Float(b),
WorkCanvas::Float(c),
) => {
for ((y0, y1), y2) in a.iter_mut().zip(b.iter_mut()).zip(c.iter_mut()) {
let (i0, i1, i2) = inverse_ict(*y0, *y1, *y2);
*y0 = i0;
*y1 = i1;
*y2 = i2;
}
}
_ => warnings.push(JpxWarning::loss(
"MCT components mix reversible and irreversible canvases; transform skipped \
(Table A.17 pairs the RCT with the 5-3 filter and the ICT with the 9-7)",
)),
}
}
pub(crate) struct ImageAssembler {
region: Rect,
components: Vec<SizComponent>,
channels: Vec<Channel>,
palette: Option<Palette>,
color: Option<ColorSpec>,
channel_definitions: Vec<ChannelDefinition>,
buffer: Vec<u8>,
warnings: Vec<JpxWarning>,
}
impl ImageAssembler {
pub(crate) fn new(
siz: &Siz,
header: Option<&Jp2Header>,
limits: &DecodeLimits,
) -> Result<ImageAssembler> {
let region = Rect {
x0: siz.xosiz,
y0: siz.yosiz,
x1: siz.xsiz,
y1: siz.ysiz,
};
let mut warnings = Vec::new();
if let Some(header) = header {
if (header.width, header.height) != (region.width(), region.height()) {
warnings.push(JpxWarning::note(format!(
"ihdr claims {}x{} but the SIZ image region is {}x{}; SIZ wins",
header.width,
header.height,
region.width(),
region.height()
)));
}
if usize::from(header.num_components) != siz.components.len() {
warnings.push(JpxWarning::note(format!(
"ihdr NC = {} but SIZ Csiz = {}; SIZ wins",
header.num_components,
siz.components.len()
)));
}
let siz_raw = |spec: &SizComponent| {
(spec.depth.saturating_sub(1) & 127) | if spec.signed { 128 } else { 0 }
};
let mismatch = if header.bit_depth == 255 {
header.component_depths.len() == siz.components.len()
&& header
.component_depths
.iter()
.zip(&siz.components)
.any(|(&raw, spec)| raw != siz_raw(spec))
} else {
siz.components
.iter()
.any(|spec| siz_raw(spec) != header.bit_depth)
};
if mismatch {
warnings.push(JpxWarning::note(
"ihdr/bpcc bit depth disagrees with SIZ Ssiz; SIZ wins",
));
}
}
let (channels, palette) = build_channels(siz, header, &mut warnings);
if channels.is_empty() {
return Err(JpxError::Malformed(
"no output channels (empty Csiz and cmap)".into(),
));
}
if channels.len() > usize::from(u8::MAX) {
return Err(JpxError::Malformed(format!(
"{} output channels exceed the representable 255",
channels.len()
)));
}
let bytes = u64::from(region.width()) * u64::from(region.height()) * channels.len() as u64;
if bytes > limits.max_decoded_bytes {
return Err(JpxError::LimitExceeded {
what: "max_decoded_bytes",
actual: bytes,
limit: limits.max_decoded_bytes,
});
}
let len = usize::try_from(bytes).map_err(|convert_error| {
JpxError::Malformed(format!(
"output buffer exceeds the address space: {convert_error}"
))
})?;
Ok(ImageAssembler {
region,
components: siz.components.clone(),
channels,
palette,
color: header.map(|header| header.color.clone()),
channel_definitions: header
.map(|header| header.channel_definitions.clone())
.unwrap_or_default(),
buffer: vec![0; len],
warnings,
})
}
pub(crate) fn push_tile(
&mut self,
tile: Rect,
mct: u8,
mct_wavelet: Option<WaveletKind>,
canvases: Vec<TileComponentCanvas>,
) -> Result<()> {
let clipped = Rect {
x0: tile.x0.max(self.region.x0),
y0: tile.y0.max(self.region.y0),
x1: tile.x1.min(self.region.x1),
y1: tile.y1.min(self.region.y1),
};
if clipped.is_empty() {
return Ok(());
}
if canvases.len() != self.components.len() {
self.warnings.push(JpxWarning::loss(format!(
"tile at ({}, {}): {} component canvases for {} components; tile skipped",
tile.x0,
tile.y0,
canvases.len(),
self.components.len()
)));
return Ok(());
}
let mut work: Vec<(Rect, WorkCanvas)> = Vec::with_capacity(canvases.len());
for (index, canvas) in canvases.into_iter().enumerate() {
let expected = u64::from(canvas.rect.width()) * u64::from(canvas.rect.height());
let actual = match &canvas.samples {
CoefficientCanvas::Reversible(values) => values.len(),
CoefficientCanvas::Irreversible(values) => values.len(),
} as u64;
if actual != expected {
self.warnings.push(JpxWarning::loss(format!(
"tile at ({}, {}): component {index} canvas holds {actual} samples \
for a {}x{} rect; component zero-filled",
tile.x0,
tile.y0,
canvas.rect.width(),
canvas.rect.height()
)));
work.push((
Rect {
x0: 0,
y0: 0,
x1: 0,
y1: 0,
},
WorkCanvas::Int(Vec::new()),
));
continue;
}
let widened = match canvas.samples {
CoefficientCanvas::Reversible(values) => {
WorkCanvas::Int(values.into_iter().map(i64::from).collect())
}
CoefficientCanvas::Irreversible(values) => {
WorkCanvas::Float(values.into_iter().map(f64::from).collect())
}
};
work.push((canvas.rect, widened));
}
match mct {
0 => {}
1 => apply_inverse_mct(&mut work, mct_wavelet, &mut self.warnings),
other => self.warnings.push(JpxWarning::loss(format!(
"reserved SGcod MCT value {other} ignored (Table A.17)"
))),
}
let planes: Vec<(Rect, Vec<i64>)> = work
.into_iter()
.zip(&self.components)
.map(|((rect, canvas), spec)| {
let ints: Vec<i64> = match canvas {
WorkCanvas::Int(values) => values,
WorkCanvas::Float(values) => {
values.into_iter().map(|v| v.round() as i64).collect()
}
};
let shifted = ints
.into_iter()
.map(|v| level_shift_and_clamp(v, spec.depth, spec.signed))
.collect();
(rect, shifted)
})
.collect();
for index in 0..self.channels.len() {
let channel = self.channels[index];
let (component, bytes) = match channel.source {
ChannelSource::Zero => continue,
ChannelSource::Direct { component } => {
let (_, plane) = &planes[component];
let bytes: Vec<u8> = plane
.iter()
.map(|&v| normalize_to_u8(v, channel.depth, channel.signed))
.collect();
(component, bytes)
}
ChannelSource::Palette { component, column } => {
let Some(palette) = &self.palette else {
continue;
};
let (_, plane) = &planes[component];
let bytes: Vec<u8> = plane
.iter()
.map(|&v| {
normalize_to_u8(
palette_value(palette, v, column),
channel.depth,
channel.signed,
)
})
.collect();
(component, bytes)
}
};
let src_rect = planes[component].0;
let spec = self.components[component];
let separation = (u32::from(spec.xrsiz.max(1)), u32::from(spec.yrsiz.max(1)));
self.blit_replicated(index, clipped, src_rect, &bytes, separation);
}
Ok(())
}
fn blit_replicated(
&mut self,
channel: usize,
dst: Rect,
src_rect: Rect,
src: &[u8],
separation: (u32, u32),
) {
if src_rect.is_empty() {
return;
}
let stride = self.channels.len();
let width = self.region.width() as usize;
let src_width = src_rect.width() as usize;
for y in dst.y0..dst.y1 {
let v = (y / separation.1).clamp(src_rect.y0, src_rect.y1 - 1);
let src_row = (v - src_rect.y0) as usize * src_width;
let dst_row = (y - self.region.y0) as usize * width;
for x in dst.x0..dst.x1 {
let u = (x / separation.0).clamp(src_rect.x0, src_rect.x1 - 1);
let sample = src
.get(src_row + (u - src_rect.x0) as usize)
.copied()
.unwrap_or(0);
let index = (dst_row + (x - self.region.x0) as usize) * stride + channel;
if let Some(slot) = self.buffer.get_mut(index) {
*slot = sample;
}
}
}
}
fn colour_channel_count(&self) -> u8 {
let mut opacity: Vec<u16> = self
.channel_definitions
.iter()
.filter(|def| def.kind == 1 || def.kind == 2)
.map(|def| def.channel)
.filter(|&channel| usize::from(channel) < self.channels.len())
.collect();
opacity.sort_unstable();
opacity.dedup();
(self.channels.len() - opacity.len()) as u8
}
fn resolve_alpha(&mut self) -> Option<u8> {
for index in 0..self.channel_definitions.len() {
let def = self.channel_definitions[index];
if (def.kind != 1 && def.kind != 2) || def.association != 0 {
continue;
}
if usize::from(def.channel) >= self.channels.len() {
self.warnings.push(JpxWarning::note(format!(
"cdef opacity channel {} does not exist; ignored",
def.channel
)));
continue;
}
if def.kind == 2 {
self.warnings.push(JpxWarning::note(
"cdef declares premultiplied opacity (Typ 2); samples are left premultiplied",
));
}
return Some(def.channel as u8);
}
None
}
fn take_icc_profile(&mut self) -> Option<Vec<u8>> {
match &mut self.color {
Some(ColorSpec::Icc { profile }) if !profile.is_empty() => {
Some(std::mem::take(profile))
}
_ => None,
}
}
fn resolve_color(&mut self, icc: Option<&[u8]>) -> ColorKind {
let colour = self.colour_channel_count();
match &self.color {
None => match self.channels.len() {
1 => ColorKind::Gray,
3 => ColorKind::Rgb,
4 => ColorKind::Cmyk,
count => {
self.warnings.push(JpxWarning::note(format!(
"raw codestream with {count} components has no colour interpretation"
)));
ColorKind::Other {
enumeration: 0,
components: colour,
}
}
},
Some(ColorSpec::Enumerated(16)) => ColorKind::Rgb,
Some(ColorSpec::Enumerated(17)) => ColorKind::Gray,
Some(ColorSpec::Enumerated(18)) => {
if self.channels.len() >= 3 {
self.convert_sycc_to_rgb();
ColorKind::Rgb
} else {
self.warnings.push(JpxWarning::note(
"colr declares sYCC but fewer than three channels exist; left unconverted",
));
ColorKind::Other {
enumeration: 18,
components: colour,
}
}
}
Some(ColorSpec::Enumerated(enumeration)) => {
let enumeration = *enumeration;
self.warnings.push(JpxWarning::note(format!(
"colr enumeration {enumeration} is not converted (Table I.10 defines 16/17/18)"
)));
ColorKind::Other {
enumeration,
components: colour,
}
}
Some(ColorSpec::Icc { .. }) => {
let message = match icc {
Some(profile) => format!(
"colr carries a restricted ICC profile ({} bytes), exported \
for the consumer to apply; colour reported as a guess from \
{colour} colour channels",
profile.len()
),
None => format!(
"colr declares a restricted ICC profile the scan did not \
carry; colour guessed from {colour} colour channels"
),
};
self.warnings.push(JpxWarning::note(message));
ColorKind::IccGuess { components: colour }
}
}
}
fn convert_sycc_to_rgb(&mut self) {
let deep = self
.channels
.iter()
.take(3)
.map(|channel| usable_depth(channel.depth))
.max()
.unwrap_or(8);
let message = if deep > 8 {
format!(
"sYCC converted to RGB per IEC 61966-2-1 Amd. 1, after the \
{deep}-bit source rounded to 8 bits"
)
} else {
"sYCC converted to RGB per IEC 61966-2-1 Amd. 1".to_string()
};
self.warnings.push(JpxWarning::note(message));
let kg = 1.0 - SYCC_KR - SYCC_KB;
let cr_r = 2.0 - 2.0 * SYCC_KR;
let cb_b = 2.0 - 2.0 * SYCC_KB;
let cb_g = SYCC_KB * cb_b / kg;
let cr_g = SYCC_KR * cr_r / kg;
let stride = self.channels.len();
for pixel in self.buffer.chunks_mut(stride) {
let y = f64::from(pixel[0]);
let cb = f64::from(pixel[1]) - 128.0;
let cr = f64::from(pixel[2]) - 128.0;
pixel[0] = clamp_round_u8(y + cr_r * cr);
pixel[1] = clamp_round_u8(y - cb_g * cb - cr_g * cr);
pixel[2] = clamp_round_u8(y + cb_b * cb);
}
}
pub(crate) fn finish(mut self, warnings: Vec<JpxWarning>) -> Result<DecodedImage> {
let mut all = warnings;
let icc_profile = self.take_icc_profile();
let color = self.resolve_color(icc_profile.as_deref());
let alpha_index = self.resolve_alpha();
all.append(&mut self.warnings);
Ok(DecodedImage {
width: self.region.width(),
height: self.region.height(),
components: self.channels.len() as u8,
samples: self.buffer,
component_depths: self.channels.iter().map(|channel| channel.depth).collect(),
color,
icc_profile,
alpha_index,
warnings: all,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boxes::{ChannelDefinition, ColorSpec, ComponentMapping, Palette};
use crate::dequant::CoefficientCanvas;
use crate::markers::SizComponent;
use crate::ColorKind;
fn component(depth: u8, signed: bool, xrsiz: u8, yrsiz: u8) -> SizComponent {
SizComponent {
depth,
signed,
xrsiz,
yrsiz,
}
}
fn siz_for(
xsiz: u32,
ysiz: u32,
xosiz: u32,
yosiz: u32,
xtsiz: u32,
ytsiz: u32,
components: Vec<SizComponent>,
) -> Siz {
Siz {
rsiz: 0,
xsiz,
ysiz,
xosiz,
yosiz,
xtsiz,
ytsiz,
xtosiz: 0,
ytosiz: 0,
components,
}
}
fn rect(x0: u32, y0: u32, x1: u32, y1: u32) -> Rect {
Rect { x0, y0, x1, y1 }
}
fn reversible(rect: Rect, values: &[i32]) -> TileComponentCanvas {
TileComponentCanvas {
rect,
levels: 0,
samples: CoefficientCanvas::Reversible(values.to_vec()),
}
}
fn irreversible(rect: Rect, values: &[f32]) -> TileComponentCanvas {
TileComponentCanvas {
rect,
levels: 0,
samples: CoefficientCanvas::Irreversible(values.to_vec()),
}
}
fn plain_header(color: ColorSpec, num_components: u16, width: u32, height: u32) -> Jp2Header {
Jp2Header {
height,
width,
num_components,
bit_depth: 7,
component_depths: Vec::new(),
color,
palette: None,
component_mapping: Vec::new(),
channel_definitions: Vec::new(),
}
}
fn forward_rct(i0: i64, i1: i64, i2: i64) -> (i64, i64, i64) {
((i0 + 2 * i1 + i2).div_euclid(4), i2 - i1, i0 - i1)
}
#[test]
fn inverse_rct_reproduces_the_g6_g8_hand_vectors() {
assert_eq!(inverse_rct(56, -25, 50), (100, 50, 25));
assert_eq!(inverse_rct(-23, -80, -130), (-100, 30, -50));
assert_eq!(inverse_rct(0, 1, 0), (0, 0, 1));
}
#[test]
fn inverse_rct_round_trips_the_forward_transform() {
for i0 in [-128i64, -77, -1, 0, 1, 89, 127] {
for i1 in [-128i64, -3, 0, 42, 127] {
for i2 in [-128i64, -90, 0, 5, 127] {
let (y0, y1, y2) = forward_rct(i0, i1, i2);
assert_eq!(
inverse_rct(y0, y1, y2),
(i0, i1, i2),
"round trip of ({i0}, {i1}, {i2})"
);
}
}
}
}
fn forward_ict(i0: f64, i1: f64, i2: f64) -> (f64, f64, f64) {
(
0.299 * i0 + 0.587 * i1 + 0.114 * i2, -0.16875 * i0 - 0.331260 * i1 + 0.5 * i2, 0.5 * i0 - 0.41869 * i1 - 0.08131 * i2, )
}
#[test]
fn inverse_ict_reproduces_the_g12_g14_hand_vectors() {
let (i0, i1, i2) = inverse_ict(100.0, -20.0, 30.0);
assert!((i0 - 142.060000).abs() < 1e-6, "I0 = {i0}");
assert!((i1 - 85.458400).abs() < 1e-6, "I1 = {i1}");
assert!((i2 - 64.560000).abs() < 1e-6, "I2 = {i2}");
let (i0, i1, i2) = inverse_ict(50.5, 10.25, -5.75);
assert!((i0 - 42.438500).abs() < 1e-6, "I0 = {i0}");
assert!((i1 - 51.0789725).abs() < 1e-6, "I1 = {i1}");
assert!((i2 - 68.663000).abs() < 1e-6, "I2 = {i2}");
}
#[test]
fn inverse_ict_round_trips_within_half_a_code_value() {
for r in [0.0f64, 1.0, 63.0, 127.0, 128.0, 254.0, 255.0] {
for g in [0.0f64, 50.0, 128.0, 255.0] {
for b in [0.0f64, 99.0, 200.0, 255.0] {
let (y0, y1, y2) = forward_ict(r, g, b);
let (i0, i1, i2) = inverse_ict(y0, y1, y2);
assert!((i0 - r).abs() <= 0.51, "R {r} came back as {i0}");
assert!((i1 - g).abs() <= 0.51, "G {g} came back as {i1}");
assert!((i2 - b).abs() <= 0.51, "B {b} came back as {i2}");
}
}
}
}
#[test]
fn dc_level_shift_recentres_unsigned_components() {
assert_eq!(level_shift_and_clamp(-128, 8, false), 0);
assert_eq!(level_shift_and_clamp(127, 8, false), 255);
assert_eq!(level_shift_and_clamp(200, 8, false), 255);
assert_eq!(level_shift_and_clamp(-300, 8, false), 0);
assert_eq!(level_shift_and_clamp(127, 8, true), 127);
assert_eq!(level_shift_and_clamp(-200, 8, true), -128);
assert_eq!(level_shift_and_clamp(-1, 1, false), 0);
assert_eq!(level_shift_and_clamp(0, 1, false), 1);
assert_eq!(level_shift_and_clamp(-2048, 12, false), 0);
assert_eq!(level_shift_and_clamp(2047, 12, false), 4095);
assert_eq!(level_shift_and_clamp(-6716, 16, false), 26052);
}
#[test]
fn normalization_hand_cases_for_depths_1_4_8_12_16() {
assert_eq!(normalize_to_u8(0, 8, false), 0);
assert_eq!(normalize_to_u8(255, 8, false), 255);
assert_eq!(normalize_to_u8(-128, 8, true), 0);
assert_eq!(normalize_to_u8(127, 8, true), 255);
assert_eq!(normalize_to_u8(4095, 12, false), 255);
assert_eq!(normalize_to_u8(2048, 12, false), 128);
assert_eq!(normalize_to_u8(100, 12, false), 6);
assert_eq!(normalize_to_u8(-2048, 12, true), 0);
assert_eq!(normalize_to_u8(100, 12, true), 134);
assert_eq!(normalize_to_u8(65535, 16, false), 255);
assert_eq!(normalize_to_u8(26052, 16, false), 102);
assert_eq!(normalize_to_u8(258, 16, false), 1);
assert_eq!(normalize_to_u8(25800, 16, false), 101);
for depth in 9..=32u8 {
let mid = 1i64 << (depth - 1);
assert_eq!(normalize_to_u8(mid, depth, false), 128, "depth {depth}");
}
assert_eq!(normalize_to_u8(0, 16, true), 128);
assert_eq!(normalize_to_u8(-32768, 16, true), 0);
assert_eq!(normalize_to_u8(0, 1, false), 0);
assert_eq!(normalize_to_u8(1, 1, false), 255);
assert_eq!(normalize_to_u8(-1, 1, true), 0);
assert_eq!(normalize_to_u8(0, 1, true), 255);
assert_eq!(normalize_to_u8(15, 4, false), 255);
assert_eq!(normalize_to_u8(7, 4, false), 119);
assert_eq!(normalize_to_u8(1, 4, false), 17);
assert_eq!(normalize_to_u8(3, 4, true), 187);
}
#[test]
fn palette_expansion_maps_indices_through_each_cmap_entry() {
let siz = siz_for(5, 1, 0, 0, 5, 1, vec![component(8, false, 1, 1)]);
let mut header = plain_header(ColorSpec::Enumerated(16), 1, 5, 1);
header.palette = Some(Palette {
entries: 4,
created_channels: 2,
channel_depths: vec![7, 3],
values: vec![10, 1, 20, 5, 30, 15, 255, 0],
});
header.component_mapping = vec![
ComponentMapping {
component: 0,
mapping_type: 1,
palette_column: 0,
},
ComponentMapping {
component: 0,
mapping_type: 1,
palette_column: 1,
},
];
let mut assembler =
ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 5, 1),
0,
None,
vec![reversible(
rect(0, 0, 5, 1),
&[-128, -127, -126, -125, -119],
)],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.components, 2);
assert_eq!(image.component_depths, vec![8, 4]);
assert_eq!(image.samples, vec![10, 17, 20, 85, 30, 255, 255, 0, 255, 0]);
}
#[test]
fn tiles_compose_at_their_image_region_offsets() {
let siz = siz_for(8, 5, 2, 1, 4, 5, vec![component(8, false, 1, 1)]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
let values: Vec<i32> = (0..16).map(|k| k - 28).collect();
assembler
.push_tile(
rect(4, 1, 8, 5),
0,
None,
vec![reversible(rect(4, 1, 8, 5), &values)],
)
.unwrap();
assembler
.push_tile(
rect(2, 1, 4, 5),
0,
None,
vec![reversible(rect(2, 1, 4, 5), &[-121; 8])],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!((image.width, image.height), (6, 4));
assert_eq!(image.samples[2], 100);
assert_eq!(image.samples[3], 101);
assert_eq!(image.samples[4], 102);
assert_eq!(image.samples[5], 103);
assert_eq!(image.samples[8], 104);
assert_eq!(image.samples[0], 7);
assert_eq!(image.samples[1], 7);
assert_eq!(image.samples[6], 7);
assert_eq!(image.samples[7], 7);
let expected: Vec<u8> = (0..4u8)
.flat_map(|row| {
let mut r = vec![7u8, 7];
r.extend((0..4u8).map(|col| 100 + 4 * row + col));
r
})
.collect();
assert_eq!(image.samples, expected);
}
#[test]
fn missing_tiles_leave_zeros() {
let siz = siz_for(8, 5, 2, 1, 4, 5, vec![component(8, false, 1, 1)]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(2, 1, 4, 5),
0,
None,
vec![reversible(rect(2, 1, 4, 5), &[-121; 8])],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.samples[2], 0);
assert_eq!(image.samples[5], 0);
assert_eq!(image.samples[0], 7);
}
#[test]
fn subsampled_components_replicate_onto_the_reference_grid() {
let siz = siz_for(4, 2, 0, 0, 4, 2, vec![component(8, false, 2, 1)]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 4, 2),
0,
None,
vec![reversible(rect(0, 0, 2, 2), &[-118, -108, -98, -88])],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!((image.width, image.height), (4, 2));
assert_eq!(image.samples, vec![10, 10, 20, 20, 30, 30, 40, 40]);
}
#[test]
fn mct_1_with_reversible_canvases_undoes_the_rct() {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 3]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 1, 1),
1,
Some(WaveletKind::Reversible53),
vec![
reversible(rect(0, 0, 1, 1), &[-72]),
reversible(rect(0, 0, 1, 1), &[-25]),
reversible(rect(0, 0, 1, 1), &[50]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.samples, vec![100, 50, 25]);
}
#[test]
fn mct_1_with_irreversible_canvases_undoes_the_ict() {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 3]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 1, 1),
1,
Some(WaveletKind::Irreversible97),
vec![
irreversible(rect(0, 0, 1, 1), &[-3.8]),
irreversible(rect(0, 0, 1, 1), &[-41.87472]),
irreversible(rect(0, 0, 1, 1), &[54.0655]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.samples, vec![200, 100, 50]);
}
#[test]
fn mct_with_mixed_canvas_kinds_is_skipped_with_a_warning() {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 3]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 1, 1),
1,
Some(WaveletKind::Reversible53),
vec![
reversible(rect(0, 0, 1, 1), &[-28]),
irreversible(rect(0, 0, 1, 1), &[0.4]),
reversible(rect(0, 0, 1, 1), &[-103]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.samples, vec![100, 128, 25]);
assert!(
image.warnings.iter().any(|w| w.message.contains("mix")),
"warnings: {:?}",
image.warnings
);
}
#[test]
fn sycc_converts_to_rgb_with_the_exact_inverse() {
let siz = siz_for(2, 1, 0, 0, 2, 1, vec![component(8, false, 1, 1); 3]);
let header = plain_header(ColorSpec::Enumerated(18), 3, 2, 1);
let mut assembler =
ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 2, 1),
0,
None,
vec![
reversible(rect(0, 0, 2, 1), &[-28, -78]),
reversible(rect(0, 0, 2, 1), &[0, 100]),
reversible(rect(0, 0, 2, 1), &[0, -100]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.color, ColorKind::Rgb);
assert!(
image
.warnings
.iter()
.any(|w| !w.data_loss && w.message.contains("IEC 61966-2-1")),
"warnings: {:?}",
image.warnings
);
assert_eq!(image.samples, vec![100, 100, 100, 0, 87, 227]);
}
#[test]
fn sycc_hand_vectors_hit_the_gray_axis_and_the_primary_corners() {
let siz = siz_for(5, 1, 0, 0, 5, 1, vec![component(8, false, 1, 1); 3]);
let header = plain_header(ColorSpec::Enumerated(18), 3, 5, 1);
let mut assembler =
ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 5, 1),
0,
None,
vec![
reversible(rect(0, 0, 5, 1), &[127, -128, -52, 22, -99]),
reversible(rect(0, 0, 5, 1), &[0, 0, -43, -85, 127]),
reversible(rect(0, 0, 5, 1), &[0, 0, 127, -107, -21]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(
image.samples,
vec![255, 255, 255, 0, 0, 0, 254, 0, 0, 0, 255, 0, 0, 0, 254]
);
}
#[test]
fn sycc_at_16_bit_centres_chroma_on_the_rounded_midpoint() {
let siz = siz_for(2, 1, 0, 0, 2, 1, vec![component(16, false, 1, 1); 3]);
let header = plain_header(ColorSpec::Enumerated(18), 3, 2, 1);
let mut assembler =
ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
assembler
.push_tile(
rect(0, 0, 2, 1),
0,
None,
vec![
reversible(rect(0, 0, 2, 1), &[-6968, -19968]),
reversible(rect(0, 0, 2, 1), &[0, 25600]),
reversible(rect(0, 0, 2, 1), &[0, -25600]),
],
)
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.color, ColorKind::Rgb);
assert!(
image
.warnings
.iter()
.any(|w| !w.data_loss && w.message.contains("16-bit source rounded to 8 bits")),
"warnings: {:?}",
image.warnings
);
assert_eq!(image.samples, vec![101, 101, 101, 0, 87, 227]);
}
#[test]
fn color_kind_defaults_follow_the_component_count_without_a_header() {
for (count, expected) in [
(1usize, ColorKind::Gray),
(3, ColorKind::Rgb),
(4, ColorKind::Cmyk),
] {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); count]);
let assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.color, expected, "{count} components");
assert_eq!(image.alpha_index, None);
}
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 2]);
let assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(
image.color,
ColorKind::Other {
enumeration: 0,
components: 2
}
);
assert!(!image.warnings.is_empty());
}
#[test]
fn color_kind_follows_the_colr_box() {
let cases: Vec<(ColorSpec, usize, ColorKind)> = vec![
(ColorSpec::Enumerated(16), 3, ColorKind::Rgb),
(ColorSpec::Enumerated(17), 1, ColorKind::Gray),
(
ColorSpec::Enumerated(12),
4,
ColorKind::Other {
enumeration: 12,
components: 4,
},
),
(
ColorSpec::Icc {
profile: vec![9; 20],
},
4,
ColorKind::IccGuess { components: 4 },
),
];
for (spec, count, expected) in cases {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); count]);
let header = plain_header(spec.clone(), count as u16, 1, 1);
let assembler =
ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.color, expected, "{spec:?}");
}
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 4]);
let header = plain_header(
ColorSpec::Icc {
profile: vec![9; 20],
},
4,
1,
1,
);
let assembler = ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.icc_profile, Some(vec![9; 20]));
assert!(
image
.warnings
.iter()
.any(|w| !w.data_loss && w.message.contains("ICC profile (20 bytes), exported")),
"warnings: {:?}",
image.warnings
);
let header = plain_header(
ColorSpec::Icc {
profile: Vec::new(),
},
4,
1,
1,
);
let assembler = ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.color, ColorKind::IccGuess { components: 4 });
assert_eq!(image.icc_profile, None);
assert!(
image
.warnings
.iter()
.any(|w| w.message.contains("did not carry")),
"warnings: {:?}",
image.warnings
);
}
#[test]
fn cdef_reports_the_whole_image_opacity_channel() {
let siz = siz_for(1, 1, 0, 0, 1, 1, vec![component(8, false, 1, 1); 4]);
let mut header = plain_header(ColorSpec::Enumerated(16), 4, 1, 1);
header.channel_definitions = vec![
ChannelDefinition {
channel: 0,
kind: 0,
association: 1,
},
ChannelDefinition {
channel: 1,
kind: 0,
association: 2,
},
ChannelDefinition {
channel: 2,
kind: 0,
association: 3,
},
ChannelDefinition {
channel: 3,
kind: 1,
association: 0,
},
];
let assembler = ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.alpha_index, Some(3));
assert_eq!(image.color, ColorKind::Rgb);
let mut header = plain_header(ColorSpec::Enumerated(16), 4, 1, 1);
header.channel_definitions = vec![ChannelDefinition {
channel: 3,
kind: 2,
association: 0,
}];
let assembler = ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.alpha_index, Some(3));
assert!(
image
.warnings
.iter()
.any(|w| w.message.contains("premultiplied")),
"warnings: {:?}",
image.warnings
);
}
#[test]
fn new_enforces_max_decoded_bytes_before_allocating() {
let siz = siz_for(8, 5, 2, 1, 4, 5, vec![component(8, false, 1, 1)]);
let limits = DecodeLimits {
max_decoded_bytes: 23,
..DecodeLimits::default()
};
match ImageAssembler::new(&siz, None, &limits) {
Err(JpxError::LimitExceeded {
what,
actual,
limit,
}) => {
assert_eq!(what, "max_decoded_bytes");
assert_eq!(actual, 24);
assert_eq!(limit, 23);
}
other => panic!("expected max_decoded_bytes breach, got {:?}", other.err()),
}
let limits = DecodeLimits {
max_decoded_bytes: 24,
..DecodeLimits::default()
};
assert!(ImageAssembler::new(&siz, None, &limits).is_ok());
}
#[test]
fn siz_wins_over_a_mismatched_ihdr() {
let siz = siz_for(8, 5, 2, 1, 4, 5, vec![component(8, false, 1, 1)]);
let header = plain_header(ColorSpec::Enumerated(17), 2, 9, 9);
let assembler = ImageAssembler::new(&siz, Some(&header), &DecodeLimits::default()).unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!((image.width, image.height), (6, 4));
assert_eq!(image.components, 1);
assert!(
image
.warnings
.iter()
.filter(|w| w.message.contains("SIZ"))
.count()
>= 2,
"warnings: {:?}",
image.warnings
);
}
#[test]
fn a_tile_with_the_wrong_canvas_count_warns_and_leaves_zeros() {
let siz = siz_for(2, 1, 0, 0, 2, 1, vec![component(8, false, 1, 1)]);
let mut assembler = ImageAssembler::new(&siz, None, &DecodeLimits::default()).unwrap();
assembler
.push_tile(rect(0, 0, 2, 1), 0, None, Vec::new())
.unwrap();
let image = assembler.finish(Vec::new()).unwrap();
assert_eq!(image.samples, vec![0, 0]);
assert!(
image
.warnings
.iter()
.any(|w| w.message.contains("tile skipped")),
"warnings: {:?}",
image.warnings
);
}
}