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.
///
/// By default, the format is detected automatically. Typically, you thus
/// only need to specify this when providing raw bytes as the `source` (
/// even then, Typst will try to figure out the format automatically, but
/// that's not always possible).
/// 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 include common exchange image formats (`{"png"}`,
/// `{"jpg"}`, `{"gif"}`, and `{"svg"}`) as well as raw pixel data.
/// Embedding PDFs as images is [not currently
/// supported](https://github.com/typst/typst/issues/145).
/// 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`]($image.source), you must
/// specify a dictionary with the following keys as the `format`:
/// 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: Brightness.)
/// - `{"lumaa8"}` (two 8-bit channels: Brightness and alpha.)
/// - `{"rgb8"}` (three 8-bit channels: red, green, blue.)
/// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha.)
/// - `{"luma8"}` (one 8-bit channel: brightness.)
/// - `{"lumaa8"}` (two 8-bit channels: brightness and alpha.)
/// - `width` ([int]): The pixel width 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.
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(
kind: impl Into<ImageKind>,
alt: Option<EcoString>,

View File

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

View File

@ -7,7 +7,8 @@ use pdf_writer::{Chunk, Filter, Finish, Ref};
use typst_library::diag::{At, SourceResult, StrResult};
use typst_library::foundations::Smart;
use typst_library::visualize::{
ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, SvgImage,
ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
RasterImage, SvgImage,
};
use typst_utils::Deferred;
@ -137,15 +138,7 @@ pub fn deferred_image(
let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
let deferred = Deferred::new(move || match image.kind() {
ImageKind::Raster(raster) => {
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::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}"))?;
@ -158,36 +151,31 @@ pub fn deferred_image(
/// Encode an image with a suitable filter.
#[typst_macros::time(name = "encode raster image")]
fn encode_raster_image(
image: &DynamicImage,
icc_profile: Option<&[u8]>,
format: EncodeFormat,
interpolate: bool,
) -> EncodedImage {
let color_space = to_color_space(image.color());
fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
let dynamic = image.dynamic();
let color_space = to_color_space(dynamic.color());
let (filter, data, bits_per_component) = match format {
EncodeFormat::DctDecode => {
let (filter, data, bits_per_component) =
if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
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)
}
EncodeFormat::Flate => {
} else {
// 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),
// Grayscale image
(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
_ => (deflate(image.to_rgb8().as_raw()), 8),
_ => (deflate(dynamic.to_rgb8().as_raw()), 8),
};
(Filter::FlateDecode, data, bits_per_component)
}
};
};
let compressed_icc = icc_profile.map(deflate);
let alpha = image.color().has_alpha().then(|| encode_alpha(image));
let compressed_icc = image.icc().map(deflate);
let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic));
EncodedImage::Raster {
data,
@ -254,12 +242,6 @@ pub enum EncodedImage {
Svg(Chunk, Ref),
}
/// How the raster image should be encoded.
enum EncodeFormat {
DctDecode,
Flate,
}
/// Matches an [`image::ColorType`] to [`ColorSpace`].
fn to_color_space(color: image::ColorType) -> ColorSpace {
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)?;
match image.kind() {
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) => {
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))
}
/// 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::layout::{Abs, Axes};
use typst_library::visualize::{
ExchangeFormat, Image, ImageFormat, ImageKind, ImageScaling, RasterFormat,
VectorFormat,
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
};
use crate::SVGRenderer;
@ -38,23 +37,18 @@ 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(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 data = match image.kind() {
let (format, data): (&str, &[u8]) = match image.kind() {
ImageKind::Raster(raster) => match raster.format() {
RasterFormat::Exchange(_) => raster.data(),
RasterFormat::Pixel(_) => {
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() {
@ -62,9 +56,9 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
}
raster.dynamic().write_with_encoder(encoder).unwrap();
buf.as_slice()
}
}),
},
ImageKind::Svg(svg) => svg.data(),
ImageKind::Svg(svg) => ("svg+xml", svg.data()),
};
let mut url = eco_format!("data:image/{format};base64,");