Repository: fschutt/printpdf (language: rust)

Issue:
Can't print image in pdf
Using the example image_alpha seems to work just fine as I can open the pdf and see the image for example in chrome
<img width="772" alt="ChromeOk" src="https://github.com/fschutt/printpdf/assets/12753392/7fd8e60e-2ae7-4135-b873-6ee3a1ea1023">

However when I try to open with AcrobatReader (version 23.008.20470), there is an error:
<img width="614" alt="AcrobatError" src="https://github.com/fschutt/printpdf/assets/12753392/a185c109-c86f-4c24-ab69-72b5ce46cb78">

And when trying to physically print, the page is just blank
<img width="780" alt="PrintBlank" src="https://github.com/fschutt/printpdf/assets/12753392/06a95bc7-76ef-4b43-a0b0-a1f8009d5c42">

Did someone manage to print an image this way? how did you manage? where is the issue coming from?

Thx!
Can't print image in pdf
Using the example image_alpha seems to work just fine as I can open the pdf and see the image for example in chrome
<img width="772" alt="ChromeOk" src="https://github.com/fschutt/printpdf/assets/12753392/7fd8e60e-2ae7-4135-b873-6ee3a1ea1023">

However when I try to open with AcrobatReader (version 23.008.20470), there is an error:
<img width="614" alt="AcrobatError" src="https://github.com/fschutt/printpdf/assets/12753392/a185c109-c86f-4c24-ab69-72b5ce46cb78">

And when trying to physically print, the page is just blank
<img width="780" alt="PrintBlank" src="https://github.com/fschutt/printpdf/assets/12753392/06a95bc7-76ef-4b43-a0b0-a1f8009d5c42">

Did someone manage to print an image this way? how did you manage? where is the issue coming from?

Thx!


Discussion / hints:
Also qpdf found some problems

```
cargo run --example image_alpha --features="embedded_images" --release
qpdf --check test_image.pdf
```
```
checking test_image.pdf
PDF Version: 1.3
File is not encrypted
File is not linearized
WARNING: test_image.pdf (object 7 0, offset 807): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 814): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1115): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1117): treating unexpected brace token as null
WARNING: test_image.pdf (object 7 0, offset 1118): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1183): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1183): too many errors; giving up on reading object
WARNING: test_image.pdf (object 7 0, offset 1195): expected endobj
qpdf: operation succeeded with warnings

```
Also qpdf found some problems

```
cargo run --example image_alpha --features="embedded_images" --release
qpdf --check test_image.pdf
```
```
checking test_image.pdf
PDF Version: 1.3
File is not encrypted
File is not linearized
WARNING: test_image.pdf (object 7 0, offset 807): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 814): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1115): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1117): treating unexpected brace token as null
WARNING: test_image.pdf (object 7 0, offset 1118): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1183): unknown token while reading object; treating as string
WARNING: test_image.pdf (object 7 0, offset 1183): too many errors; giving up on reading object
WARNING: test_image.pdf (object 7 0, offset 1195): expected endobj
qpdf: operation succeeded with warnings

```

Fix patch:
diff --git a/src/image.rs b/src/image.rs
index b40e988..540150e 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -11,25 +11,29 @@ use image_crate::{self, DynamicImage, ImageDecoder};
 pub struct Image {
     /// The actual image
     pub image: ImageXObject,
+    /// The soft mask (transparency layer)
+    pub smask: Option<ImageXObject>,
 }
 
 impl From<ImageXObject> for Image {
     fn from(image: ImageXObject) -> Self {
-        Self { image }
+        Self {
+            image,
+            smask: None,
+        }
     }
 }
 
 #[cfg(feature = "embedded_images")]
 impl<'a> Image {
     pub fn try_from<T: ImageDecoder<'a>>(image: T) -> Result<Self, image_crate::ImageError> {
-        let image = ImageXObject::try_from(image)?;
-        Ok(Self { image })
+        let (image, smask) = ImageXObject::try_from(image)?;
+        Ok(Self { image, smask })
     }
 
     pub fn from_dynamic_image(image: &DynamicImage) -> Self {
-        Self {
-            image: ImageXObject::from_dynamic_image(image),
-        }
+        let (image, smask) = ImageXObject::from_dynamic_image(image);
+        Self { image, smask }
     }
 }
 
