Refactor image handling

This commit is contained in:
Laurenz 2023-09-27 12:10:24 +02:00
parent d7928a8ea3
commit ffcd951bc8
7 changed files with 629 additions and 620 deletions

View File

@ -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())?;

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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),
}
}

View File

@ -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),
}

View File

@ -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})"),
}
}

View File

@ -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 {}
}