1use crate::blocks::{Block, BlockKind, ExtractOptions};
20use crate::error::{EngineError, Result};
21use crate::render::{PixelFormat, RenderOptions};
22
23pub const FIGURE_DPI: f64 = 150.0;
27
28pub const MAX_GRAPHICS_SCAN_PAGES: usize = 50;
36
37#[derive(Debug, Clone, PartialEq)]
41pub struct Figure {
42 pub page: usize,
44 pub bbox: [f64; 4],
46 pub name: String,
48 pub png: Vec<u8>,
50}
51
52pub fn figure_name(page: usize, ordinal: usize) -> String {
57 format!("figure-{:04}-{:02}.png", page + 1, ordinal + 1)
58}
59
60fn contains(point: (f64, f64), bbox: &[f64; 4]) -> bool {
62 point.0 > bbox[0] && point.0 < bbox[2] && point.1 > bbox[1] && point.1 < bbox[3]
63}
64
65pub(crate) fn drop_text_over_images(blocks: Vec<Block>, images: &[Vec<[f64; 4]>]) -> Vec<Block> {
71 blocks
72 .into_iter()
73 .filter(|block| {
74 if block.text.trim().is_empty() {
75 return true;
76 }
77 if matches!(block.kind, BlockKind::Header | BlockKind::Footer) {
78 return true;
79 }
80 let centre = (
81 (block.bbox[0] + block.bbox[2]) / 2.0,
82 (block.bbox[1] + block.bbox[3]) / 2.0,
83 );
84 !images
85 .get(block.page)
86 .is_some_and(|boxes| boxes.iter().any(|bbox| contains(centre, bbox)))
87 })
88 .collect()
89}
90
91pub(crate) fn merge_figures(
101 blocks: Vec<Block>,
102 images: &[Vec<[f64; 4]>],
103 graphics: &[Vec<[f64; 4]>],
104 page_has_text: &[bool],
105) -> Vec<Block> {
106 let (mut body, furniture): (Vec<Block>, Vec<Block>) = blocks
107 .into_iter()
108 .partition(|block| !matches!(block.kind, BlockKind::Header | BlockKind::Footer));
109 body.sort_by_key(|block| block.page);
110 let pages = images.len().max(graphics.len());
111 let mut merged: Vec<Block> = Vec::with_capacity(body.len() + pages);
112 let mut at = 0usize;
113 for page in 0..pages {
114 let end = body[at..]
115 .iter()
116 .take_while(|block| block.page == page)
117 .count()
118 + at;
119 let text = &body[at..end];
120 at = end;
121 if !page_has_text.get(page).copied().unwrap_or(false) {
122 if let Some(boxes) = images.get(page) {
126 let mut figures: Vec<[f64; 4]> = boxes.clone();
127 figures.sort_by(|a, b| b[3].total_cmp(&a[3]));
128 for bbox in figures {
129 merged.push(Block::new(BlockKind::Figure, page, bbox, String::new(), 0));
130 }
131 }
132 merged.extend(text.iter().cloned());
133 continue;
134 }
135 let mut figures: Vec<([f64; 4], f64)> = Vec::new();
136 if let Some(boxes) = images.get(page) {
137 figures.extend(boxes.iter().map(|bbox| (*bbox, bbox[3])));
138 }
139 if let Some(boxes) = graphics.get(page) {
140 figures.extend(boxes.iter().map(|bbox| (*bbox, bbox[3])));
141 }
142 figures.sort_by(|a, b| b.1.total_cmp(&a.1));
143 let mut figures = figures.into_iter().peekable();
144 for block in text {
145 while figures.peek().is_some_and(|(_, top)| *top >= block.bbox[3]) {
146 if let Some((bbox, _)) = figures.next() {
147 merged.push(Block::new(BlockKind::Figure, page, bbox, String::new(), 0));
148 }
149 }
150 merged.push(block.clone());
151 }
152 for (bbox, _) in figures {
153 merged.push(Block::new(BlockKind::Figure, page, bbox, String::new(), 0));
154 }
155 }
156 merged.extend(body[at..].iter().cloned());
157 merged.extend(furniture);
158 for (index, block) in merged.iter_mut().enumerate() {
159 block.index = index;
160 }
161 merged
162}
163
164pub(crate) fn render_crops(
172 doc: &crate::PdfDocument,
173 page: usize,
174 boxes: &[[f64; 4]],
175) -> Result<Vec<Vec<u8>>> {
176 if boxes.is_empty() {
177 return Ok(Vec::new());
178 }
179 let rendered = doc.render_page(
180 page,
181 &RenderOptions {
182 dpi: FIGURE_DPI,
183 ..Default::default()
184 },
185 )?;
186 if rendered.pixel_format != PixelFormat::Rgba8 {
187 return Err(EngineError::RenderError(format!(
188 "figure render of page {} came back {:?}, not RGBA",
189 page, rendered.pixel_format
190 )));
191 }
192 let scale = FIGURE_DPI / 72.0;
193 let (width, height) = (rendered.width, rendered.height);
194 let mut out = Vec::with_capacity(boxes.len());
195 for bbox in boxes {
196 let x0 = ((bbox[0] * scale).floor() as i64).clamp(0, width as i64);
197 let x1 = ((bbox[2] * scale).ceil() as i64).clamp(0, width as i64);
198 let y1 = (height as i64 - (bbox[1] * scale).floor() as i64).clamp(0, height as i64);
199 let y0 = (height as i64 - (bbox[3] * scale).ceil() as i64).clamp(0, height as i64);
200 let (x0, x1) = (x0.min(x1), x0.max(x1));
201 let (y0, y1) = (y0.min(y1), y0.max(y1));
202 let (crop_w, crop_h) = ((x1 - x0).max(1) as u32, (y1 - y0).max(1) as u32);
203 let mut rgb = vec![255u8; crop_w as usize * crop_h as usize * 3];
204 if x1 > x0 && y1 > y0 {
205 for (row, y) in (y0..y1).enumerate() {
206 for (col, x) in (x0..x1).enumerate() {
207 let from = (y as usize * width as usize + x as usize) * 4;
208 let Some(px) = rendered.pixels.get(from..from + 4) else {
209 continue;
210 };
211 let missing = 255 - px[3];
214 let to = (row * crop_w as usize + col) * 3;
215 if let Some(slot) = rgb.get_mut(to..to + 3) {
216 slot[0] = px[0].saturating_add(missing);
217 slot[1] = px[1].saturating_add(missing);
218 slot[2] = px[2].saturating_add(missing);
219 }
220 }
221 }
222 }
223 out.push(png_bytes(crop_w, crop_h, &rgb));
224 }
225 Ok(out)
226}
227
228pub(crate) fn png_bytes(width: u32, height: u32, rgb: &[u8]) -> Vec<u8> {
234 let stride = width as usize * 3;
235 let mut raw = Vec::with_capacity(height as usize * (stride + 1));
236 for y in 0..height as usize {
237 raw.push(0);
238 match rgb.get(y * stride..(y + 1) * stride) {
239 Some(row) => raw.extend_from_slice(row),
240 None => raw.extend(std::iter::repeat_n(0, stride)),
241 }
242 }
243 let mut zlib = vec![0x78, 0x01];
246 let mut chunks = raw.chunks(65535).peekable();
247 while let Some(chunk) = chunks.next() {
248 let last = chunks.peek().is_none();
249 zlib.push(u8::from(last));
250 let len = chunk.len() as u16;
251 zlib.extend_from_slice(&len.to_le_bytes());
252 zlib.extend_from_slice(&(!len).to_le_bytes());
253 zlib.extend_from_slice(chunk);
254 }
255 if raw.is_empty() {
257 zlib.extend_from_slice(&[1, 0, 0, 255, 255]);
258 }
259 zlib.extend_from_slice(&adler32(&raw).to_be_bytes());
260 let mut png = vec![137, 80, 78, 71, 13, 10, 26, 10];
261 let mut ihdr = Vec::with_capacity(13);
262 ihdr.extend_from_slice(&width.to_be_bytes());
263 ihdr.extend_from_slice(&height.to_be_bytes());
264 ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
265 push_chunk(&mut png, b"IHDR", &ihdr);
266 push_chunk(&mut png, b"IDAT", &zlib);
267 push_chunk(&mut png, b"IEND", &[]);
268 png
269}
270
271fn push_chunk(png: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
273 png.extend_from_slice(&(data.len() as u32).to_be_bytes());
274 png.extend_from_slice(kind);
275 png.extend_from_slice(data);
276 let mut crc_input = Vec::with_capacity(4 + data.len());
277 crc_input.extend_from_slice(kind);
278 crc_input.extend_from_slice(data);
279 png.extend_from_slice(&crc32(&crc_input).to_be_bytes());
280}
281
282fn crc32(data: &[u8]) -> u32 {
284 let mut crc = 0xffff_ffffu32;
285 for byte in data {
286 crc ^= u32::from(*byte);
287 for _ in 0..8 {
288 let lsb = crc & 1;
289 crc >>= 1;
290 if lsb == 1 {
291 crc ^= 0xedb8_8320;
292 }
293 }
294 }
295 !crc
296}
297
298fn adler32(data: &[u8]) -> u32 {
300 let mut low: u32 = 1;
301 let mut high: u32 = 0;
302 for byte in data {
303 low = (low + u32::from(*byte)) % 65521;
304 high = (high + low) % 65521;
305 }
306 (high << 16) | low
307}
308
309pub(crate) fn resolve_figure_texts(
317 doc: &crate::PdfDocument,
318 blocks: &mut [Block],
319 options: &ExtractOptions,
320) -> Result<()> {
321 use base64::Engine as _;
322 use std::collections::BTreeMap;
323 let mut pages: BTreeMap<usize, Vec<(usize, [f64; 4])>> = BTreeMap::new();
327 for (at, block) in blocks.iter().enumerate() {
328 if matches!(block.kind, BlockKind::Figure) {
329 pages.entry(block.page).or_default().push((at, block.bbox));
330 }
331 }
332 if pages.is_empty() {
333 return Ok(());
334 }
335 if let Some(dir) = options.figure_image_dir.as_ref() {
336 std::fs::create_dir_all(dir).map_err(EngineError::Io)?;
337 }
338 for (page, refs) in &pages {
339 let boxes: Vec<[f64; 4]> = refs.iter().map(|(_, bbox)| *bbox).collect();
340 let renders = render_crops(doc, *page, &boxes)?;
341 for (ordinal, ((at, _), png)) in refs.iter().zip(renders).enumerate() {
342 let name = figure_name(*page, ordinal);
343 let text = match options.figure_image_dir.as_ref() {
344 Some(dir) => {
345 let path = dir.join(&name);
346 std::fs::write(&path, &png).map_err(EngineError::Io)?;
347 path.to_string_lossy().into_owned()
348 }
349 None => {
350 let encoded = base64::engine::general_purpose::STANDARD.encode(&png);
351 format!("data:image/png;base64,{encoded}")
352 }
353 };
354 if let Some(block) = blocks.get_mut(*at) {
355 block.text = text;
356 }
357 }
358 }
359 Ok(())
360}