From 27771bc329ab23cc551637fb3feac7b5689e64c7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 11 May 2023 14:19:19 +0200 Subject: [PATCH] Let `Document` be `Sync` again Fixes #930. --- src/doc.rs | 17 ++- src/export/pdf/image.rs | 2 +- src/export/render.rs | 2 +- src/image.rs | 256 +++++++++++++++++++++++++++------------- 4 files changed, 187 insertions(+), 90 deletions(-) diff --git a/src/doc.rs b/src/doc.rs index 0a9b9a8dd..c21b6546e 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -683,20 +683,19 @@ cast_to_value! { #[cfg(test)] mod tests { - use crate::{doc::Region, util::option_eq}; - - #[test] - fn test_partialeq_str() { - let region = Region([b'U', b'S']); - assert_eq!(region, "US"); - assert_ne!(region, "AB"); - } + use super::*; + use crate::util::option_eq; #[test] fn test_region_option_eq() { let region = Some(Region([b'U', b'S'])); - assert!(option_eq(region, "US")); assert!(!option_eq(region, "AB")); } + + #[test] + fn test_document_is_send() { + fn ensure_send() {} + ensure_send::(); + } } diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs index dcd5a45a2..7d5656ca0 100644 --- a/src/export/pdf/image.rs +++ b/src/export/pdf/image.rs @@ -19,7 +19,7 @@ pub fn write_images(ctx: &mut PdfContext) { // Add the primary image. // TODO: Error if image could not be encoded. - match image.decoded() { + match image.decoded().as_ref() { DecodedImage::Raster(dynamic, icc, format) => { // TODO: Error if image could not be encoded. let (data, filter, has_color) = encode_image(*format, dynamic).unwrap(); diff --git a/src/export/render.rs b/src/export/render.rs index 31e440d1f..3a5f0d0e2 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -518,7 +518,7 @@ fn render_image( #[comemo::memoize] fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { let mut pixmap = sk::Pixmap::new(w, h)?; - match image.decoded() { + match image.decoded().as_ref() { DecodedImage::Raster(dynamic, _, _) => { let downscale = w < image.width(); let filter = diff --git a/src/image.rs b/src/image.rs index f99745990..210aeddaa 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,13 +1,13 @@ //! Image handling. +use std::cell::RefCell; use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::io; use std::sync::Arc; -use comemo::Tracked; -use ecow::EcoString; +use comemo::{Prehashed, Track, Tracked}; +use ecow::{EcoString, EcoVec}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; @@ -16,40 +16,60 @@ use image::{ImageDecoder, ImageResult}; use usvg::{TreeParsing, TreeTextToPath}; use crate::diag::{format_xml_like_error, StrResult}; +use crate::font::Font; +use crate::geom::Axes; use crate::util::Buffer; use crate::World; /// A raster or vector image. /// /// Values of this type are cheap to clone and hash. -#[derive(Clone)] -pub struct Image { +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Image(Arc>); + +/// The internal representation. +#[derive(Hash)] +struct Repr { /// The raw, undecoded image data. data: Buffer, /// The format of the encoded `buffer`. format: ImageFormat, - /// The decoded image. - decoded: Arc, + /// The size of the image. + size: Axes, + /// A loader for fonts referenced by an image (currently, only applies to + /// SVG). + loader: PreparedLoader, /// A text describing the image. alt: Option, } impl Image { /// Create an image from a buffer and a format. + #[comemo::memoize] pub fn new( data: Buffer, format: ImageFormat, alt: Option, ) -> StrResult { + let loader = PreparedLoader::default(); let decoded = match format { ImageFormat::Raster(format) => decode_raster(&data, format)?, - ImageFormat::Vector(VectorFormat::Svg) => decode_svg(&data)?, + ImageFormat::Vector(VectorFormat::Svg) => { + decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? + } }; - Ok(Self { data, format, decoded, alt }) + Ok(Self(Arc::new(Prehashed::new(Repr { + data, + format, + size: decoded.size(), + loader, + alt, + })))) } /// Create a font-dependant image from a buffer and a format. + #[comemo::memoize] pub fn with_fonts( data: Buffer, format: ImageFormat, @@ -57,44 +77,62 @@ impl Image { fallback_family: Option<&str>, alt: Option, ) -> StrResult { + let loader = WorldLoader::new(world, fallback_family); let decoded = match format { ImageFormat::Raster(format) => decode_raster(&data, format)?, ImageFormat::Vector(VectorFormat::Svg) => { - decode_svg_with_fonts(&data, world, fallback_family)? + decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? } }; - Ok(Self { data, format, decoded, alt }) + Ok(Self(Arc::new(Prehashed::new(Repr { + data, + format, + size: decoded.size(), + loader: loader.into_prepared(), + alt, + })))) } /// The raw image data. pub fn data(&self) -> &Buffer { - &self.data + &self.0.data } /// The format of the image. pub fn format(&self) -> ImageFormat { - self.format + self.0.format } - /// The decoded version of the image. - pub fn decoded(&self) -> &DecodedImage { - &self.decoded + /// The size of the image in pixels. + pub fn size(&self) -> Axes { + self.0.size } /// The width of the image in pixels. pub fn width(&self) -> u32 { - self.decoded().width() + self.size().x } /// The height of the image in pixels. pub fn height(&self) -> u32 { - self.decoded().height() + self.size().y } /// A text describing the image. pub fn alt(&self) -> Option<&str> { - self.alt.as_deref() + self.0.alt.as_deref() + } + + /// The decoded version of the image. + pub fn decoded(&self) -> Arc { + match self.format() { + ImageFormat::Raster(format) => decode_raster(self.data(), format), + ImageFormat::Vector(VectorFormat::Svg) => { + decode_svg(self.data(), (&self.0.loader as &dyn SvgFontLoader).track()) + } + } + .unwrap() } } @@ -109,21 +147,6 @@ impl Debug for Image { } } -impl Eq for Image {} - -impl PartialEq for Image { - fn eq(&self, other: &Self) -> bool { - self.data() == other.data() && self.format() == other.format() - } -} - -impl Hash for Image { - fn hash(&self, state: &mut H) { - self.data().hash(state); - self.format().hash(state); - } -} - /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { @@ -184,6 +207,11 @@ pub enum DecodedImage { } impl DecodedImage { + /// The size of the image in pixels. + pub fn size(&self) -> Axes { + Axes::new(self.width(), self.height()) + } + /// The width of the image in pixels. pub fn width(&self) -> u32 { match self { @@ -230,74 +258,54 @@ fn decode_raster(data: &Buffer, format: RasterFormat) -> StrResult StrResult> { - let opts = usvg::Options::default(); - let tree = usvg::Tree::from_data(data, &opts).map_err(format_usvg_error)?; - Ok(Arc::new(DecodedImage::Svg(tree))) -} - -/// Decode an SVG image with access to fonts. -#[comemo::memoize] -fn decode_svg_with_fonts( +fn decode_svg( data: &Buffer, - world: Tracked, - fallback_family: Option<&str>, + loader: Tracked, ) -> StrResult> { - let mut opts = usvg::Options::default(); - - // Recover the non-lowercased version of the family because - // usvg is case sensitive. - let book = world.book(); - let fallback_family = fallback_family - .and_then(|lowercase| book.select_family(lowercase).next()) - .and_then(|index| book.info(index)) - .map(|info| info.family.clone()); - - if let Some(family) = &fallback_family { - opts.font_family = family.clone(); - } - + // Disable usvg's default to "Times New Roman". Instead, we default to + // the empty family and later, when we traverse the SVG, we check for + // empty and non-existing family names and replace them with the true + // fallback family. This way, we can memoize SVG decoding with and without + // fonts if the SVG does not contain text. + let opts = usvg::Options { font_family: String::new(), ..Default::default() }; 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()); + let fontdb = load_svg_fonts(&tree, loader); tree.convert_text(&fontdb); } - Ok(Arc::new(DecodedImage::Svg(tree))) } /// Discover and load the fonts referenced by an SVG. fn load_svg_fonts( tree: &usvg::Tree, - world: Tracked, - fallback_family: Option<&str>, + loader: Tracked, ) -> fontdb::Database { let mut referenced = BTreeMap::::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) { + let mut load = |family_cased: &str| { + let family = EcoString::from(family_cased.trim()).to_lowercase(); + if let Some(&success) = referenced.get(&family) { return success; } // We load all variants for the family, since we don't know which will // be used. 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; - } + for font in loader.load(&family) { + let source = Arc::new(font.data().clone()); + fontdb.load_font_source(fontdb::Source::Binary(source)); + success = true; } - referenced.insert(lower, success); + referenced.insert(family, success); success }; // Load fallback family. - if let Some(family) = fallback_family { - load(family); + let fallback_cased = loader.fallback(); + if let Some(family_cased) = fallback_cased { + load(family_cased); } // Find out which font families are referenced by the SVG. @@ -305,10 +313,10 @@ fn load_svg_fonts( 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(); + for family_cased in &mut span.font.families { + if family_cased.is_empty() || !load(family_cased) { + let Some(fallback) = fallback_cased else { continue }; + *family_cased = fallback.into(); } } } @@ -329,6 +337,96 @@ where } } +/// Interface for loading fonts for an SVG. +/// +/// Can be backed by a `WorldLoader` or a `PreparedLoader`. The first is used +/// when the image is initially decoded. It records all required fonts and +/// produces a `PreparedLoader` from it. This loader can then be used to +/// redecode the image with a cache hit from the initial decoding. This way, we +/// can cheaply access the decoded version of an image. +/// +/// The alternative would be to store the decoded image directly in the image, +/// but that would make `Image` not `Send` because `usvg::Tree` is not `Send`. +/// The current design also has the added benefit that large decoded images can +/// be evicted if they are not used anymore. +#[comemo::track] +trait SvgFontLoader { + /// Load all fonts for the given lowercased font family. + fn load(&self, lower_family: &str) -> EcoVec; + + /// The case-sensitive name of the fallback family. + fn fallback(&self) -> Option<&str>; +} + +/// Loads fonts for an SVG from a world +struct WorldLoader<'a> { + world: Tracked<'a, dyn World + 'a>, + seen: RefCell>>, + fallback_family_cased: Option, +} + +impl<'a> WorldLoader<'a> { + fn new(world: Tracked<'a, dyn World + 'a>, fallback_family: Option<&str>) -> Self { + // Recover the non-lowercased version of the family because + // usvg is case sensitive. + let book = world.book(); + let fallback_family_cased = fallback_family + .and_then(|lowercase| book.select_family(lowercase).next()) + .and_then(|index| book.info(index)) + .map(|info| info.family.clone()); + + Self { + world, + fallback_family_cased, + seen: Default::default(), + } + } + + fn into_prepared(self) -> PreparedLoader { + PreparedLoader { + families: self.seen.into_inner(), + fallback_family_cased: self.fallback_family_cased, + } + } +} + +impl SvgFontLoader for WorldLoader<'_> { + fn load(&self, family: &str) -> EcoVec { + self.seen + .borrow_mut() + .entry(family.into()) + .or_insert_with(|| { + self.world + .book() + .select_family(family) + .filter_map(|id| self.world.font(id)) + .collect() + }) + .clone() + } + + fn fallback(&self) -> Option<&str> { + self.fallback_family_cased.as_deref() + } +} + +/// Loads fonts for an SVG from a prepared list. +#[derive(Default, Hash)] +struct PreparedLoader { + families: BTreeMap>, + fallback_family_cased: Option, +} + +impl SvgFontLoader for PreparedLoader { + fn load(&self, family: &str) -> EcoVec { + self.families.get(family).cloned().unwrap_or_default() + } + + fn fallback(&self) -> Option<&str> { + self.fallback_family_cased.as_deref() + } +} + /// Format the user-facing raster graphic decoding error message. fn format_image_error(error: image::ImageError) -> EcoString { match error {