mirror of
https://github.com/typst/typst
synced 2025-08-13 14:47:54 +08:00
Refactor a bit
This commit is contained in:
parent
1e5b82f95c
commit
19e719ffe1
@ -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>,
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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::*;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
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",
|
||||
},
|
||||
ImageFormat::Raster(RasterFormat::Pixel(_)) => "png",
|
||||
ImageFormat::Vector(f) => match f {
|
||||
VectorFormat::Svg => "svg+xml",
|
||||
},
|
||||
};
|
||||
|
||||
let mut buf;
|
||||
let data = match image.kind() {
|
||||
ImageKind::Raster(raster) => match raster.format() {
|
||||
RasterFormat::Exchange(_) => raster.data(),
|
||||
RasterFormat::Pixel(_) => {
|
||||
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,");
|
||||
|
Loading…
x
Reference in New Issue
Block a user