summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-09-27 12:10:24 +0200
committerLaurenz <laurmaedje@gmail.com>2023-09-27 12:20:29 +0200
commitffcd951bc8a698af99d43f5245aea47a7c09adaf (patch)
tree00cc0eaaa8deb0eb2a958e9c41060a7a842fcbf3 /crates
parentd7928a8ea347df89f501e5b3ecbb52402cae8edb (diff)
Refactor image handling
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/visualize/image.rs4
-rw-r--r--crates/typst/src/export/pdf/image.rs65
-rw-r--r--crates/typst/src/export/render.rs28
-rw-r--r--crates/typst/src/image.rs575
-rw-r--r--crates/typst/src/image/mod.rs175
-rw-r--r--crates/typst/src/image/raster.rs139
-rw-r--r--crates/typst/src/image/svg.rs263
7 files changed, 629 insertions, 620 deletions
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index 93c8a542..70001292 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::<Vec<_>>(),
)
.at(self.span())?;
diff --git a/crates/typst/src/export/pdf/image.rs b/crates/typst/src/export/pdf/image.rs
index a6dda355..d8064e3c 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<Vec<u8>>, 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<u8>, Filter) {
- let pixels: Vec<_> = dynamic.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
- (deflate(&pixels), Filter::FlateDecode)
+fn encode_alpha(raster: &RasterImage) -> (Arc<Vec<u8>>, 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 c2ae888e..81442ad0 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<Arc<sk::Pixmap>> {
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 040c7f41..00000000
--- 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<Prehashed<Repr>>);
-
-/// 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<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: Bytes,
- 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, (&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<dyn World + '_>,
- fallback_families: EcoVec<String>,
- alt: Option<EcoString>,
- ) -> StrResult<Self> {
- 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<u32> {
- 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<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()
- }
-}
-
-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<RasterFormat> for ImageFormat {
- fn from(format: RasterFormat) -> Self {
- Self::Raster(format)
- }
-}
-
-impl From<VectorFormat> 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<Self> {
- guess_format(data).ok().and_then(|format| format.try_into().ok())
- }
-}
-
-impl From<RasterFormat> 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<image::ImageFormat> for RasterFormat {
- type Error = EcoString;
-
- fn try_from(format: image::ImageFormat) -> StrResult<Self> {
- 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<IccProfile>, RasterFormat),
- /// A decoded SVG tree.
- Svg(usvg::Tree),
-}
-
-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 {
- 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<u8>);
-
-/// Decode a raster image.
-#[comemo::memoize]
-fn decode_raster(data: &Bytes, format: RasterFormat) -> StrResult<Rc<DecodedImage>> {
- fn decode_with<'a, T: ImageDecoder<'a>>(
- decoder: ImageResult<T>,
- ) -> ImageResult<(image::DynamicImage, Option<IccProfile>)> {
- 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<dyn SvgFontLoader + '_>,
-) -> StrResult<Rc<DecodedImage>> {
- // 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<Font>,
-}
-
-/// Discover and load the fonts referenced by an SVG.
-fn load_svg_fonts(
- tree: &usvg::Tree,
- loader: Tracked<dyn SvgFontLoader + '_>,
-) -> fontdb::Database {
- let mut fontdb = fontdb::Database::new();
- let mut font_cache = HashMap::<EcoString, Option<FontData>>::new();
- let mut loaded = HashSet::<EcoString>::new();
-
- // Loads a font family by its Typst name and returns its data.
- let mut load = |family: &str| -> Option<FontData> {
- 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::<EcoString>::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::<EcoVec<_>>();
-
- // 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::<EcoVec<_>>();
-
- // 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<F>(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<Font>;
-
- /// 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<FontInfo>,
- font: FontVariant,
- ) -> Option<EcoString>;
-}
-
-/// Loads fonts for an SVG from a world
-struct WorldLoader<'a> {
- world: Tracked<'a, dyn World + 'a>,
- seen: RefCell<BTreeMap<EcoString, EcoVec<Font>>>,
- fallback_families: EcoVec<String>,
-}
-
-impl<'a> WorldLoader<'a> {
- fn new(
- world: Tracked<'a, dyn World + 'a>,
- fallback_families: EcoVec<String>,
- ) -> Self {
- Self { world, seen: Default::default(), fallback_families }
- }
-
- fn into_prepared(self) -> PreparedLoader {
- let fonts = self.seen.into_inner().into_values().flatten().collect::<EcoVec<_>>();
- PreparedLoader {
- book: FontBook::from_fonts(fonts.iter()),
- fonts,
- fallback_families: self.fallback_families,
- }
- }
-}
-
-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_families(&self) -> &[String] {
- self.fallback_families.as_slice()
- }
-
- fn find_fallback(
- &self,
- text: &str,
- like: Option<FontInfo>,
- variant: FontVariant,
- ) -> Option<EcoString> {
- 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<Font>,
- fallback_families: EcoVec<String>,
-}
-
-impl SvgFontLoader for PreparedLoader {
- fn load(&self, family: &str) -> EcoVec<Font> {
- 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<FontInfo>,
- variant: FontVariant,
- ) -> Option<EcoString> {
- 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 00000000..793e2a75
--- /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<Prehashed<Repr>>);
+
+/// The internal representation.
+#[derive(Hash)]
+struct Repr {
+ /// The raw, undecoded image data.
+ kind: ImageKind,
+ /// A text describing the image.
+ alt: Option<EcoString>,
+}
+
+/// 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<EcoString>,
+ ) -> StrResult<Self> {
+ 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<EcoString>,
+ world: Tracked<dyn World + '_>,
+ families: &[String],
+ ) -> StrResult<Self> {
+ 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<RasterFormat> for ImageFormat {
+ fn from(format: RasterFormat) -> Self {
+ Self::Raster(format)
+ }
+}
+
+impl From<VectorFormat> 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 00000000..d8235039
--- /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<Repr>);
+
+/// The internal representation.
+struct Repr {
+ data: Bytes,
+ format: RasterFormat,
+ dynamic: image::DynamicImage,
+ icc: Option<Vec<u8>>,
+}
+
+impl RasterImage {
+ /// Decode a raster image.
+ #[comemo::memoize]
+ pub fn new(data: Bytes, format: RasterFormat) -> StrResult<Self> {
+ fn decode_with<'a, T: ImageDecoder<'a>>(
+ decoder: ImageResult<T>,
+ ) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
+ 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<H: Hasher>(&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<Self> {
+ guess_format(data).ok().and_then(|format| format.try_into().ok())
+ }
+}
+
+impl From<RasterFormat> 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<image::ImageFormat> for RasterFormat {
+ type Error = EcoString;
+
+ fn try_from(format: image::ImageFormat) -> StrResult<Self> {
+ 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 00000000..c8db63a5
--- /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<Repr>);
+
+/// The internal representation.
+struct Repr {
+ data: Bytes,
+ size: Axes<u32>,
+ font_hash: u128,
+ tree: sync::SyncTree,
+}
+
+impl SvgImage {
+ /// Decode an SVG image without fonts.
+ #[comemo::memoize]
+ pub fn new(data: Bytes) -> StrResult<Self> {
+ 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<dyn World + '_>,
+ families: &[String],
+ ) -> StrResult<Self> {
+ // 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: <https://github.com/RazrFalcon/resvg/issues/544>
+ pub unsafe fn with<F>(&self, f: F)
+ where
+ F: FnOnce(&usvg::Tree),
+ {
+ self.0.tree.with(f)
+ }
+}
+
+impl Hash for Repr {
+ fn hash<H: Hasher>(&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<dyn World + '_>,
+ 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::<usize, Option<String>>::new();
+
+ // Loads a font into the database and return it's usvg-compatible name.
+ let mut load_into_db = |id: usize| -> Option<String> {
+ 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<F>(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<u32> {
+ 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<usvg::Tree>);
+
+ 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<F>(&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 {}
+}