Skip to content

Commit

Permalink
adding support for rgba8 images
Browse files Browse the repository at this point in the history
  • Loading branch information
michael.paris committed Oct 26, 2023
1 parent cec012a commit 7dc8c69
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 10 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ required-features = []
name = "image"
required-features = ["embedded_images"]

[[example]]
name = "image_alpha"
required-features = ["embedded_images"]

[[example]]
name = "no_icc"
required-features = []
Expand Down
Binary file added assets/img/dog_alpha.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions examples/image_alpha.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
extern crate printpdf;

use image_crate::codecs::png::PngDecoder;
use printpdf::*;
use std::fs::File;
use std::io::BufWriter;
use std::io::Cursor;

fn main() {
let (doc, page1, layer1) =
PdfDocument::new("printpdf graphics test", Mm(210.0), Mm(297.0), "Layer 1");
let current_layer = doc.get_page(page1).get_layer(layer1);

// currently, the only reliable file formats are bmp/jpeg/png
// this is an issue of the image library, not a fault of printpdf

let image_bytes = include_bytes!("../assets/img/dog_alpha.png");
let mut reader = Cursor::new(image_bytes.as_ref());

let decoder = PngDecoder::new(&mut reader).unwrap();
let image = Image::try_from(decoder).unwrap();

let rotation_center_x = Px((image.image.width.0 as f32 / 2.0) as usize);
let rotation_center_y = Px((image.image.height.0 as f32 / 2.0) as usize);

// layer,
image.add_to_layer(
current_layer.clone(),
ImageTransform {
rotate: Some(ImageRotation {
angle_ccw_degrees: 0.0,
rotation_center_x,
rotation_center_y,
}),
translate_x: Some(Mm(50.0)),
translate_y: Some(Mm(50.0)),
..Default::default()
},
);

doc.save(&mut BufWriter::new(File::create("test_image.pdf").unwrap()))
.unwrap();
}
14 changes: 14 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,17 @@ pub(crate) fn random_character_string_32() -> String {
fn u8_to_char(input: u8) -> char {
(b'A' + input) as char
}

/// Takes a Vec<u8> of RGBA data and returns two Vec<u8> of RGB and alpha data
pub(crate) fn rgba_to_rgb(data: Vec<u8>) -> (Vec<u8>, Vec<u8>) {
let mut rgb = Vec::with_capacity(data.len() / 4 * 3);
let mut alpha = Vec::with_capacity(data.len() / 4);
for i in (0..data.len()).step_by(4) {
rgb.push(data[i]);
rgb.push(data[i + 1]);
rgb.push(data[i + 2]);
alpha.push(data[i + 3]);
}

(rgb, alpha)
}
87 changes: 77 additions & 10 deletions src/xobject.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// clippy lints when serializing PDF strings, in this case its wrong
#![cfg_attr(feature = "cargo-clippy", allow(clippy::string_lit_as_bytes))]

use crate::OffsetDateTime;
use crate::{OffsetDateTime, rgba_to_rgb};
use crate::{ColorBits, ColorSpace, CurTransMat, Px};
use image_crate::ColorType;
#[cfg(feature = "embedded_images")]
use image_crate::{DynamicImage, GenericImageView, ImageDecoder, ImageError};
use lopdf;
Expand Down Expand Up @@ -146,6 +147,8 @@ pub struct ImageXObject {
pub image_data: Vec<u8>,
/// 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>,
/* /BBox << dictionary >> */
/* todo: find out if this is really required */
/// Required bounds to clip the image, in unit space
Expand Down Expand Up @@ -195,38 +198,55 @@ impl ImageXObject {
let mut image_data = vec![0; num_image_bytes as usize];
image.read_image(&mut image_data)?;

let color_bits = ColorBits::from(color_type);
let color_space = ColorSpace::from(color_type);
let (processed_color_type, processed_image_data, smask) = 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,
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;

let dim = image.dimensions();
let color_type = image.color();
let data = image.as_bytes().to_vec();
let color_bits = ColorBits::from(color_type);
let color_space = ColorSpace::from(color_type);

let (processed_color_type, processed_image_data, smask) = 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: data,
image_data: processed_image_data,
interpolate: true,
image_filter: None,
clipping_bbox: None,
smask,
}
}
}
Expand All @@ -238,6 +258,24 @@ 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.width,
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())),
Expand All @@ -246,6 +284,7 @@ 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),
]);

Expand Down Expand Up @@ -397,11 +436,22 @@ impl From<FormXObject> for lopdf::Stream {
fn from(val: FormXObject) -> Self {
use lopdf::Object::*;

let dict = lopdf::Dictionary::from_iter(vec![
let components = vec![
("Type", Name("XObject".as_bytes().to_vec())),
("Subtype", Name("Form".as_bytes().to_vec())),
("FormType", Integer(val.form_type.into())),
]);
];

// if let Some(_) = val.group {
// components.push((
// "Group",
// Dictionary(lopdf::Dictionary::from_iter(vec![
// ("S", Name("Transparency".as_bytes().to_vec())), // the only valid option
// ])),
// ));
// }

let dict = lopdf::Dictionary::from_iter(components);

lopdf::Stream::new(dict, val.bytes)
}
Expand Down Expand Up @@ -477,7 +527,7 @@ impl From<FormType> for i64 {
/* 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)]
#[derive(Debug, Clone)]
pub struct SMask {
/* /Type /XObject */
/* /Subtype /Image */
Expand Down Expand Up @@ -591,3 +641,20 @@ impl From<PostScriptXObject> for lopdf::Stream {
lopdf::Stream::new(lopdf::Dictionary::new(), Vec::new())
}
}

fn preprocess_image_with_alpha(color_type: ColorType, image_data: Vec<u8>, dim: (u32, u32)) -> (ColorType, Vec<u8>, Option<SMask>) {
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)
},
_ => (color_type, image_data, None)
}
}

0 comments on commit 7dc8c69

Please sign in to comment.