fschutt / printpdf

A fully-featured PDF library for Rust, WASM-ready
https://fschutt.github.io/printpdf/
MIT License
832 stars 98 forks source link

Cannot add Png image to pdf page #119

Open anhtumai opened 2 years ago

anhtumai commented 2 years ago

Every time I try to decode a PNG file with PngDecoded and add it to the PDF page, the PDF page is blank.

My Rust code:

use printpdf::*;

use std::fs::File;
use std::io::BufWriter;

fn main() {
    let (doc, page1, layer1) =
        PdfDocument::new("PDF_Document_title", Mm(247.0), Mm(210.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 mut image_file = File::open("inputs/screenshot.png").unwrap();
    let image =
        Image::try_from(image_crate::codecs::png::PngDecoder::new(&mut image_file).unwrap())
            .unwrap();

    println!("{:?}", image.image);

    image.add_to_layer(current_layer.clone(), ImageTransform::default());

    doc.save(&mut BufWriter::new(File::create("output.pdf").unwrap()))
        .unwrap();
}

Cargo.toml:

[package]
name = "test_printpdf"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
printpdf = { version = "0.5", features = ["embedded_images"] }

The PNG file, inputs/screenshot.png: screenshot

The output PDF, output.pdf: image

P/s: This problem does not happen when I decode JPEG files with JpegDecoder. Maybe it is only applied for PNG files

anhtumai commented 2 years ago

One thing to notice is when I try to use img2pdf (https://github.com/josch/img2pdf) with the PNG image, this is the output:

$> img2pdf inputs/screenshot.png
WARNING:root:Image contains transparency which cannot be retained in PDF.
WARNING:root:img2pdf will not perform a lossy operation.
WARNING:root:You can remove the alpha channel using imagemagick:
WARNING:root:  $ convert input.png -background white -alpha remove -alpha off output.png
ERROR:root:error: Refusing to work on images with alpha channel
fschutt commented 2 years ago

see #84

anhtumai commented 2 years ago

as I see, the only way to solve this problem to is convert ImageXObject with color_space RGBA to color_space RGB

anhtumai commented 2 years ago

@fschutt I manage to solve the problem by converting ImageXObject with color space Rgba to color space Rgb with white background.

Editted: this only works with Rgba8 or Bgra8 image.

This is the function:

use printpdf::{xobject::ImageXObject, ColorSpace};

pub fn remove_alpha_channel_from_image_x_object(image_x_object: ImageXObject) -> ImageXObject {
    if !matches!(image_x_object.color_space, ColorSpace::Rgba)
    {
        return image_x_object;
    };
    let ImageXObject {
        color_space,
        image_data,
        ..
    } = image_x_object;

    let new_image_data = image_data
        .chunks(4)
        .map(|rgba| {
            let [red, green, blue, alpha]: [u8; 4] = rgba.try_into().ok().unwrap();
            let alpha = alpha as f64 / 255.0;
            let new_red = ((1.0 - alpha) * 255.0 + alpha * red as f64) as u8;
            let new_green = ((1.0 - alpha) * 255.0 + alpha * green as f64) as u8;
            let new_blue = ((1.0 - alpha) * 255.0 + alpha * blue as f64) as u8;
            return [new_red, new_green, new_blue];
        })
        .collect::<Vec<[u8; 3]>>()
        .concat();

    let new_color_space = match color_space {
        ColorSpace::Rgba => ColorSpace::Rgb,
        ColorSpace::GreyscaleAlpha => ColorSpace::Greyscale,
        other_type => other_type,
    };

    ImageXObject {
        color_space: new_color_space,
        image_data: new_image_data,
        ..image_x_object
    }
}

Do you want this to be part of printpdf source code?

fschutt commented 2 years ago

@anhtumai yeah at least for now it would be a good workaround because this issue comes up again and again

flying-sheep commented 2 years ago

The workaround doesn’t seem to work for me:

/edit: the workaround works as expected, I just had an interfering layer.set_blend_mode(...)

let mut image = Image::try_from(PngDecoder::new(&mut image_file).unwrap()).unwrap();
image.image = remove_alpha_channel_from_image_x_object(image.image);

this PNG (ColourType=Rgba8)

octagon

renders like this (colorful background for illustration purposes):

octagon

anhtumai commented 2 years ago

@flying-sheep How would you render the image?

I rendered the image with printpdf 0.5.3 and it looks like this:

image

Here is my code

use std::{fs::File, io::BufWriter};

use image_crate::codecs::png::PngDecoder;
use printpdf::{
    image_crate, xobject::ImageXObject, ColorSpace, Image, ImageTransform, Mm, PdfDocument,
};

pub fn remove_alpha_channel_from_image_x_object(image_x_object: ImageXObject) -> ImageXObject {
    if !matches!(image_x_object.color_space, ColorSpace::Rgba) {
        return image_x_object;
    };
    let ImageXObject {
        color_space,
        image_data,
        ..
    } = image_x_object;

    let new_image_data = image_data
        .chunks(4)
        .map(|rgba| {
            let [red, green, blue, alpha]: [u8; 4] = rgba.try_into().ok().unwrap();
            let alpha = alpha as f64 / 255.0;
            let new_red = ((1.0 - alpha) * 255.0 + alpha * red as f64) as u8;
            let new_green = ((1.0 - alpha) * 255.0 + alpha * green as f64) as u8;
            let new_blue = ((1.0 - alpha) * 255.0 + alpha * blue as f64) as u8;
            return [new_red, new_green, new_blue];
        })
        .collect::<Vec<[u8; 3]>>()
        .concat();

    let new_color_space = match color_space {
        ColorSpace::Rgba => ColorSpace::Rgb,
        ColorSpace::GreyscaleAlpha => ColorSpace::Greyscale,
        other_type => other_type,
    };

    ImageXObject {
        color_space: new_color_space,
        image_data: new_image_data,
        ..image_x_object
    }
}

fn main() {
    let img_file_name = "/tmp/polygon.png"; // the full path to your polygon image

    // open file
    let mut image_file = File::open(&img_file_name).unwrap();
    let mut image = Image::try_from(PngDecoder::new(&mut image_file).unwrap()).unwrap();

    // turn rbga to rgb
    image.image = remove_alpha_channel_from_image_x_object(image.image);

    // use printpdf to render that image to the new pdf file
    let doc = PdfDocument::empty("output-pdf-file");
    let (page, layer_index) = doc.add_page(Mm(10.0), Mm(10.0), "ImageLayer");
    let current_layer = doc.get_page(page).get_layer(layer_index);
    image.add_to_layer(current_layer, ImageTransform::default());
    doc.save(&mut BufWriter::new(
        File::create("output-pdf-file.pdf").unwrap(),
    ))
    .unwrap();
}

Here is my Cargo.toml content

[package]
name = "test-remove-alpha"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
printpdf = { version = "0.5.3", features = ["embedded_images"] }
flying-sheep commented 2 years ago

you’re going to have to render it in front of anything that isn’t white background to see if it renders correctly or not.

anhtumai commented 2 years ago

you’re going to have to render it in front of anything that isn’t white background to see if it renders correctly or not.

Can you paste your code here?

anhtumai commented 2 years ago

@flying-sheep have you solved the problem?

flying-sheep commented 2 years ago

I looked into it and it’s because in the shape drawing example, global state is modified, sorry.

grafik

(bit hard to see but it’s all transparent)

Adding a layer.set_blend_mode(BlendMode::Seperable(SeperableBlendMode::Normal)); to the last line of draw_bg gives it the expected pre-alpha-channel IE6 webpage look.

grafik

PS: I think there should be some things implementing Default, e.g. BlendMode and SeparableBlendMode.

use std::{fs::File, io::BufWriter};

use image_crate::codecs::png::PngDecoder;
use printpdf::{
    image_crate, xobject::ImageXObject, ColorSpace, Image, ImageTransform, Mm, PdfDocument, Line, Point, PdfLayerReference, Color, Cmyk, Rgb, LineDashPattern, BlendMode, LineCapStyle, LineJoinStyle, SeperableBlendMode, Greyscale,
};

const OCTAGON_BYTES: &'static [u8] = include_bytes!("octagon.png");

pub fn remove_alpha_channel_from_image_x_object(image_x_object: ImageXObject) -> ImageXObject {
    if !matches!(image_x_object.color_space, ColorSpace::Rgba) {
        return image_x_object;
    };
    let ImageXObject {
        color_space,
        image_data,
        ..
    } = image_x_object;

    let new_image_data = image_data
        .chunks(4)
        .map(|rgba| {
            let [red, green, blue, alpha]: [u8; 4] = rgba.try_into().ok().unwrap();
            let alpha = alpha as f64 / 255.0;
            let new_red = ((1.0 - alpha) * 255.0 + alpha * red as f64) as u8;
            let new_green = ((1.0 - alpha) * 255.0 + alpha * green as f64) as u8;
            let new_blue = ((1.0 - alpha) * 255.0 + alpha * blue as f64) as u8;
            return [new_red, new_green, new_blue];
        })
        .collect::<Vec<[u8; 3]>>()
        .concat();

    let new_color_space = match color_space {
        ColorSpace::Rgba => ColorSpace::Rgb,
        ColorSpace::GreyscaleAlpha => ColorSpace::Greyscale,
        other_type => other_type,
    };

    ImageXObject {
        color_space: new_color_space,
        image_data: new_image_data,
        ..image_x_object
    }
}

fn draw_bg(layer: PdfLayerReference) {
    let points1 = vec![
        (Point::new(Mm(4.0), Mm(4.0)), false),
        (Point::new(Mm(4.0), Mm(7.0)), false),
        (Point::new(Mm(7.0), Mm(7.0)), false),
        (Point::new(Mm(7.0), Mm(4.0)), false),
    ];
    let line1 = Line { 
        points: points1, 
        is_closed: true, 
        has_fill: true,
        has_stroke: true,
        is_clipping_path: false,
    };
    let fill_color_2 = Color::Cmyk(Cmyk::new(0.0, 0.0, 0.0, 0.0, None));
    let outline_color_2 = Color::Greyscale(Greyscale::new(0.45, None));

    // More advanced graphical options
    layer.set_overprint_stroke(true);
    layer.set_blend_mode(BlendMode::Seperable(SeperableBlendMode::Multiply));
    layer.set_line_cap_style(LineCapStyle::Round);
    layer.set_line_join_style(LineJoinStyle::Round);
    layer.set_fill_color(fill_color_2);
    layer.set_outline_color(outline_color_2);
    layer.set_outline_thickness(15.0);
    layer.add_shape(line1);

   // layer.set_blend_mode(BlendMode::Seperable(SeperableBlendMode::Normal));
}

fn main() {    
    // open file
    let mut image = Image::try_from(PngDecoder::new(OCTAGON_BYTES).unwrap()).unwrap();

    // turn rbga to rgb
    image.image = remove_alpha_channel_from_image_x_object(image.image);

    // use printpdf to render that image to the new pdf file
    let doc = PdfDocument::empty("output-pdf-file");
    let (page, layer_index) = doc.add_page(Mm(10.0), Mm(10.0), "ImageLayer");
    let current_layer = doc.get_page(page).get_layer(layer_index);

    draw_bg(current_layer.clone());
    image.add_to_layer(current_layer.clone(), ImageTransform::default());

    doc.save(&mut BufWriter::new(
        File::create("output-pdf-file.pdf").unwrap(),
    ))
    .unwrap();
}