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.
|
/// 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>,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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::*;
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user