docling_pdf/render/
jbig2.rs1use hayro_jbig2::{Decoder, Image};
23
24pub struct Decoded {
27 pub stride: usize,
28 pub data: Vec<u8>,
29}
30
31struct Rows {
34 image_width: usize,
37 row: Vec<u8>,
38 x: usize,
39 stride: usize,
41 height: usize,
42 out: Vec<u8>,
43 y: usize,
44}
45
46impl Rows {
47 fn file_row(&mut self) {
48 if self.y < self.height {
49 let n = self.stride.min(self.row.len());
50 let at = self.y * self.stride;
51 self.out[at..at + n].copy_from_slice(&self.row[..n]);
52 }
53 self.y += 1;
54 self.row.fill(0xff);
55 self.x = 0;
56 }
57}
58
59impl Decoder for Rows {
60 fn push_pixel(&mut self, black: bool) {
61 if black && self.x < self.image_width {
62 self.row[self.x / 8] &= !(0x80 >> (self.x % 8));
63 }
64 self.x += 1;
65 }
66
67 fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) {
68 let bytes = chunk_count as usize;
70 if black {
71 let start = (self.x / 8).min(self.row.len());
72 let end = (start + bytes).min(self.row.len());
73 self.row[start..end].fill(0x00);
74 }
75 self.x += bytes * 8;
76 }
77
78 fn next_line(&mut self) {
79 self.file_row();
80 }
81}
82
83pub fn decode(
87 data: &[u8],
88 globals: Option<&[u8]>,
89 width: usize,
90 height: usize,
91) -> Result<Decoded, String> {
92 if width == 0 || height == 0 {
93 return Err("empty image".into());
94 }
95 let image = Image::new_embedded(data, globals).map_err(|e| format!("{e:?}"))?;
96 let (iw, ih) = (image.width() as usize, image.height() as usize);
97 if iw == 0 || ih == 0 {
98 return Err(format!("JBIG2 image is {iw} × {ih}"));
99 }
100 if iw.saturating_mul(ih) > 80_000_000 || width.saturating_mul(height) > 80_000_000 {
103 return Err(format!("JBIG2 image too large ({iw} × {ih})"));
104 }
105 let stride = width.div_ceil(8);
106 let mut rows = Rows {
107 image_width: iw,
108 row: vec![0xff; iw.div_ceil(8) + 1],
110 x: 0,
111 stride,
112 height,
113 out: vec![0xff; stride * height],
114 y: 0,
115 };
116 image.decode(&mut rows).map_err(|e| format!("{e:?}"))?;
117 if rows.x > 0 {
119 rows.file_row();
120 }
121 Ok(Decoded {
122 stride,
123 data: rows.out,
124 })
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn fixture(name: &str) -> Vec<u8> {
132 std::fs::read(
133 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
134 .join("tests/data/jbig2")
135 .join(name),
136 )
137 .unwrap_or_else(|e| panic!("{name}: {e}"))
138 }
139
140 fn reference(name: &str) -> (usize, usize, Vec<u8>) {
144 let img = image::load_from_memory(&fixture(name)).unwrap().to_luma8();
145 let (w, h) = (img.width() as usize, img.height() as usize);
146 let stride = w.div_ceil(8);
147 let mut out = vec![0xffu8; stride * h];
148 for (y, row) in img.rows().enumerate() {
149 for (x, p) in row.enumerate() {
150 if p.0[0] < 128 {
151 out[y * stride + x / 8] &= !(0x80 >> (x % 8));
152 }
153 }
154 }
155 (w, h, out)
156 }
157
158 #[test]
161 fn mmr_generic_region_matches_the_reference() {
162 let (w, h, want) = reference("generic_mmr.png");
163 let got = decode(&fixture("generic_mmr.jbig2"), None, w, h).unwrap();
164 assert_eq!(got.stride, w.div_ceil(8));
165 assert_eq!(got.data, want);
166 assert!(got.data.iter().any(|&b| b != 0xff));
168 }
169
170 #[test]
174 fn text_region_with_global_symbol_dictionary() {
175 let (w, h, want) = reference("symbol_global.png");
176 let globals = fixture("symbol_global.globals.jbig2");
177 let got = decode(&fixture("symbol_global.jbig2"), Some(&globals), w, h).unwrap();
178 assert_eq!(got.data, want);
179 assert!(decode(&fixture("symbol_global.jbig2"), None, w, h).is_err());
181 }
182
183 #[test]
186 fn dictionary_size_crops_or_pads() {
187 let (w, h, want) = reference("generic_mmr.png");
188 let padded = decode(&fixture("generic_mmr.jbig2"), None, w + 9, h + 2).unwrap();
189 assert_eq!(padded.stride, (w + 9).div_ceil(8));
190 for y in 0..h {
191 let got = &padded.data[y * padded.stride..y * padded.stride + w.div_ceil(8)];
192 let exp = &want[y * w.div_ceil(8)..(y + 1) * w.div_ceil(8)];
193 let mask = 0xffu8 << ((8 - w % 8) % 8);
195 let (gl, el) = (got.last().unwrap() & mask, exp.last().unwrap() & mask);
196 assert_eq!(&got[..got.len() - 1], &exp[..exp.len() - 1], "row {y}");
197 assert_eq!(gl, el, "row {y} tail");
198 }
199 assert!(padded.data[h * padded.stride..].iter().all(|&b| b == 0xff));
200 let cropped = decode(&fixture("generic_mmr.jbig2"), None, 8, 3).unwrap();
201 assert_eq!(cropped.data.len(), 3);
202 assert_eq!(
203 &cropped.data[..],
204 &[want[0], want[w.div_ceil(8)], want[2 * w.div_ceil(8)]]
205 );
206 }
207
208 #[test]
209 fn garbage_is_an_error_not_a_panic() {
210 assert!(decode(b"not a jbig2 stream", None, 10, 10).is_err());
211 assert!(decode(&[], None, 10, 10).is_err());
212 assert!(decode(&fixture("generic_mmr.jbig2"), None, 0, 10).is_err());
213 }
214}