summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/visualize
diff options
context:
space:
mode:
authorfrozolotl <44589151+frozolotl@users.noreply.github.com>2025-01-31 10:56:25 +0100
committerGitHub <noreply@github.com>2025-01-31 09:56:25 +0000
commit3eb6e87af1d8870a38cc5914e345d07373e1e8c1 (patch)
tree800de2bc1ed9a5c7f8efc21e2741c0cd5c4728f6 /crates/typst-library/src/visualize
parentbe1fa91a00a9bff6c5eb9744266f252b8cc23fe4 (diff)
Include images from raw pixmaps and more (#5632)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-library/src/visualize')
-rw-r--r--crates/typst-library/src/visualize/image/mod.rs216
-rw-r--r--crates/typst-library/src/visualize/image/raster.rs267
-rw-r--r--crates/typst-library/src/visualize/image/svg.rs2
3 files changed, 365 insertions, 120 deletions
diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs
index 77f8426e..0e5c9e32 100644
--- a/crates/typst-library/src/visualize/image/mod.rs
+++ b/crates/typst-library/src/visualize/image/mod.rs
@@ -3,13 +3,14 @@
mod raster;
mod svg;
-pub use self::raster::{RasterFormat, RasterImage};
+pub use self::raster::{
+ ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
+};
pub use self::svg::SvgImage;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
-use comemo::Tracked;
use ecow::EcoString;
use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash;
@@ -24,7 +25,6 @@ use crate::layout::{BlockElem, Length, Rel, Sizing};
use crate::loading::{DataSource, Load, Readable};
use crate::model::Figurable;
use crate::text::LocalName;
-use crate::World;
/// A raster or vector graphic.
///
@@ -46,7 +46,8 @@ use crate::World;
/// ```
#[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem {
- /// A path to an image file or raw bytes making up an encoded image.
+ /// A path to an image file or raw bytes making up an image in one of the
+ /// supported [formats]($image.format).
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
#[required]
@@ -57,10 +58,50 @@ pub struct ImageElem {
)]
pub source: Derived<DataSource, Bytes>,
- /// The image's format. Detected automatically by default.
+ /// The image's format.
+ ///
+ /// By default, the format is detected automatically. Typically, you thus
+ /// only need to specify this when providing raw bytes as the
+ /// [`source`]($image.source) (even then, Typst will try to figure out the
+ /// format automatically, but that's not always possible).
+ ///
+ /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well
+ /// as raw pixel data. Embedding PDFs as images is
+ /// [not currently supported](https://github.com/typst/typst/issues/145).
+ ///
+ /// When providing raw pixel data as the `source`, you must specify a
+ /// dictionary with the following keys as the `format`:
+ /// - `encoding` ([str]): The encoding of the pixel data. One of:
+ /// - `{"rgb8"}` (three 8-bit channels: red, green, blue)
+ /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha)
+ /// - `{"luma8"}` (one 8-bit channel)
+ /// - `{"lumaa8"}` (two 8-bit channels: luma and alpha)
+ /// - `width` ([int]): The pixel width of the image.
+ /// - `height` ([int]): The pixel height of the image.
+ ///
+ /// The pixel width multiplied by the height multiplied by the channel count
+ /// for the specified encoding must then match the `source` data.
+ ///
+ /// ```example
+ /// #image(
+ /// read(
+ /// "tetrahedron.svg",
+ /// encoding: none,
+ /// ),
+ /// format: "svg",
+ /// width: 2cm,
+ /// )
///
- /// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image
- /// is [not currently supported](https://github.com/typst/typst/issues/145).
+ /// #image(
+ /// bytes(range(16).map(x => x * 16)),
+ /// format: (
+ /// encoding: "luma8",
+ /// width: 4,
+ /// height: 4,
+ /// ),
+ /// width: 2cm,
+ /// )
+ /// ```
pub format: Smart<ImageFormat>,
/// The width of the image.
@@ -86,6 +127,30 @@ pub struct ImageElem {
#[default(ImageFit::Cover)]
pub fit: ImageFit,
+ /// A hint to viewers how they should scale the image.
+ ///
+ /// When set to `{auto}`, the default is left up to the viewer. For PNG
+ /// export, Typst will default to smooth scaling, like most PDF and SVG
+ /// viewers.
+ ///
+ /// _Note:_ The exact look may differ across PDF viewers.
+ pub scaling: Smart<ImageScaling>,
+
+ /// An ICC profile for the image.
+ ///
+ /// ICC profiles define how to interpret the colors in an image. When set
+ /// to `{auto}`, Typst will try to extract an ICC profile from the image.
+ #[parse(match args.named::<Spanned<Smart<DataSource>>>("icc")? {
+ Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({
+ let data = Spanned::new(&source, span).load(engine.world)?;
+ Derived::new(source, data)
+ })),
+ Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
+ None => None,
+ })]
+ #[borrowed]
+ pub icc: Smart<Derived<DataSource, Bytes>>,
+
/// Whether text in SVG images should be converted into curves before
/// embedding. This will result in the text becoming unselectable in the
/// output.
@@ -94,6 +159,7 @@ pub struct ImageElem {
}
#[scope]
+#[allow(clippy::too_many_arguments)]
impl ImageElem {
/// Decode a raster or vector graphic from bytes or a string.
///
@@ -130,6 +196,13 @@ impl ImageElem {
/// How the image should adjust itself to a given area.
#[named]
fit: Option<ImageFit>,
+ /// A hint to viewers how they should scale the image.
+ #[named]
+ scaling: Option<Smart<ImageScaling>>,
+ /// Whether text in SVG images should be converted into curves before
+ /// embedding.
+ #[named]
+ flatten_text: Option<bool>,
) -> StrResult<Content> {
let bytes = data.into_bytes();
let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes);
@@ -149,6 +222,12 @@ impl ImageElem {
if let Some(fit) = fit {
elem.push_fit(fit);
}
+ if let Some(scaling) = scaling {
+ elem.push_scaling(scaling);
+ }
+ if let Some(flatten_text) = flatten_text {
+ elem.push_flatten_text(flatten_text);
+ }
Ok(elem.pack().spanned(span))
}
}
@@ -199,15 +278,8 @@ struct Repr {
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),
+ /// The scaling algorithm to use.
+ scaling: Smart<ImageScaling>,
}
impl Image {
@@ -218,55 +290,29 @@ impl Image {
/// Should always be the same as the default DPI used by usvg.
pub const USVG_DEFAULT_DPI: f64 = 96.0;
- /// Create an image from a buffer and a format.
- #[comemo::memoize]
- #[typst_macros::time(name = "load image")]
+ /// Create an image from a `RasterImage` or `SvgImage`.
pub fn new(
- data: Bytes,
- format: ImageFormat,
+ kind: impl Into<ImageKind>,
alt: Option<EcoString>,
- ) -> StrResult<Image> {
- 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(LazyHash::new(Repr { kind, alt }))))
+ scaling: Smart<ImageScaling>,
+ ) -> Self {
+ Self::new_impl(kind.into(), alt, scaling)
}
- /// Create a possibly font-dependent image from a buffer and a format.
- #[comemo::memoize]
- #[typst_macros::time(name = "load image")]
- pub fn with_fonts(
- data: Bytes,
- format: ImageFormat,
- alt: Option<EcoString>,
- world: Tracked<dyn World + '_>,
- families: &[&str],
- flatten_text: bool,
- ) -> StrResult<Image> {
- let kind = match format {
- ImageFormat::Raster(format) => {
- ImageKind::Raster(RasterImage::new(data, format)?)
- }
- ImageFormat::Vector(VectorFormat::Svg) => {
- ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?)
- }
- };
-
- Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
+ /// Create an image with optional properties set to the default.
+ pub fn plain(kind: impl Into<ImageKind>) -> Self {
+ Self::new(kind, None, Smart::Auto)
}
- /// The raw image data.
- pub fn data(&self) -> &Bytes {
- match &self.0.kind {
- ImageKind::Raster(raster) => raster.data(),
- ImageKind::Svg(svg) => svg.data(),
- }
+ /// The internal, non-generic implementation. This is memoized to reuse
+ /// the `Arc` and `LazyHash`.
+ #[comemo::memoize]
+ fn new_impl(
+ kind: ImageKind,
+ alt: Option<EcoString>,
+ scaling: Smart<ImageScaling>,
+ ) -> Image {
+ Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling })))
}
/// The format of the image.
@@ -306,6 +352,11 @@ impl Image {
self.0.alt.as_deref()
}
+ /// The image scaling algorithm to use for this image.
+ pub fn scaling(&self) -> Smart<ImageScaling> {
+ self.0.scaling
+ }
+
/// The decoded image.
pub fn kind(&self) -> &ImageKind {
&self.0.kind
@@ -319,10 +370,32 @@ impl Debug for Image {
.field("width", &self.width())
.field("height", &self.height())
.field("alt", &self.alt())
+ .field("scaling", &self.scaling())
.finish()
}
}
+/// A kind of image.
+#[derive(Clone, Hash)]
+pub enum ImageKind {
+ /// A raster image.
+ Raster(RasterImage),
+ /// An SVG image.
+ Svg(SvgImage),
+}
+
+impl From<RasterImage> for ImageKind {
+ fn from(image: RasterImage) -> Self {
+ Self::Raster(image)
+ }
+}
+
+impl From<SvgImage> for ImageKind {
+ fn from(image: SvgImage) -> Self {
+ Self::Svg(image)
+ }
+}
+
/// A raster or vector image format.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ImageFormat {
@@ -335,8 +408,8 @@ pub enum ImageFormat {
impl ImageFormat {
/// Try to detect the format of an image from data.
pub fn detect(data: &[u8]) -> Option<Self> {
- if let Some(format) = RasterFormat::detect(data) {
- return Some(Self::Raster(format));
+ if let Some(format) = ExchangeFormat::detect(data) {
+ return Some(Self::Raster(RasterFormat::Exchange(format)));
}
// SVG or compressed SVG.
@@ -355,9 +428,12 @@ pub enum VectorFormat {
Svg,
}
-impl From<RasterFormat> for ImageFormat {
- fn from(format: RasterFormat) -> Self {
- Self::Raster(format)
+impl<R> From<R> for ImageFormat
+where
+ R: Into<RasterFormat>,
+{
+ fn from(format: R) -> Self {
+ Self::Raster(format.into())
}
}
@@ -371,8 +447,18 @@ cast! {
ImageFormat,
self => match self {
Self::Raster(v) => v.into_value(),
- Self::Vector(v) => v.into_value()
+ Self::Vector(v) => v.into_value(),
},
v: RasterFormat => Self::Raster(v),
v: VectorFormat => Self::Vector(v),
}
+
+/// The image scaling algorithm a viewer should use.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum ImageScaling {
+ /// Scale with a smoothing algorithm such as bilinear interpolation.
+ Smooth,
+ /// Scale with nearest neighbor or a similar algorithm to preserve the
+ /// pixelated look of the image.
+ Pixelated,
+}
diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs
index 098843a2..d43b1548 100644
--- a/crates/typst-library/src/visualize/image/raster.rs
+++ b/crates/typst-library/src/visualize/image/raster.rs
@@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString};
use image::codecs::gif::GifDecoder;
use image::codecs::jpeg::JpegDecoder;
use image::codecs::png::PngDecoder;
-use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits};
+use image::{
+ guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel,
+};
use crate::diag::{bail, StrResult};
-use crate::foundations::{Bytes, Cast};
+use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value};
/// A decoded raster image.
#[derive(Clone, Hash)]
@@ -21,43 +23,118 @@ struct Repr {
data: Bytes,
format: RasterFormat,
dynamic: image::DynamicImage,
- icc: Option<Vec<u8>>,
+ icc: Option<Bytes>,
dpi: Option<f64>,
}
impl RasterImage {
/// Decode a raster image.
- #[comemo::memoize]
- pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> {
- fn decode_with<T: ImageDecoder>(
- decoder: ImageResult<T>,
- ) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
- let mut decoder = decoder?;
- let icc = decoder.icc_profile().ok().flatten().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 (mut 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)?;
-
- let exif = exif::Reader::new()
- .read_from_container(&mut std::io::Cursor::new(&data))
- .ok();
+ pub fn new(
+ data: Bytes,
+ format: impl Into<RasterFormat>,
+ icc: Smart<Bytes>,
+ ) -> StrResult<Self> {
+ Self::new_impl(data, format.into(), icc)
+ }
- // Apply rotation from EXIF metadata.
- if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
- apply_rotation(&mut dynamic, rotation);
- }
+ /// Create a raster image with optional properties set to the default.
+ pub fn plain(data: Bytes, format: impl Into<RasterFormat>) -> StrResult<Self> {
+ Self::new(data, format, Smart::Auto)
+ }
- // Extract pixel density.
- let dpi = determine_dpi(&data, exif.as_ref());
+ /// The internal, non-generic implementation.
+ #[comemo::memoize]
+ #[typst_macros::time(name = "load raster image")]
+ fn new_impl(
+ data: Bytes,
+ format: RasterFormat,
+ icc: Smart<Bytes>,
+ ) -> StrResult<RasterImage> {
+ let (dynamic, icc, dpi) = match format {
+ RasterFormat::Exchange(format) => {
+ fn decode<T: ImageDecoder>(
+ decoder: ImageResult<T>,
+ icc: Smart<Bytes>,
+ ) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
+ let mut decoder = decoder?;
+ let icc = icc.custom().or_else(|| {
+ decoder
+ .icc_profile()
+ .ok()
+ .flatten()
+ .filter(|icc| !icc.is_empty())
+ .map(Bytes::new)
+ });
+ decoder.set_limits(Limits::default())?;
+ let dynamic = image::DynamicImage::from_decoder(decoder)?;
+ Ok((dynamic, icc))
+ }
+
+ let cursor = io::Cursor::new(&data);
+ let (mut dynamic, icc) = match format {
+ ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
+ ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
+ ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
+ }
+ .map_err(format_image_error)?;
+
+ let exif = exif::Reader::new()
+ .read_from_container(&mut std::io::Cursor::new(&data))
+ .ok();
+
+ // Apply rotation from EXIF metadata.
+ if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
+ apply_rotation(&mut dynamic, rotation);
+ }
+
+ // Extract pixel density.
+ let dpi = determine_dpi(&data, exif.as_ref());
+
+ (dynamic, icc, dpi)
+ }
+
+ RasterFormat::Pixel(format) => {
+ if format.width == 0 || format.height == 0 {
+ bail!("zero-sized images are not allowed");
+ }
+
+ let channels = match format.encoding {
+ PixelEncoding::Rgb8 => 3,
+ PixelEncoding::Rgba8 => 4,
+ PixelEncoding::Luma8 => 1,
+ PixelEncoding::Lumaa8 => 2,
+ };
+
+ let Some(expected_size) = format
+ .width
+ .checked_mul(format.height)
+ .and_then(|size| size.checked_mul(channels))
+ else {
+ bail!("pixel dimensions are too large");
+ };
+
+ if expected_size as usize != data.len() {
+ bail!("pixel dimensions and pixel data do not match");
+ }
+
+ fn to<P: Pixel<Subpixel = u8>>(
+ data: &Bytes,
+ format: PixelFormat,
+ ) -> ImageBuffer<P, Vec<u8>> {
+ ImageBuffer::from_raw(format.width, format.height, data.to_vec())
+ .unwrap()
+ }
+
+ let dynamic = match format.encoding {
+ PixelEncoding::Rgb8 => to::<image::Rgb<u8>>(&data, format).into(),
+ PixelEncoding::Rgba8 => to::<image::Rgba<u8>>(&data, format).into(),
+ PixelEncoding::Luma8 => to::<image::Luma<u8>>(&data, format).into(),
+ PixelEncoding::Lumaa8 => to::<image::LumaA<u8>>(&data, format).into(),
+ };
+
+ (dynamic, icc.custom(), None)
+ }
+ };
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
}
@@ -93,60 +170,141 @@ impl RasterImage {
}
/// Access the ICC profile, if any.
- pub fn icc(&self) -> Option<&[u8]> {
- self.0.icc.as_deref()
+ pub fn icc(&self) -> Option<&Bytes> {
+ self.0.icc.as_ref()
}
}
impl Hash for Repr {
fn hash<H: Hasher>(&self, state: &mut H) {
- // The image is fully defined by data and format.
+ // The image is fully defined by data, format, and ICC profile.
self.data.hash(state);
self.format.hash(state);
+ self.icc.hash(state);
}
}
/// A raster graphics format.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum RasterFormat {
+ /// A format typically used in image exchange.
+ Exchange(ExchangeFormat),
+ /// A format of raw pixel data.
+ Pixel(PixelFormat),
+}
+
+impl From<ExchangeFormat> for RasterFormat {
+ fn from(format: ExchangeFormat) -> Self {
+ Self::Exchange(format)
+ }
+}
+
+impl From<PixelFormat> for RasterFormat {
+ fn from(format: PixelFormat) -> Self {
+ Self::Pixel(format)
+ }
+}
+
+cast! {
+ RasterFormat,
+ self => match self {
+ Self::Exchange(v) => v.into_value(),
+ Self::Pixel(v) => v.into_value(),
+ },
+ v: ExchangeFormat => Self::Exchange(v),
+ v: PixelFormat => Self::Pixel(v),
+}
+
+/// A raster format typically used in image exchange, with efficient encoding.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum ExchangeFormat {
/// 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.
+ /// Raster format that is typically used for short animated clips. Typst can
+ /// load GIFs, but they will become static.
Gif,
}
-impl RasterFormat {
+impl ExchangeFormat {
/// 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 {
+impl From<ExchangeFormat> for image::ImageFormat {
+ fn from(format: ExchangeFormat) -> Self {
match format {
- RasterFormat::Png => image::ImageFormat::Png,
- RasterFormat::Jpg => image::ImageFormat::Jpeg,
- RasterFormat::Gif => image::ImageFormat::Gif,
+ ExchangeFormat::Png => image::ImageFormat::Png,
+ ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
+ ExchangeFormat::Gif => image::ImageFormat::Gif,
}
}
}
-impl TryFrom<image::ImageFormat> for RasterFormat {
+impl TryFrom<image::ImageFormat> for ExchangeFormat {
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."),
+ image::ImageFormat::Png => ExchangeFormat::Png,
+ image::ImageFormat::Jpeg => ExchangeFormat::Jpg,
+ image::ImageFormat::Gif => ExchangeFormat::Gif,
+ _ => bail!("format not yet supported"),
})
}
}
+/// Information that is needed to understand a pixmap buffer.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct PixelFormat {
+ /// The channel encoding.
+ encoding: PixelEncoding,
+ /// The pixel width.
+ width: u32,
+ /// The pixel height.
+ height: u32,
+}
+
+/// Determines the channel encoding of raw pixel data.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
+pub enum PixelEncoding {
+ /// Three 8-bit channels: Red, green, blue.
+ Rgb8,
+ /// Four 8-bit channels: Red, green, blue, alpha.
+ Rgba8,
+ /// One 8-bit channel.
+ Luma8,
+ /// Two 8-bit channels: Luma and alpha.
+ Lumaa8,
+}
+
+cast! {
+ PixelFormat,
+ self => Value::Dict(self.into()),
+ mut dict: Dict => {
+ let format = Self {
+ encoding: dict.take("encoding")?.cast()?,
+ width: dict.take("width")?.cast()?,
+ height: dict.take("height")?.cast()?,
+ };
+ dict.finish(&["encoding", "width", "height"])?;
+ format
+ }
+}
+
+impl From<PixelFormat> for Dict {
+ fn from(format: PixelFormat) -> Self {
+ dict! {
+ "encoding" => format.encoding,
+ "width" => format.width,
+ "height" => format.height,
+ }
+ }
+}
+
/// Try to get the rotation from the EXIF metadata.
fn exif_rotation(exif: &exif::Exif) -> Option<u32> {
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?
@@ -266,21 +424,20 @@ fn format_image_error(error: image::ImageError) -> EcoString {
#[cfg(test)]
mod tests {
- use super::{RasterFormat, RasterImage};
- use crate::foundations::Bytes;
+ use super::*;
#[test]
fn test_image_dpi() {
#[track_caller]
- fn test(path: &str, format: RasterFormat, dpi: f64) {
+ fn test(path: &str, format: ExchangeFormat, dpi: f64) {
let data = typst_dev_assets::get(path).unwrap();
let bytes = Bytes::new(data);
- let image = RasterImage::new(bytes, format).unwrap();
+ let image = RasterImage::plain(bytes, format).unwrap();
assert_eq!(image.dpi().map(f64::round), Some(dpi));
}
- test("images/f2t.jpg", RasterFormat::Jpg, 220.0);
- test("images/tiger.jpg", RasterFormat::Jpg, 72.0);
- test("images/graph.png", RasterFormat::Png, 144.0);
+ test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0);
+ test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0);
+ test("images/graph.png", ExchangeFormat::Png, 144.0);
}
}
diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs
index 089f0543..dcc55077 100644
--- a/crates/typst-library/src/visualize/image/svg.rs
+++ b/crates/typst-library/src/visualize/image/svg.rs
@@ -30,6 +30,7 @@ struct Repr {
impl SvgImage {
/// Decode an SVG image without fonts.
#[comemo::memoize]
+ #[typst_macros::time(name = "load svg")]
pub fn new(data: Bytes) -> StrResult<SvgImage> {
let tree =
usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?;
@@ -44,6 +45,7 @@ impl SvgImage {
/// Decode an SVG image with access to fonts.
#[comemo::memoize]
+ #[typst_macros::time(name = "load svg")]
pub fn with_fonts(
data: Bytes,
world: Tracked<dyn World + '_>,