summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2023-05-02 13:53:20 +0200
committerGitHub <noreply@github.com>2023-05-02 13:53:20 +0200
commit17cef8dcee022ca89ccef62eaec23ff6c0e4cdf9 (patch)
tree253ea22f94ac90f0df91d6e20758ecc9d7271ce8 /src
parent021694de239ca5d6d799dde98423036bb1da405e (diff)
Add ICC profiles to images in PDF and update `usvg`, `svg2pdf` (#822)
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs1
-rw-r--r--src/export/pdf/image.rs22
-rw-r--r--src/export/pdf/outline.rs2
-rw-r--r--src/export/pdf/page.rs4
-rw-r--r--src/export/render.rs72
-rw-r--r--src/image.rs132
6 files changed, 150 insertions, 83 deletions
diff --git a/src/diag.rs b/src/diag.rs
index 88141062..556d3255 100644
--- a/src/diag.rs
+++ b/src/diag.rs
@@ -273,7 +273,6 @@ pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString
roxmltree::Error::NoRootNode => {
eco_format!("failed to parse {format}: missing root node")
}
- roxmltree::Error::SizeLimit => "file is too large".into(),
_ => eco_format!("failed to parse {format}"),
}
}
diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs
index 04d4dcc3..dcd5a45a 100644
--- a/src/export/pdf/image.rs
+++ b/src/export/pdf/image.rs
@@ -11,6 +11,7 @@ use crate::image::{DecodedImage, RasterFormat};
pub fn write_images(ctx: &mut PdfContext) {
for image in ctx.image_map.items() {
let image_ref = ctx.alloc.bump();
+ let icc_ref = ctx.alloc.bump();
ctx.image_refs.push(image_ref);
let width = image.width();
@@ -19,7 +20,7 @@ pub fn write_images(ctx: &mut PdfContext) {
// Add the primary image.
// TODO: Error if image could not be encoded.
match image.decoded() {
- DecodedImage::Raster(dynamic, format) => {
+ DecodedImage::Raster(dynamic, icc, format) => {
// TODO: Error if image could not be encoded.
let (data, filter, has_color) = encode_image(*format, dynamic).unwrap();
let mut image = ctx.writer.image_xobject(image_ref, &data);
@@ -29,7 +30,9 @@ pub fn write_images(ctx: &mut PdfContext) {
image.bits_per_component(8);
let space = image.color_space();
- if has_color {
+ if icc.is_some() {
+ space.icc_based(icc_ref);
+ } else if has_color {
space.device_rgb();
} else {
space.device_gray();
@@ -49,6 +52,21 @@ pub fn write_images(ctx: &mut PdfContext) {
mask.height(height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
+ } else {
+ image.finish();
+ }
+
+ if let Some(icc) = icc {
+ let compressed = deflate(&icc.0);
+ let mut stream = ctx.writer.icc_profile(icc_ref, &compressed);
+ stream.filter(Filter::FlateDecode);
+ if has_color {
+ stream.n(3);
+ stream.alternate().srgb();
+ } else {
+ stream.n(1);
+ stream.alternate().d65_gray();
+ }
}
}
DecodedImage::Svg(svg) => {
diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs
index f8f12d71..c156ecaf 100644
--- a/src/export/pdf/outline.rs
+++ b/src/export/pdf/outline.rs
@@ -118,7 +118,7 @@ fn write_outline_item(
let index = pos.page.get() - 1;
if let Some(&height) = ctx.page_heights.get(index) {
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
- outline.dest_direct().page(ctx.page_refs[index]).xyz(
+ outline.dest().page(ctx.page_refs[index]).xyz(
pos.point.x.to_f32(),
height - y.to_f32(),
None,
diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs
index acf5062e..35a4f5dc 100644
--- a/src/export/pdf/page.rs
+++ b/src/export/pdf/page.rs
@@ -139,7 +139,7 @@ fn write_page(ctx: &mut PdfContext, page: Page) {
annotation
.action()
.action_type(ActionType::GoTo)
- .destination_direct()
+ .destination()
.page(ctx.page_refs[index])
.xyz(pos.point.x.to_f32(), height - y.to_f32(), None);
}
@@ -499,7 +499,7 @@ fn write_image(ctx: &mut PageContext, x: f32, y: f32, image: &Image, size: Size)
if let Some(alt) = image.alt() {
let mut image_span =
ctx.content.begin_marked_content_with_properties(Name(b"Span"));
- let mut image_alt = image_span.properties_direct();
+ let mut image_alt = image_span.properties();
image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes()));
image_alt.finish();
image_span.finish();
diff --git a/src/export/render.rs b/src/export/render.rs
index fa3dc4b5..31e440d1 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -5,9 +5,10 @@ use std::sync::Arc;
use image::imageops::FilterType;
use image::{GenericImageView, Rgba};
+use resvg::FitTo;
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
-use usvg::{FitTo, NodeExt};
+use usvg::{NodeExt, TreeParsing};
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::geom::{
@@ -38,7 +39,7 @@ pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
fn render_frame(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
frame: &Frame,
) {
for (pos, item) in frame.items() {
@@ -73,13 +74,13 @@ fn render_frame(
fn render_group(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
group: &GroupItem,
) {
let ts = ts.pre_concat(group.transform.into());
let mut mask = mask;
- let mut storage;
+ let storage;
if group.clips {
let size = group.frame.size();
let w = size.x.to_f32();
@@ -88,22 +89,33 @@ fn render_group(
.map(sk::PathBuilder::from_rect)
.and_then(|path| path.transform(ts))
{
- let result = if let Some(mask) = mask {
- storage = mask.clone();
- storage.intersect_path(&path, sk::FillRule::default(), false)
+ if let Some(mask) = mask {
+ let mut mask = mask.clone();
+ mask.intersect_path(
+ &path,
+ sk::FillRule::default(),
+ false,
+ sk::Transform::default(),
+ );
+ storage = mask;
} else {
let pxw = canvas.width();
let pxh = canvas.height();
- storage = sk::ClipMask::new();
- storage.set_path(pxw, pxh, &path, sk::FillRule::default(), false)
+ let Some(mut mask) = sk::Mask::new(pxw, pxh) else {
+ // Fails if clipping rect is empty. In that case we just
+ // clip everything by returning.
+ return;
+ };
+
+ mask.fill_path(
+ &path,
+ sk::FillRule::default(),
+ false,
+ sk::Transform::default(),
+ );
+ storage = mask;
};
- // Clipping fails if clipping rect is empty. In that case we just
- // clip everything by returning.
- if result.is_none() {
- return;
- }
-
mask = Some(&storage);
}
}
@@ -115,7 +127,7 @@ fn render_group(
fn render_text(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
text: &TextItem,
) {
let mut x = 0.0;
@@ -136,7 +148,7 @@ fn render_text(
fn render_svg_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@@ -157,8 +169,8 @@ fn render_svg_glyph(
// Parse SVG.
let opts = usvg::Options::default();
- let tree = usvg::Tree::from_xmltree(&document, &opts.to_ref()).ok()?;
- let view_box = tree.svg_node().view_box.rect;
+ let tree = usvg::Tree::from_xmltree(&document, &opts).ok()?;
+ let view_box = tree.view_box.rect;
// If there's no viewbox defined, use the em square for our scale
// transformation ...
@@ -182,7 +194,7 @@ fn render_svg_glyph(
// See https://github.com/RazrFalcon/resvg/issues/602 for why
// using the svg size is problematic here.
let mut bbox = usvg::Rect::new_bbox();
- for node in tree.root().descendants() {
+ for node in tree.root.descendants() {
if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) {
bbox = bbox.expand(rect);
}
@@ -224,14 +236,16 @@ fn render_svg_glyph(
&sk::PixmapPaint::default(),
sk::Transform::identity(),
mask,
- )
+ );
+
+ Some(())
}
/// Render a bitmap glyph into the canvas.
fn render_bitmap_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@@ -255,7 +269,7 @@ fn render_bitmap_glyph(
fn render_outline_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@@ -278,7 +292,7 @@ fn render_outline_glyph(
// system is Y-up.
let scale = text.size.to_f32() / text.font.units_per_em() as f32;
let ts = ts.pre_scale(scale, -scale);
- canvas.fill_path(&path, &paint, rule, ts, mask)?;
+ canvas.fill_path(&path, &paint, rule, ts, mask);
return Some(());
}
@@ -318,7 +332,9 @@ fn render_outline_glyph(
&sk::PixmapPaint::default(),
sk::Transform::identity(),
mask,
- )
+ );
+
+ Some(())
} else {
let cw = canvas.width() as i32;
let ch = canvas.height() as i32;
@@ -365,7 +381,7 @@ fn render_outline_glyph(
fn render_shape(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
shape: &Shape,
) -> Option<()> {
let path = match shape.geometry {
@@ -465,7 +481,7 @@ fn convert_path(path: &geom::Path) -> Option<sk::Path> {
fn render_image(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- mask: Option<&sk::ClipMask>,
+ mask: Option<&sk::Mask>,
image: &Image,
size: Size,
) -> Option<()> {
@@ -503,7 +519,7 @@ fn render_image(
fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let mut pixmap = sk::Pixmap::new(w, h)?;
match image.decoded() {
- DecodedImage::Raster(dynamic, _) => {
+ DecodedImage::Raster(dynamic, _, _) => {
let downscale = w < image.width();
let filter =
if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
diff --git a/src/image.rs b/src/image.rs
index 49d91908..d78c7428 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,6 +1,6 @@
//! Image handling.
-use std::collections::BTreeSet;
+use std::collections::BTreeMap;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::io;
@@ -8,6 +8,12 @@ use std::sync::Arc;
use comemo::Tracked;
use ecow::EcoString;
+use image::codecs::gif::GifDecoder;
+use image::codecs::jpeg::JpegDecoder;
+use image::codecs::png::PngDecoder;
+use image::io::Limits;
+use image::{ImageDecoder, ImageResult};
+use usvg::{TreeParsing, TreeTextToPath};
use crate::diag::{format_xml_like_error, StrResult};
use crate::util::Buffer;
@@ -171,8 +177,8 @@ impl From<ttf_parser::RasterImageFormat> for ImageFormat {
/// A decoded image.
pub enum DecodedImage {
- /// A decoded pixel raster.
- Raster(image::DynamicImage, RasterFormat),
+ /// A decoded pixel raster with its ICC profile.
+ Raster(image::DynamicImage, Option<IccProfile>, RasterFormat),
/// An decoded SVG tree.
Svg(usvg::Tree),
}
@@ -181,34 +187,52 @@ impl DecodedImage {
/// The width of the image in pixels.
pub fn width(&self) -> u32 {
match self {
- Self::Raster(dynamic, _) => dynamic.width(),
- Self::Svg(tree) => tree.svg_node().size.width().ceil() as u32,
+ Self::Raster(dynamic, _, _) => dynamic.width(),
+ Self::Svg(tree) => tree.size.width().ceil() as u32,
}
}
/// The height of the image in pixels.
pub fn height(&self) -> u32 {
match self {
- Self::Raster(dynamic, _) => dynamic.height(),
- Self::Svg(tree) => tree.svg_node().size.height().ceil() as u32,
+ Self::Raster(dynamic, _, _) => dynamic.height(),
+ Self::Svg(tree) => tree.size.height().ceil() as u32,
}
}
}
+/// Raw data for of an ICC profile.
+pub struct IccProfile(pub Vec<u8>);
+
/// Decode a raster image.
#[comemo::memoize]
fn decode_raster(data: &Buffer, format: RasterFormat) -> StrResult<Arc<DecodedImage>> {
- let cursor = io::Cursor::new(&data);
- let reader = image::io::Reader::with_format(cursor, format.into());
- let dynamic = reader.decode().map_err(format_image_error)?;
- Ok(Arc::new(DecodedImage::Raster(dynamic, format)))
+ fn decode_with<'a, T: ImageDecoder<'a>>(
+ decoder: ImageResult<T>,
+ ) -> ImageResult<(image::DynamicImage, Option<IccProfile>)> {
+ let mut decoder = decoder?;
+ let icc = decoder.icc_profile().map(IccProfile);
+ decoder.set_limits(Limits::default())?;
+ let dynamic = image::DynamicImage::from_decoder(decoder)?;
+ Ok((dynamic, icc))
+ }
+
+ let cursor = io::Cursor::new(data);
+ let (dynamic, icc) = match format {
+ RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
+ RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
+ RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
+ }
+ .map_err(format_image_error)?;
+
+ Ok(Arc::new(DecodedImage::Raster(dynamic, icc, format)))
}
/// Decode an SVG image.
#[comemo::memoize]
fn decode_svg(data: &Buffer) -> StrResult<Arc<DecodedImage>> {
let opts = usvg::Options::default();
- let tree = usvg::Tree::from_data(data, &opts.to_ref()).map_err(format_usvg_error)?;
+ let tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?;
Ok(Arc::new(DecodedImage::Svg(tree)))
}
@@ -219,79 +243,89 @@ fn decode_svg_with_fonts(
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> StrResult<Arc<DecodedImage>> {
- // Parse XML.
- let xml = std::str::from_utf8(data)
- .map_err(|_| format_usvg_error(usvg::Error::NotAnUtf8Str))?;
- let document = roxmltree::Document::parse(xml)
- .map_err(|err| format_xml_like_error("svg", err))?;
-
- // Parse SVG.
- let mut opts = usvg::Options {
- fontdb: load_svg_fonts(&document, world, fallback_family),
- ..Default::default()
- };
+ let mut opts = usvg::Options::default();
// Recover the non-lowercased version of the family because
// usvg is case sensitive.
let book = world.book();
- if let Some(family) = fallback_family
+ let fallback_family = fallback_family
.and_then(|lowercase| book.select_family(lowercase).next())
.and_then(|index| book.info(index))
- .map(|info| info.family.clone())
- {
- opts.font_family = family;
+ .map(|info| info.family.clone());
+
+ if let Some(family) = &fallback_family {
+ opts.font_family = family.clone();
}
- let tree =
- usvg::Tree::from_xmltree(&document, &opts.to_ref()).map_err(format_usvg_error)?;
+ let mut tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?;
+ if tree.has_text_nodes() {
+ let fontdb = load_svg_fonts(&tree, world, fallback_family.as_deref());
+ tree.convert_text(&fontdb);
+ }
Ok(Arc::new(DecodedImage::Svg(tree)))
}
/// Discover and load the fonts referenced by an SVG.
fn load_svg_fonts(
- document: &roxmltree::Document,
+ tree: &usvg::Tree,
world: Tracked<dyn World>,
fallback_family: Option<&str>,
) -> fontdb::Database {
- // Find out which font families are referenced by the SVG. We simply do a
- // search for `font-family` attributes. This won't help with CSS, but usvg
- // 22.0 doesn't seem to support it anyway. Once we bump to the latest usvg,
- // this can be replaced by a scan for text elements in the SVG:
- // https://github.com/RazrFalcon/resvg/issues/555
- let mut referenced = BTreeSet::<EcoString>::new();
- traverse_xml(&document.root(), &mut |node| {
- if let Some(list) = node.attribute("font-family") {
- for family in list.split(',') {
- referenced.insert(EcoString::from(family.trim()).to_lowercase());
- }
+ let mut referenced = BTreeMap::<EcoString, bool>::new();
+ let mut fontdb = fontdb::Database::new();
+ let mut load = |family: &str| {
+ let lower = EcoString::from(family.trim()).to_lowercase();
+ if let Some(&success) = referenced.get(&lower) {
+ return success;
}
- });
- // Prepare font database.
- let mut fontdb = fontdb::Database::new();
- for family in referenced.iter().map(|family| family.as_str()).chain(fallback_family) {
// We load all variants for the family, since we don't know which will
// be used.
- for id in world.book().select_family(family) {
+ let mut success = false;
+ for id in world.book().select_family(&lower) {
if let Some(font) = world.font(id) {
let source = Arc::new(font.data().clone());
fontdb.load_font_source(fontdb::Source::Binary(source));
+ success = true;
}
}
+
+ referenced.insert(lower, success);
+ success
+ };
+
+ // Load fallback family.
+ if let Some(family) = fallback_family {
+ load(family);
}
+ // Find out which font families are referenced by the SVG.
+ traverse_svg(&tree.root, &mut |node| {
+ let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return };
+ for chunk in &mut text.chunks {
+ for span in &mut chunk.spans {
+ for family in &mut span.font.families {
+ if !load(family) {
+ let Some(fallback) = fallback_family else { continue };
+ *family = fallback.into();
+ }
+ }
+ }
+ }
+ });
+
fontdb
}
/// Search for all font families referenced by an SVG.
-fn traverse_xml<F>(node: &roxmltree::Node, f: &mut F)
+fn traverse_svg<F>(node: &usvg::Node, f: &mut F)
where
- F: FnMut(&roxmltree::Node),
+ F: FnMut(&usvg::Node),
{
f(node);
for child in node.children() {
- traverse_xml(&child, f);
+ traverse_svg(&child, f);
}
}