use hayro_jbig2::{Decoder, Image};
pub struct Decoded {
pub stride: usize,
pub data: Vec<u8>,
}
struct Rows {
image_width: usize,
row: Vec<u8>,
x: usize,
stride: usize,
height: usize,
out: Vec<u8>,
y: usize,
}
impl Rows {
fn file_row(&mut self) {
if self.y < self.height {
let n = self.stride.min(self.row.len());
let at = self.y * self.stride;
self.out[at..at + n].copy_from_slice(&self.row[..n]);
}
self.y += 1;
self.row.fill(0xff);
self.x = 0;
}
}
impl Decoder for Rows {
fn push_pixel(&mut self, black: bool) {
if black && self.x < self.image_width {
self.row[self.x / 8] &= !(0x80 >> (self.x % 8));
}
self.x += 1;
}
fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) {
let bytes = chunk_count as usize;
if black {
let start = (self.x / 8).min(self.row.len());
let end = (start + bytes).min(self.row.len());
self.row[start..end].fill(0x00);
}
self.x += bytes * 8;
}
fn next_line(&mut self) {
self.file_row();
}
}
pub fn decode(
data: &[u8],
globals: Option<&[u8]>,
width: usize,
height: usize,
) -> Result<Decoded, String> {
if width == 0 || height == 0 {
return Err("empty image".into());
}
let image = Image::new_embedded(data, globals).map_err(|e| format!("{e:?}"))?;
let (iw, ih) = (image.width() as usize, image.height() as usize);
if iw == 0 || ih == 0 {
return Err(format!("JBIG2 image is {iw} × {ih}"));
}
if iw.saturating_mul(ih) > 80_000_000 || width.saturating_mul(height) > 80_000_000 {
return Err(format!("JBIG2 image too large ({iw} × {ih})"));
}
let stride = width.div_ceil(8);
let mut rows = Rows {
image_width: iw,
row: vec![0xff; iw.div_ceil(8) + 1],
x: 0,
stride,
height,
out: vec![0xff; stride * height],
y: 0,
};
image.decode(&mut rows).map_err(|e| format!("{e:?}"))?;
if rows.x > 0 {
rows.file_row();
}
Ok(Decoded {
stride,
data: rows.out,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> Vec<u8> {
std::fs::read(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/jbig2")
.join(name),
)
.unwrap_or_else(|e| panic!("{name}: {e}"))
}
fn reference(name: &str) -> (usize, usize, Vec<u8>) {
let img = image::load_from_memory(&fixture(name)).unwrap().to_luma8();
let (w, h) = (img.width() as usize, img.height() as usize);
let stride = w.div_ceil(8);
let mut out = vec![0xffu8; stride * h];
for (y, row) in img.rows().enumerate() {
for (x, p) in row.enumerate() {
if p.0[0] < 128 {
out[y * stride + x / 8] &= !(0x80 >> (x % 8));
}
}
}
(w, h, out)
}
#[test]
fn mmr_generic_region_matches_the_reference() {
let (w, h, want) = reference("generic_mmr.png");
let got = decode(&fixture("generic_mmr.jbig2"), None, w, h).unwrap();
assert_eq!(got.stride, w.div_ceil(8));
assert_eq!(got.data, want);
assert!(got.data.iter().any(|&b| b != 0xff));
}
#[test]
fn text_region_with_global_symbol_dictionary() {
let (w, h, want) = reference("symbol_global.png");
let globals = fixture("symbol_global.globals.jbig2");
let got = decode(&fixture("symbol_global.jbig2"), Some(&globals), w, h).unwrap();
assert_eq!(got.data, want);
assert!(decode(&fixture("symbol_global.jbig2"), None, w, h).is_err());
}
#[test]
fn dictionary_size_crops_or_pads() {
let (w, h, want) = reference("generic_mmr.png");
let padded = decode(&fixture("generic_mmr.jbig2"), None, w + 9, h + 2).unwrap();
assert_eq!(padded.stride, (w + 9).div_ceil(8));
for y in 0..h {
let got = &padded.data[y * padded.stride..y * padded.stride + w.div_ceil(8)];
let exp = &want[y * w.div_ceil(8)..(y + 1) * w.div_ceil(8)];
let mask = 0xffu8 << ((8 - w % 8) % 8);
let (gl, el) = (got.last().unwrap() & mask, exp.last().unwrap() & mask);
assert_eq!(&got[..got.len() - 1], &exp[..exp.len() - 1], "row {y}");
assert_eq!(gl, el, "row {y} tail");
}
assert!(padded.data[h * padded.stride..].iter().all(|&b| b == 0xff));
let cropped = decode(&fixture("generic_mmr.jbig2"), None, 8, 3).unwrap();
assert_eq!(cropped.data.len(), 3);
assert_eq!(
&cropped.data[..],
&[want[0], want[w.div_ceil(8)], want[2 * w.div_ceil(8)]]
);
}
#[test]
fn garbage_is_an_error_not_a_panic() {
assert!(decode(b"not a jbig2 stream", None, 10, 10).is_err());
assert!(decode(&[], None, 10, 10).is_err());
assert!(decode(&fixture("generic_mmr.jbig2"), None, 0, 10).is_err());
}
}