From 006f621a30fdd57e81d1cf133f109322037748f0 Mon Sep 17 00:00:00 2001 From: Martin Haug Date: Tue, 1 Mar 2022 13:00:27 +0100 Subject: [PATCH] Support GIFs, bump deps --- Cargo.toml | 9 +-- src/render.rs | 163 ++++++++++++++++++++++++++++---------------------- 2 files changed, 97 insertions(+), 75 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f1965bc..44151a21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,19 @@ categories = ["encoding", "graphics", "multimedia"] keywords = ["svg", "pdf", "vector-graphics", "conversion"] [features] -default = ["png", "jpeg"] +default = ["png", "jpeg", "gif"] png = ["image/png"] jpeg = ["image/jpeg"] +gif = ["image/gif"] cli = ["clap", "termcolor", "usvg/text", "fontdb"] [dependencies] miniz_oxide = "0.4" pdf-writer = "0.4.1" -usvg = { version = "0.20", default-features = false } +usvg = { version = "0.22", default-features = false } clap = { version = "3", features = ["derive"], optional = true } -fontdb = { version = "0.7", optional = true } -image = { version = "0.23", default-features = false, optional = true } +fontdb = { version = "0.9", optional = true } +image = { version = "0.24", default-features = false, optional = true } termcolor = { version = "1", optional = true } [[bin]] diff --git a/src/render.rs b/src/render.rs index 23e2c6a0..619a879e 100644 --- a/src/render.rs +++ b/src/render.rs @@ -15,7 +15,7 @@ use usvg::{ #[cfg(any(feature = "png", feature = "jpeg"))] use { image::io::Reader as ImageReader, - image::{DynamicImage, GenericImageView, ImageFormat, Rgb, Rgba, Luma}, + image::{DynamicImage, ImageFormat, Luma, Rgb, Rgba}, pdf_writer::writers::ImageXObject, }; @@ -553,7 +553,7 @@ impl Render for usvg::Image { let image_ref = ctx.alloc_ref(); - #[cfg(any(feature = "png", feature = "jpeg"))] + #[cfg(any(feature = "png", feature = "jpeg", feature = "gif"))] let set_image_props = | image: &mut ImageXObject, raster_size: &mut Option<(u32, u32)>, @@ -576,14 +576,75 @@ impl Render for usvg::Image { } }; - #[cfg(any(feature = "png", feature = "jpeg"))] + #[cfg(any(feature = "png", feature = "jpeg", feature = "gif"))] let mut raster_size: Option<(u32, u32)> = None; let rect = self.view_box.rect; + #[cfg(any(feature = "png", feature = "gif"))] + let mut apply_transparent = |decoded: DynamicImage| { + let color = decoded.color(); + + let bits = color.bits_per_pixel(); + let channels = color.channel_count() as u16; + let image_bytes: Vec = match (channels, bits / channels > 8) { + (1, false) => { + decoded.to_luma8().pixels().flat_map(|&Luma(c)| c).collect() + } + (1, true) => decoded + .to_luma16() + .pixels() + .flat_map(|&Luma(x)| x) + .flat_map(|x| x.to_be_bytes()) + .collect(), + (3 | 4, false) => { + decoded.to_rgb8().pixels().flat_map(|&Rgb(c)| c).collect() + } + (3 | 4, true) => decoded + .to_rgb16() + .pixels() + .flat_map(|&Rgb(c)| c) + .flat_map(|x| x.to_be_bytes()) + .collect(), + _ => panic!("unknown number of channels={channels}"), + }; + let compressed = compress_to_vec_zlib(&image_bytes, 8); + + let mut image = writer.image_xobject(image_ref, &compressed); + set_image_props(&mut image, &mut raster_size, &decoded, false); + image.filter(Filter::FlateDecode); + + // The alpha channel has to be written separately, as a Soft + // Mask. + if color.has_alpha() { + let mask_id = ctx.alloc_ref(); + image.pair(Name(b"SMask"), mask_id); + image.finish(); + + let bits = color.bits_per_pixel(); + let channels = color.channel_count() as u16; + let alpha_bytes: Vec = if bits / channels > 8 { + decoded + .to_rgba16() + .pixels() + .flat_map(|&Rgba([.., a])| a.to_be_bytes()) + .collect() + } else { + decoded.to_rgba8().pixels().map(|&Rgba([.., a])| a).collect() + }; + + let compressed = compress_to_vec_zlib(&alpha_bytes, 8); + let mut mask = writer.image_xobject(mask_id, &compressed); + let mut void = None; + + set_image_props(&mut mask, &mut void, &decoded, true); + mask.filter(Filter::FlateDecode); + } + }; + match &self.kind { #[cfg(feature = "jpeg")] ImageKind::JPEG(buf) => { - let cursor = std::io::Cursor::new(buf); + let cursor = std::io::Cursor::new(buf.as_ref()); let decoded = if let Ok(decoded) = ImageReader::with_format(cursor, ImageFormat::Jpeg).decode() { @@ -598,72 +659,29 @@ impl Render for usvg::Image { } #[cfg(feature = "png")] ImageKind::PNG(buf) => { - let cursor = std::io::Cursor::new(buf); - let decoded = if let Ok(decoded) = - ImageReader::with_format(cursor, ImageFormat::Png).decode() - { - decoded - } else { - return; - }; - - let color = decoded.color(); - - let bits = color.bits_per_pixel(); - let channels = color.channel_count() as u16; - let image_bytes: Vec = match (channels, bits / channels > 8) { - (1, false) => { - decoded.to_luma8().pixels().flat_map(|&Luma(c)| c).collect() - } - (1, true) => decoded - .to_luma16() - .pixels() - .flat_map(|&Luma(x)| x) - .flat_map(|x| x.to_be_bytes()) - .collect(), - (3 | 4, false) => { - decoded.to_rgb8().pixels().flat_map(|&Rgb(c)| c).collect() - } - (3 | 4, true) => decoded - .to_rgb16() - .pixels() - .flat_map(|&Rgb(c)| c) - .flat_map(|x| x.to_be_bytes()) - .collect(), - _ => panic!("unknown number of channels={channels}"), - }; - let compressed = compress_to_vec_zlib(&image_bytes, 8); - - let mut image = writer.image_xobject(image_ref, &compressed); - set_image_props(&mut image, &mut raster_size, &decoded, false); - image.filter(Filter::FlateDecode); - - // The alpha channel has to be written separately, as a Soft - // Mask. - if color.has_alpha() { - let mask_id = ctx.alloc_ref(); - image.pair(Name(b"SMask"), mask_id); - image.finish(); - - let bits = color.bits_per_pixel(); - let channels = color.channel_count() as u16; - let alpha_bytes: Vec = if bits / channels > 8 { + let cursor = std::io::Cursor::new(buf.as_ref()); + apply_transparent( + if let Ok(decoded) = + ImageReader::with_format(cursor, ImageFormat::Png).decode() + { decoded - .to_rgba16() - .pixels() - .flat_map(|&Rgba([.., a])| a.to_be_bytes()) - .collect() } else { - decoded.to_rgba8().pixels().map(|&Rgba([.., a])| a).collect() - }; - - let compressed = compress_to_vec_zlib(&alpha_bytes, 8); - let mut mask = writer.image_xobject(mask_id, &compressed); - let mut void = None; - - set_image_props(&mut mask, &mut void, &decoded, true); - mask.filter(Filter::FlateDecode); - } + return; + }, + ); + } + #[cfg(feature = "gif")] + ImageKind::GIF(buf) => { + let cursor = std::io::Cursor::new(buf.as_ref()); + apply_transparent( + if let Ok(decoded) = + ImageReader::with_format(cursor, ImageFormat::Gif).decode() + { + decoded + } else { + return; + }, + ); } ImageKind::SVG(tree) => { // An SVG image means that the file gets embedded in a @@ -677,13 +695,16 @@ impl Render for usvg::Image { ctx.next_id = convert_tree_into(tree, opt, writer, image_ref).get(); } - - #[cfg(any(not(feature = "jpeg"), not(feature = "png")))] + #[cfg(any( + not(feature = "jpeg"), + not(feature = "png"), + not(feature = "gif") + ))] _ => {} } // Common operations for raster image formats. - #[cfg(any(feature = "png", feature = "jpeg"))] + #[cfg(any(feature = "png", feature = "jpeg", feature = "gif"))] let image_ref = if let Some((width, height)) = raster_size { let mut content = Content::new(); let xobj_name = Name(b"EmbRaster");