1use crate::error::{ExtractError, Result};
12use lopdf::{Document, Object, ObjectId};
13use std::collections::BTreeMap;
14use std::io::Read;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ImageFilter {
19 Jpeg,
21 Flate,
23 Jbig2,
25 Jpx,
27 CcittFax,
29 Raw,
31 Unknown(String),
33}
34
35#[derive(Debug, Clone)]
37pub struct ExtractedImage {
38 pub object_id: ObjectId,
40 pub page: u32,
42 pub width: u32,
44 pub height: u32,
46 pub bits_per_component: u32,
48 pub color_space: String,
50 pub filter: ImageFilter,
52 pub data: Vec<u8>,
54}
55
56#[derive(Debug, Clone)]
60pub struct EncodedImage {
61 pub data: Vec<u8>,
63 pub mime: &'static str,
65 pub ext: &'static str,
67}
68
69pub fn encode_image_for_document(img: &ExtractedImage) -> Option<EncodedImage> {
90 match &img.filter {
91 ImageFilter::Jpeg => {
93 if img.data.starts_with(&[0xFF, 0xD8]) {
94 Some(EncodedImage {
95 data: img.data.clone(),
96 mime: "image/jpeg",
97 ext: "jpeg",
98 })
99 } else {
100 None
101 }
102 }
103 ImageFilter::Flate | ImageFilter::Raw => encode_samples_as_png(img),
105 _ => None,
108 }
109}
110
111fn components_for_color_space(cs: &str) -> Option<usize> {
115 match cs {
116 "DeviceGray" | "CalGray" | "G" => Some(1),
117 "DeviceRGB" | "CalRGB" | "RGB" => Some(3),
118 "DeviceCMYK" | "CMYK" => Some(4),
119 _ => None,
120 }
121}
122
123fn infer_components(byte_len: usize, pixels: usize) -> Option<usize> {
125 if pixels == 0 {
126 return None;
127 }
128 match byte_len / pixels {
129 1 => Some(1),
130 3 => Some(3),
131 4 => Some(4),
132 _ => None,
133 }
134}
135
136fn cmyk_to_rgb(samples: &[u8]) -> Vec<u8> {
138 let mut out = Vec::with_capacity(samples.len() / 4 * 3);
139 for px in samples.chunks_exact(4) {
140 let (c, m, y, k) = (px[0] as u32, px[1] as u32, px[2] as u32, px[3] as u32);
141 out.push(((255 - c) * (255 - k) / 255) as u8);
142 out.push(((255 - m) * (255 - k) / 255) as u8);
143 out.push(((255 - y) * (255 - k) / 255) as u8);
144 }
145 out
146}
147
148fn encode_samples_as_png(img: &ExtractedImage) -> Option<EncodedImage> {
151 use image::codecs::png::{CompressionType, FilterType, PngEncoder};
152 use image::{ExtendedColorType, ImageEncoder};
153
154 if img.bits_per_component != 8 || img.width == 0 || img.height == 0 {
157 return None;
158 }
159 let pixels = (img.width as usize).checked_mul(img.height as usize)?;
160
161 let components = components_for_color_space(&img.color_space)
164 .or_else(|| infer_components(img.data.len(), pixels))?;
165
166 let expected = pixels.checked_mul(components)?;
167 if img.data.len() < expected {
168 return None;
170 }
171 let samples = &img.data[..expected];
172
173 let (buf, color) = match components {
174 1 => (samples.to_vec(), ExtendedColorType::L8),
175 3 => (samples.to_vec(), ExtendedColorType::Rgb8),
176 4 => (cmyk_to_rgb(samples), ExtendedColorType::Rgb8),
177 _ => return None,
178 };
179
180 let mut out = Vec::new();
181 PngEncoder::new_with_quality(&mut out, CompressionType::Fast, FilterType::Adaptive)
184 .write_image(&buf, img.width, img.height, color)
185 .ok()?;
186 Some(EncodedImage {
187 data: out,
188 mime: "image/png",
189 ext: "png",
190 })
191}
192
193pub fn extract_all_images(doc: &Document) -> Result<Vec<ExtractedImage>> {
195 let page_map = build_page_image_map(doc);
196 let mut images = Vec::new();
197
198 for (page_num, obj_ids) in &page_map {
199 for &obj_id in obj_ids {
200 if let Ok(img) = decode_image(doc, obj_id, *page_num) {
201 images.push(img);
202 }
203 }
204 }
205
206 Ok(images)
207}
208
209pub fn extract_page_images(doc: &Document, page_num: u32) -> Result<Vec<ExtractedImage>> {
211 let pages = doc.get_pages();
212 let total = pages.len() as u32;
213
214 if page_num == 0 || page_num > total {
215 return Err(ExtractError::PageOutOfRange(page_num, total));
216 }
217
218 let page_id = *pages
219 .get(&page_num)
220 .ok_or(ExtractError::PageOutOfRange(page_num, total))?;
221
222 extract_images_from_page_id(doc, page_id, page_num)
223}
224
225pub fn extract_images_from_page_id(
232 doc: &Document,
233 page_id: lopdf::ObjectId,
234 page_num: u32,
235) -> Result<Vec<ExtractedImage>> {
236 let obj_ids = collect_page_xobject_ids(doc, page_id);
237 let mut images = Vec::new();
238
239 for obj_id in obj_ids {
240 if let Ok(img) = decode_image(doc, obj_id, page_num) {
241 images.push(img);
242 }
243 }
244
245 Ok(images)
246}
247
248fn is_image_stream(dict: &lopdf::Dictionary) -> bool {
250 if let Ok(subtype) = dict.get(b"Subtype") {
251 if let Ok(name) = subtype.as_name() {
252 return name == b"Image";
253 }
254 }
255 false
256}
257
258fn get_filter(dict: &lopdf::Dictionary) -> ImageFilter {
260 if let Ok(filter_obj) = dict.get(b"Filter") {
261 match filter_obj {
262 Object::Name(name) => filter_from_name(name),
263 Object::Array(arr) => {
264 if let Some(Object::Name(name)) = arr.first() {
266 filter_from_name(name)
267 } else {
268 ImageFilter::Raw
269 }
270 }
271 _ => ImageFilter::Raw,
272 }
273 } else {
274 ImageFilter::Raw
275 }
276}
277
278fn filter_from_name(name: &[u8]) -> ImageFilter {
280 match name {
281 b"DCTDecode" => ImageFilter::Jpeg,
282 b"FlateDecode" => ImageFilter::Flate,
283 b"JBIG2Decode" => ImageFilter::Jbig2,
284 b"JPXDecode" => ImageFilter::Jpx,
285 b"CCITTFaxDecode" => ImageFilter::CcittFax,
286 _ => ImageFilter::Unknown(String::from_utf8_lossy(name).to_string()),
287 }
288}
289
290fn get_color_space(dict: &lopdf::Dictionary) -> String {
292 if let Ok(cs) = dict.get(b"ColorSpace") {
293 match cs {
294 Object::Name(name) => String::from_utf8_lossy(name).to_string(),
295 Object::Array(arr) => {
296 if let Some(Object::Name(name)) = arr.first() {
297 String::from_utf8_lossy(name).to_string()
298 } else {
299 "Unknown".to_string()
300 }
301 }
302 _ => "Unknown".to_string(),
303 }
304 } else {
305 "Unknown".to_string()
306 }
307}
308
309fn get_int(dict: &lopdf::Dictionary, key: &[u8]) -> u32 {
311 dict.get(key)
312 .ok()
313 .and_then(|v| match v {
314 Object::Integer(i) => Some(*i as u32),
315 _ => None,
316 })
317 .unwrap_or(0)
318}
319
320fn decode_image(doc: &Document, obj_id: ObjectId, page: u32) -> Result<ExtractedImage> {
322 let obj = doc
323 .get_object(obj_id)
324 .map_err(|e| ExtractError::Other(format!("object not found: {e}")))?;
325
326 let stream = match obj {
327 Object::Stream(ref s) => s,
328 _ => return Err(ExtractError::Other("not a stream object".into())),
329 };
330
331 let dict = &stream.dict;
332 if !is_image_stream(dict) {
333 return Err(ExtractError::Other("not an image stream".into()));
334 }
335
336 let width = get_int(dict, b"Width");
337 let height = get_int(dict, b"Height");
338 let bits_per_component = get_int(dict, b"BitsPerComponent");
339 let color_space = get_color_space(dict);
340 let filter = get_filter(dict);
341
342 let data = match filter {
343 ImageFilter::Jpeg | ImageFilter::Jbig2 | ImageFilter::Jpx | ImageFilter::CcittFax => {
344 get_raw_stream_bytes(stream)
346 }
347 ImageFilter::Flate => {
348 let raw = get_raw_stream_bytes(stream);
349 decompress_flate(&raw).unwrap_or(raw)
350 }
351 ImageFilter::Raw => get_raw_stream_bytes(stream),
352 ImageFilter::Unknown(_) => get_raw_stream_bytes(stream),
353 };
354
355 Ok(ExtractedImage {
356 object_id: obj_id,
357 page,
358 width,
359 height,
360 bits_per_component,
361 color_space,
362 filter,
363 data,
364 })
365}
366
367fn get_raw_stream_bytes(stream: &lopdf::Stream) -> Vec<u8> {
369 stream.content.clone()
370}
371
372#[allow(dead_code)]
374fn get_stream_bytes(stream: &lopdf::Stream, _doc: &Document) -> Vec<u8> {
375 let mut s = stream.clone();
376 if s.decompress().is_ok() {
377 s.content.clone()
378 } else {
379 stream.content.clone()
380 }
381}
382
383const FLATE_MAX_DECOMPRESS_BYTES: u64 = 64 * 1024 * 1024;
386
387fn decompress_flate(data: &[u8]) -> std::result::Result<Vec<u8>, std::io::Error> {
389 let decoder = flate2::read::ZlibDecoder::new(data);
390 let mut decoded = Vec::new();
391 decoder
392 .take(FLATE_MAX_DECOMPRESS_BYTES)
393 .read_to_end(&mut decoded)?;
394 Ok(decoded)
395}
396
397fn build_page_image_map(doc: &Document) -> BTreeMap<u32, Vec<ObjectId>> {
399 let mut map = BTreeMap::new();
400 let pages = doc.get_pages();
401
402 for (&page_num, &page_id) in &pages {
403 let ids = collect_page_xobject_ids(doc, page_id);
404 if !ids.is_empty() {
405 map.insert(page_num, ids);
406 }
407 }
408
409 map
410}
411
412fn collect_page_xobject_ids(doc: &Document, page_id: ObjectId) -> Vec<ObjectId> {
414 let mut ids = Vec::new();
415
416 let page_obj = match doc.get_object(page_id) {
417 Ok(obj) => obj,
418 Err(_) => return ids,
419 };
420
421 let page_dict = match page_obj {
422 Object::Dictionary(ref d) => d,
423 _ => return ids,
424 };
425
426 let resources = match page_dict.get(b"Resources") {
428 Ok(res_obj) => match res_obj {
429 Object::Dictionary(ref d) => d.clone(),
430 Object::Reference(r) => match doc.get_object(*r) {
431 Ok(Object::Dictionary(ref d)) => d.clone(),
432 _ => return ids,
433 },
434 _ => return ids,
435 },
436 Err(_) => return ids,
437 };
438
439 let xobjects = match resources.get(b"XObject") {
441 Ok(xo) => match xo {
442 Object::Dictionary(ref d) => d.clone(),
443 Object::Reference(r) => match doc.get_object(*r) {
444 Ok(Object::Dictionary(ref d)) => d.clone(),
445 _ => return ids,
446 },
447 _ => return ids,
448 },
449 Err(_) => return ids,
450 };
451
452 for (_name, obj) in xobjects.iter() {
453 if let Object::Reference(obj_id) = obj {
454 if let Ok(Object::Stream(ref stream)) = doc.get_object(*obj_id) {
455 if is_image_stream(&stream.dict) {
456 ids.push(*obj_id);
457 }
458 }
459 }
460 }
461
462 ids
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use lopdf::{dictionary, Document, Object, Stream};
469
470 fn mk_img(
473 filter: ImageFilter,
474 cs: &str,
475 bpc: u32,
476 w: u32,
477 h: u32,
478 data: Vec<u8>,
479 ) -> ExtractedImage {
480 ExtractedImage {
481 object_id: (1, 0),
482 page: 1,
483 width: w,
484 height: h,
485 bits_per_component: bpc,
486 color_space: cs.to_string(),
487 filter,
488 data,
489 }
490 }
491
492 fn assert_decodes_to(enc: &EncodedImage, w: u32, h: u32) {
493 assert_eq!(
494 &enc.data[..8],
495 b"\x89PNG\r\n\x1a\n",
496 "missing PNG signature"
497 );
498 let decoded = image::load_from_memory(&enc.data).expect("encoded PNG must decode");
499 assert_eq!((decoded.width(), decoded.height()), (w, h));
500 }
501
502 #[test]
503 fn encode_rgb_flate_samples_produce_valid_png() {
504 let img = mk_img(ImageFilter::Flate, "DeviceRGB", 8, 2, 2, vec![10u8; 12]);
506 let enc = encode_image_for_document(&img).expect("RGB samples should encode");
507 assert_eq!(enc.mime, "image/png");
508 assert_eq!(enc.ext, "png");
509 assert_decodes_to(&enc, 2, 2);
510 }
511
512 #[test]
513 fn encode_grayscale_samples_produce_valid_png() {
514 let img = mk_img(ImageFilter::Flate, "DeviceGray", 8, 2, 2, vec![200u8; 4]);
516 let enc = encode_image_for_document(&img).expect("gray samples should encode");
517 assert_eq!(enc.mime, "image/png");
518 assert_decodes_to(&enc, 2, 2);
519 }
520
521 #[test]
522 fn encode_cmyk_samples_produce_valid_rgb_png() {
523 let img = mk_img(ImageFilter::Flate, "DeviceCMYK", 8, 2, 2, vec![0u8; 16]);
525 let enc = encode_image_for_document(&img).expect("cmyk samples should encode");
526 assert_eq!(enc.mime, "image/png");
527 assert_decodes_to(&enc, 2, 2);
528 }
529
530 #[test]
531 fn encode_iccbased_infers_components_from_length() {
532 let img = mk_img(ImageFilter::Flate, "ICCBased", 8, 2, 2, vec![55u8; 12]);
534 let enc = encode_image_for_document(&img).expect("ICCBased should infer RGB");
535 assert_decodes_to(&enc, 2, 2);
536 }
537
538 #[test]
539 fn encode_raw_uncompressed_samples_produce_valid_png() {
540 let img = mk_img(ImageFilter::Raw, "DeviceRGB", 8, 2, 2, vec![255u8; 12]);
541 let enc = encode_image_for_document(&img).expect("raw samples should encode");
542 assert_decodes_to(&enc, 2, 2);
543 }
544
545 #[test]
546 fn encode_jpeg_stream_passes_through() {
547 let mut data = vec![0xFF, 0xD8, 0xFF, 0xE0];
549 data.extend_from_slice(&[1, 2, 3, 4]);
550 let img = mk_img(ImageFilter::Jpeg, "DeviceRGB", 8, 10, 10, data.clone());
551 let enc = encode_image_for_document(&img).expect("jpeg should pass through");
552 assert_eq!(enc.mime, "image/jpeg");
553 assert_eq!(enc.ext, "jpeg");
554 assert_eq!(enc.data, data, "jpeg bytes must be preserved verbatim");
555 }
556
557 #[test]
558 fn encode_jpeg_without_signature_is_skipped() {
559 let img = mk_img(ImageFilter::Jpeg, "DeviceRGB", 8, 10, 10, vec![0u8; 8]);
560 assert!(encode_image_for_document(&img).is_none());
561 }
562
563 #[test]
564 fn encode_short_buffer_is_skipped_not_corrupted() {
565 let img = mk_img(ImageFilter::Flate, "DeviceRGB", 8, 2, 2, vec![1u8; 6]);
567 assert!(encode_image_for_document(&img).is_none());
568 }
569
570 #[test]
571 fn encode_non_8bit_depth_is_skipped() {
572 let img = mk_img(ImageFilter::Flate, "DeviceGray", 1, 8, 8, vec![0u8; 8]);
573 assert!(encode_image_for_document(&img).is_none());
574 }
575
576 #[test]
577 fn encode_unsupported_filters_are_skipped() {
578 for f in [
579 ImageFilter::Jpx,
580 ImageFilter::Jbig2,
581 ImageFilter::CcittFax,
582 ImageFilter::Unknown("Foo".into()),
583 ] {
584 let img = mk_img(f, "DeviceRGB", 8, 2, 2, vec![0u8; 12]);
585 assert!(
586 encode_image_for_document(&img).is_none(),
587 "compressed/unknown filters must never be embedded as PNG"
588 );
589 }
590 }
591
592 fn make_doc_with_jpeg_image() -> Document {
594 let mut doc = Document::with_version("1.7");
595
596 let img_dict = dictionary! {
597 "Type" => "XObject",
598 "Subtype" => "Image",
599 "Width" => 100_i64,
600 "Height" => 50_i64,
601 "BitsPerComponent" => 8_i64,
602 "ColorSpace" => "DeviceRGB",
603 "Filter" => "DCTDecode",
604 };
605 let img_stream = Stream::new(img_dict, vec![0xFF, 0xD8, 0xFF, 0xE0]);
606 let img_id = doc.add_object(Object::Stream(img_stream));
607
608 let xobject_dict = dictionary! {
609 "Im0" => Object::Reference(img_id),
610 };
611 let resources_dict = dictionary! {
612 "XObject" => Object::Dictionary(xobject_dict),
613 };
614 let content_data = b"q 100 0 0 50 0 0 cm /Im0 Do Q".to_vec();
615 let content_stream = Stream::new(dictionary! {}, content_data);
616 let content_id = doc.add_object(Object::Stream(content_stream));
617
618 let page_dict = dictionary! {
619 "Type" => "Page",
620 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
621 "Resources" => Object::Dictionary(resources_dict),
622 "Contents" => Object::Reference(content_id),
623 };
624 let page_id = doc.add_object(Object::Dictionary(page_dict));
625
626 let pages_dict = dictionary! {
627 "Type" => "Pages",
628 "Kids" => vec![Object::Reference(page_id)],
629 "Count" => 1_i64,
630 };
631 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
632
633 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
634 d.set("Parent", Object::Reference(pages_id));
635 }
636
637 let catalog = dictionary! {
638 "Type" => "Catalog",
639 "Pages" => Object::Reference(pages_id),
640 };
641 let catalog_id = doc.add_object(Object::Dictionary(catalog));
642 doc.trailer.set("Root", Object::Reference(catalog_id));
643
644 doc
645 }
646
647 fn make_doc_with_raw_image() -> Document {
649 let mut doc = Document::with_version("1.7");
650
651 let img_dict = dictionary! {
652 "Type" => "XObject",
653 "Subtype" => "Image",
654 "Width" => 2_i64,
655 "Height" => 2_i64,
656 "BitsPerComponent" => 8_i64,
657 "ColorSpace" => "DeviceRGB",
658 };
659 let img_stream = Stream::new(img_dict, vec![255; 12]);
661 let img_id = doc.add_object(Object::Stream(img_stream));
662
663 let xobject_dict = dictionary! {
664 "Im0" => Object::Reference(img_id),
665 };
666 let resources_dict = dictionary! {
667 "XObject" => Object::Dictionary(xobject_dict),
668 };
669 let content_data = b"q 2 0 0 2 0 0 cm /Im0 Do Q".to_vec();
670 let content_stream = Stream::new(dictionary! {}, content_data);
671 let content_id = doc.add_object(Object::Stream(content_stream));
672
673 let page_dict = dictionary! {
674 "Type" => "Page",
675 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
676 "Resources" => Object::Dictionary(resources_dict),
677 "Contents" => Object::Reference(content_id),
678 };
679 let page_id = doc.add_object(Object::Dictionary(page_dict));
680
681 let pages_dict = dictionary! {
682 "Type" => "Pages",
683 "Kids" => vec![Object::Reference(page_id)],
684 "Count" => 1_i64,
685 };
686 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
687
688 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
689 d.set("Parent", Object::Reference(pages_id));
690 }
691
692 let catalog = dictionary! {
693 "Type" => "Catalog",
694 "Pages" => Object::Reference(pages_id),
695 };
696 let catalog_id = doc.add_object(Object::Dictionary(catalog));
697 doc.trailer.set("Root", Object::Reference(catalog_id));
698
699 doc
700 }
701
702 fn make_doc_with_flate_image() -> Document {
704 let mut doc = Document::with_version("1.7");
705
706 let raw_data = vec![128u8; 12]; let mut encoder =
709 flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
710 std::io::Write::write_all(&mut encoder, &raw_data).unwrap();
711 let compressed = encoder.finish().unwrap();
712
713 let img_dict = dictionary! {
714 "Type" => "XObject",
715 "Subtype" => "Image",
716 "Width" => 2_i64,
717 "Height" => 2_i64,
718 "BitsPerComponent" => 8_i64,
719 "ColorSpace" => "DeviceRGB",
720 "Filter" => "FlateDecode",
721 };
722 let img_stream = Stream::new(img_dict, compressed);
723 let img_id = doc.add_object(Object::Stream(img_stream));
724
725 let xobject_dict = dictionary! {
726 "Im0" => Object::Reference(img_id),
727 };
728 let resources_dict = dictionary! {
729 "XObject" => Object::Dictionary(xobject_dict),
730 };
731 let content_data = b"q 2 0 0 2 0 0 cm /Im0 Do Q".to_vec();
732 let content_stream = Stream::new(dictionary! {}, content_data);
733 let content_id = doc.add_object(Object::Stream(content_stream));
734
735 let page_dict = dictionary! {
736 "Type" => "Page",
737 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
738 "Resources" => Object::Dictionary(resources_dict),
739 "Contents" => Object::Reference(content_id),
740 };
741 let page_id = doc.add_object(Object::Dictionary(page_dict));
742
743 let pages_dict = dictionary! {
744 "Type" => "Pages",
745 "Kids" => vec![Object::Reference(page_id)],
746 "Count" => 1_i64,
747 };
748 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
749
750 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
751 d.set("Parent", Object::Reference(pages_id));
752 }
753
754 let catalog = dictionary! {
755 "Type" => "Catalog",
756 "Pages" => Object::Reference(pages_id),
757 };
758 let catalog_id = doc.add_object(Object::Dictionary(catalog));
759 doc.trailer.set("Root", Object::Reference(catalog_id));
760
761 doc
762 }
763
764 #[test]
765 fn extract_jpeg_image() {
766 let doc = make_doc_with_jpeg_image();
767 let images = extract_all_images(&doc).unwrap();
768 assert_eq!(images.len(), 1);
769 assert_eq!(images[0].width, 100);
770 assert_eq!(images[0].height, 50);
771 assert_eq!(images[0].bits_per_component, 8);
772 assert_eq!(images[0].color_space, "DeviceRGB");
773 assert_eq!(images[0].filter, ImageFilter::Jpeg);
774 assert_eq!(images[0].page, 1);
775 }
776
777 #[test]
778 fn extract_from_specific_page() {
779 let doc = make_doc_with_jpeg_image();
780 let images = extract_page_images(&doc, 1).unwrap();
781 assert_eq!(images.len(), 1);
782 assert_eq!(images[0].filter, ImageFilter::Jpeg);
783 }
784
785 #[test]
786 fn extract_page_out_of_range() {
787 let doc = make_doc_with_jpeg_image();
788 let result = extract_page_images(&doc, 5);
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn extract_raw_image() {
794 let doc = make_doc_with_raw_image();
795 let images = extract_all_images(&doc).unwrap();
796 assert_eq!(images.len(), 1);
797 assert_eq!(images[0].filter, ImageFilter::Raw);
798 assert_eq!(images[0].data.len(), 12);
799 }
800
801 #[test]
802 fn extract_flate_compressed_image() {
803 let doc = make_doc_with_flate_image();
804 let images = extract_all_images(&doc).unwrap();
805 assert_eq!(images.len(), 1);
806 assert_eq!(images[0].filter, ImageFilter::Flate);
807 assert_eq!(images[0].data.len(), 12);
809 }
810
811 #[test]
812 fn no_images_returns_empty() {
813 let mut doc = Document::with_version("1.7");
814
815 let content_stream = Stream::new(dictionary! {}, b"BT /F1 12 Tf (Hello) Tj ET".to_vec());
816 let content_id = doc.add_object(Object::Stream(content_stream));
817
818 let page_dict = dictionary! {
819 "Type" => "Page",
820 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
821 "Contents" => Object::Reference(content_id),
822 };
823 let page_id = doc.add_object(Object::Dictionary(page_dict));
824
825 let pages_dict = dictionary! {
826 "Type" => "Pages",
827 "Kids" => vec![Object::Reference(page_id)],
828 "Count" => 1_i64,
829 };
830 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
831
832 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
833 d.set("Parent", Object::Reference(pages_id));
834 }
835
836 let catalog = dictionary! {
837 "Type" => "Catalog",
838 "Pages" => Object::Reference(pages_id),
839 };
840 let catalog_id = doc.add_object(Object::Dictionary(catalog));
841 doc.trailer.set("Root", Object::Reference(catalog_id));
842
843 let images = extract_all_images(&doc).unwrap();
844 assert!(images.is_empty());
845 }
846}