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>
5
Cargo.lock
generated
@ -1122,9 +1122,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.2"
|
version = "0.25.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
@ -3036,6 +3036,7 @@ dependencies = [
|
|||||||
"comemo",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"image",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
|
@ -67,7 +67,7 @@ icu_provider_adapters = "1.4"
|
|||||||
icu_provider_blob = "1.4"
|
icu_provider_blob = "1.4"
|
||||||
icu_segmenter = { version = "1.4", features = ["serde"] }
|
icu_segmenter = { version = "1.4", features = ["serde"] }
|
||||||
if_chain = "1"
|
if_chain = "1"
|
||||||
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] }
|
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
kamadak-exif = "0.5"
|
kamadak-exif = "0.5"
|
||||||
kurbo = "0.11"
|
kurbo = "0.11"
|
||||||
|
@ -10,7 +10,8 @@ use typst_library::layout::{
|
|||||||
use typst_library::loading::DataSource;
|
use typst_library::loading::DataSource;
|
||||||
use typst_library::text::families;
|
use typst_library::text::families;
|
||||||
use typst_library::visualize::{
|
use typst_library::visualize::{
|
||||||
Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat,
|
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
|
||||||
|
RasterImage, SvgImage, VectorFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Layout the image.
|
/// Layout the image.
|
||||||
@ -49,15 +50,27 @@ pub fn layout_image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Construct the image itself.
|
// Construct the image itself.
|
||||||
let image = Image::with_fonts(
|
let kind = match format {
|
||||||
|
ImageFormat::Raster(format) => ImageKind::Raster(
|
||||||
|
RasterImage::new(
|
||||||
data.clone(),
|
data.clone(),
|
||||||
format,
|
format,
|
||||||
elem.alt(styles),
|
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
|
||||||
engine.world,
|
|
||||||
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
|
||||||
elem.flatten_text(styles),
|
|
||||||
)
|
)
|
||||||
.at(span)?;
|
.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.
|
// Determine the image's pixel aspect ratio.
|
||||||
let pxw = image.width();
|
let pxw = image.width();
|
||||||
@ -129,10 +142,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat>
|
|||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
|
"png" => return Ok(ExchangeFormat::Png.into()),
|
||||||
"jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
|
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
||||||
"gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
|
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
||||||
"svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
|
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,9 @@ use xmlwriter::XmlWriter;
|
|||||||
use crate::foundations::Bytes;
|
use crate::foundations::Bytes;
|
||||||
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||||
use crate::text::{Font, Glyph};
|
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
|
/// Whether this glyph should be rendered via simple outlining instead of via
|
||||||
/// `glyph_frame`.
|
/// `glyph_frame`.
|
||||||
@ -102,12 +104,8 @@ fn draw_raster_glyph(
|
|||||||
upem: Abs,
|
upem: Abs,
|
||||||
raster_image: ttf_parser::RasterGlyphImage,
|
raster_image: ttf_parser::RasterGlyphImage,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let image = Image::new(
|
let data = Bytes::new(raster_image.data.to_vec());
|
||||||
Bytes::new(raster_image.data.to_vec()),
|
let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);
|
||||||
RasterFormat::Png.into(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
// Apple Color emoji doesn't provide offset information (or at least
|
// Apple Color emoji doesn't provide offset information (or at least
|
||||||
// not in a way ttf-parser understands), so we artificially shift their
|
// 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)?;
|
ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?;
|
||||||
svg.end_element();
|
svg.end_element();
|
||||||
|
|
||||||
let data = svg.end_document().into_bytes();
|
let data = Bytes::from_string(svg.end_document());
|
||||||
|
let image = Image::plain(SvgImage::new(data).ok()?);
|
||||||
let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?;
|
|
||||||
|
|
||||||
let y_shift = Abs::pt(upem.to_pt() - y_max);
|
let y_shift = Abs::pt(upem.to_pt() - y_max);
|
||||||
let position = Point::new(Abs::pt(x_min), y_shift);
|
let position = Point::new(Abs::pt(x_min), y_shift);
|
||||||
@ -255,9 +252,8 @@ fn draw_svg_glyph(
|
|||||||
ty = -top,
|
ty = -top,
|
||||||
);
|
);
|
||||||
|
|
||||||
let image =
|
let data = Bytes::from_string(wrapper_svg);
|
||||||
Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None)
|
let image = Image::plain(SvgImage::new(data).ok()?);
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
||||||
let size = Size::new(Abs::pt(width), Abs::pt(height));
|
let size = Size::new(Abs::pt(width), Abs::pt(height));
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
mod raster;
|
mod raster;
|
||||||
mod svg;
|
mod svg;
|
||||||
|
|
||||||
pub use self::raster::{RasterFormat, RasterImage};
|
pub use self::raster::{
|
||||||
|
ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
|
||||||
|
};
|
||||||
pub use self::svg::SvgImage;
|
pub use self::svg::SvgImage;
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use comemo::Tracked;
|
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
@ -24,7 +25,6 @@ use crate::layout::{BlockElem, Length, Rel, Sizing};
|
|||||||
use crate::loading::{DataSource, Load, Readable};
|
use crate::loading::{DataSource, Load, Readable};
|
||||||
use crate::model::Figurable;
|
use crate::model::Figurable;
|
||||||
use crate::text::LocalName;
|
use crate::text::LocalName;
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// A raster or vector graphic.
|
/// A raster or vector graphic.
|
||||||
///
|
///
|
||||||
@ -46,7 +46,8 @@ use crate::World;
|
|||||||
/// ```
|
/// ```
|
||||||
#[elem(scope, Show, LocalName, Figurable)]
|
#[elem(scope, Show, LocalName, Figurable)]
|
||||||
pub struct ImageElem {
|
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).
|
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||||
#[required]
|
#[required]
|
||||||
@ -57,10 +58,50 @@ pub struct ImageElem {
|
|||||||
)]
|
)]
|
||||||
pub source: Derived<DataSource, Bytes>,
|
pub source: Derived<DataSource, Bytes>,
|
||||||
|
|
||||||
/// The image's format. Detected automatically by default.
|
/// The image's format.
|
||||||
///
|
///
|
||||||
/// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image
|
/// By default, the format is detected automatically. Typically, you thus
|
||||||
/// is [not currently supported](https://github.com/typst/typst/issues/145).
|
/// 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,
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// #image(
|
||||||
|
/// bytes(range(16).map(x => x * 16)),
|
||||||
|
/// format: (
|
||||||
|
/// encoding: "luma8",
|
||||||
|
/// width: 4,
|
||||||
|
/// height: 4,
|
||||||
|
/// ),
|
||||||
|
/// width: 2cm,
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
pub format: Smart<ImageFormat>,
|
pub format: Smart<ImageFormat>,
|
||||||
|
|
||||||
/// The width of the image.
|
/// The width of the image.
|
||||||
@ -86,6 +127,30 @@ pub struct ImageElem {
|
|||||||
#[default(ImageFit::Cover)]
|
#[default(ImageFit::Cover)]
|
||||||
pub fit: ImageFit,
|
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
|
/// Whether text in SVG images should be converted into curves before
|
||||||
/// embedding. This will result in the text becoming unselectable in the
|
/// embedding. This will result in the text becoming unselectable in the
|
||||||
/// output.
|
/// output.
|
||||||
@ -94,6 +159,7 @@ pub struct ImageElem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[scope]
|
#[scope]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
impl ImageElem {
|
impl ImageElem {
|
||||||
/// Decode a raster or vector graphic from bytes or a string.
|
/// 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.
|
/// How the image should adjust itself to a given area.
|
||||||
#[named]
|
#[named]
|
||||||
fit: Option<ImageFit>,
|
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> {
|
) -> StrResult<Content> {
|
||||||
let bytes = data.into_bytes();
|
let bytes = data.into_bytes();
|
||||||
let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes);
|
let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes);
|
||||||
@ -149,6 +222,12 @@ impl ImageElem {
|
|||||||
if let Some(fit) = fit {
|
if let Some(fit) = fit {
|
||||||
elem.push_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))
|
Ok(elem.pack().spanned(span))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,15 +278,8 @@ struct Repr {
|
|||||||
kind: ImageKind,
|
kind: ImageKind,
|
||||||
/// A text describing the image.
|
/// A text describing the image.
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
}
|
/// The scaling algorithm to use.
|
||||||
|
scaling: Smart<ImageScaling>,
|
||||||
/// A kind of image.
|
|
||||||
#[derive(Hash)]
|
|
||||||
pub enum ImageKind {
|
|
||||||
/// A raster image.
|
|
||||||
Raster(RasterImage),
|
|
||||||
/// An SVG image.
|
|
||||||
Svg(SvgImage),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@ -218,55 +290,29 @@ 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 buffer and a format.
|
/// Create an image from a `RasterImage` or `SvgImage`.
|
||||||
#[comemo::memoize]
|
|
||||||
#[typst_macros::time(name = "load image")]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
data: Bytes,
|
kind: impl Into<ImageKind>,
|
||||||
format: ImageFormat,
|
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
) -> StrResult<Image> {
|
scaling: Smart<ImageScaling>,
|
||||||
let kind = match format {
|
) -> Self {
|
||||||
ImageFormat::Raster(format) => {
|
Self::new_impl(kind.into(), alt, scaling)
|
||||||
ImageKind::Raster(RasterImage::new(data, format)?)
|
|
||||||
}
|
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => {
|
|
||||||
ImageKind::Svg(SvgImage::new(data)?)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a possibly font-dependent image from a buffer and a format.
|
/// 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 internal, non-generic implementation. This is memoized to reuse
|
||||||
|
/// the `Arc` and `LazyHash`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[typst_macros::time(name = "load image")]
|
fn new_impl(
|
||||||
pub fn with_fonts(
|
kind: ImageKind,
|
||||||
data: Bytes,
|
|
||||||
format: ImageFormat,
|
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
world: Tracked<dyn World + '_>,
|
scaling: Smart<ImageScaling>,
|
||||||
families: &[&str],
|
) -> Image {
|
||||||
flatten_text: bool,
|
Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling })))
|
||||||
) -> 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 }))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The raw image data.
|
|
||||||
pub fn data(&self) -> &Bytes {
|
|
||||||
match &self.0.kind {
|
|
||||||
ImageKind::Raster(raster) => raster.data(),
|
|
||||||
ImageKind::Svg(svg) => svg.data(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format of the image.
|
/// The format of the image.
|
||||||
@ -306,6 +352,11 @@ impl Image {
|
|||||||
self.0.alt.as_deref()
|
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.
|
/// The decoded image.
|
||||||
pub fn kind(&self) -> &ImageKind {
|
pub fn kind(&self) -> &ImageKind {
|
||||||
&self.0.kind
|
&self.0.kind
|
||||||
@ -319,10 +370,32 @@ impl Debug for Image {
|
|||||||
.field("width", &self.width())
|
.field("width", &self.width())
|
||||||
.field("height", &self.height())
|
.field("height", &self.height())
|
||||||
.field("alt", &self.alt())
|
.field("alt", &self.alt())
|
||||||
|
.field("scaling", &self.scaling())
|
||||||
.finish()
|
.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.
|
/// A raster or vector image format.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum ImageFormat {
|
pub enum ImageFormat {
|
||||||
@ -335,8 +408,8 @@ pub enum ImageFormat {
|
|||||||
impl ImageFormat {
|
impl ImageFormat {
|
||||||
/// Try to detect the format of an image from data.
|
/// Try to detect the format of an image from data.
|
||||||
pub fn detect(data: &[u8]) -> Option<Self> {
|
pub fn detect(data: &[u8]) -> Option<Self> {
|
||||||
if let Some(format) = RasterFormat::detect(data) {
|
if let Some(format) = ExchangeFormat::detect(data) {
|
||||||
return Some(Self::Raster(format));
|
return Some(Self::Raster(RasterFormat::Exchange(format)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG or compressed SVG.
|
// SVG or compressed SVG.
|
||||||
@ -355,9 +428,12 @@ pub enum VectorFormat {
|
|||||||
Svg,
|
Svg,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RasterFormat> for ImageFormat {
|
impl<R> From<R> for ImageFormat
|
||||||
fn from(format: RasterFormat) -> Self {
|
where
|
||||||
Self::Raster(format)
|
R: Into<RasterFormat>,
|
||||||
|
{
|
||||||
|
fn from(format: R) -> Self {
|
||||||
|
Self::Raster(format.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,8 +447,18 @@ cast! {
|
|||||||
ImageFormat,
|
ImageFormat,
|
||||||
self => match self {
|
self => match self {
|
||||||
Self::Raster(v) => v.into_value(),
|
Self::Raster(v) => v.into_value(),
|
||||||
Self::Vector(v) => v.into_value()
|
Self::Vector(v) => v.into_value(),
|
||||||
},
|
},
|
||||||
v: RasterFormat => Self::Raster(v),
|
v: RasterFormat => Self::Raster(v),
|
||||||
v: VectorFormat => Self::Vector(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,
|
||||||
|
}
|
||||||
|
@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString};
|
|||||||
use image::codecs::gif::GifDecoder;
|
use image::codecs::gif::GifDecoder;
|
||||||
use image::codecs::jpeg::JpegDecoder;
|
use image::codecs::jpeg::JpegDecoder;
|
||||||
use image::codecs::png::PngDecoder;
|
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::diag::{bail, StrResult};
|
||||||
use crate::foundations::{Bytes, Cast};
|
use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value};
|
||||||
|
|
||||||
/// A decoded raster image.
|
/// A decoded raster image.
|
||||||
#[derive(Clone, Hash)]
|
#[derive(Clone, Hash)]
|
||||||
@ -21,19 +23,48 @@ struct Repr {
|
|||||||
data: Bytes,
|
data: Bytes,
|
||||||
format: RasterFormat,
|
format: RasterFormat,
|
||||||
dynamic: image::DynamicImage,
|
dynamic: image::DynamicImage,
|
||||||
icc: Option<Vec<u8>>,
|
icc: Option<Bytes>,
|
||||||
dpi: Option<f64>,
|
dpi: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RasterImage {
|
impl RasterImage {
|
||||||
/// Decode a raster image.
|
/// Decode a raster image.
|
||||||
|
pub fn new(
|
||||||
|
data: Bytes,
|
||||||
|
format: impl Into<RasterFormat>,
|
||||||
|
icc: Smart<Bytes>,
|
||||||
|
) -> StrResult<Self> {
|
||||||
|
Self::new_impl(data, format.into(), icc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal, non-generic implementation.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> {
|
#[typst_macros::time(name = "load raster image")]
|
||||||
fn decode_with<T: ImageDecoder>(
|
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>,
|
decoder: ImageResult<T>,
|
||||||
) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
|
icc: Smart<Bytes>,
|
||||||
|
) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
|
||||||
let mut decoder = decoder?;
|
let mut decoder = decoder?;
|
||||||
let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty());
|
let icc = icc.custom().or_else(|| {
|
||||||
|
decoder
|
||||||
|
.icc_profile()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.filter(|icc| !icc.is_empty())
|
||||||
|
.map(Bytes::new)
|
||||||
|
});
|
||||||
decoder.set_limits(Limits::default())?;
|
decoder.set_limits(Limits::default())?;
|
||||||
let dynamic = image::DynamicImage::from_decoder(decoder)?;
|
let dynamic = image::DynamicImage::from_decoder(decoder)?;
|
||||||
Ok((dynamic, icc))
|
Ok((dynamic, icc))
|
||||||
@ -41,9 +72,9 @@ impl RasterImage {
|
|||||||
|
|
||||||
let cursor = io::Cursor::new(&data);
|
let cursor = io::Cursor::new(&data);
|
||||||
let (mut dynamic, icc) = match format {
|
let (mut dynamic, icc) = match format {
|
||||||
RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
|
ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
|
||||||
RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
|
ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
|
||||||
RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
|
ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
|
||||||
}
|
}
|
||||||
.map_err(format_image_error)?;
|
.map_err(format_image_error)?;
|
||||||
|
|
||||||
@ -59,6 +90,52 @@ impl RasterImage {
|
|||||||
// Extract pixel density.
|
// Extract pixel density.
|
||||||
let dpi = determine_dpi(&data, exif.as_ref());
|
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 })))
|
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,60 +170,141 @@ impl RasterImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Access the ICC profile, if any.
|
/// Access the ICC profile, if any.
|
||||||
pub fn icc(&self) -> Option<&[u8]> {
|
pub fn icc(&self) -> Option<&Bytes> {
|
||||||
self.0.icc.as_deref()
|
self.0.icc.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Repr {
|
impl Hash for Repr {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
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.data.hash(state);
|
||||||
self.format.hash(state);
|
self.format.hash(state);
|
||||||
|
self.icc.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A raster graphics format.
|
/// A raster graphics format.
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum RasterFormat {
|
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.
|
/// Raster format for illustrations and transparent graphics.
|
||||||
Png,
|
Png,
|
||||||
/// Lossy raster format suitable for photos.
|
/// Lossy raster format suitable for photos.
|
||||||
Jpg,
|
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,
|
Gif,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RasterFormat {
|
impl ExchangeFormat {
|
||||||
/// Try to detect the format of data in a buffer.
|
/// Try to detect the format of data in a buffer.
|
||||||
pub fn detect(data: &[u8]) -> Option<Self> {
|
pub fn detect(data: &[u8]) -> Option<Self> {
|
||||||
guess_format(data).ok().and_then(|format| format.try_into().ok())
|
guess_format(data).ok().and_then(|format| format.try_into().ok())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RasterFormat> for image::ImageFormat {
|
impl From<ExchangeFormat> for image::ImageFormat {
|
||||||
fn from(format: RasterFormat) -> Self {
|
fn from(format: ExchangeFormat) -> Self {
|
||||||
match format {
|
match format {
|
||||||
RasterFormat::Png => image::ImageFormat::Png,
|
ExchangeFormat::Png => image::ImageFormat::Png,
|
||||||
RasterFormat::Jpg => image::ImageFormat::Jpeg,
|
ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
|
||||||
RasterFormat::Gif => image::ImageFormat::Gif,
|
ExchangeFormat::Gif => image::ImageFormat::Gif,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<image::ImageFormat> for RasterFormat {
|
impl TryFrom<image::ImageFormat> for ExchangeFormat {
|
||||||
type Error = EcoString;
|
type Error = EcoString;
|
||||||
|
|
||||||
fn try_from(format: image::ImageFormat) -> StrResult<Self> {
|
fn try_from(format: image::ImageFormat) -> StrResult<Self> {
|
||||||
Ok(match format {
|
Ok(match format {
|
||||||
image::ImageFormat::Png => RasterFormat::Png,
|
image::ImageFormat::Png => ExchangeFormat::Png,
|
||||||
image::ImageFormat::Jpeg => RasterFormat::Jpg,
|
image::ImageFormat::Jpeg => ExchangeFormat::Jpg,
|
||||||
image::ImageFormat::Gif => RasterFormat::Gif,
|
image::ImageFormat::Gif => ExchangeFormat::Gif,
|
||||||
_ => bail!("Format not yet supported."),
|
_ => 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.
|
/// Try to get the rotation from the EXIF metadata.
|
||||||
fn exif_rotation(exif: &exif::Exif) -> Option<u32> {
|
fn exif_rotation(exif: &exif::Exif) -> Option<u32> {
|
||||||
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?
|
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?
|
||||||
@ -266,21 +424,20 @@ fn format_image_error(error: image::ImageError) -> EcoString {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{RasterFormat, RasterImage};
|
use super::*;
|
||||||
use crate::foundations::Bytes;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_image_dpi() {
|
fn test_image_dpi() {
|
||||||
#[track_caller]
|
#[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 data = typst_dev_assets::get(path).unwrap();
|
||||||
let bytes = Bytes::new(data);
|
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));
|
assert_eq!(image.dpi().map(f64::round), Some(dpi));
|
||||||
}
|
}
|
||||||
|
|
||||||
test("images/f2t.jpg", RasterFormat::Jpg, 220.0);
|
test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0);
|
||||||
test("images/tiger.jpg", RasterFormat::Jpg, 72.0);
|
test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0);
|
||||||
test("images/graph.png", RasterFormat::Png, 144.0);
|
test("images/graph.png", ExchangeFormat::Png, 144.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ struct Repr {
|
|||||||
impl SvgImage {
|
impl SvgImage {
|
||||||
/// Decode an SVG image without fonts.
|
/// Decode an SVG image without fonts.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
#[typst_macros::time(name = "load svg")]
|
||||||
pub fn new(data: Bytes) -> StrResult<SvgImage> {
|
pub fn new(data: Bytes) -> StrResult<SvgImage> {
|
||||||
let tree =
|
let tree =
|
||||||
usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?;
|
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.
|
/// Decode an SVG image with access to fonts.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
#[typst_macros::time(name = "load svg")]
|
||||||
pub fn with_fonts(
|
pub fn with_fonts(
|
||||||
data: Bytes,
|
data: Bytes,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
|
@ -5,8 +5,10 @@ use ecow::eco_format;
|
|||||||
use image::{DynamicImage, GenericImageView, Rgba};
|
use image::{DynamicImage, GenericImageView, Rgba};
|
||||||
use pdf_writer::{Chunk, Filter, Finish, Ref};
|
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::visualize::{
|
use typst_library::visualize::{
|
||||||
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
|
ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||||
|
RasterImage, SvgImage,
|
||||||
};
|
};
|
||||||
use typst_utils::Deferred;
|
use typst_utils::Deferred;
|
||||||
|
|
||||||
@ -32,11 +34,13 @@ pub fn write_images(
|
|||||||
EncodedImage::Raster {
|
EncodedImage::Raster {
|
||||||
data,
|
data,
|
||||||
filter,
|
filter,
|
||||||
has_color,
|
color_space,
|
||||||
|
bits_per_component,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
icc,
|
compressed_icc,
|
||||||
alpha,
|
alpha,
|
||||||
|
interpolate,
|
||||||
} => {
|
} => {
|
||||||
let image_ref = chunk.alloc();
|
let image_ref = chunk.alloc();
|
||||||
out.insert(image.clone(), image_ref);
|
out.insert(image.clone(), image_ref);
|
||||||
@ -45,23 +49,18 @@ pub fn write_images(
|
|||||||
image.filter(*filter);
|
image.filter(*filter);
|
||||||
image.width(*width as i32);
|
image.width(*width as i32);
|
||||||
image.height(*height 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 mut icc_ref = None;
|
||||||
let space = image.color_space();
|
let space = image.color_space();
|
||||||
if icc.is_some() {
|
if compressed_icc.is_some() {
|
||||||
let id = chunk.alloc.bump();
|
let id = chunk.alloc.bump();
|
||||||
space.icc_based(id);
|
space.icc_based(id);
|
||||||
icc_ref = Some(id);
|
icc_ref = Some(id);
|
||||||
} else if *has_color {
|
|
||||||
color::write(
|
|
||||||
ColorSpace::Srgb,
|
|
||||||
space,
|
|
||||||
&context.globals.color_functions,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
color::write(
|
color::write(
|
||||||
ColorSpace::D65Gray,
|
*color_space,
|
||||||
space,
|
space,
|
||||||
&context.globals.color_functions,
|
&context.globals.color_functions,
|
||||||
);
|
);
|
||||||
@ -79,21 +78,28 @@ pub fn write_images(
|
|||||||
mask.width(*width as i32);
|
mask.width(*width as i32);
|
||||||
mask.height(*height as i32);
|
mask.height(*height as i32);
|
||||||
mask.color_space().device_gray();
|
mask.color_space().device_gray();
|
||||||
mask.bits_per_component(8);
|
mask.bits_per_component(i32::from(*bits_per_component));
|
||||||
|
mask.interpolate(*interpolate);
|
||||||
} else {
|
} else {
|
||||||
image.finish();
|
image.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
|
if let (Some(compressed_icc), Some(icc_ref)) =
|
||||||
let mut stream = chunk.icc_profile(icc_ref, icc);
|
(compressed_icc, icc_ref)
|
||||||
|
{
|
||||||
|
let mut stream = chunk.icc_profile(icc_ref, compressed_icc);
|
||||||
stream.filter(Filter::FlateDecode);
|
stream.filter(Filter::FlateDecode);
|
||||||
if *has_color {
|
match color_space {
|
||||||
|
ColorSpace::Srgb => {
|
||||||
stream.n(3);
|
stream.n(3);
|
||||||
stream.alternate().srgb();
|
stream.alternate().srgb();
|
||||||
} else {
|
}
|
||||||
|
ColorSpace::D65Gray => {
|
||||||
stream.n(1);
|
stream.n(1);
|
||||||
stream.alternate().d65_gray();
|
stream.alternate().d65_gray();
|
||||||
}
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EncodedImage::Svg(svg_chunk, id) => {
|
EncodedImage::Svg(svg_chunk, id) => {
|
||||||
@ -122,35 +128,17 @@ pub fn deferred_image(
|
|||||||
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
|
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
|
||||||
let color_space = match image.kind() {
|
let color_space = match image.kind() {
|
||||||
ImageKind::Raster(raster) if raster.icc().is_none() => {
|
ImageKind::Raster(raster) if raster.icc().is_none() => {
|
||||||
if raster.dynamic().color().channel_count() > 2 {
|
Some(to_color_space(raster.dynamic().color()))
|
||||||
Some(ColorSpace::Srgb)
|
|
||||||
} else {
|
|
||||||
Some(ColorSpace::D65Gray)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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() {
|
let deferred = Deferred::new(move || match image.kind() {
|
||||||
ImageKind::Raster(raster) => {
|
ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)),
|
||||||
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::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}"))?;
|
||||||
@ -161,42 +149,51 @@ pub fn deferred_image(
|
|||||||
(deferred, color_space)
|
(deferred, color_space)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode an image with a suitable filter and return the data, filter and
|
/// Encode an image with a suitable filter.
|
||||||
/// whether the image has color.
|
|
||||||
///
|
|
||||||
/// Skips the alpha channel as that's encoded separately.
|
|
||||||
#[typst_macros::time(name = "encode raster image")]
|
#[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 dynamic = image.dynamic();
|
||||||
let channel_count = dynamic.color().channel_count();
|
let color_space = to_color_space(dynamic.color());
|
||||||
let has_color = channel_count > 2;
|
|
||||||
|
|
||||||
if image.format() == RasterFormat::Jpg {
|
let (filter, data, bits_per_component) =
|
||||||
|
if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
|
||||||
let mut data = Cursor::new(vec![]);
|
let mut data = Cursor::new(vec![]);
|
||||||
dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
||||||
(data.into_inner(), Filter::DctDecode, has_color)
|
(Filter::DctDecode, data.into_inner(), 8)
|
||||||
} else {
|
} else {
|
||||||
// TODO: Encode flate streams with PNG-predictor?
|
// TODO: Encode flate streams with PNG-predictor?
|
||||||
let data = match (dynamic, channel_count) {
|
let (data, bits_per_component) = match (dynamic, color_space) {
|
||||||
(DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
|
// RGB image.
|
||||||
(DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
|
(DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
|
||||||
// Grayscale image
|
// Grayscale image
|
||||||
(_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()),
|
(DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
|
||||||
|
(_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8),
|
||||||
// Anything else
|
// Anything else
|
||||||
_ => deflate(dynamic.to_rgb8().as_raw()),
|
_ => (deflate(dynamic.to_rgb8().as_raw()), 8),
|
||||||
};
|
};
|
||||||
(data, Filter::FlateDecode, has_color)
|
(Filter::FlateDecode, data, bits_per_component)
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
/// Encode an image's alpha channel if present.
|
||||||
#[typst_macros::time(name = "encode alpha")]
|
#[typst_macros::time(name = "encode alpha")]
|
||||||
fn encode_alpha(raster: &RasterImage) -> (Vec<u8>, Filter) {
|
fn encode_alpha(image: &DynamicImage) -> (Vec<u8>, Filter) {
|
||||||
let pixels: Vec<_> = raster
|
let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
||||||
.dynamic()
|
|
||||||
.pixels()
|
|
||||||
.map(|(_, _, Rgba([_, _, _, a]))| a)
|
|
||||||
.collect();
|
|
||||||
(deflate(&pixels), Filter::FlateDecode)
|
(deflate(&pixels), Filter::FlateDecode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,19 +221,33 @@ pub enum EncodedImage {
|
|||||||
data: Vec<u8>,
|
data: Vec<u8>,
|
||||||
/// The filter to use for the image.
|
/// The filter to use for the image.
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
/// Whether the image has color.
|
/// Which color space this image is encoded in.
|
||||||
has_color: bool,
|
color_space: ColorSpace,
|
||||||
|
/// How many bits of each color component are stored.
|
||||||
|
bits_per_component: u8,
|
||||||
/// The image's width.
|
/// The image's width.
|
||||||
width: u32,
|
width: u32,
|
||||||
/// The image's height.
|
/// The image's height.
|
||||||
height: u32,
|
height: u32,
|
||||||
/// The image's ICC profile, pre-deflated, if any.
|
/// The image's ICC profile, deflated, if any.
|
||||||
icc: Option<Vec<u8>>,
|
compressed_icc: Option<Vec<u8>>,
|
||||||
/// The alpha channel of the image, pre-deflated, if any.
|
/// The alpha channel of the image, pre-deflated, if any.
|
||||||
alpha: Option<(Vec<u8>, Filter)>,
|
alpha: Option<(Vec<u8>, Filter)>,
|
||||||
|
/// Whether image interpolation should be enabled.
|
||||||
|
interpolate: bool,
|
||||||
},
|
},
|
||||||
/// A vector graphic.
|
/// A vector graphic.
|
||||||
///
|
///
|
||||||
/// The chunk is the SVG converted to PDF objects.
|
/// The chunk is the SVG converted to PDF objects.
|
||||||
Svg(Chunk, Ref),
|
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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,8 +3,9 @@ use std::sync::Arc;
|
|||||||
use image::imageops::FilterType;
|
use image::imageops::FilterType;
|
||||||
use image::{GenericImageView, Rgba};
|
use image::{GenericImageView, Rgba};
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
|
use typst_library::foundations::Smart;
|
||||||
use typst_library::layout::Size;
|
use typst_library::layout::Size;
|
||||||
use typst_library::visualize::{Image, ImageKind};
|
use typst_library::visualize::{Image, ImageKind, ImageScaling};
|
||||||
|
|
||||||
use crate::{AbsExt, State};
|
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 w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32;
|
||||||
let h = ((w as f32) / aspect).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_x = view_width / pixmap.width() as f32;
|
||||||
let paint_scale_y = view_height / pixmap.height() 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.
|
/// Prepare a texture for an image at a scaled size.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn scaled_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||||
let mut pixmap = 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) => {
|
||||||
let downscale = w < raster.width();
|
let w = texture.width();
|
||||||
let filter =
|
let h = texture.height();
|
||||||
if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
|
|
||||||
let buf = raster.dynamic().resize(w, h, filter);
|
let buf;
|
||||||
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
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;
|
let Rgba([r, g, b, a]) = src;
|
||||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
*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) => {
|
ImageKind::Svg(svg) => {
|
||||||
let tree = svg.tree();
|
let tree = svg.tree();
|
||||||
let ts = tiny_skia::Transform::from_scale(
|
let ts = tiny_skia::Transform::from_scale(
|
||||||
w as f32 / tree.size().width(),
|
w as f32 / tree.size().width(),
|
||||||
h as f32 / tree.size().height(),
|
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))
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ base64 = { workspace = true }
|
|||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
flate2 = { workspace = true }
|
flate2 = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
ttf-parser = { workspace = true }
|
ttf-parser = { workspace = true }
|
||||||
xmlparser = { workspace = true }
|
xmlparser = { workspace = true }
|
||||||
xmlwriter = { workspace = true }
|
xmlwriter = { workspace = true }
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ecow::{eco_format, EcoString};
|
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::layout::{Abs, Axes};
|
||||||
use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat};
|
use typst_library::visualize::{
|
||||||
|
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::SVGRenderer;
|
use crate::SVGRenderer;
|
||||||
|
|
||||||
@ -14,6 +18,17 @@ impl SVGRenderer {
|
|||||||
self.xml.write_attribute("width", &size.x.to_pt());
|
self.xml.write_attribute("width", &size.x.to_pt());
|
||||||
self.xml.write_attribute("height", &size.y.to_pt());
|
self.xml.write_attribute("height", &size.y.to_pt());
|
||||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
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();
|
self.xml.end_element();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,19 +37,32 @@ 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() {
|
let mut buf;
|
||||||
ImageFormat::Raster(f) => match f {
|
let (format, data): (&str, &[u8]) = match image.kind() {
|
||||||
RasterFormat::Png => "png",
|
ImageKind::Raster(raster) => match raster.format() {
|
||||||
RasterFormat::Jpg => "jpeg",
|
RasterFormat::Exchange(format) => (
|
||||||
RasterFormat::Gif => "gif",
|
match format {
|
||||||
|
ExchangeFormat::Png => "png",
|
||||||
|
ExchangeFormat::Jpg => "jpeg",
|
||||||
|
ExchangeFormat::Gif => "gif",
|
||||||
},
|
},
|
||||||
ImageFormat::Vector(f) => match f {
|
raster.data(),
|
||||||
VectorFormat::Svg => "svg+xml",
|
),
|
||||||
|
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 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.push_str(&data);
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,9 @@ use ttf_parser::GlyphId;
|
|||||||
use typst_library::foundations::Bytes;
|
use typst_library::foundations::Bytes;
|
||||||
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
|
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
|
||||||
use typst_library::text::{Font, TextItem};
|
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 typst_utils::hash128;
|
||||||
|
|
||||||
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
|
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 {
|
if raster.format != ttf_parser::RasterImageFormat::PNG {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let image =
|
let image = Image::plain(
|
||||||
Image::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None)
|
RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?,
|
||||||
.ok()?;
|
);
|
||||||
Some((image, raster.x as f64, raster.y as f64))
|
Some((image, raster.x as f64, raster.y as f64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
tests/ref/image-pixmap-luma8.png
Normal file
After Width: | Height: | Size: 321 B |
BIN
tests/ref/image-pixmap-lumaa8.png
Normal file
After Width: | Height: | Size: 299 B |
BIN
tests/ref/image-pixmap-rgb8.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
tests/ref/image-pixmap-rgba8.png
Normal file
After Width: | Height: | Size: 854 B |
BIN
tests/ref/image-scaling-methods.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@ -65,6 +65,82 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
caption: [Bilingual text]
|
caption: [Bilingual text]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-rgb8 ---
|
||||||
|
#image(
|
||||||
|
bytes((
|
||||||
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80,
|
||||||
|
)),
|
||||||
|
format: (
|
||||||
|
encoding: "rgb8",
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
),
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-rgba8 ---
|
||||||
|
#image(
|
||||||
|
bytes((
|
||||||
|
0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
|
||||||
|
0xFF, 0x00, 0x00, 0x80, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x00, 0xFF, 0x80,
|
||||||
|
0xFF, 0x00, 0x00, 0x10, 0x00, 0xFF, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x10,
|
||||||
|
)),
|
||||||
|
format: (
|
||||||
|
encoding: "rgba8",
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
),
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-luma8 ---
|
||||||
|
#image(
|
||||||
|
bytes(range(16).map(x => x * 16)),
|
||||||
|
format: (
|
||||||
|
encoding: "luma8",
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-lumaa8 ---
|
||||||
|
#image(
|
||||||
|
bytes(range(16).map(x => (0x80, x * 16)).flatten()),
|
||||||
|
format: (
|
||||||
|
encoding: "lumaa8",
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-scaling-methods ---
|
||||||
|
#let img(scaling) = image(
|
||||||
|
bytes((
|
||||||
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80,
|
||||||
|
)),
|
||||||
|
format: (
|
||||||
|
encoding: "rgb8",
|
||||||
|
width: 3,
|
||||||
|
height: 3,
|
||||||
|
),
|
||||||
|
width: 1cm,
|
||||||
|
scaling: scaling,
|
||||||
|
)
|
||||||
|
|
||||||
|
#stack(
|
||||||
|
dir: ltr,
|
||||||
|
spacing: 4pt,
|
||||||
|
img(auto),
|
||||||
|
img("smooth"),
|
||||||
|
img("pixelated"),
|
||||||
|
)
|
||||||
|
|
||||||
--- image-natural-dpi-sizing ---
|
--- image-natural-dpi-sizing ---
|
||||||
// Test that images aren't upscaled.
|
// Test that images aren't upscaled.
|
||||||
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
||||||
@ -103,6 +179,58 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
// Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.)
|
// Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.)
|
||||||
#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%)
|
#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%)
|
||||||
|
|
||||||
|
--- image-pixmap-empty ---
|
||||||
|
// Error: 1:2-8:2 zero-sized images are not allowed
|
||||||
|
#image(
|
||||||
|
bytes(()),
|
||||||
|
format: (
|
||||||
|
encoding: "rgb8",
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-invalid-size ---
|
||||||
|
// Error: 1:2-8:2 pixel dimensions and pixel data do not match
|
||||||
|
#image(
|
||||||
|
bytes((0x00, 0x00, 0x00)),
|
||||||
|
format: (
|
||||||
|
encoding: "rgb8",
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-unknown-attribute ---
|
||||||
|
#image(
|
||||||
|
bytes((0x00, 0x00, 0x00)),
|
||||||
|
// Error: 1:11-6:4 unexpected key "stowaway", valid keys are "encoding", "width", and "height"
|
||||||
|
format: (
|
||||||
|
encoding: "rgb8",
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
stowaway: "I do work here, promise",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-but-png-format ---
|
||||||
|
#image(
|
||||||
|
bytes((0x00, 0x00, 0x00)),
|
||||||
|
// Error: 1:11-5:4 expected "rgb8", "rgba8", "luma8", or "lumaa8"
|
||||||
|
format: (
|
||||||
|
encoding: "png",
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-png-but-pixmap-format ---
|
||||||
|
#image(
|
||||||
|
read("/assets/images/tiger.jpg", encoding: none),
|
||||||
|
// Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto
|
||||||
|
format: "rgba8",
|
||||||
|
)
|
||||||
|
|
||||||
--- issue-870-image-rotation ---
|
--- issue-870-image-rotation ---
|
||||||
// Ensure that EXIF rotation is applied.
|
// Ensure that EXIF rotation is applied.
|
||||||
// https://github.com/image-rs/image/issues/1045
|
// https://github.com/image-rs/image/issues/1045
|
||||||
|