summaryrefslogtreecommitdiff
path: root/crates
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
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')
-rw-r--r--crates/typst-layout/src/image.rs41
-rw-r--r--crates/typst-library/src/text/font/color.rs22
-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
-rw-r--r--crates/typst-pdf/src/image.rs165
-rw-r--r--crates/typst-render/src/image.rs40
-rw-r--r--crates/typst-svg/Cargo.toml1
-rw-r--r--crates/typst-svg/src/image.rs48
-rw-r--r--crates/typst-svg/src/text.rs10
10 files changed, 561 insertions, 251 deletions
diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs
index e521b993..503c3082 100644
--- a/crates/typst-layout/src/image.rs
+++ b/crates/typst-layout/src/image.rs
@@ -10,7 +10,8 @@ use typst_library::layout::{
use typst_library::loading::DataSource;
use typst_library::text::families;
use typst_library::visualize::{
- Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
+ Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
+ RasterImage, SvgImage, VectorFormat,
};
/// Layout the image.
@@ -49,15 +50,27 @@ pub fn layout_image(
}
// Construct the image itself.
- let image = Image::with_fonts(
- data.clone(),
- format,
- elem.alt(styles),
- engine.world,
- &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
- elem.flatten_text(styles),
- )
- .at(span)?;
+ let kind = match format {
+ ImageFormat::Raster(format) => ImageKind::Raster(
+ RasterImage::new(
+ data.clone(),
+ format,
+ elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
+ )
+ .at(span)?,
+ ),
+ ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
+ SvgImage::with_fonts(
+ data.clone(),
+ engine.world,
+ elem.flatten_text(styles),
+ &families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
+ )
+ .at(span)?,
+ ),
+ };
+
+ let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@@ -129,10 +142,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
.to_lowercase();
match ext.as_str() {
- "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
- "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
- "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
- "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
+ "png" => return Ok(ExchangeFormat::Png.into()),
+ "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
+ "gif" => return Ok(ExchangeFormat::Gif.into()),
+ "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
_ => {}
}
}
diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs
index e3183e88..0a7b13c9 100644
--- a/crates/typst-library/src/text/font/color.rs
+++ b/crates/typst-library/src/text/font/color.rs
@@ -10,7 +10,9 @@ use xmlwriter::XmlWriter;
use crate::foundations::Bytes;
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
use crate::text::{Font, Glyph};
-use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat};
+use crate::visualize::{
+ ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage,
+};
/// Whether this glyph should be rendered via simple outlining instead of via
/// `glyph_frame`.
@@ -102,12 +104,8 @@ fn draw_raster_glyph(
upem: Abs,
raster_image: ttf_parser::RasterGlyphImage,
) -> Option<()> {
- let image = Image::new(
- Bytes::new(raster_image.data.to_vec()),
- RasterFormat::Png.into(),
- None,
- )
- .ok()?;
+ let data = Bytes::new(raster_image.data.to_vec());
+ let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);
// Apple Color emoji doesn't provide offset information (or at least
// not in a way ttf-parser understands), so we artificially shift their
@@ -178,9 +176,8 @@ fn draw_colr_glyph(
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
svg.end_element();
- let data = svg.end_document().into_bytes();
-
- let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?;
+ let data = Bytes::from_string(svg.end_document());
+ let image = Image::plain(SvgImage::new(data).ok()?);
let y_shift = Abs::pt(upem.to_pt() - y_max);
let position = Point::new(Abs::pt(x_min), y_shift);
@@ -255,9 +252,8 @@ fn draw_svg_glyph(
ty = -top,
);
- let image =
- Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None)
- .ok()?;
+ let data = Bytes::from_string(wrapper_svg);
+ let image = Image::plain(SvgImage::new(data).ok()?);
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
let size = Size::new(Abs::pt(width), Abs::pt(height));
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 + '_>,
diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs
index bff7bfef..550f60a4 100644
--- a/crates/typst-pdf/src/image.rs
+++ b/crates/typst-pdf/src/image.rs
@@ -5,8 +5,10 @@ use ecow::eco_format;
use image::{DynamicImage, GenericImageView, Rgba};
use pdf_writer::{Chunk, Filter, Finish, Ref};
use typst_library::diag::{At, SourceResult, StrResult};
+use typst_library::foundations::Smart;
use typst_library::visualize::{
- ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
+ ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
+ RasterImage, SvgImage,
};
use typst_utils::Deferred;
@@ -32,11 +34,13 @@ pub fn write_images(
EncodedImage::Raster {
data,
filter,
- has_color,
+ color_space,
+ bits_per_component,
width,
height,
- icc,
+ compressed_icc,
alpha,
+ interpolate,
} => {
let image_ref = chunk.alloc();
out.insert(image.clone(), image_ref);
@@ -45,23 +49,18 @@ pub fn write_images(
image.filter(*filter);
image.width(*width as i32);
image.height(*height as i32);
- image.bits_per_component(8);
+ image.bits_per_component(i32::from(*bits_per_component));
+ image.interpolate(*interpolate);
let mut icc_ref = None;
let space = image.color_space();
- if icc.is_some() {
+ if compressed_icc.is_some() {
let id = chunk.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
- } else if *has_color {
- color::write(
- ColorSpace::Srgb,
- space,
- &context.globals.color_functions,
- );
} else {
color::write(
- ColorSpace::D65Gray,
+ *color_space,
space,
&context.globals.color_functions,
);
@@ -79,20 +78,27 @@ pub fn write_images(
mask.width(*width as i32);
mask.height(*height as i32);
mask.color_space().device_gray();
- mask.bits_per_component(8);
+ mask.bits_per_component(i32::from(*bits_per_component));
+ mask.interpolate(*interpolate);
} else {
image.finish();
}
- if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
- let mut stream = chunk.icc_profile(icc_ref, icc);
+ if let (Some(compressed_icc), Some(icc_ref)) =
+ (compressed_icc, icc_ref)
+ {
+ let mut stream = chunk.icc_profile(icc_ref, compressed_icc);
stream.filter(Filter::FlateDecode);
- if *has_color {
- stream.n(3);
- stream.alternate().srgb();
- } else {
- stream.n(1);
- stream.alternate().d65_gray();
+ match color_space {
+ ColorSpace::Srgb => {
+ stream.n(3);
+ stream.alternate().srgb();
+ }
+ ColorSpace::D65Gray => {
+ stream.n(1);
+ stream.alternate().d65_gray();
+ }
+ _ => unimplemented!(),
}
}
}
@@ -122,35 +128,17 @@ pub fn deferred_image(
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
let color_space = match image.kind() {
ImageKind::Raster(raster) if raster.icc().is_none() => {
- if raster.dynamic().color().channel_count() > 2 {
- Some(ColorSpace::Srgb)
- } else {
- Some(ColorSpace::D65Gray)
- }
+ Some(to_color_space(raster.dynamic().color()))
}
_ => None,
};
+ // PDF/A does not appear to allow interpolation.
+ // See https://github.com/typst/typst/issues/2942.
+ let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
+
let deferred = Deferred::new(move || match image.kind() {
- ImageKind::Raster(raster) => {
- let raster = raster.clone();
- let (width, height) = (raster.width(), raster.height());
- let (data, filter, has_color) = encode_raster_image(&raster);
- let icc = raster.icc().map(deflate);
-
- let alpha =
- raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster));
-
- Ok(EncodedImage::Raster {
- data,
- filter,
- has_color,
- width,
- height,
- icc,
- alpha,
- })
- }
+ ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)),
ImageKind::Svg(svg) => {
let (chunk, id) = encode_svg(svg, pdfa)
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
@@ -161,42 +149,51 @@ pub fn deferred_image(
(deferred, color_space)
}
-/// Encode an image with a suitable filter and return the data, filter and
-/// whether the image has color.
-///
-/// Skips the alpha channel as that's encoded separately.
+/// Encode an image with a suitable filter.
#[typst_macros::time(name = "encode raster image")]
-fn encode_raster_image(image: &RasterImage) -> (Vec<u8>, Filter, bool) {
+fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
let dynamic = image.dynamic();
- let channel_count = dynamic.color().channel_count();
- let has_color = channel_count > 2;
-
- if image.format() == RasterFormat::Jpg {
- let mut data = Cursor::new(vec![]);
- dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
- (data.into_inner(), Filter::DctDecode, has_color)
- } else {
- // TODO: Encode flate streams with PNG-predictor?
- let data = match (dynamic, channel_count) {
- (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
- (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
- // Grayscale image
- (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()),
- // Anything else
- _ => deflate(dynamic.to_rgb8().as_raw()),
+ let color_space = to_color_space(dynamic.color());
+
+ let (filter, data, bits_per_component) =
+ if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
+ let mut data = Cursor::new(vec![]);
+ dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
+ (Filter::DctDecode, data.into_inner(), 8)
+ } else {
+ // TODO: Encode flate streams with PNG-predictor?
+ let (data, bits_per_component) = match (dynamic, color_space) {
+ // RGB image.
+ (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
+ // Grayscale image
+ (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
+ (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8),
+ // Anything else
+ _ => (deflate(dynamic.to_rgb8().as_raw()), 8),
+ };
+ (Filter::FlateDecode, data, bits_per_component)
};
- (data, Filter::FlateDecode, has_color)
+
+ let compressed_icc = image.icc().map(|data| deflate(data));
+ let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic));
+
+ EncodedImage::Raster {
+ data,
+ filter,
+ color_space,
+ bits_per_component,
+ width: image.width(),
+ height: image.height(),
+ compressed_icc,
+ alpha,
+ interpolate,
}
}
/// Encode an image's alpha channel if present.
#[typst_macros::time(name = "encode alpha")]
-fn encode_alpha(raster: &RasterImage) -> (Vec<u8>, Filter) {
- let pixels: Vec<_> = raster
- .dynamic()
- .pixels()
- .map(|(_, _, Rgba([_, _, _, a]))| a)
- .collect();
+fn encode_alpha(image: &DynamicImage) -> (Vec<u8>, Filter) {
+ let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
(deflate(&pixels), Filter::FlateDecode)
}
@@ -224,19 +221,33 @@ pub enum EncodedImage {
data: Vec<u8>,
/// The filter to use for the image.
filter: Filter,
- /// Whether the image has color.
- has_color: bool,
+ /// Which color space this image is encoded in.
+ color_space: ColorSpace,
+ /// How many bits of each color component are stored.
+ bits_per_component: u8,
/// The image's width.
width: u32,
/// The image's height.
height: u32,
- /// The image's ICC profile, pre-deflated, if any.
- icc: Option<Vec<u8>>,
+ /// The image's ICC profile, deflated, if any.
+ compressed_icc: Option<Vec<u8>>,
/// The alpha channel of the image, pre-deflated, if any.
alpha: Option<(Vec<u8>, Filter)>,
+ /// Whether image interpolation should be enabled.
+ interpolate: bool,
},
/// A vector graphic.
///
/// The chunk is the SVG converted to PDF objects.
Svg(Chunk, Ref),
}
+
+/// Matches an [`image::ColorType`] to [`ColorSpace`].
+fn to_color_space(color: image::ColorType) -> ColorSpace {
+ use image::ColorType::*;
+ match color {
+ L8 | La8 | L16 | La16 => ColorSpace::D65Gray,
+ Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb,
+ _ => unimplemented!(),
+ }
+}
diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs
index 27b03911..7425bdd2 100644
--- a/crates/typst-render/src/image.rs
+++ b/crates/typst-render/src/image.rs
@@ -3,8 +3,9 @@ use std::sync::Arc;
use image::imageops::FilterType;
use image::{GenericImageView, Rgba};
use tiny_skia as sk;
+use typst_library::foundations::Smart;
use typst_library::layout::Size;
-use typst_library::visualize::{Image, ImageKind};
+use typst_library::visualize::{Image, ImageKind, ImageScaling};
use crate::{AbsExt, State};
@@ -34,7 +35,7 @@ pub fn render_image(
let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32;
let h = ((w as f32) / aspect).ceil() as u32;
- let pixmap = scaled_texture(image, w, h)?;
+ let pixmap = build_texture(image, w, h)?;
let paint_scale_x = view_width / pixmap.width() as f32;
let paint_scale_y = view_height / pixmap.height() as f32;
@@ -57,29 +58,42 @@ pub fn render_image(
/// Prepare a texture for an image at a scaled size.
#[comemo::memoize]
-fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
- let mut pixmap = sk::Pixmap::new(w, h)?;
+fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
+ let mut texture = sk::Pixmap::new(w, h)?;
match image.kind() {
ImageKind::Raster(raster) => {
- let downscale = w < raster.width();
- let filter =
- if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
- let buf = raster.dynamic().resize(w, h, filter);
- for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
+ let w = texture.width();
+ let h = texture.height();
+
+ let buf;
+ let dynamic = raster.dynamic();
+ let resized = if (w, h) == (dynamic.width(), dynamic.height()) {
+ // Small optimization to not allocate in case image is not resized.
+ dynamic
+ } else {
+ let upscale = w > dynamic.width();
+ let filter = match image.scaling() {
+ Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest,
+ _ if upscale => FilterType::CatmullRom,
+ _ => FilterType::Lanczos3, // downscale
+ };
+ buf = dynamic.resize_exact(w, h, filter);
+ &buf
+ };
+
+ for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) {
let Rgba([r, g, b, a]) = src;
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
}
}
- // Safety: We do not keep any references to tree nodes beyond the scope
- // of `with`.
ImageKind::Svg(svg) => {
let tree = svg.tree();
let ts = tiny_skia::Transform::from_scale(
w as f32 / tree.size().width(),
h as f32 / tree.size().height(),
);
- resvg::render(tree, ts, &mut pixmap.as_mut())
+ resvg::render(tree, ts, &mut texture.as_mut());
}
}
- Some(Arc::new(pixmap))
+ Some(Arc::new(texture))
}
diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml
index 41d35565..5416621e 100644
--- a/crates/typst-svg/Cargo.toml
+++ b/crates/typst-svg/Cargo.toml
@@ -21,6 +21,7 @@ base64 = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
flate2 = { workspace = true }
+image = { workspace = true }
ttf-parser = { workspace = true }
xmlparser = { workspace = true }
xmlwriter = { workspace = true }
diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs
index ede4e76e..d7443202 100644
--- a/crates/typst-svg/src/image.rs
+++ b/crates/typst-svg/src/image.rs
@@ -1,7 +1,11 @@
use base64::Engine;
use ecow::{eco_format, EcoString};
+use image::{codecs::png::PngEncoder, ImageEncoder};
+use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Axes};
-use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat};
+use typst_library::visualize::{
+ ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
+};
use crate::SVGRenderer;
@@ -14,6 +18,17 @@ impl SVGRenderer {
self.xml.write_attribute("width", &size.x.to_pt());
self.xml.write_attribute("height", &size.y.to_pt());
self.xml.write_attribute("preserveAspectRatio", "none");
+ match image.scaling() {
+ Smart::Auto => {}
+ Smart::Custom(ImageScaling::Smooth) => {
+ // This is still experimental and not implemented in all major browsers.
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
+ self.xml.write_attribute("style", "image-rendering: smooth")
+ }
+ Smart::Custom(ImageScaling::Pixelated) => {
+ self.xml.write_attribute("style", "image-rendering: pixelated")
+ }
+ }
self.xml.end_element();
}
}
@@ -22,19 +37,32 @@ impl SVGRenderer {
/// `data:image/{format};base64,`.
#[comemo::memoize]
pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
- let format = match image.format() {
- ImageFormat::Raster(f) => match f {
- RasterFormat::Png => "png",
- RasterFormat::Jpg => "jpeg",
- RasterFormat::Gif => "gif",
- },
- ImageFormat::Vector(f) => match f {
- VectorFormat::Svg => "svg+xml",
+ let mut buf;
+ let (format, data): (&str, &[u8]) = match image.kind() {
+ ImageKind::Raster(raster) => match raster.format() {
+ RasterFormat::Exchange(format) => (
+ match format {
+ ExchangeFormat::Png => "png",
+ ExchangeFormat::Jpg => "jpeg",
+ ExchangeFormat::Gif => "gif",
+ },
+ raster.data(),
+ ),
+ RasterFormat::Pixel(_) => ("png", {
+ buf = vec![];
+ let mut encoder = PngEncoder::new(&mut buf);
+ if let Some(icc_profile) = raster.icc() {
+ encoder.set_icc_profile(icc_profile.to_vec()).ok();
+ }
+ raster.dynamic().write_with_encoder(encoder).unwrap();
+ buf.as_slice()
+ }),
},
+ ImageKind::Svg(svg) => ("svg+xml", svg.data()),
};
let mut url = eco_format!("data:image/{format};base64,");
- let data = base64::engine::general_purpose::STANDARD.encode(image.data());
+ let data = base64::engine::general_purpose::STANDARD.encode(data);
url.push_str(&data);
url
}
diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs
index fa471b2a..e6620a59 100644
--- a/crates/typst-svg/src/text.rs
+++ b/crates/typst-svg/src/text.rs
@@ -6,7 +6,9 @@ use ttf_parser::GlyphId;
use typst_library::foundations::Bytes;
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
use typst_library::text::{Font, TextItem};
-use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo};
+use typst_library::visualize::{
+ ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo,
+};
use typst_utils::hash128;
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
@@ -244,9 +246,9 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64
if raster.format != ttf_parser::RasterImageFormat::PNG {
return None;
}
- let image =
- Image::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None)
- .ok()?;
+ let image = Image::plain(
+ RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?,
+ );
Some((image, raster.x as f64, raster.y as f64))
}