Skip to main content

pdfluent_lopdf/
xobject.rs

1use crate::*;
2use crate::{Dictionary, Stream};
3
4#[cfg(feature = "embed_image")]
5use image::{self, ColorType, ImageFormat};
6
7#[cfg(feature = "embed_image")]
8use std::path::Path;
9
10#[cfg(feature = "embed_image")]
11use crate::Result;
12
13#[derive(Debug, Clone)]
14pub struct PdfImage<'a> {
15    pub id: ObjectId,
16    pub width: i64,
17    pub height: i64,
18    pub color_space: Option<String>,
19    pub filters: Option<Vec<String>>,
20    pub bits_per_component: Option<i64>,
21    /// Image Data
22    pub content: &'a [u8],
23    /// Origin Stream Dictionary
24    pub origin_dict: &'a Dictionary,
25}
26
27pub fn form(boundingbox: Vec<f32>, matrix: Vec<f32>, content: Vec<u8>) -> Stream {
28    let mut dict = Dictionary::new();
29    dict.set("Type", Object::Name(b"XObject".to_vec()));
30    dict.set("Subtype", Object::Name(b"Form".to_vec()));
31    dict.set(
32        "BBox",
33        Object::Array(boundingbox.into_iter().map(Object::Real).collect()),
34    );
35    dict.set(
36        "Matrix",
37        Object::Array(matrix.into_iter().map(Object::Real).collect()),
38    );
39    let mut xobject = Stream::new(dict, content);
40    // Ignore any compression error.
41    let _ = xobject.compress();
42    xobject
43}
44
45#[cfg(feature = "embed_image")]
46pub fn image<P: AsRef<Path>>(path: P) -> Result<Stream> {
47    use std::fs::File;
48    use std::io::prelude::*;
49
50    let mut file = File::open(&path)?;
51    let mut buffer = Vec::new();
52    file.read_to_end(&mut buffer)?;
53
54    image_from(buffer)
55}
56
57#[cfg(feature = "embed_image")]
58pub fn image_from(buffer: Vec<u8>) -> Result<Stream> {
59    let ((width, height), color_type) = get_dimensions_and_color_type(&buffer)?;
60
61    let (bpc, color_space) = match color_type {
62        // 8-bit per channel types
63        ColorType::L8 => (8, b"DeviceGray".to_vec()),
64        ColorType::La8 => (8, b"DeviceGray".to_vec()),
65        ColorType::Rgb8 => (8, b"DeviceRGB".to_vec()),
66        ColorType::Rgba8 => (8, b"DeviceRGB".to_vec()),
67        // 16-bit per channel types
68        ColorType::L16 => (16, b"DeviceGray".to_vec()),
69        ColorType::La16 => (16, b"DeviceGray".to_vec()),
70        ColorType::Rgb16 => (16, b"DeviceRGB".to_vec()),
71        ColorType::Rgba16 => (16, b"DeviceRGB".to_vec()),
72        // f32 not supported, maybe JPXDecode?
73        ColorType::Rgb32F => {
74            return Err(Error::Unimplemented("ColorType::Rgb32F is not supported"));
75        }
76        ColorType::Rgba32F => {
77            return Err(Error::Unimplemented("ColorType::Rgba32F is not supported"));
78        }
79        // The above ColorType is all the types currently supported by the image crate
80        // But ColorType is #[non_exhaustive], there may be new types supported in the future
81        _ => {
82            return Err(Error::Unimplemented(
83                "The image crate supports a new color type, but lopdf has not been updated yet",
84            ));
85        }
86    };
87
88    let mut dict = Dictionary::new();
89    dict.set("Type", Object::Name(b"XObject".to_vec()));
90    dict.set("Subtype", Object::Name(b"Image".to_vec()));
91    dict.set("Width", width);
92    dict.set("Height", height);
93    dict.set("ColorSpace", Object::Name(color_space));
94    dict.set("BitsPerComponent", bpc);
95
96    let format = image::guess_format(&buffer)?;
97    if format == ImageFormat::Jpeg {
98        // JPEG do not need to be decoded
99        dict.set("Filter", Object::Name(b"DCTDecode".to_vec()));
100        Ok(Stream::new(dict, buffer))
101    } else {
102        // Other formats need to be decoded
103        let img = image::load_from_memory(&buffer)?;
104        let content = match img.color() {
105            // can be used directly
106            ColorType::L8 => img.into_bytes(),
107            // need to remove alpha channel
108            ColorType::La8 => img.into_luma8().into_raw(),
109            // can be used directly
110            ColorType::Rgb8 => img.into_bytes(),
111            // need to remove alpha channel
112            ColorType::Rgba8 => img.into_rgb8().into_raw(),
113            // need to convert each 16-bit pixel to big-endian bytes
114            ColorType::L16 => img
115                .into_luma16()
116                .into_raw()
117                .iter()
118                .flat_map(|&pixel| pixel.to_be_bytes()) // convert each 16-bit pixel to big-endian bytes
119                .collect(),
120            // need to remove alpha channel, then convert each 16-bit pixel to big-endian bytes
121            ColorType::La16 => img
122                .into_luma16() // remove alpha channel
123                .into_raw()
124                .iter()
125                .flat_map(|&pixel| pixel.to_be_bytes()) // convert each 16-bit pixel to big-endian bytes
126                .collect(),
127            // need to convert each 16-bit pixel to big-endian bytes
128            ColorType::Rgb16 => img
129                .into_rgb16()
130                .into_raw()
131                .iter()
132                .flat_map(|&pixel| pixel.to_be_bytes()) // convert each 16-bit pixel to big-endian bytes
133                .collect(),
134            // need to remove alpha channel, then convert each 16-bit pixel to big-endian bytes
135            ColorType::Rgba16 => img
136                .into_rgb16() // remove alpha channel
137                .into_raw()
138                .iter()
139                .flat_map(|&pixel| pixel.to_be_bytes()) // convert each 16-bit pixel to big-endian bytes
140                .collect(),
141            // f32 not supported, maybe JPXDecode?
142            ColorType::Rgb32F => {
143                return Err(Error::Unimplemented("ColorType::Rgb32F is not supported"));
144            }
145            ColorType::Rgba32F => {
146                return Err(Error::Unimplemented("ColorType::Rgba32F is not supported"));
147            }
148            // The above ColorType is all the types currently supported by the image crate
149            // But ColorType is #[non_exhaustive], there may be new types supported in the future
150            _ => {
151                return Err(Error::Unimplemented(
152                    "The image library supports a new color type, but lopdf has not been updated yet",
153                ));
154            }
155        };
156
157        let mut img_object = Stream::new(dict, content);
158        // Ignore any compression error.
159        let _ = img_object.compress();
160        Ok(img_object)
161    }
162}
163
164/// Get the `dimensions` and `color type` without decode, for performance
165#[cfg(feature = "embed_image")]
166fn get_dimensions_and_color_type(buffer: &Vec<u8>) -> Result<((u32, u32), ColorType)> {
167    use image::{ImageDecoder, ImageReader};
168
169    let reader = ImageReader::new(std::io::Cursor::new(buffer));
170    let decoder = reader.with_guessed_format()?.into_decoder()?;
171
172    let dimensions = decoder.dimensions();
173    let color_type = decoder.color_type();
174
175    Ok((dimensions, color_type))
176}
177
178#[cfg(all(feature = "embed_image", not(feature = "async")))]
179#[test]
180#[ignore = "depends on assets/pdf_icon.jpg fixture not committed to the repo"]
181fn insert_image() {
182    use super::xobject;
183    let mut doc = Document::load("assets/example.pdf").unwrap();
184    let pages = doc.get_pages();
185    let page_id = *pages
186        .get(&1)
187        .unwrap_or_else(|| panic!("Page {} not exist.", 1));
188    let img = xobject::image("assets/pdf_icon.jpg").unwrap();
189    doc.insert_image(page_id, img, (100.0, 210.0), (400.0, 225.0))
190        .unwrap();
191    doc.save("test_5_image.pdf").unwrap();
192}
193
194#[cfg(all(feature = "embed_image", feature = "async"))]
195#[tokio::test]
196#[ignore = "depends on assets/pdf_icon.jpg fixture not committed to the repo"]
197async fn insert_image() {
198    use super::xobject;
199    let mut doc = Document::load("assets/example.pdf").await.unwrap();
200    let pages = doc.get_pages();
201    let page_id = *pages
202        .get(&1)
203        .unwrap_or_else(|| panic!("Page {} not exist.", 1));
204    let img = xobject::image("assets/pdf_icon.jpg").unwrap();
205    doc.insert_image(page_id, img, (100.0, 210.0), (400.0, 225.0))
206        .unwrap();
207    doc.save("test_5_image.pdf").unwrap();
208}
209
210#[cfg(feature = "embed_image")]
211#[test]
212#[ignore = "depends on assets/supported_color_type/ fixture dir not committed to the repo"]
213fn embed_supported_color_type() -> Result<()> {
214    use content::{Content, Operation};
215    use image::GenericImageView;
216
217    let mut img_paths = std::fs::read_dir("assets/supported_color_type")?
218        .filter_map(|entry| entry.ok())
219        .map(|entry| entry.path())
220        .collect::<Vec<_>>();
221    // sort by file name
222    img_paths.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
223
224    let mut doc = Document::with_version("1.5");
225    let pages_id = doc.new_object_id();
226    let mut page_ids = vec![];
227
228    for img_path in img_paths {
229        let img = image::open(&img_path)?;
230        let (width, height) = img.dimensions();
231        let color_type = img.color();
232        println!(
233            "Image: {img_path:?}, width: {width}, height: {height}, color type: {color_type:?}"
234        );
235
236        let image_stream = xobject::image(img_path)?;
237
238        let img_id = doc.add_object(image_stream);
239        let img_name = format!("X{}", img_id.0);
240
241        let cm_operation = Operation::new(
242            "cm",
243            vec![
244                width.into(),
245                0.into(),
246                0.into(),
247                height.into(),
248                0.into(),
249                0.into(),
250            ],
251        );
252
253        let do_operation = Operation::new("Do", vec![Object::Name(img_name.as_bytes().to_vec())]);
254        let content = Content {
255            operations: vec![cm_operation, do_operation],
256        };
257
258        let content_id = doc.add_object(Stream::new(dictionary! {}, content.encode()?));
259        let page_id = doc.add_object(dictionary! {
260            "Type" => "Page",
261            "Parent" => pages_id,
262            "Contents" => content_id,
263            "MediaBox" => vec![0.into(), 0.into(), width.into(), height.into()],
264        });
265
266        doc.add_xobject(page_id, img_name.as_bytes(), img_id)?;
267        // add page to doc
268        page_ids.push(page_id);
269    }
270
271    let pages_dict = dictionary! {
272        "Type" => "Pages",
273        "Count" => page_ids.len() as u32,
274        "Kids" => page_ids.into_iter().map(Object::Reference).collect::<Vec<_>>(),
275    };
276    doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
277
278    let catalog_id = doc.add_object(dictionary! {
279        "Type" => "Catalog",
280        "Pages" => pages_id,
281    });
282    doc.trailer.set("Root", catalog_id);
283
284    doc.compress();
285
286    doc.save("supported_color_type.pdf")?;
287    Ok(())
288}