@@ -64,7 +68,7 @@ impl Image {
     ///
     /// You can use the "transform.dpi" parameter to specify a scaling -
     /// the default is 300dpi
-    pub fn add_to_layer(self, layer: PdfLayerReference, transform: ImageTransform) {
+    pub fn add_to_layer(mut self, layer: PdfLayerReference, transform: ImageTransform) {
         use crate::CurTransMat;
         use crate::Pt;
 
@@ -76,6 +80,16 @@ impl Image {
         let image_w = self.image.width.into_pt(dpi);
         let image_h = self.image.height.into_pt(dpi);
 
+        self.image.smask = match self.smask {
+            None => None,
+            Some(smask) => {
+                let doc = layer.document.upgrade().unwrap();
+                let mut doc = doc.borrow_mut();
+                let stream = lopdf::Stream::from(smask);
+                let id = doc.inner_doc.add_object(stream);
+                Some(id)
+            }
+        };
         let image = layer.add_image(self.image);
 
         let scale_x = transform.scale_x.unwrap_or(1.0);
diff --git a/src/xobject.rs b/src/xobject.rs
index 6dd2b00..b475a9b 100644
--- a/src/xobject.rs
+++ b/src/xobject.rs
@@ -150,7 +150,7 @@ pub struct ImageXObject {
     /// Decompression filter for `image_data`, if `None` assumes uncompressed raw pixels in the expected color format.
     pub image_filter: Option<ImageFilter>,
     // SoftMask for transparency, if `None` assumes no transparency. See page 444 of the adope pdf 1.4 reference
-    pub smask: Option<SMask>,
+    pub smask: Option<lopdf::ObjectId>,
     /* /BBox << dictionary >> */
     /* todo: find out if this is really required */
     /// Required bounds to clip the image, in unit space
@@ -183,7 +183,7 @@ impl ImageXObject {
     // }
 
     #[cfg(feature = "embedded_images")]
-    pub fn try_from<'a, T: ImageDecoder<'a>>(image: T) -> Result<Self, ImageError> {
+    pub fn try_from<'a, T: ImageDecoder<'a>>(image: T) -> Result<(Self, Option<Self>), ImageError> {
         use image_crate::error::{LimitError, LimitErrorKind};
         use std::usize;
 
@@ -200,56 +200,24 @@ impl ImageXObject {
         let mut image_data = vec![0; num_image_bytes as usize];
         image.read_image(&mut image_data)?;
 
-        let (processed_color_type, processed_image_data, smask) = preprocess_image_with_alpha(
+        Ok(preprocess_image_with_alpha(
             color_type,
             image_data,
             dim
-        );
-
-        let color_bits = ColorBits::from(processed_color_type);
-        let color_space = ColorSpace::from(processed_color_type);
-
-        Ok(Self {
-            width: Px(dim.0 as usize),
-            height: Px(dim.1 as usize),
-            color_space,
-            bits_per_component: color_bits,
-            image_data: processed_image_data,
-            interpolate: true,
-            image_filter: None,
-            clipping_bbox: None,
-            smask,
-        })
+        ))
     }
 
     #[cfg(feature = "embedded_images")]
-    pub fn from_dynamic_image(image: &DynamicImage) -> Self {
-        use image_crate::EncodableLayout;
-
+    pub fn from_dynamic_image(image: &DynamicImage) -> (Self, Option<Self>) {
         let dim = image.dimensions();
         let color_type = image.color();
         let data = image.as_bytes().to_vec();
 
-        let (processed_color_type, processed_image_data, smask) = preprocess_image_with_alpha(
+        preprocess_image_with_alpha(
             color_type,
             data,
             dim
-        );
-
-        let color_bits = ColorBits::from(processed_color_type);
-        let color_space = ColorSpace::from(processed_color_type);
-
-        Self {
-            width: Px(dim.0 as usize),
-            height: Px(dim.1 as usize),
-            color_space,
-            bits_per_component: color_bits,
-            image_data: processed_image_data,
-            interpolate: true,
-            image_filter: None,
-            clipping_bbox: None,
-            smask,
-        }
+        )
     }
 }
 
@@ -260,24 +228,6 @@ impl From<ImageXObject> for lopdf::Stream {
         let cs: &'static str = img.color_space.into();
         let bbox: lopdf::Object = img.clipping_bbox.unwrap_or(CurTransMat::Identity).into();
 
-        let smask = if let Some(mask) = img.smask {
-            // This is using the "Soft-Mask Images" approach. See page 447 of the adobe PDF 1.4 reference
-            XObject::Image(ImageXObject {
-                width: img.width,
-                height: img.height,
-                color_space: ColorSpace::Greyscale,
-                bits_per_component: ColorBits::Bit8,
-                interpolate: false,
-                image_data: mask.matte.into_iter().map(|i| i as u8).collect(),
-                image_filter: None,
-                smask: None,
-                clipping_bbox: None,
-            })
-            .into()
-        } else {
-            Null
-        };
-
         let mut dict = lopdf::Dictionary::from_iter(vec![
             ("Type", Name("XObject".as_bytes().to_vec())),
             ("Subtype", Name("Image".as_bytes().to_vec())),
@@ -286,9 +236,12 @@ impl From<ImageXObject> for lopdf::Stream {
             ("Interpolate", img.interpolate.into()),
             ("BitsPerComponent", Integer(img.bits_per_component.into())),
             ("ColorSpace", Name(cs.as_bytes().to_vec())),
-            ("SMask", smask),
             ("BBox", bbox),
         ]);
+        if let Some(mask) = img.smask {
+            dict.set("SMask", mask);
+        }
+
 
         if let Some(filter) = img.image_filter {
             let params = match filter {
@@ -515,38 +468,6 @@ impl From<FormType> for i64 {
     /Matte                                      % if present, the SMask must have the same w / h as the picture.
 */
 
-/* Parent: XObject with /Subtype /Image */
-/// `SMask` dictionary. A soft mask (or `SMask`) is a greyscale image
-/// that is used to mask another image
-#[derive(Debug, Clone)]
-pub struct SMask {
-    /* /Type /XObject */
-    /* /Subtype /Image */
-    /* /ColorSpace /DeviceGray */
-
-    /* /Intent (is ignored, don't set) */
-    /* /ImageMask (fals or not set, is ignored, don't set) */
-    /* /Mask (must be absent) */
-    /* /SMask (must be absent) */
-    /* /Decode (must be [0 1]) */
-    /* /Alternates (ignored, don't set) */
-    /* /Name (ignored, don't set) */
-    /* /StructParent (ignored, don't set) */
-    /* /ID (ignored, don't set) */
-    /* /OPI (ignored, don't set) */
-    /// If `self.matte` is set to true, this entry must be the same
-    /// width as the parent image. If not, the `SMask` is resampled to the parent unit square
-    pub width: i64,
-    /// See width
-    pub height: i64,
-    /* /Interpolate (optional, set to true)*/
-    pub interpolate: bool,
-    /// Bits per component, required (warning: this is a grayscale image)
-    pub bits_per_component: i64,
-    /// Vec of component values
-    pub matte: Vec<i64>,
-}
-
 // in the PDF content stream, reference an XObject like this
 
 /*
@@ -634,19 +555,40 @@ impl From<PostScriptXObject> for lopdf::Stream {
 }
 
 #[cfg(feature = "embedded_images")]
-fn preprocess_image_with_alpha(color_type: ColorType, image_data: Vec<u8>, dim: (u32, u32)) -> (ColorType, Vec<u8>, Option<SMask>) {
-    match color_type {
+fn preprocess_image_with_alpha(color_type: ColorType, image_data: Vec<u8>, dim: (u32, u32)) -> (ImageXObject, Option<ImageXObject>) {
+    let (color_type, image_data, smask_data) = match color_type {
         image_crate::ColorType::Rgba8 => {
             let (rgb, alpha) = rgba_to_rgb(image_data);
-            let smask = Some(SMask {
-                bits_per_component: 8,
-                width: dim.0 as i64,
-                height: dim.1 as i64,
-                interpolate: false,
-                matte: alpha.into_iter().map(|b| b as i64).collect(),
-            });
-            (ColorType::Rgb8, rgb, smask)
+            (ColorType::Rgb8, rgb, Some(alpha))
         },
         _ => (color_type, image_data, None)
-    }
+    };
+    let color_bits = ColorBits::from(color_type);
+    let color_space = ColorSpace::from(color_type);
+
+    let img = ImageXObject {
+        width: Px(dim.0 as usize),
+        height: Px(dim.1 as usize),
+        color_space,
+        bits_per_component: color_bits,
+        image_data,
+        interpolate: true,
+        image_filter: None,
+        clipping_bbox: None,
+        smask: None,
+    };
+    let img_mask = smask_data.map(|smask| {
+        ImageXObject {
+            width: img.width,
+            height: img.height,
+            color_space: ColorSpace::Greyscale,
+            bits_per_component: ColorBits::Bit8,
+            interpolate: false,
+            image_data: smask,
+            image_filter: None,
+            clipping_bbox: None,
+            smask: None,
+        }
+    });
+    (img, img_mask)
 }
\ No newline at end of file


Test patch: