Skip to main content

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