1use lopdf::{Dictionary, Document, Object, Stream};
4
5use crate::models::bbox::BoundingBox;
6use crate::models::chunks::ImageChunk;
7use crate::EdgePdfError;
8
9#[derive(Debug, Clone)]
11pub struct ExtractedImage {
12 pub chunk: ImageChunk,
14 pub data: Vec<u8>,
16 pub width: u32,
18 pub height: u32,
20 pub color_space: String,
22 pub bits_per_component: u8,
24 pub filter: String,
26}
27
28struct ListedImage {
30 index: u32,
32 width: u32,
33 height: u32,
34 stream: Stream,
35}
36
37pub fn extract_image_chunks(
42 doc: &Document,
43 page_number: u32,
44 page_id: lopdf::ObjectId,
45) -> Result<Vec<ImageChunk>, EdgePdfError> {
46 let page_dict = doc
47 .get_object(page_id)
48 .map_err(|e| EdgePdfError::PipelineError {
49 stage: 1,
50 message: format!("Failed to get page {}: {}", page_number, e),
51 })?
52 .as_dict()
53 .map_err(|e| EdgePdfError::PipelineError {
54 stage: 1,
55 message: format!("Page {} is not a dictionary: {}", page_number, e),
56 })?;
57
58 let resources = match page_dict.get(b"Resources") {
59 Ok(r) => resolve_obj(doc, r),
60 Err(_) => return Ok(Vec::new()),
61 };
62
63 let resources_dict = match resources.as_dict() {
64 Ok(d) => d,
65 Err(_) => return Ok(Vec::new()),
66 };
67
68 let listed = list_page_images(doc, resources_dict);
69 Ok(listed
70 .into_iter()
71 .map(|img| ImageChunk {
72 bbox: BoundingBox::new(
73 Some(page_number),
74 0.0,
75 0.0,
76 img.width as f64,
77 img.height as f64,
78 ),
79 index: Some(img.index),
80 level: None,
81 source: Default::default(),
82 })
83 .collect())
84}
85
86pub fn extract_image_data(
88 doc: &Document,
89 page_id: lopdf::ObjectId,
90 image_index: u32,
91) -> Result<Option<ExtractedImage>, EdgePdfError> {
92 let page_dict = doc
93 .get_object(page_id)
94 .map_err(|e| EdgePdfError::PipelineError {
95 stage: 1,
96 message: format!("Failed to get page: {}", e),
97 })?
98 .as_dict()
99 .map_err(|e| EdgePdfError::PipelineError {
100 stage: 1,
101 message: format!("Page is not a dictionary: {}", e),
102 })?;
103
104 let resources = match page_dict.get(b"Resources") {
105 Ok(r) => resolve_obj(doc, r),
106 Err(_) => return Ok(None),
107 };
108
109 let resources_dict = match resources.as_dict() {
110 Ok(d) => d,
111 Err(_) => return Ok(None),
112 };
113
114 let listed = list_page_images(doc, resources_dict);
115 let Some(img) = listed.into_iter().find(|i| i.index == image_index) else {
116 return Ok(None);
117 };
118
119 Ok(Some(extracted_from_stream(img.stream, image_index)))
120}
121
122pub fn find_pattern_image_index(
130 doc: &Document,
131 resources: &Dictionary,
132 pattern_name: &[u8],
133) -> Option<u32> {
134 let listed = list_page_images(doc, resources);
135 let (pattern_images, bbox_w, bbox_h) =
136 list_pattern_images_with_bbox(doc, resources, pattern_name)?;
137 if pattern_images.is_empty() {
138 return None;
139 }
140
141 let cell = pattern_images.iter().find(|img| {
143 !is_bleed_strip(img.width, img.height)
144 && bbox_w
145 .map(|bw| (img.width as f64 - bw).abs() < 1.0)
146 .unwrap_or(false)
147 && bbox_h
148 .map(|bh| (img.height as f64 - bh).abs() < 1.0)
149 .unwrap_or(false)
150 });
151 let best = cell.or_else(|| {
152 let non_strips: Vec<_> = pattern_images
154 .iter()
155 .filter(|img| !is_bleed_strip(img.width, img.height))
156 .collect();
157 if non_strips.len() == 1 {
158 Some(non_strips[0])
159 } else {
160 None
161 }
162 })?;
163
164 listed
165 .into_iter()
166 .find(|img| {
167 img.width == best.width
168 && img.height == best.height
169 && img.stream.content == best.stream.content
170 })
171 .map(|img| img.index)
172}
173
174fn is_bleed_strip(width: u32, height: u32) -> bool {
176 width <= 1 || height <= 1
177}
178
179fn list_pattern_images_with_bbox(
180 doc: &Document,
181 resources: &Dictionary,
182 pattern_name: &[u8],
183) -> Option<(Vec<ListedImage>, Option<f64>, Option<f64>)> {
184 let Ok(patterns) = resources.get(b"Pattern") else {
185 return None;
186 };
187 let patterns = resolve_obj(doc, patterns);
188 let Ok(pattern_dict) = patterns.as_dict() else {
189 return None;
190 };
191 let Ok(pref) = pattern_dict.get(pattern_name) else {
192 return None;
193 };
194 let pattern_obj = resolve_obj(doc, pref);
195 let (bbox_w, bbox_h) = pattern_bbox_size(doc, &pattern_obj);
196 let pr = pattern_resources(doc, &pattern_obj)?;
197 let mut out = Vec::new();
198 let mut index = 0u32;
199 append_xobject_images(doc, &pr, &mut out, &mut index);
200 Some((out, bbox_w, bbox_h))
201}
202
203fn pattern_bbox_size(doc: &Document, pattern_obj: &Object) -> (Option<f64>, Option<f64>) {
204 let dict = match pattern_obj {
205 Object::Stream(s) => &s.dict,
206 Object::Dictionary(d) => d,
207 _ => return (None, None),
208 };
209 let Ok(bbox_obj) = dict.get(b"BBox") else {
210 return (None, None);
211 };
212 let resolved = resolve_obj(doc, bbox_obj);
213 let Ok(arr) = resolved.as_array() else {
214 return (None, None);
215 };
216 if arr.len() < 4 {
217 return (None, None);
218 }
219 let nums: Vec<f64> = arr
220 .iter()
221 .filter_map(|o| match resolve_obj(doc, o) {
222 Object::Integer(i) => Some(i as f64),
223 Object::Real(f) => Some(f),
224 _ => None,
225 })
226 .collect();
227 if nums.len() < 4 {
228 return (None, None);
229 }
230 (
231 Some((nums[2] - nums[0]).abs()),
232 Some((nums[3] - nums[1]).abs()),
233 )
234}
235
236fn list_page_images(doc: &Document, resources: &Dictionary) -> Vec<ListedImage> {
239 let mut out = Vec::new();
240 let mut index = 0u32;
241
242 append_xobject_images(doc, resources, &mut out, &mut index);
243
244 if let Ok(patterns) = resources.get(b"Pattern") {
245 let patterns = resolve_obj(doc, patterns);
246 if let Ok(pattern_dict) = patterns.as_dict() {
247 for (_name, pref) in pattern_dict.iter() {
248 let pattern_obj = resolve_obj(doc, pref);
249 let pattern_resources = pattern_resources(doc, &pattern_obj);
250 if let Some(pr) = pattern_resources.as_ref() {
251 append_xobject_images(doc, pr, &mut out, &mut index);
252 }
253 }
254 }
255 }
256
257 out
258}
259
260fn pattern_resources(doc: &Document, pattern_obj: &Object) -> Option<Dictionary> {
261 let dict = match pattern_obj {
263 Object::Stream(s) => &s.dict,
264 Object::Dictionary(d) => d,
265 _ => return None,
266 };
267 let resources = dict.get(b"Resources").ok()?;
268 let resolved = resolve_obj(doc, resources);
269 resolved.as_dict().ok().cloned()
270}
271
272fn append_xobject_images(
273 doc: &Document,
274 resources: &Dictionary,
275 out: &mut Vec<ListedImage>,
276 index: &mut u32,
277) {
278 let Ok(xobjects) = resources.get(b"XObject") else {
279 return;
280 };
281 let xobjects = resolve_obj(doc, xobjects);
282 let Ok(xobject_dict) = xobjects.as_dict() else {
283 return;
284 };
285
286 for (_name, xobj_ref) in xobject_dict.iter() {
287 let xobj = resolve_obj(doc, xobj_ref);
288 let Ok(stream) = xobj.as_stream() else {
289 continue;
290 };
291 let dict = &stream.dict;
292 let subtype = dict.get(b"Subtype").ok().and_then(|o| {
293 if let Object::Name(ref n) = o {
294 Some(String::from_utf8_lossy(n).to_string())
295 } else {
296 None
297 }
298 });
299 if subtype.as_deref() != Some("Image") {
300 continue;
301 }
302 let width = get_int(dict, b"Width").unwrap_or(0) as u32;
303 let height = get_int(dict, b"Height").unwrap_or(0) as u32;
304 if width == 0 || height == 0 {
305 continue;
306 }
307 *index = index.saturating_add(1);
308 out.push(ListedImage {
309 index: *index,
310 width,
311 height,
312 stream: stream.clone(),
313 });
314 }
315}
316
317fn extracted_from_stream(stream: Stream, image_index: u32) -> ExtractedImage {
318 let dict = &stream.dict;
319 let width = get_int(dict, b"Width").unwrap_or(0) as u32;
320 let height = get_int(dict, b"Height").unwrap_or(0) as u32;
321 let bpc = get_int(dict, b"BitsPerComponent").unwrap_or(8) as u8;
322
323 let color_space = dict
324 .get(b"ColorSpace")
325 .ok()
326 .and_then(|o| match o {
327 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
328 _ => None,
329 })
330 .unwrap_or_else(|| "DeviceRGB".to_string());
331
332 let filter = dict
333 .get(b"Filter")
334 .ok()
335 .and_then(|o| match o {
336 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
337 _ => None,
338 })
339 .unwrap_or_default();
340
341 let data = if filter == "DCTDecode" {
342 stream.content.clone()
343 } else {
344 stream
345 .decompressed_content()
346 .unwrap_or_else(|_| stream.content.clone())
347 };
348
349 let bbox = BoundingBox::new(Some(0), 0.0, 0.0, width as f64, height as f64);
350
351 ExtractedImage {
352 chunk: ImageChunk {
353 bbox,
354 index: Some(image_index),
355 level: None,
356 source: Default::default(),
357 },
358 data,
359 width,
360 height,
361 color_space,
362 bits_per_component: bpc,
363 filter,
364 }
365}
366
367fn resolve_obj(doc: &Document, obj: &Object) -> Object {
368 match obj {
369 Object::Reference(id) => doc.get_object(*id).cloned().unwrap_or(Object::Null),
370 other => other.clone(),
371 }
372}
373
374fn get_int(dict: &lopdf::Dictionary, key: &[u8]) -> Option<i64> {
375 dict.get(key).ok().and_then(|o| match o {
376 Object::Integer(i) => Some(*i),
377 Object::Real(f) => Some(*f as i64),
378 _ => None,
379 })
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use lopdf::{dictionary, Stream};
386
387 #[test]
388 fn test_extract_no_images() {
389 let mut doc = Document::with_version("1.5");
390 let pages_id = doc.new_object_id();
391 let page_id = doc.add_object(dictionary! {
392 "Type" => "Page",
393 "Parent" => pages_id,
394 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
395 "Resources" => dictionary! {},
396 });
397 let pages = dictionary! {
398 "Type" => "Pages",
399 "Kids" => vec![page_id.into()],
400 "Count" => 1,
401 };
402 doc.objects.insert(pages_id, Object::Dictionary(pages));
403 let catalog_id = doc.add_object(dictionary! {
404 "Type" => "Catalog",
405 "Pages" => pages_id,
406 });
407 doc.trailer.set("Root", catalog_id);
408
409 let pages = doc.get_pages();
410 let (&page_num, &page_id) = pages.iter().next().unwrap();
411 let chunks = extract_image_chunks(&doc, page_num, page_id).unwrap();
412 assert!(chunks.is_empty());
413 }
414
415 #[test]
416 fn test_extract_image_chunk() {
417 let mut doc = Document::with_version("1.5");
418 let pages_id = doc.new_object_id();
419
420 let img_id = doc.add_object(Stream::new(
421 dictionary! {
422 "Type" => "XObject",
423 "Subtype" => "Image",
424 "Width" => 100,
425 "Height" => 50,
426 "ColorSpace" => "DeviceRGB",
427 "BitsPerComponent" => 8,
428 },
429 vec![0u8; 100],
430 ));
431
432 let resources_id = doc.add_object(dictionary! {
433 "XObject" => dictionary! {
434 "Im1" => img_id,
435 },
436 });
437
438 let page_id = doc.add_object(dictionary! {
439 "Type" => "Page",
440 "Parent" => pages_id,
441 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
442 "Resources" => resources_id,
443 });
444
445 let pages = dictionary! {
446 "Type" => "Pages",
447 "Kids" => vec![page_id.into()],
448 "Count" => 1,
449 };
450 doc.objects.insert(pages_id, Object::Dictionary(pages));
451 let catalog_id = doc.add_object(dictionary! {
452 "Type" => "Catalog",
453 "Pages" => pages_id,
454 });
455 doc.trailer.set("Root", catalog_id);
456
457 let pages = doc.get_pages();
458 let (&page_num, &page_id) = pages.iter().next().unwrap();
459 let chunks = extract_image_chunks(&doc, page_num, page_id).unwrap();
460 assert_eq!(chunks.len(), 1);
461 assert_eq!(chunks[0].index, Some(1));
462 }
463
464 #[test]
465 fn test_pattern_nested_image_index() {
466 let mut doc = Document::with_version("1.5");
467 let pages_id = doc.new_object_id();
468
469 let img_id = doc.add_object(Stream::new(
470 dictionary! {
471 "Type" => "XObject",
472 "Subtype" => "Image",
473 "Width" => 200,
474 "Height" => 100,
475 "ColorSpace" => "DeviceRGB",
476 "BitsPerComponent" => 8,
477 },
478 vec![1u8; 200],
479 ));
480
481 let pattern_id = doc.add_object(dictionary! {
482 "Type" => "Pattern",
483 "PatternType" => 1,
484 "PaintType" => 1,
485 "TilingType" => 1,
486 "BBox" => vec![0.into(), 0.into(), 200.into(), 100.into()],
487 "XStep" => 200,
488 "YStep" => 100,
489 "Resources" => dictionary! {
490 "XObject" => dictionary! {
491 "X1" => img_id,
492 },
493 },
494 });
495
496 let resources = dictionary! {
497 "Pattern" => dictionary! {
498 "P1" => pattern_id,
499 },
500 };
501
502 let page_id = doc.add_object(dictionary! {
503 "Type" => "Page",
504 "Parent" => pages_id,
505 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
506 "Resources" => resources.clone(),
507 });
508
509 let pages = dictionary! {
510 "Type" => "Pages",
511 "Kids" => vec![page_id.into()],
512 "Count" => 1,
513 };
514 doc.objects.insert(pages_id, Object::Dictionary(pages));
515 let catalog_id = doc.add_object(dictionary! {
516 "Type" => "Catalog",
517 "Pages" => pages_id,
518 });
519 doc.trailer.set("Root", catalog_id);
520
521 let idx = find_pattern_image_index(&doc, &resources, b"P1");
522 assert_eq!(idx, Some(1));
523
524 let pages = doc.get_pages();
525 let (&_page_num, &page_id) = pages.iter().next().unwrap();
526 let extracted = extract_image_data(&doc, page_id, 1).unwrap().unwrap();
527 assert_eq!(extracted.width, 200);
528 assert_eq!(extracted.height, 100);
529 }
530}