pdfrum_edit/image/mod.rs
1//! Embed caller-supplied pixels, or a compressed codestream, as an image
2//! `XObject`.
3//!
4//! A caller hands over bytes — a JPEG codestream or raw interleaved samples —
5//! and this allocates the `/XObject` a content stream's `Do` can name.
6
7mod jpeg;
8mod png;
9mod raw;
10
11use pdfrum_object::ObjRef;
12
13use crate::doc::EditDoc;
14use crate::error::Error;
15
16pub use raw::PixelFormat;
17
18/// An image `XObject` this session added, ready to place on a page.
19///
20/// [`EmbeddedImage::object`] is the `/XObject` a page resource names, and what
21/// `ImageBuilder::at` in the facade takes. The dimensions come back because
22/// they are the caller's only statement of the aspect ratio the placement
23/// rectangle should keep — a JPEG's are read out of its header, not supplied.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct EmbeddedImage {
26 image: ObjRef,
27 width: u32,
28 height: u32,
29}
30
31impl EmbeddedImage {
32 /// The `/XObject` to name from a page resource.
33 #[must_use]
34 pub fn object(&self) -> ObjRef {
35 self.image
36 }
37
38 /// Width in samples.
39 #[must_use]
40 pub fn width(&self) -> u32 {
41 self.width
42 }
43
44 /// Height in samples.
45 #[must_use]
46 pub fn height(&self) -> u32 {
47 self.height
48 }
49}
50
51impl EditDoc<'_> {
52 /// Embed a JPEG or JPEG 2000 codestream as a new image `XObject`.
53 ///
54 /// The bytes become the stream verbatim under `/DCTDecode` or
55 /// `/JPXDecode`; nothing is decoded or re-encoded. `/Width`, `/Height`,
56 /// `/ColorSpace` and `/BitsPerComponent` are read from the codestream's
57 /// own header, which is the file's statement of them and overrides any a
58 /// caller could pass.
59 ///
60 /// # Errors
61 ///
62 /// [`Error::UnrecognisedImageData`] when the bytes are neither a JPEG nor
63 /// a JPEG 2000 codestream, or their header cannot be read.
64 pub fn embed_jpeg(&mut self, bytes: &[u8]) -> Result<EmbeddedImage, Error> {
65 jpeg::embed(self, bytes)
66 }
67
68 /// Embed a PNG as a new image `XObject`, its compressed data passed
69 /// through unchanged.
70 ///
71 /// PNG's `IDAT` stream is what `/FlateDecode` with the PNG predictors
72 /// reads, so a greyscale, RGB or palette PNG that is not interlaced is
73 /// stored as it came: no decode, no re-compression, no larger than the
74 /// file. `/Width`, `/Height`, `/ColorSpace` and `/BitsPerComponent` come
75 /// from its `IHDR` (and `PLTE`).
76 ///
77 /// # Errors
78 ///
79 /// [`Error::PngNeedsDecoding`] for a PNG PDF cannot take as stored — one
80 /// with an alpha channel or `tRNS` transparency (PDF keeps alpha in a
81 /// separate soft mask), an interlaced one, or 16 bits deep: decode it and
82 /// use [`EditDoc::embed_image`]. [`Error::UnrecognisedImageData`] when
83 /// the bytes are not a PNG.
84 ///
85 /// ```
86 /// use pdfrum_edit::{EditDoc, Error, Size, blank_document};
87 ///
88 /// let base = blank_document(&[Size::new(100.0, 100.0)])?;
89 /// let mut edit = EditDoc::new(&base);
90 /// assert!(matches!(edit.embed_png(b"not a png"), Err(Error::UnrecognisedImageData)));
91 /// # Ok::<(), pdfrum_edit::Error>(())
92 /// ```
93 pub fn embed_png(&mut self, bytes: &[u8]) -> Result<EmbeddedImage, Error> {
94 png::embed(self, bytes)
95 }
96
97 /// Embed raw interleaved samples as a new image `XObject`.
98 ///
99 /// The samples are stored uncompressed and the stream writer flate-encodes
100 /// them (there is no `/Filter` on the dictionary this writes). An
101 /// [`PixelFormat::Rgba8`] alpha channel is split off into a separate
102 /// `/DeviceGray` `/SMask` image; the colour channels keep their own
103 /// stream.
104 ///
105 /// # Errors
106 ///
107 /// [`Error::EmptyImage`] when either dimension is zero, and
108 /// [`Error::ImageDataLength`] when `pixels` is not exactly the length the
109 /// dimensions and format require.
110 pub fn embed_image(
111 &mut self,
112 pixels: &[u8],
113 width: u32,
114 height: u32,
115 format: PixelFormat,
116 ) -> Result<EmbeddedImage, Error> {
117 raw::embed(self, pixels, width, height, format)
118 }
119}