Refactor image handling
This commit is contained in:
parent
d7928a8ea3
commit
ffcd951bc8
@ -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())?;
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
175
crates/typst/src/image/mod.rs
Normal file
175
crates/typst/src/image/mod.rs
Normal 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),
|
||||
}
|
139
crates/typst/src/image/raster.rs
Normal file
139
crates/typst/src/image/raster.rs
Normal 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})"),
|
||||
}
|
||||
}
|
263
crates/typst/src/image/svg.rs
Normal file
263
crates/typst/src/image/svg.rs
Normal 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 {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user