diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs index 93c8a542a..70001292f 100644 --- a/crates/typst-library/src/visualize/image.rs +++ b/crates/typst-library/src/visualize/image.rs @@ -159,9 +159,9 @@ impl Layout for ImageElem { let image = Image::with_fonts( data.into(), format, - vt.world, - families(styles).map(|s| s.as_str().into()).collect(), self.alt(styles), + vt.world, + &families(styles).map(|s| s.as_str().into()).collect::>(), ) .at(self.span())?; diff --git a/crates/typst/src/export/pdf/image.rs b/crates/typst/src/export/pdf/image.rs index a6dda3559..d8064e3c1 100644 --- a/crates/typst/src/export/pdf/image.rs +++ b/crates/typst/src/export/pdf/image.rs @@ -1,11 +1,11 @@ use std::io::Cursor; +use std::sync::Arc; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Filter, Finish}; use super::{deflate, PdfContext, RefExt}; -use crate::eval::Bytes; -use crate::image::{DecodedImage, Image, RasterFormat}; +use crate::image::{ImageKind, RasterFormat, RasterImage}; /// Embed all used images into the PDF. #[tracing::instrument(skip_all)] @@ -19,11 +19,10 @@ pub fn write_images(ctx: &mut PdfContext) { let height = image.height(); // Add the primary image. - // TODO: Error if image could not be encoded. - match image.decoded().as_ref() { - DecodedImage::Raster(dynamic, icc, _) => { + match image.kind() { + ImageKind::Raster(raster) => { // TODO: Error if image could not be encoded. - let (data, filter, has_color) = encode_image(image); + let (data, filter, has_color) = encode_image(raster); let mut image = ctx.writer.image_xobject(image_ref, &data); image.filter(filter); image.width(width as i32); @@ -31,7 +30,7 @@ pub fn write_images(ctx: &mut PdfContext) { image.bits_per_component(8); let space = image.color_space(); - if icc.is_some() { + if raster.icc().is_some() { space.icc_based(icc_ref); } else if has_color { space.device_rgb(); @@ -41,8 +40,8 @@ pub fn write_images(ctx: &mut PdfContext) { // Add a second gray-scale image containing the alpha values if // this image has an alpha channel. - if dynamic.color().has_alpha() { - let (alpha_data, alpha_filter) = encode_alpha(dynamic); + if raster.dynamic().color().has_alpha() { + let (alpha_data, alpha_filter) = encode_alpha(raster); let mask_ref = ctx.alloc.bump(); image.s_mask(mask_ref); image.finish(); @@ -57,8 +56,8 @@ pub fn write_images(ctx: &mut PdfContext) { image.finish(); } - if let Some(icc) = icc { - let compressed = deflate(&icc.0); + if let Some(icc) = raster.icc() { + let compressed = deflate(icc); let mut stream = ctx.writer.icc_profile(icc_ref, &compressed); stream.filter(Filter::FlateDecode); if has_color { @@ -70,15 +69,19 @@ pub fn write_images(ctx: &mut PdfContext) { } } } - DecodedImage::Svg(svg) => { - let next_ref = svg2pdf::convert_tree_into( - svg, - svg2pdf::Options::default(), - &mut ctx.writer, - image_ref, - ); - ctx.alloc = next_ref; - } + // Safety: We do not keep any references to tree nodes beyond the + // scope of `with`. + ImageKind::Svg(svg) => unsafe { + svg.with(|tree| { + let next_ref = svg2pdf::convert_tree_into( + tree, + svg2pdf::Options::default(), + &mut ctx.writer, + image_ref, + ); + ctx.alloc = next_ref; + }); + }, } } } @@ -89,14 +92,9 @@ pub fn write_images(ctx: &mut PdfContext) { /// Skips the alpha channel as that's encoded separately. #[comemo::memoize] #[tracing::instrument(skip_all)] -fn encode_image(image: &Image) -> (Bytes, Filter, bool) { - let decoded = image.decoded(); - let (dynamic, format) = match decoded.as_ref() { - DecodedImage::Raster(dynamic, _, format) => (dynamic, *format), - _ => panic!("can only encode raster image"), - }; - - match (format, dynamic) { +fn encode_image(image: &RasterImage) -> (Arc>, Filter, bool) { + let dynamic = image.dynamic(); + match (image.format(), dynamic) { // 8-bit gray JPEG. (RasterFormat::Jpg, DynamicImage::ImageLuma8(_)) => { let mut data = Cursor::new(vec![]); @@ -136,8 +134,13 @@ fn encode_image(image: &Image) -> (Bytes, Filter, bool) { } /// Encode an image's alpha channel if present. +#[comemo::memoize] #[tracing::instrument(skip_all)] -fn encode_alpha(dynamic: &DynamicImage) -> (Vec, Filter) { - let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - (deflate(&pixels), Filter::FlateDecode) +fn encode_alpha(raster: &RasterImage) -> (Arc>, Filter) { + let pixels: Vec<_> = raster + .dynamic() + .pixels() + .map(|(_, _, Rgba([_, _, _, a]))| a) + .collect(); + (Arc::new(deflate(&pixels)), Filter::FlateDecode) } diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs index c2ae888ed..81442ad06 100644 --- a/crates/typst/src/export/render.rs +++ b/crates/typst/src/export/render.rs @@ -17,7 +17,7 @@ use crate::geom::{ self, Abs, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Transform, }; -use crate::image::{DecodedImage, Image, RasterFormat}; +use crate::image::{Image, ImageKind, RasterFormat}; /// Export a frame into a raster image. /// @@ -585,25 +585,29 @@ 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().as_ref() { - DecodedImage::Raster(dynamic, _, _) => { + match image.kind() { + ImageKind::Raster(raster) => { let downscale = w < image.width(); let filter = if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = dynamic.resize(w, h, filter); + let buf = raster.dynamic().resize(w, h, filter); for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } - DecodedImage::Svg(tree) => { - let tree = resvg::Tree::from_usvg(tree); - let ts = tiny_skia::Transform::from_scale( - w as f32 / tree.size.width(), - h as f32 / tree.size.height(), - ); - tree.render(ts, &mut pixmap.as_mut()) - } + // Safety: We do not keep any references to tree nodes beyond the scope + // of `with`. + ImageKind::Svg(svg) => unsafe { + svg.with(|tree| { + let tree = resvg::Tree::from_usvg(tree); + let ts = tiny_skia::Transform::from_scale( + w as f32 / tree.size.width(), + h as f32 / tree.size.height(), + ); + tree.render(ts, &mut pixmap.as_mut()) + }); + }, } Some(Arc::new(pixmap)) } diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs deleted file mode 100644 index 040c7f41a..000000000 --- a/crates/typst/src/image.rs +++ /dev/null @@ -1,575 +0,0 @@ -//! Image handling. - -use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::fmt::{self, Debug, Formatter}; -use std::io; -use std::rc::Rc; -use std::sync::Arc; - -use comemo::{Prehashed, Track, Tracked}; -use ecow::{eco_format, EcoString, EcoVec}; -use image::codecs::gif::GifDecoder; -use image::codecs::jpeg::JpegDecoder; -use image::codecs::png::PngDecoder; -use image::io::Limits; -use image::{guess_format, ImageDecoder, ImageResult}; -use typst_macros::{cast, Cast}; -use usvg::{NodeExt, TreeParsing, TreeTextToPath}; - -use crate::diag::{bail, format_xml_like_error, StrResult}; -use crate::eval::Bytes; -use crate::font::{Font, FontBook, FontInfo, FontVariant, FontWeight}; -use crate::geom::Axes; -use crate::World; - -/// A raster or vector image. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Image(Arc>); - -/// The internal representation. -#[derive(Hash)] -struct Repr { - /// The raw, undecoded image data. - data: Bytes, - /// The format of the encoded `buffer`. - format: ImageFormat, - /// 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: Bytes, - 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, (&loader as &dyn SvgFontLoader).track())? - } - }; - - 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: Bytes, - format: ImageFormat, - world: Tracked, - fallback_families: EcoVec, - alt: Option, - ) -> StrResult { - let loader = WorldLoader::new(world, fallback_families); - let decoded = match format { - ImageFormat::Raster(format) => decode_raster(&data, format)?, - ImageFormat::Vector(VectorFormat::Svg) => { - decode_svg(&data, (&loader as &dyn SvgFontLoader).track())? - } - }; - - 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) -> &Bytes { - &self.0.data - } - - /// The format of the image. - pub fn format(&self) -> ImageFormat { - self.0.format - } - - /// 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.size().x - } - - /// The height of the image in pixels. - pub fn height(&self) -> u32 { - self.size().y - } - - /// A text describing the image. - pub fn alt(&self) -> Option<&str> { - self.0.alt.as_deref() - } - - /// The decoded version of the image. - pub fn decoded(&self) -> Rc { - 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() - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format()) - .field("width", &self.width()) - .field("height", &self.height()) - .field("alt", &self.alt()) - .finish() - } -} - -/// A raster or vector image format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ImageFormat { - /// A raster graphics format. - Raster(RasterFormat), - /// A vector graphics format. - Vector(VectorFormat), -} - -impl From for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) - } -} - -impl From for ImageFormat { - fn from(format: VectorFormat) -> Self { - Self::Vector(format) - } -} - -cast! { - ImageFormat, - self => match self { - Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() - }, - v: RasterFormat => Self::Raster(v), - v: VectorFormat => Self::Vector(v), -} - -/// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum RasterFormat { - /// Raster format for illustrations and transparent graphics. - Png, - /// Lossy raster format suitable for photos. - Jpg, - /// Raster format that is typically used for short animated clips. - Gif, -} - -/// A vector graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum VectorFormat { - /// The vector graphics format of the web. - Svg, -} - -impl RasterFormat { - /// Try to detect the format of data in a buffer. - pub fn detect(data: &[u8]) -> Option { - guess_format(data).ok().and_then(|format| format.try_into().ok()) - } -} - -impl From for image::ImageFormat { - fn from(format: RasterFormat) -> Self { - match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, - } - } -} - -impl TryFrom for RasterFormat { - type Error = EcoString; - - fn try_from(format: image::ImageFormat) -> StrResult { - Ok(match format { - image::ImageFormat::Png => RasterFormat::Png, - image::ImageFormat::Jpeg => RasterFormat::Jpg, - image::ImageFormat::Gif => RasterFormat::Gif, - _ => bail!("Format not yet supported."), - }) - } -} - -/// A decoded image. -pub enum DecodedImage { - /// A decoded pixel raster with its ICC profile. - Raster(image::DynamicImage, Option, RasterFormat), - /// A decoded SVG tree. - Svg(usvg::Tree), -} - -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 { - 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.size.height().ceil() as u32, - } - } -} - -/// Raw data for of an ICC profile. -pub struct IccProfile(pub Vec); - -/// Decode a raster image. -#[comemo::memoize] -fn decode_raster(data: &Bytes, format: RasterFormat) -> StrResult> { - fn decode_with<'a, T: ImageDecoder<'a>>( - decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option)> { - let mut decoder = decoder?; - let icc = decoder.icc_profile().filter(|data| !data.is_empty()).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(Rc::new(DecodedImage::Raster(dynamic, icc, format))) -} - -/// Decode an SVG image. -#[comemo::memoize] -fn decode_svg( - data: &Bytes, - loader: Tracked, -) -> StrResult> { - // 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, loader); - tree.convert_text(&fontdb); - } - Ok(Rc::new(DecodedImage::Svg(tree))) -} - -/// A font family and its variants. -#[derive(Clone)] -struct FontData { - /// The usvg-compatible font family name. - usvg_family: EcoString, - /// The font variants included in the family. - fonts: EcoVec, -} - -/// Discover and load the fonts referenced by an SVG. -fn load_svg_fonts( - tree: &usvg::Tree, - loader: Tracked, -) -> fontdb::Database { - let mut fontdb = fontdb::Database::new(); - let mut font_cache = HashMap::>::new(); - let mut loaded = HashSet::::new(); - - // Loads a font family by its Typst name and returns its data. - let mut load = |family: &str| -> Option { - let family = EcoString::from(family.trim()).to_lowercase(); - if let Some(success) = font_cache.get(&family) { - return success.clone(); - } - - let fonts = loader.load(&family); - let usvg_family = fonts.iter().find_map(|font| { - font.find_name(ttf_parser::name_id::TYPOGRAPHIC_FAMILY) - .or_else(|| font.find_name(ttf_parser::name_id::FAMILY)) - .map(Into::::into) - }); - - let font_data = usvg_family.map(|usvg_family| FontData { usvg_family, fonts }); - font_cache.insert(family, font_data.clone()); - font_data - }; - - // Loads a font family into the fontdb database. - let mut load_into_db = |font_data: &FontData| { - if loaded.contains(&font_data.usvg_family) { - return; - } - - // We load all variants for the family, since we don't know which will - // be used. - for font in &font_data.fonts { - fontdb.load_font_data(font.data().to_vec()); - } - - loaded.insert(font_data.usvg_family.clone()); - }; - - let fallback_families = loader.fallback_families(); - let fallback_fonts = fallback_families - .iter() - .filter_map(|family| load(family.as_str())) - .collect::>(); - - // Determine the best font for each text node. - 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 { - let Some(text) = chunk.text.get(span.start..span.end) else { continue }; - - let inline_families = &span.font.families; - let inline_fonts = inline_families - .iter() - .filter(|family| !family.is_empty()) - .filter_map(|family| load(family.as_str())) - .collect::>(); - - // Find a font that covers all characters in the span while - // taking the fallback order into account. - let font = - inline_fonts.iter().chain(fallback_fonts.iter()).find(|font_data| { - font_data.fonts.iter().any(|font| { - text.chars().all(|c| font.info().coverage.contains(c as u32)) - }) - }); - - if let Some(font) = font { - load_into_db(font); - span.font.families = vec![font.usvg_family.to_string()]; - } else if !fallback_families.is_empty() { - // If no font covers all characters, use last resort fallback - // (only if fallback is enabled <=> fallback_families is not empty) - let like = inline_fonts - .first() - .or(fallback_fonts.first()) - .and_then(|font_data| font_data.fonts.first()) - .map(|font| font.info().clone()); - - let variant = FontVariant { - style: span.font.style.into(), - weight: FontWeight::from_number(span.font.weight), - stretch: span.font.stretch.into(), - }; - - let fallback = loader - .find_fallback(text, like, variant) - .and_then(|family| load(family.as_str())); - - if let Some(font) = fallback { - load_into_db(&font); - span.font.families = vec![font.usvg_family.to_string()]; - } - } - } - } - }); - - fontdb -} - -/// Search for all font families referenced by an SVG. -fn traverse_svg(node: &usvg::Node, f: &mut F) -where - F: FnMut(&usvg::Node), -{ - for descendant in node.descendants() { - f(&descendant); - descendant.subroots(|subroot| traverse_svg(&subroot, f)) - } -} - -/// 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, family: &str) -> EcoVec; - - /// Prioritized sequence of fallback font families. - fn fallback_families(&self) -> &[String]; - - /// Find a last resort fallback for a given text and font variant. - fn find_fallback( - &self, - text: &str, - like: Option, - font: FontVariant, - ) -> Option; -} - -/// Loads fonts for an SVG from a world -struct WorldLoader<'a> { - world: Tracked<'a, dyn World + 'a>, - seen: RefCell>>, - fallback_families: EcoVec, -} - -impl<'a> WorldLoader<'a> { - fn new( - world: Tracked<'a, dyn World + 'a>, - fallback_families: EcoVec, - ) -> Self { - Self { world, seen: Default::default(), fallback_families } - } - - fn into_prepared(self) -> PreparedLoader { - let fonts = self.seen.into_inner().into_values().flatten().collect::>(); - PreparedLoader { - book: FontBook::from_fonts(fonts.iter()), - fonts, - fallback_families: self.fallback_families, - } - } -} - -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_families(&self) -> &[String] { - self.fallback_families.as_slice() - } - - fn find_fallback( - &self, - text: &str, - like: Option, - variant: FontVariant, - ) -> Option { - self.world - .book() - .select_fallback(like.as_ref(), variant, text) - .and_then(|id| self.world.font(id)) - .map(|font| font.info().family.to_lowercase().as_str().into()) - } -} - -/// Loads fonts for an SVG from a prepared list. -#[derive(Default, Hash)] -struct PreparedLoader { - book: FontBook, - fonts: EcoVec, - fallback_families: EcoVec, -} - -impl SvgFontLoader for PreparedLoader { - fn load(&self, family: &str) -> EcoVec { - self.book - .select_family(family) - .filter_map(|id| self.fonts.get(id)) - .cloned() - .collect() - } - - fn fallback_families(&self) -> &[String] { - self.fallback_families.as_slice() - } - - fn find_fallback( - &self, - text: &str, - like: Option, - variant: FontVariant, - ) -> Option { - self.book - .select_fallback(like.as_ref(), variant, text) - .and_then(|id| self.fonts.get(id)) - .map(|font| font.info().family.to_lowercase().as_str().into()) - } -} - -/// Format the user-facing raster graphic decoding error message. -fn format_image_error(error: image::ImageError) -> EcoString { - match error { - image::ImageError::Limits(_) => "file is too large".into(), - err => eco_format!("failed to decode image ({err})"), - } -} - -/// Format the user-facing SVG decoding error message. -fn format_usvg_error(error: usvg::Error) -> EcoString { - match error { - usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), - usvg::Error::MalformedGZip => "file is not compressed correctly".into(), - usvg::Error::ElementsLimitReached => "file is too large".into(), - usvg::Error::InvalidSize => { - "failed to parse SVG (width, height, or viewbox is invalid)".into() - } - usvg::Error::ParsingFailed(error) => format_xml_like_error("SVG", error), - } -} diff --git a/crates/typst/src/image/mod.rs b/crates/typst/src/image/mod.rs new file mode 100644 index 000000000..793e2a75a --- /dev/null +++ b/crates/typst/src/image/mod.rs @@ -0,0 +1,175 @@ +//! Image handling. + +mod raster; +mod svg; + +pub use self::raster::{RasterFormat, RasterImage}; +pub use self::svg::SvgImage; + +use std::fmt::{self, Debug, Formatter}; +use std::sync::Arc; + +use comemo::{Prehashed, Tracked}; +use ecow::EcoString; +use typst_macros::{cast, Cast}; + +use crate::diag::StrResult; +use crate::eval::Bytes; +use crate::World; + +/// A raster or vector image. +/// +/// Values of this type are cheap to clone and hash. +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct Image(Arc>); + +/// The internal representation. +#[derive(Hash)] +struct Repr { + /// The raw, undecoded image data. + kind: ImageKind, + /// A text describing the image. + alt: Option, +} + +/// A kind of image. +#[derive(Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl Image { + /// Create an image from a buffer and a format. + #[comemo::memoize] + pub fn new( + data: Bytes, + format: ImageFormat, + alt: Option, + ) -> StrResult { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::new(data)?) + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) + } + + /// Create a possibly font-dependant image from a buffer and a format. + #[comemo::memoize] + pub fn with_fonts( + data: Bytes, + format: ImageFormat, + alt: Option, + world: Tracked, + families: &[String], + ) -> StrResult { + let kind = match format { + ImageFormat::Raster(format) => { + ImageKind::Raster(RasterImage::new(data, format)?) + } + ImageFormat::Vector(VectorFormat::Svg) => { + ImageKind::Svg(SvgImage::with_fonts(data, world, families)?) + } + }; + + Ok(Self(Arc::new(Prehashed::new(Repr { kind, alt })))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + match &self.0.kind { + ImageKind::Raster(raster) => raster.data(), + ImageKind::Svg(svg) => svg.data(), + } + } + + /// The format of the image. + pub fn format(&self) -> ImageFormat { + match &self.0.kind { + ImageKind::Raster(raster) => raster.format().into(), + ImageKind::Svg(_) => VectorFormat::Svg.into(), + } + } + + /// The width of the image in pixels. + pub fn width(&self) -> u32 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.width(), + ImageKind::Svg(svg) => svg.width(), + } + } + + /// The height of the image in pixels. + pub fn height(&self) -> u32 { + match &self.0.kind { + ImageKind::Raster(raster) => raster.height(), + ImageKind::Svg(svg) => svg.height(), + } + } + + /// A text describing the image. + pub fn alt(&self) -> Option<&str> { + self.0.alt.as_deref() + } + + /// The decoded image. + pub fn kind(&self) -> &ImageKind { + &self.0.kind + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_struct("Image") + .field("format", &self.format()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("alt", &self.alt()) + .finish() + } +} + +/// A raster or vector image format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ImageFormat { + /// A raster graphics format. + Raster(RasterFormat), + /// A vector graphics format. + Vector(VectorFormat), +} + +/// A vector graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum VectorFormat { + /// The vector graphics format of the web. + Svg, +} + +impl From for ImageFormat { + fn from(format: RasterFormat) -> Self { + Self::Raster(format) + } +} + +impl From for ImageFormat { + fn from(format: VectorFormat) -> Self { + Self::Vector(format) + } +} + +cast! { + ImageFormat, + self => match self { + Self::Raster(v) => v.into_value(), + Self::Vector(v) => v.into_value() + }, + v: RasterFormat => Self::Raster(v), + v: VectorFormat => Self::Vector(v), +} diff --git a/crates/typst/src/image/raster.rs b/crates/typst/src/image/raster.rs new file mode 100644 index 000000000..d82350392 --- /dev/null +++ b/crates/typst/src/image/raster.rs @@ -0,0 +1,139 @@ +use std::hash::{Hash, Hasher}; +use std::io; +use std::sync::Arc; + +use ecow::{eco_format, EcoString}; +use image::codecs::gif::GifDecoder; +use image::codecs::jpeg::JpegDecoder; +use image::codecs::png::PngDecoder; +use image::io::Limits; +use image::{guess_format, ImageDecoder, ImageResult}; +use typst_macros::Cast; + +use crate::diag::{bail, StrResult}; +use crate::eval::Bytes; + +/// A decoded raster image. +#[derive(Clone, Hash)] +pub struct RasterImage(Arc); + +/// The internal representation. +struct Repr { + data: Bytes, + format: RasterFormat, + dynamic: image::DynamicImage, + icc: Option>, +} + +impl RasterImage { + /// Decode a raster image. + #[comemo::memoize] + pub fn new(data: Bytes, format: RasterFormat) -> StrResult { + fn decode_with<'a, T: ImageDecoder<'a>>( + decoder: ImageResult, + ) -> ImageResult<(image::DynamicImage, Option>)> { + let mut decoder = decoder?; + let icc = decoder.icc_profile().filter(|icc| !icc.is_empty()); + 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(Self(Arc::new(Repr { data, format, dynamic, icc }))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The image's format. + pub fn format(&self) -> RasterFormat { + self.0.format + } + + /// The image's pixel width. + pub fn width(&self) -> u32 { + self.dynamic().width() + } + + /// The image's pixel height. + pub fn height(&self) -> u32 { + self.dynamic().height() + } + + /// Access the underlying dynamic image. + pub fn dynamic(&self) -> &image::DynamicImage { + &self.0.dynamic + } + + /// Access the ICC profile, if any. + pub fn icc(&self) -> Option<&[u8]> { + self.0.icc.as_deref() + } +} + +impl Hash for Repr { + fn hash(&self, state: &mut H) { + // The image is fully defined by data and format. + self.data.hash(state); + self.format.hash(state); + } +} + +/// A raster graphics format. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum RasterFormat { + /// Raster format for illustrations and transparent graphics. + Png, + /// Lossy raster format suitable for photos. + Jpg, + /// Raster format that is typically used for short animated clips. + Gif, +} + +impl RasterFormat { + /// Try to detect the format of data in a buffer. + pub fn detect(data: &[u8]) -> Option { + guess_format(data).ok().and_then(|format| format.try_into().ok()) + } +} + +impl From for image::ImageFormat { + fn from(format: RasterFormat) -> Self { + match format { + RasterFormat::Png => image::ImageFormat::Png, + RasterFormat::Jpg => image::ImageFormat::Jpeg, + RasterFormat::Gif => image::ImageFormat::Gif, + } + } +} + +impl TryFrom for RasterFormat { + type Error = EcoString; + + fn try_from(format: image::ImageFormat) -> StrResult { + Ok(match format { + image::ImageFormat::Png => RasterFormat::Png, + image::ImageFormat::Jpeg => RasterFormat::Jpg, + image::ImageFormat::Gif => RasterFormat::Gif, + _ => bail!("Format not yet supported."), + }) + } +} + +/// Format the user-facing raster graphic decoding error message. +fn format_image_error(error: image::ImageError) -> EcoString { + match error { + image::ImageError::Limits(_) => "file is too large".into(), + err => eco_format!("failed to decode image ({err})"), + } +} diff --git a/crates/typst/src/image/svg.rs b/crates/typst/src/image/svg.rs new file mode 100644 index 000000000..c8db63a5d --- /dev/null +++ b/crates/typst/src/image/svg.rs @@ -0,0 +1,263 @@ +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +use comemo::Tracked; +use ecow::EcoString; +use siphasher::sip128::Hasher128; +use usvg::{NodeExt, TreeParsing, TreeTextToPath}; + +use crate::diag::{format_xml_like_error, StrResult}; +use crate::eval::Bytes; +use crate::font::{FontVariant, FontWeight}; +use crate::geom::Axes; +use crate::World; + +/// A decoded SVG. +#[derive(Clone, Hash)] +pub struct SvgImage(Arc); + +/// The internal representation. +struct Repr { + data: Bytes, + size: Axes, + font_hash: u128, + tree: sync::SyncTree, +} + +impl SvgImage { + /// Decode an SVG image without fonts. + #[comemo::memoize] + pub fn new(data: Bytes) -> StrResult { + let opts = usvg::Options::default(); + let tree = usvg::Tree::from_data(&data, &opts).map_err(format_usvg_error)?; + Ok(Self(Arc::new(Repr { + data, + size: tree_size(&tree), + font_hash: 0, + // Safety: We just created the tree and hold the only reference. + tree: unsafe { sync::SyncTree::new(tree) }, + }))) + } + + /// Decode an SVG image with access to fonts. + #[comemo::memoize] + pub fn with_fonts( + data: Bytes, + world: Tracked, + families: &[String], + ) -> StrResult { + // 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)?; + let mut font_hash = 0; + if tree.has_text_nodes() { + let (fontdb, hash) = load_svg_fonts(world, &tree, families); + tree.convert_text(&fontdb); + font_hash = hash; + } + Ok(Self(Arc::new(Repr { + data, + size: tree_size(&tree), + font_hash, + // Safety: We just created the tree and hold the only reference. + tree: unsafe { sync::SyncTree::new(tree) }, + }))) + } + + /// The raw image data. + pub fn data(&self) -> &Bytes { + &self.0.data + } + + /// The SVG's width in pixels. + pub fn width(&self) -> u32 { + self.0.size.x + } + + /// The SVG's height in pixels. + pub fn height(&self) -> u32 { + self.0.size.y + } + + /// Performs an operation with the usvg tree. + /// + /// This makes the tree uniquely available to the current thread and blocks + /// other accesses to it. + /// + /// # Safety + /// The caller may not hold any references to `Rc`s contained in the usvg + /// Tree after `f` returns. + /// + /// # Why is it unsafe? + /// Sadly, usvg's Tree is neither `Sync` nor `Send` because it uses `Rc` + /// internally and sending a tree to another thread could result in data + /// races when an `Rc`'s ref-count is modified from two threads at the same + /// time. + /// + /// However, access to the tree is actually safe if we don't clone `Rc`s / + /// only clone them while holding a mutex and drop all clones before the + /// mutex is released. Sadly, we can't enforce this variant at the type + /// system level. Therefore, access is guarded by this function (which makes + /// it reasonable hard to keep references around) and its usage still + /// remains `unsafe` (because it's still possible to have `Rc`s escape). + /// + /// See also: + pub unsafe fn with(&self, f: F) + where + F: FnOnce(&usvg::Tree), + { + self.0.tree.with(f) + } +} + +impl Hash for Repr { + fn hash(&self, state: &mut H) { + // An SVG might contain fonts, which must be incorporated into the hash. + // We can't hash a usvg tree directly, but the raw SVG data + a hash of + // all used fonts gives us something similar. + self.data.hash(state); + self.font_hash.hash(state); + } +} + +/// Discover and load the fonts referenced by an SVG. +fn load_svg_fonts( + world: Tracked, + tree: &usvg::Tree, + families: &[String], +) -> (fontdb::Database, u128) { + let book = world.book(); + let mut fontdb = fontdb::Database::new(); + let mut hasher = siphasher::sip128::SipHasher13::new(); + let mut loaded = HashMap::>::new(); + + // Loads a font into the database and return it's usvg-compatible name. + let mut load_into_db = |id: usize| -> Option { + loaded + .entry(id) + .or_insert_with(|| { + let font = world.font(id)?; + fontdb.load_font_source(fontdb::Source::Binary(Arc::new( + font.data().clone(), + ))); + font.data().hash(&mut hasher); + font.find_name(ttf_parser::name_id::TYPOGRAPHIC_FAMILY) + .or_else(|| font.find_name(ttf_parser::name_id::FAMILY)) + }) + .clone() + }; + + // Determine the best font for each text node. + traverse_svg(&tree.root, &mut |node| { + let usvg::NodeKind::Text(text) = &mut *node.borrow_mut() else { return }; + for chunk in &mut text.chunks { + 'spans: for span in &mut chunk.spans { + let Some(text) = chunk.text.get(span.start..span.end) else { continue }; + let variant = FontVariant { + style: span.font.style.into(), + weight: FontWeight::from_number(span.font.weight), + stretch: span.font.stretch.into(), + }; + + // Find a font that covers the whole text among the span's fonts + // and the current document font families. + let mut like = None; + for family in span.font.families.iter().chain(families) { + let Some(id) = book.select(&family.to_lowercase(), variant) else { + continue; + }; + let Some(info) = book.info(id) else { continue }; + like.get_or_insert(info); + + if text.chars().all(|c| info.coverage.contains(c as u32)) { + if let Some(usvg_family) = load_into_db(id) { + span.font.families = vec![usvg_family]; + continue 'spans; + } + } + } + + // If we didn't find a match, select a fallback font. + if let Some(id) = book.select_fallback(like, variant, text) { + if let Some(usvg_family) = load_into_db(id) { + span.font.families = vec![usvg_family]; + } + } + } + } + }); + + (fontdb, hasher.finish128().as_u128()) +} + +/// Search for all font families referenced by an SVG. +fn traverse_svg(node: &usvg::Node, f: &mut F) +where + F: FnMut(&usvg::Node), +{ + for descendant in node.descendants() { + f(&descendant); + descendant.subroots(|subroot| traverse_svg(&subroot, f)) + } +} + +/// The ceiled pixel size of an SVG. +fn tree_size(tree: &usvg::Tree) -> Axes { + Axes::new(tree.size.width().ceil() as u32, tree.size.height().ceil() as u32) +} + +/// Format the user-facing SVG decoding error message. +fn format_usvg_error(error: usvg::Error) -> EcoString { + match error { + usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), + usvg::Error::MalformedGZip => "file is not compressed correctly".into(), + usvg::Error::ElementsLimitReached => "file is too large".into(), + usvg::Error::InvalidSize => { + "failed to parse SVG (width, height, or viewbox is invalid)".into() + } + usvg::Error::ParsingFailed(error) => format_xml_like_error("SVG", error), + } +} + +mod sync { + use std::sync::Mutex; + + /// A synchronized wrapper around a `usvg::Tree`. + pub struct SyncTree(Mutex); + + impl SyncTree { + /// Create a new synchronized tree. + /// + /// # Safety + /// The tree must be completely owned by `tree`, there may not be any + /// other references to `Rc`s contained in it. + pub unsafe fn new(tree: usvg::Tree) -> Self { + Self(Mutex::new(tree)) + } + + /// Perform an operation with the usvg tree. + /// + /// # Safety + /// The caller may not hold any references to `Rc`s contained in + /// the usvg Tree after returning. + pub unsafe fn with(&self, f: F) + where + F: FnOnce(&usvg::Tree), + { + let tree = self.0.lock().unwrap(); + f(&tree) + } + } + + // Safety: usvg's Tree is only non-Sync and non-Send because it uses `Rc` + // internally. By wrapping it in a mutex and forbidding outstanding + // references to the tree to remain after a `with` call, we guarantee that + // no two threads try to change a ref-count at the same time. + unsafe impl Sync for SyncTree {} + unsafe impl Send for SyncTree {} +}