summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/doc.rs17
-rw-r--r--src/export/pdf/image.rs2
-rw-r--r--src/export/render.rs2
-rw-r--r--src/image.rs256
4 files changed, 187 insertions, 90 deletions
diff --git a/src/doc.rs b/src/doc.rs
index 0a9b9a8d..c21b6546 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<T: Send>() {}
+ ensure_send::<Document>();
+ }
}
diff --git a/src/export/pdf/image.rs b/src/export/pdf/image.rs
index dcd5a45a..7d5656ca 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 31e440d1..3a5f0d0e 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<Arc<sk::Pixmap>> {
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 f9974599..210aedda 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<Prehashed<Repr>>);
+
+/// 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<DecodedImage>,
+ /// The size of the image.
+ size: Axes<u32>,
+ /// A loader for fonts referenced by an image (currently, only applies to
+ /// SVG).
+ loader: PreparedLoader,
/// A text describing the image.
alt: Option<EcoString>,
}
impl Image {
/// Create an image from a buffer and a format.
+ #[comemo::memoize]
pub fn new(
data: Buffer,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Self> {
+ 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<EcoString>,
) -> StrResult<Self> {
+ 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<u32> {
+ 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<DecodedImage> {
+ 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<H: Hasher>(&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<u32> {
+ 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<Arc<DecodedIm
/// 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).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<dyn World + '_>,
- fallback_family: Option<&str>,
+ loader: Tracked<dyn SvgFontLoader + '_>,
) -> StrResult<Arc<DecodedImage>> {
- 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<dyn World + '_>,
- fallback_family: Option<&str>,
+ loader: Tracked<dyn SvgFontLoader + '_>,
) -> fontdb::Database {
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) {
+ 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<Font>;
+
+ /// 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<BTreeMap<EcoString, EcoVec<Font>>>,
+ fallback_family_cased: Option<String>,
+}
+
+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<Font> {
+ 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<EcoString, EcoVec<Font>>,
+ fallback_family_cased: Option<String>,
+}
+
+impl SvgFontLoader for PreparedLoader {
+ fn load(&self, family: &str) -> EcoVec<Font> {
+ 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 {