diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/diag.rs | 1 | ||||
| -rw-r--r-- | src/export/pdf/image.rs | 22 | ||||
| -rw-r--r-- | src/export/pdf/outline.rs | 2 | ||||
| -rw-r--r-- | src/export/pdf/page.rs | 4 | ||||
| -rw-r--r-- | src/export/render.rs | 72 | ||||
| -rw-r--r-- | src/image.rs | 132 |
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); } } |
