Refactor a bit

This commit is contained in:
Laurenz 2025-01-30 19:14:39 +01:00
parent 1e5b82f95c
commit 19e719ffe1
5 changed files with 77 additions and 118 deletions

View File

@ -61,22 +61,21 @@ pub struct ImageElem {
/// The image's format. /// The image's format.
/// ///
/// By default, the format is detected automatically. Typically, you thus /// By default, the format is detected automatically. Typically, you thus
/// only need to specify this when providing raw bytes as the `source` ( /// only need to specify this when providing raw bytes as the
/// even then, Typst will try to figure out the format automatically, but /// [`source`]($image.source) (even then, Typst will try to figure out the
/// that's not always possible). /// format automatically, but that's not always possible).
/// ///
/// Supported formats include common exchange image formats (`{"png"}`, /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well
/// `{"jpg"}`, `{"gif"}`, and `{"svg"}`) as well as raw pixel data. /// as raw pixel data. Embedding PDFs as images is
/// Embedding PDFs as images is [not currently /// [not currently supported](https://github.com/typst/typst/issues/145).
/// supported](https://github.com/typst/typst/issues/145).
/// ///
/// When providing raw pixel data as the [`source`]($image.source), you must /// When providing raw pixel data as the `source`, you must specify a
/// specify a dictionary with the following keys as the `format`: /// dictionary with the following keys as the `format`:
/// - `encoding` ([str]): The encoding of the pixel data. One of: /// - `encoding` ([str]): The encoding of the pixel data. One of:
/// - `{"rgb8"}` (three 8-bit channels: Red, green, blue.) /// - `{"rgb8"}` (three 8-bit channels: red, green, blue.)
/// - `{"rgba8"}` (four 8-bit channels: Red, green, blue, alpha.) /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha.)
/// - `{"luma8"}` (one 8-bit channel: Brightness.) /// - `{"luma8"}` (one 8-bit channel: brightness.)
/// - `{"lumaa8"}` (two 8-bit channels: Brightness and alpha.) /// - `{"lumaa8"}` (two 8-bit channels: brightness and alpha.)
/// - `width` ([int]): The pixel width of the image. /// - `width` ([int]): The pixel width of the image.
/// - `height` ([int]): The pixel height of the image. /// - `height` ([int]): The pixel height of the image.
/// ///
@ -270,7 +269,7 @@ impl Image {
/// Should always be the same as the default DPI used by usvg. /// Should always be the same as the default DPI used by usvg.
pub const USVG_DEFAULT_DPI: f64 = 96.0; pub const USVG_DEFAULT_DPI: f64 = 96.0;
/// Create an image from a kind. /// Create an image from a `RasterImage` or `SvgImage`.
pub fn new( pub fn new(
kind: impl Into<ImageKind>, kind: impl Into<ImageKind>,
alt: Option<EcoString>, alt: Option<EcoString>,

View File

@ -100,7 +100,7 @@ impl RasterImage {
bail!("pixel dimensions and pixel data do not match"); bail!("pixel dimensions and pixel data do not match");
} }
fn cast_as<P: Pixel<Subpixel = u8>>( fn to<P: Pixel<Subpixel = u8>>(
data: &Bytes, data: &Bytes,
format: PixelFormat, format: PixelFormat,
) -> ImageBuffer<P, Vec<u8>> { ) -> ImageBuffer<P, Vec<u8>> {
@ -109,18 +109,10 @@ impl RasterImage {
} }
let dynamic = match format.encoding { let dynamic = match format.encoding {
PixelEncoding::Rgb8 => { PixelEncoding::Rgb8 => to::<image::Rgb<u8>>(&data, format).into(),
cast_as::<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::Rgba8 => { PixelEncoding::Lumaa8 => to::<image::LumaA<u8>>(&data, format).into(),
cast_as::<image::Rgba<u8>>(&data, format).into()
}
PixelEncoding::Luma8 => {
cast_as::<image::Luma<u8>>(&data, format).into()
}
PixelEncoding::Lumaa8 => {
cast_as::<image::LumaA<u8>>(&data, format).into()
}
}; };
(dynamic, None, None) (dynamic, None, None)
@ -177,7 +169,7 @@ impl Hash for Repr {
/// A raster graphics format. /// A raster graphics format.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum RasterFormat { pub enum RasterFormat {
/// A format used in image exchange. /// A format typically used in image exchange.
Exchange(ExchangeFormat), Exchange(ExchangeFormat),
/// A format of raw pixel data. /// A format of raw pixel data.
Pixel(PixelFormat), Pixel(PixelFormat),
@ -205,7 +197,7 @@ cast! {
v: PixelFormat => Self::Pixel(v), v: PixelFormat => Self::Pixel(v),
} }
/// An raster format typically used in image exchange, with efficient encoding. /// A raster format typically used in image exchange, with efficient encoding.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ExchangeFormat { pub enum ExchangeFormat {
/// Raster format for illustrations and transparent graphics. /// Raster format for illustrations and transparent graphics.
@ -260,13 +252,13 @@ pub struct PixelFormat {
/// Determines the channel encoding of raw pixel data. /// Determines the channel encoding of raw pixel data.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum PixelEncoding { pub enum PixelEncoding {
/// Raw image data with three 8-bit channels: Red, green, blue. /// Three 8-bit channels: Red, green, blue.
Rgb8, Rgb8,
/// Raw image data with four 8-bit channels: Red, green, blue, alpha. /// Four 8-bit channels: Red, green, blue, alpha.
Rgba8, Rgba8,
/// Raw image data with one 8-bit channel: Brightness. /// One 8-bit channel: Brightness.
Luma8, Luma8,
/// Raw image data with two 8-bit channels: Brightness and alpha. /// Two 8-bit channels: Brightness and alpha.
Lumaa8, Lumaa8,
} }

View File

@ -7,7 +7,8 @@ use pdf_writer::{Chunk, Filter, Finish, Ref};
use typst_library::diag::{At, SourceResult, StrResult}; use typst_library::diag::{At, SourceResult, StrResult};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::visualize::{ use typst_library::visualize::{
ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, SvgImage, ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
RasterImage, SvgImage,
}; };
use typst_utils::Deferred; use typst_utils::Deferred;
@ -137,15 +138,7 @@ pub fn deferred_image(
let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
let deferred = Deferred::new(move || match image.kind() { let deferred = Deferred::new(move || match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)),
let format = if raster.format() == RasterFormat::Exchange(ExchangeFormat::Jpg)
{
EncodeFormat::DctDecode
} else {
EncodeFormat::Flate
};
Ok(encode_raster_image(raster.dynamic(), raster.icc(), format, interpolate))
}
ImageKind::Svg(svg) => { ImageKind::Svg(svg) => {
let (chunk, id) = encode_svg(svg, pdfa) let (chunk, id) = encode_svg(svg, pdfa)
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
@ -158,36 +151,31 @@ pub fn deferred_image(
/// Encode an image with a suitable filter. /// Encode an image with a suitable filter.
#[typst_macros::time(name = "encode raster image")] #[typst_macros::time(name = "encode raster image")]
fn encode_raster_image( fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
image: &DynamicImage, let dynamic = image.dynamic();
icc_profile: Option<&[u8]>, let color_space = to_color_space(dynamic.color());
format: EncodeFormat,
interpolate: bool,
) -> EncodedImage {
let color_space = to_color_space(image.color());
let (filter, data, bits_per_component) = match format { let (filter, data, bits_per_component) =
EncodeFormat::DctDecode => { if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
let mut data = Cursor::new(vec![]); let mut data = Cursor::new(vec![]);
image.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
(Filter::DctDecode, data.into_inner(), 8) (Filter::DctDecode, data.into_inner(), 8)
} } else {
EncodeFormat::Flate => {
// TODO: Encode flate streams with PNG-predictor? // TODO: Encode flate streams with PNG-predictor?
let (data, bits_per_component) = match (image, color_space) { let (data, bits_per_component) = match (dynamic, color_space) {
// RGB image.
(DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
// Grayscale image // Grayscale image
(DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
(_, ColorSpace::D65Gray) => (deflate(image.to_luma8().as_raw()), 8), (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8),
// Anything else // Anything else
_ => (deflate(image.to_rgb8().as_raw()), 8), _ => (deflate(dynamic.to_rgb8().as_raw()), 8),
}; };
(Filter::FlateDecode, data, bits_per_component) (Filter::FlateDecode, data, bits_per_component)
} };
};
let compressed_icc = icc_profile.map(deflate); let compressed_icc = image.icc().map(deflate);
let alpha = image.color().has_alpha().then(|| encode_alpha(image)); let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic));
EncodedImage::Raster { EncodedImage::Raster {
data, data,
@ -254,12 +242,6 @@ pub enum EncodedImage {
Svg(Chunk, Ref), Svg(Chunk, Ref),
} }
/// How the raster image should be encoded.
enum EncodeFormat {
DctDecode,
Flate,
}
/// Matches an [`image::ColorType`] to [`ColorSpace`]. /// Matches an [`image::ColorType`] to [`ColorSpace`].
fn to_color_space(color: image::ColorType) -> ColorSpace { fn to_color_space(color: image::ColorType) -> ColorSpace {
use image::ColorType::*; use image::ColorType::*;

View File

@ -62,7 +62,29 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let mut texture = sk::Pixmap::new(w, h)?; let mut texture = sk::Pixmap::new(w, h)?;
match image.kind() { match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
scale_image(&mut texture, raster.dynamic(), image.scaling()) 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();
}
} }
ImageKind::Svg(svg) => { ImageKind::Svg(svg) => {
let tree = svg.tree(); let tree = svg.tree();
@ -75,33 +97,3 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
} }
Some(Arc::new(texture)) Some(Arc::new(texture))
} }
/// Scale a rastered image to a given size and write it into the `texture`.
fn scale_image(
texture: &mut sk::Pixmap,
image: &image::DynamicImage,
scaling: Smart<ImageScaling>,
) {
let w = texture.width();
let h = texture.height();
let buf;
let resized = if (w, h) == (image.width(), image.height()) {
// Small optimization to not allocate in case image is not resized.
image
} else {
let upscale = w > image.width();
let filter = match scaling {
Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest,
_ if upscale => FilterType::CatmullRom,
_ => FilterType::Lanczos3, // downscale
};
buf = image.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();
}
}

View File

@ -4,8 +4,7 @@ use image::{codecs::png::PngEncoder, ImageEncoder};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Axes}; use typst_library::layout::{Abs, Axes};
use typst_library::visualize::{ use typst_library::visualize::{
ExchangeFormat, Image, ImageFormat, ImageKind, ImageScaling, RasterFormat, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
VectorFormat,
}; };
use crate::SVGRenderer; use crate::SVGRenderer;
@ -38,23 +37,18 @@ impl SVGRenderer {
/// `data:image/{format};base64,`. /// `data:image/{format};base64,`.
#[comemo::memoize] #[comemo::memoize]
pub fn convert_image_to_base64_url(image: &Image) -> EcoString { pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
let format = match image.format() {
ImageFormat::Raster(RasterFormat::Exchange(f)) => match f {
ExchangeFormat::Png => "png",
ExchangeFormat::Jpg => "jpeg",
ExchangeFormat::Gif => "gif",
},
ImageFormat::Raster(RasterFormat::Pixel(_)) => "png",
ImageFormat::Vector(f) => match f {
VectorFormat::Svg => "svg+xml",
},
};
let mut buf; let mut buf;
let data = match image.kind() { let (format, data): (&str, &[u8]) = match image.kind() {
ImageKind::Raster(raster) => match raster.format() { ImageKind::Raster(raster) => match raster.format() {
RasterFormat::Exchange(_) => raster.data(), RasterFormat::Exchange(format) => (
RasterFormat::Pixel(_) => { match format {
ExchangeFormat::Png => "png",
ExchangeFormat::Jpg => "jpeg",
ExchangeFormat::Gif => "gif",
},
raster.data(),
),
RasterFormat::Pixel(_) => ("png", {
buf = vec![]; buf = vec![];
let mut encoder = PngEncoder::new(&mut buf); let mut encoder = PngEncoder::new(&mut buf);
if let Some(icc_profile) = raster.icc() { if let Some(icc_profile) = raster.icc() {
@ -62,9 +56,9 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
} }
raster.dynamic().write_with_encoder(encoder).unwrap(); raster.dynamic().write_with_encoder(encoder).unwrap();
buf.as_slice() buf.as_slice()
} }),
}, },
ImageKind::Svg(svg) => svg.data(), ImageKind::Svg(svg) => ("svg+xml", svg.data()),
}; };
let mut url = eco_format!("data:image/{format};base64,"); let mut url = eco_format!("data:image/{format};base64,");