mirror of
https://github.com/typst/typst
synced 2025-08-14 07:07:54 +08:00
parent
bb38a01d06
commit
259a029723
5
Cargo.lock
generated
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::Readable;
|
use typst_library::loading::Readable;
|
||||||
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, Image, ImageElem, ImageFit, ImageFormat, ImageSource, RasterFormat,
|
||||||
|
VectorFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Layout the image.
|
/// Layout the image.
|
||||||
@ -26,31 +27,38 @@ pub fn layout_image(
|
|||||||
|
|
||||||
// Take the format that was explicitly defined, or parse the extension,
|
// Take the format that was explicitly defined, or parse the extension,
|
||||||
// or try to detect the format.
|
// or try to detect the format.
|
||||||
let data = elem.data();
|
let source = elem.source();
|
||||||
let format = match elem.format(styles) {
|
let format = match (elem.format(styles), source) {
|
||||||
Smart::Custom(v) => v,
|
(Smart::Custom(v), _) => v,
|
||||||
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
|
(Smart::Auto, ImageSource::Readable(data)) => {
|
||||||
|
determine_format(elem.path().as_str(), data).at(span)?
|
||||||
|
}
|
||||||
|
(Smart::Auto, ImageSource::Pixmap(_)) => {
|
||||||
|
bail!(span, "pixmaps require an explicit image format to be given");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Warn the user if the image contains a foreign object. Not perfect
|
// Warn the user if the image contains a foreign object. Not perfect
|
||||||
// because the svg could also be encoded, but that's an edge case.
|
// because the svg could also be encoded, but that's an edge case.
|
||||||
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
if let ImageSource::Readable(data) = source {
|
||||||
let has_foreign_object =
|
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||||
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
|
let has_foreign_object =
|
||||||
|
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
|
||||||
|
|
||||||
if has_foreign_object {
|
if has_foreign_object {
|
||||||
engine.sink.warn(warning!(
|
engine.sink.warn(warning!(
|
||||||
span,
|
span,
|
||||||
"image contains foreign object";
|
"image contains foreign object";
|
||||||
hint: "SVG images with foreign objects might render incorrectly in typst";
|
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||||
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||||
));
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the image itself.
|
// Construct the image itself.
|
||||||
let image = Image::with_fonts(
|
let image = Image::with_fonts(
|
||||||
data.clone().into(),
|
source.clone(),
|
||||||
format,
|
format,
|
||||||
elem.alt(styles),
|
elem.alt(styles),
|
||||||
engine.world,
|
engine.world,
|
||||||
|
@ -7,9 +7,13 @@ use typst_syntax::Span;
|
|||||||
use usvg::tiny_skia_path;
|
use usvg::tiny_skia_path;
|
||||||
use xmlwriter::XmlWriter;
|
use xmlwriter::XmlWriter;
|
||||||
|
|
||||||
|
use crate::foundations::Bytes;
|
||||||
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||||
|
use crate::loading::Readable;
|
||||||
use crate::text::{Font, Glyph};
|
use crate::text::{Font, Glyph};
|
||||||
use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat};
|
use crate::visualize::{
|
||||||
|
FixedStroke, Geometry, Image, ImageSource, RasterFormat, VectorFormat,
|
||||||
|
};
|
||||||
|
|
||||||
/// 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,7 +106,8 @@ fn draw_raster_glyph(
|
|||||||
raster_image: ttf_parser::RasterGlyphImage,
|
raster_image: ttf_parser::RasterGlyphImage,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let image =
|
let image =
|
||||||
Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?;
|
Image::new(Bytes::from(raster_image.data).into(), 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
|
||||||
@ -175,7 +180,8 @@ fn draw_colr_glyph(
|
|||||||
|
|
||||||
let data = svg.end_document().into_bytes();
|
let data = svg.end_document().into_bytes();
|
||||||
|
|
||||||
let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?;
|
let image =
|
||||||
|
Image::new(Bytes::from(data).into(), 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);
|
||||||
@ -250,9 +256,8 @@ fn draw_svg_glyph(
|
|||||||
ty = -top,
|
ty = -top,
|
||||||
);
|
);
|
||||||
|
|
||||||
let image =
|
let source = ImageSource::Readable(Readable::Str(wrapper_svg.into()));
|
||||||
Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None)
|
let image = Image::new(source, VectorFormat::Svg.into(), None).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));
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! Image handling.
|
//! Image handling.
|
||||||
|
|
||||||
|
mod pixmap;
|
||||||
mod raster;
|
mod raster;
|
||||||
mod svg;
|
mod svg;
|
||||||
|
|
||||||
@ -11,14 +12,15 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
|
use pixmap::{Pixmap, PixmapFormat, PixmapSource};
|
||||||
use typst_syntax::{Span, Spanned};
|
use typst_syntax::{Span, Spanned};
|
||||||
use typst_utils::LazyHash;
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::diag::{At, SourceResult, StrResult};
|
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart,
|
cast, elem, func, scope, Bytes, Cast, Content, Dict, NativeElement, Packed, Show,
|
||||||
StyleChain,
|
Smart, StyleChain,
|
||||||
};
|
};
|
||||||
use crate::layout::{BlockElem, Length, Rel, Sizing};
|
use crate::layout::{BlockElem, Length, Rel, Sizing};
|
||||||
use crate::loading::Readable;
|
use crate::loading::Readable;
|
||||||
@ -60,11 +62,11 @@ pub struct ImageElem {
|
|||||||
#[borrowed]
|
#[borrowed]
|
||||||
pub path: EcoString,
|
pub path: EcoString,
|
||||||
|
|
||||||
/// The raw file data.
|
/// The data required to decode the image.
|
||||||
#[internal]
|
#[internal]
|
||||||
#[required]
|
#[required]
|
||||||
#[parse(Readable::Bytes(data))]
|
#[parse(data.into())]
|
||||||
pub data: Readable,
|
pub source: ImageSource,
|
||||||
|
|
||||||
/// The image's format. Detected automatically by default.
|
/// The image's format. Detected automatically by default.
|
||||||
///
|
///
|
||||||
@ -103,6 +105,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.
|
||||||
///
|
///
|
||||||
@ -121,7 +124,7 @@ impl ImageElem {
|
|||||||
/// The call span of this function.
|
/// The call span of this function.
|
||||||
span: Span,
|
span: Span,
|
||||||
/// The data to decode as an image. Can be a string for SVGs.
|
/// The data to decode as an image. Can be a string for SVGs.
|
||||||
data: Readable,
|
source: ImageSource,
|
||||||
/// The image's format. Detected automatically by default.
|
/// The image's format. Detected automatically by default.
|
||||||
#[named]
|
#[named]
|
||||||
format: Option<Smart<ImageFormat>>,
|
format: Option<Smart<ImageFormat>>,
|
||||||
@ -138,7 +141,7 @@ impl ImageElem {
|
|||||||
#[named]
|
#[named]
|
||||||
fit: Option<ImageFit>,
|
fit: Option<ImageFit>,
|
||||||
) -> StrResult<Content> {
|
) -> StrResult<Content> {
|
||||||
let mut elem = ImageElem::new(EcoString::new(), data);
|
let mut elem = ImageElem::new(EcoString::new(), source);
|
||||||
if let Some(format) = format {
|
if let Some(format) = format {
|
||||||
elem.push_format(format);
|
elem.push_format(format);
|
||||||
}
|
}
|
||||||
@ -213,6 +216,8 @@ pub enum ImageKind {
|
|||||||
Raster(RasterImage),
|
Raster(RasterImage),
|
||||||
/// An SVG image.
|
/// An SVG image.
|
||||||
Svg(SvgImage),
|
Svg(SvgImage),
|
||||||
|
/// An image constructed from a pixmap.
|
||||||
|
Pixmap(Pixmap),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
@ -223,20 +228,32 @@ 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 source and a format.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[typst_macros::time(name = "load image")]
|
#[typst_macros::time(name = "load image")]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
data: Bytes,
|
source: ImageSource,
|
||||||
format: ImageFormat,
|
format: ImageFormat,
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
) -> StrResult<Image> {
|
) -> StrResult<Image> {
|
||||||
let kind = match format {
|
let kind = match format {
|
||||||
ImageFormat::Raster(format) => {
|
ImageFormat::Raster(format) => {
|
||||||
ImageKind::Raster(RasterImage::new(data, format)?)
|
let ImageSource::Readable(readable) = source else {
|
||||||
|
bail!("expected readable source for the given format (str or bytes)");
|
||||||
|
};
|
||||||
|
ImageKind::Raster(RasterImage::new(readable.into(), format)?)
|
||||||
}
|
}
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => {
|
ImageFormat::Vector(VectorFormat::Svg) => {
|
||||||
ImageKind::Svg(SvgImage::new(data)?)
|
let ImageSource::Readable(readable) = source else {
|
||||||
|
bail!("expected readable source for the given format (str or bytes)");
|
||||||
|
};
|
||||||
|
ImageKind::Svg(SvgImage::new(readable.into())?)
|
||||||
|
}
|
||||||
|
ImageFormat::Pixmap(format) => {
|
||||||
|
let ImageSource::Pixmap(source) = source else {
|
||||||
|
bail!("source must be a pixmap");
|
||||||
|
};
|
||||||
|
ImageKind::Pixmap(Pixmap::new(source, format)?)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -247,7 +264,7 @@ impl Image {
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[typst_macros::time(name = "load image")]
|
#[typst_macros::time(name = "load image")]
|
||||||
pub fn with_fonts(
|
pub fn with_fonts(
|
||||||
data: Bytes,
|
source: ImageSource,
|
||||||
format: ImageFormat,
|
format: ImageFormat,
|
||||||
alt: Option<EcoString>,
|
alt: Option<EcoString>,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
@ -256,29 +273,39 @@ impl Image {
|
|||||||
) -> StrResult<Image> {
|
) -> StrResult<Image> {
|
||||||
let kind = match format {
|
let kind = match format {
|
||||||
ImageFormat::Raster(format) => {
|
ImageFormat::Raster(format) => {
|
||||||
ImageKind::Raster(RasterImage::new(data, format)?)
|
let ImageSource::Readable(readable) = source else {
|
||||||
|
bail!("expected readable source for the given format (str or bytes)");
|
||||||
|
};
|
||||||
|
ImageKind::Raster(RasterImage::new(readable.into(), format)?)
|
||||||
}
|
}
|
||||||
ImageFormat::Vector(VectorFormat::Svg) => {
|
ImageFormat::Vector(VectorFormat::Svg) => {
|
||||||
ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?)
|
let ImageSource::Readable(readable) = source else {
|
||||||
|
bail!("expected readable source for the given format (str or bytes)");
|
||||||
|
};
|
||||||
|
ImageKind::Svg(SvgImage::with_fonts(
|
||||||
|
readable.into(),
|
||||||
|
world,
|
||||||
|
flatten_text,
|
||||||
|
families,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
ImageFormat::Pixmap(format) => {
|
||||||
|
let ImageSource::Pixmap(source) = source else {
|
||||||
|
bail!("source must be pixmap");
|
||||||
|
};
|
||||||
|
ImageKind::Pixmap(Pixmap::new(source, format)?)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
|
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.
|
||||||
pub fn format(&self) -> ImageFormat {
|
pub fn format(&self) -> ImageFormat {
|
||||||
match &self.0.kind {
|
match &self.0.kind {
|
||||||
ImageKind::Raster(raster) => raster.format().into(),
|
ImageKind::Raster(raster) => raster.format().into(),
|
||||||
ImageKind::Svg(_) => VectorFormat::Svg.into(),
|
ImageKind::Svg(_) => VectorFormat::Svg.into(),
|
||||||
|
ImageKind::Pixmap(pixmap) => pixmap.format().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,6 +314,7 @@ impl Image {
|
|||||||
match &self.0.kind {
|
match &self.0.kind {
|
||||||
ImageKind::Raster(raster) => raster.width() as f64,
|
ImageKind::Raster(raster) => raster.width() as f64,
|
||||||
ImageKind::Svg(svg) => svg.width(),
|
ImageKind::Svg(svg) => svg.width(),
|
||||||
|
ImageKind::Pixmap(pixmap) => pixmap.width() as f64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +323,7 @@ impl Image {
|
|||||||
match &self.0.kind {
|
match &self.0.kind {
|
||||||
ImageKind::Raster(raster) => raster.height() as f64,
|
ImageKind::Raster(raster) => raster.height() as f64,
|
||||||
ImageKind::Svg(svg) => svg.height(),
|
ImageKind::Svg(svg) => svg.height(),
|
||||||
|
ImageKind::Pixmap(pixmap) => pixmap.height() as f64,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +332,7 @@ impl Image {
|
|||||||
match &self.0.kind {
|
match &self.0.kind {
|
||||||
ImageKind::Raster(raster) => raster.dpi(),
|
ImageKind::Raster(raster) => raster.dpi(),
|
||||||
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
|
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
|
||||||
|
ImageKind::Pixmap(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,6 +358,34 @@ impl Debug for Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information required to decode an image.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||||
|
pub enum ImageSource {
|
||||||
|
Readable(Readable),
|
||||||
|
Pixmap(Arc<PixmapSource>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bytes> for ImageSource {
|
||||||
|
fn from(bytes: Bytes) -> Self {
|
||||||
|
ImageSource::Readable(Readable::Bytes(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cast! {
|
||||||
|
ImageSource,
|
||||||
|
data: Readable => ImageSource::Readable(data),
|
||||||
|
mut dict: Dict => {
|
||||||
|
let source = ImageSource::Pixmap(Arc::new(PixmapSource {
|
||||||
|
data: dict.take("data")?.cast()?,
|
||||||
|
pixel_width: dict.take("pixel-width")?.cast()?,
|
||||||
|
pixel_height: dict.take("pixel-height")?.cast()?,
|
||||||
|
icc_profile: dict.take("icc-profile").ok().map(|value| value.cast()).transpose()?,
|
||||||
|
}));
|
||||||
|
dict.finish(&["data", "pixel-width", "pixel-height", "icc-profile"])?;
|
||||||
|
source
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// 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,6 +393,8 @@ pub enum ImageFormat {
|
|||||||
Raster(RasterFormat),
|
Raster(RasterFormat),
|
||||||
/// A vector graphics format.
|
/// A vector graphics format.
|
||||||
Vector(VectorFormat),
|
Vector(VectorFormat),
|
||||||
|
/// A format made up of flat pixels without metadata or compression.
|
||||||
|
Pixmap(PixmapFormat),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A vector graphics format.
|
/// A vector graphics format.
|
||||||
@ -356,12 +416,20 @@ impl From<VectorFormat> for ImageFormat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PixmapFormat> for ImageFormat {
|
||||||
|
fn from(format: PixmapFormat) -> Self {
|
||||||
|
Self::Pixmap(format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cast! {
|
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(),
|
||||||
|
Self::Pixmap(v) => v.into_value(),
|
||||||
},
|
},
|
||||||
v: RasterFormat => Self::Raster(v),
|
v: RasterFormat => Self::Raster(v),
|
||||||
v: VectorFormat => Self::Vector(v),
|
v: VectorFormat => Self::Vector(v),
|
||||||
|
v: PixmapFormat => Self::Pixmap(v),
|
||||||
}
|
}
|
||||||
|
115
crates/typst-library/src/visualize/image/pixmap.rs
Normal file
115
crates/typst-library/src/visualize/image/pixmap.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use image::{DynamicImage, ImageBuffer, Pixel};
|
||||||
|
|
||||||
|
use crate::diag::{bail, StrResult};
|
||||||
|
use crate::foundations::{Bytes, Cast};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Hash)]
|
||||||
|
pub struct PixmapSource {
|
||||||
|
pub data: Bytes,
|
||||||
|
pub pixel_width: u32,
|
||||||
|
pub pixel_height: u32,
|
||||||
|
pub icc_profile: Option<Bytes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A raster image based on a flat pixmap.
|
||||||
|
#[derive(Clone, Hash)]
|
||||||
|
pub struct Pixmap(Arc<Repr>);
|
||||||
|
|
||||||
|
/// The internal representation.
|
||||||
|
#[derive(Hash)]
|
||||||
|
struct Repr {
|
||||||
|
source: Arc<PixmapSource>,
|
||||||
|
format: PixmapFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pixmap {
|
||||||
|
/// Build a new [`Pixmap`] from a flat, uncompressed byte sequence.
|
||||||
|
#[comemo::memoize]
|
||||||
|
pub fn new(source: Arc<PixmapSource>, format: PixmapFormat) -> StrResult<Pixmap> {
|
||||||
|
if source.pixel_width == 0 || source.pixel_height == 0 {
|
||||||
|
bail!("zero-sized images are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pixel_size = match format {
|
||||||
|
PixmapFormat::Rgb8 => 3,
|
||||||
|
PixmapFormat::Rgba8 => 4,
|
||||||
|
PixmapFormat::Luma8 => 1,
|
||||||
|
PixmapFormat::Lumaa8 => 2,
|
||||||
|
};
|
||||||
|
let Some(expected_size) = source
|
||||||
|
.pixel_width
|
||||||
|
.checked_mul(source.pixel_height)
|
||||||
|
.and_then(|size| size.checked_mul(pixel_size))
|
||||||
|
else {
|
||||||
|
bail!("provided pixel dimensions are too large");
|
||||||
|
};
|
||||||
|
if expected_size as usize != source.data.len() {
|
||||||
|
bail!("provided pixel dimensions and pixmap data do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(Arc::new(Repr { source, format })))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The image's format.
|
||||||
|
pub fn format(&self) -> PixmapFormat {
|
||||||
|
self.0.format
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The image's pixel width.
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.0.source.pixel_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The image's pixel height.
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.0.source.pixel_height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw data encoded in the given format.
|
||||||
|
pub fn data(&self) -> &[u8] {
|
||||||
|
self.0.source.data.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform the image data into an [`DynamicImage`].
|
||||||
|
#[comemo::memoize]
|
||||||
|
pub fn to_image(&self) -> Arc<DynamicImage> {
|
||||||
|
// TODO optimize by returning a `View` if possible?
|
||||||
|
fn decode<P: Pixel<Subpixel = u8>>(
|
||||||
|
source: &PixmapSource,
|
||||||
|
) -> ImageBuffer<P, Vec<u8>> {
|
||||||
|
ImageBuffer::from_raw(
|
||||||
|
source.pixel_width,
|
||||||
|
source.pixel_height,
|
||||||
|
source.data.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
Arc::new(match self.0.format {
|
||||||
|
PixmapFormat::Rgb8 => decode::<image::Rgb<u8>>(&self.0.source).into(),
|
||||||
|
PixmapFormat::Rgba8 => decode::<image::Rgba<u8>>(&self.0.source).into(),
|
||||||
|
PixmapFormat::Luma8 => decode::<image::Luma<u8>>(&self.0.source).into(),
|
||||||
|
PixmapFormat::Lumaa8 => decode::<image::LumaA<u8>>(&self.0.source).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the ICC profile, if any.
|
||||||
|
pub fn icc_profile(&self) -> Option<&[u8]> {
|
||||||
|
self.0.source.icc_profile.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines how the given image is interpreted and encoded.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||||
|
pub enum PixmapFormat {
|
||||||
|
/// The red, green, and blue channels are each eight bit integers.
|
||||||
|
/// There is no alpha channel.
|
||||||
|
Rgb8,
|
||||||
|
/// The red, green, blue, and alpha channels are each eight bit integers.
|
||||||
|
Rgba8,
|
||||||
|
/// A single eight bit channel representing brightness.
|
||||||
|
Luma8,
|
||||||
|
/// One byte of brightness, another for alpha.
|
||||||
|
Lumaa8,
|
||||||
|
}
|
@ -21,7 +21,7 @@ struct Repr {
|
|||||||
data: Bytes,
|
data: Bytes,
|
||||||
format: RasterFormat,
|
format: RasterFormat,
|
||||||
dynamic: image::DynamicImage,
|
dynamic: image::DynamicImage,
|
||||||
icc: Option<Vec<u8>>,
|
icc_profile: Option<Vec<u8>>,
|
||||||
dpi: Option<f64>,
|
dpi: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ impl RasterImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cursor = io::Cursor::new(&data);
|
let cursor = io::Cursor::new(&data);
|
||||||
let (mut dynamic, icc) = match format {
|
let (mut dynamic, icc_profile) = match format {
|
||||||
RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
|
RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
|
||||||
RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
|
RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
|
||||||
RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
|
RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
|
||||||
@ -59,7 +59,7 @@ impl RasterImage {
|
|||||||
// Extract pixel density.
|
// Extract pixel density.
|
||||||
let dpi = determine_dpi(&data, exif.as_ref());
|
let dpi = determine_dpi(&data, exif.as_ref());
|
||||||
|
|
||||||
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
|
Ok(Self(Arc::new(Repr { data, format, dynamic, icc_profile, dpi })))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The raw image data.
|
/// The raw image data.
|
||||||
@ -93,8 +93,8 @@ impl RasterImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Access the ICC profile, if any.
|
/// Access the ICC profile, if any.
|
||||||
pub fn icc(&self) -> Option<&[u8]> {
|
pub fn icc_profile(&self) -> Option<&[u8]> {
|
||||||
self.0.icc.as_deref()
|
self.0.icc_profile.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,7 @@ 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::visualize::{
|
use typst_library::visualize::{ColorSpace, Image, ImageKind, RasterFormat, SvgImage};
|
||||||
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
|
|
||||||
};
|
|
||||||
use typst_utils::Deferred;
|
use typst_utils::Deferred;
|
||||||
|
|
||||||
use crate::{color, deflate, PdfChunk, WithGlobalRefs};
|
use crate::{color, deflate, PdfChunk, WithGlobalRefs};
|
||||||
@ -32,10 +30,11 @@ pub fn write_images(
|
|||||||
EncodedImage::Raster {
|
EncodedImage::Raster {
|
||||||
data,
|
data,
|
||||||
filter,
|
filter,
|
||||||
has_color,
|
color_space,
|
||||||
|
bits_per_component,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
icc,
|
icc_profile,
|
||||||
alpha,
|
alpha,
|
||||||
} => {
|
} => {
|
||||||
let image_ref = chunk.alloc();
|
let image_ref = chunk.alloc();
|
||||||
@ -45,23 +44,17 @@ 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));
|
||||||
|
|
||||||
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 icc_profile.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,20 +72,24 @@ 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));
|
||||||
} else {
|
} else {
|
||||||
image.finish();
|
image.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
|
if let (Some(icc_profile), Some(icc_ref)) = (icc_profile, icc_ref) {
|
||||||
let mut stream = chunk.icc_profile(icc_ref, icc);
|
let mut stream = chunk.icc_profile(icc_ref, icc_profile);
|
||||||
stream.filter(Filter::FlateDecode);
|
stream.filter(Filter::FlateDecode);
|
||||||
if *has_color {
|
match color_space {
|
||||||
stream.n(3);
|
ColorSpace::Srgb => {
|
||||||
stream.alternate().srgb();
|
stream.n(3);
|
||||||
} else {
|
stream.alternate().srgb();
|
||||||
stream.n(1);
|
}
|
||||||
stream.alternate().d65_gray();
|
ColorSpace::D65Gray => {
|
||||||
|
stream.n(1);
|
||||||
|
stream.alternate().d65_gray();
|
||||||
|
}
|
||||||
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -121,82 +118,97 @@ pub fn deferred_image(
|
|||||||
pdfa: bool,
|
pdfa: bool,
|
||||||
) -> (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_profile().is_none() => {
|
||||||
if raster.dynamic().color().channel_count() > 2 {
|
Some(to_color_space(raster.dynamic().color()))
|
||||||
Some(ColorSpace::Srgb)
|
}
|
||||||
} else {
|
ImageKind::Pixmap(pixmap) if pixmap.icc_profile().is_none() => {
|
||||||
Some(ColorSpace::D65Gray)
|
Some(to_color_space(pixmap.to_image().color()))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let deferred = Deferred::new(move || match image.kind() {
|
let deferred = Deferred::new(move || match image.kind() {
|
||||||
ImageKind::Raster(raster) => {
|
ImageKind::Raster(raster) => {
|
||||||
let raster = raster.clone();
|
let format = if raster.format() == RasterFormat::Jpg {
|
||||||
let (width, height) = (raster.width(), raster.height());
|
EncodeFormat::DctDecode
|
||||||
let (data, filter, has_color) = encode_raster_image(&raster);
|
} else {
|
||||||
let icc = raster.icc().map(deflate);
|
EncodeFormat::Flate
|
||||||
|
};
|
||||||
let alpha =
|
Ok(encode_raster_image(&raster.dynamic(), raster.icc_profile(), format))
|
||||||
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}"))?;
|
||||||
Ok(EncodedImage::Svg(chunk, id))
|
Ok(EncodedImage::Svg(chunk, id))
|
||||||
}
|
}
|
||||||
|
ImageKind::Pixmap(pixmap) => Ok(encode_raster_image(
|
||||||
|
&pixmap.to_image(),
|
||||||
|
pixmap.icc_profile(),
|
||||||
|
EncodeFormat::Flate,
|
||||||
|
)),
|
||||||
});
|
});
|
||||||
|
|
||||||
(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(
|
||||||
let dynamic = image.dynamic();
|
image: &DynamicImage,
|
||||||
let channel_count = dynamic.color().channel_count();
|
icc_profile: Option<&[u8]>,
|
||||||
let has_color = channel_count > 2;
|
format: EncodeFormat,
|
||||||
|
) -> EncodedImage {
|
||||||
|
let color_space = to_color_space(image.color());
|
||||||
|
|
||||||
if image.format() == RasterFormat::Jpg {
|
let (filter, data, bits_per_component) = match format {
|
||||||
let mut data = Cursor::new(vec![]);
|
EncodeFormat::DctDecode => {
|
||||||
dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
let mut data = Cursor::new(vec![]);
|
||||||
(data.into_inner(), Filter::DctDecode, has_color)
|
image.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
||||||
} else {
|
(Filter::DctDecode, data.into_inner(), 8)
|
||||||
// TODO: Encode flate streams with PNG-predictor?
|
}
|
||||||
let data = match (dynamic, channel_count) {
|
EncodeFormat::Flate => {
|
||||||
(DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
|
// TODO: Encode flate streams with PNG-predictor?
|
||||||
(DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
|
let (data, bits_per_component) = match (image, color_space) {
|
||||||
// Grayscale image
|
(DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
|
||||||
(_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()),
|
// Grayscale image
|
||||||
// Anything else
|
(DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
|
||||||
_ => deflate(dynamic.to_rgb8().as_raw()),
|
(_, ColorSpace::D65Gray) => (deflate(image.to_luma8().as_raw()), 8),
|
||||||
};
|
// Anything else
|
||||||
(data, Filter::FlateDecode, has_color)
|
_ => (deflate(image.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));
|
||||||
|
|
||||||
|
EncodedImage::Raster {
|
||||||
|
data,
|
||||||
|
filter,
|
||||||
|
color_space,
|
||||||
|
bits_per_component,
|
||||||
|
width: image.width(),
|
||||||
|
height: image.height(),
|
||||||
|
icc_profile: compressed_icc,
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,14 +236,16 @@ 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, pre-deflated, if any.
|
||||||
icc: Option<Vec<u8>>,
|
icc_profile: 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)>,
|
||||||
},
|
},
|
||||||
@ -240,3 +254,9 @@ pub enum EncodedImage {
|
|||||||
/// The chunk is the SVG converted to PDF objects.
|
/// The chunk is the SVG converted to PDF objects.
|
||||||
Svg(Chunk, Ref),
|
Svg(Chunk, Ref),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How the raster image should be encoded.
|
||||||
|
enum EncodeFormat {
|
||||||
|
DctDecode,
|
||||||
|
Flate,
|
||||||
|
}
|
||||||
|
@ -34,7 +34,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 +57,35 @@ 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)?;
|
|
||||||
match image.kind() {
|
match image.kind() {
|
||||||
ImageKind::Raster(raster) => {
|
ImageKind::Raster(raster) => scale_image(raster.dynamic(), w, h),
|
||||||
let downscale = w < raster.width();
|
ImageKind::Pixmap(raster) => scale_image(&raster.to_image(), w, h),
|
||||||
let filter =
|
|
||||||
if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
|
|
||||||
let buf = raster.dynamic().resize(w, h, filter);
|
|
||||||
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
|
||||||
let Rgba([r, g, b, a]) = src;
|
|
||||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Safety: We do not keep any references to tree nodes beyond the scope
|
// Safety: We do not keep any references to tree nodes beyond the scope
|
||||||
// of `with`.
|
// of `with`.
|
||||||
ImageKind::Svg(svg) => {
|
ImageKind::Svg(svg) => {
|
||||||
|
let mut pixmap = sk::Pixmap::new(w, h)?;
|
||||||
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 pixmap.as_mut());
|
||||||
|
Some(Arc::new(pixmap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale a rastered image to a given size and return texture.
|
||||||
|
// TODO(frozolotl): optimize pixmap allocation
|
||||||
|
fn scale_image(image: &image::DynamicImage, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||||
|
let mut pixmap = sk::Pixmap::new(w, h)?;
|
||||||
|
let downscale = w < image.width();
|
||||||
|
let filter = if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom };
|
||||||
|
let buf = image.resize(w, h, filter);
|
||||||
|
for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) {
|
||||||
|
let Rgba([r, g, b, a]) = src;
|
||||||
|
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||||
|
}
|
||||||
Some(Arc::new(pixmap))
|
Some(Arc::new(pixmap))
|
||||||
}
|
}
|
||||||
|
@ -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,13 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
use image::error::UnsupportedError;
|
||||||
|
use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||||
use typst_library::layout::{Abs, Axes};
|
use typst_library::layout::{Abs, Axes};
|
||||||
use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat};
|
use typst_library::visualize::{
|
||||||
|
Image, ImageFormat, ImageKind, RasterFormat, VectorFormat,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::SVGRenderer;
|
use crate::SVGRenderer;
|
||||||
|
|
||||||
@ -31,10 +37,27 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
|
|||||||
ImageFormat::Vector(f) => match f {
|
ImageFormat::Vector(f) => match f {
|
||||||
VectorFormat::Svg => "svg+xml",
|
VectorFormat::Svg => "svg+xml",
|
||||||
},
|
},
|
||||||
|
ImageFormat::Pixmap(_) => "png",
|
||||||
|
};
|
||||||
|
let data_owned;
|
||||||
|
let data = match image.kind() {
|
||||||
|
ImageKind::Raster(raster) => raster.data(),
|
||||||
|
ImageKind::Svg(svg) => svg.data(),
|
||||||
|
ImageKind::Pixmap(pixmap) => {
|
||||||
|
let mut data = Cursor::new(vec![]);
|
||||||
|
let mut encoder = PngEncoder::new(&mut data);
|
||||||
|
if let Some(icc_profile) = pixmap.icc_profile() {
|
||||||
|
let _: Result<(), UnsupportedError> =
|
||||||
|
encoder.set_icc_profile(icc_profile.to_vec());
|
||||||
|
}
|
||||||
|
pixmap.to_image().write_with_encoder(encoder).unwrap();
|
||||||
|
data_owned = data.into_inner();
|
||||||
|
&*data_owned
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ use std::io::Read;
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use ttf_parser::GlyphId;
|
use ttf_parser::GlyphId;
|
||||||
|
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::{FillRule, Image, Paint, RasterFormat, RelativeTo};
|
||||||
@ -243,7 +244,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 = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?;
|
let image =
|
||||||
|
Image::new(Bytes::from(raster.data).into(), RasterFormat::Png.into(), None)
|
||||||
|
.ok()?;
|
||||||
Some((image, raster.x as f64, raster.y as f64))
|
Some((image, raster.x as f64, raster.y as f64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
tests/ref/image-pixmap-luma8.png
Normal file
BIN
tests/ref/image-pixmap-luma8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 321 B |
BIN
tests/ref/image-pixmap-lumaa8.png
Normal file
BIN
tests/ref/image-pixmap-lumaa8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 299 B |
BIN
tests/ref/image-pixmap-rgb8.png
Normal file
BIN
tests/ref/image-pixmap-rgb8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/image-pixmap-rgba8.png
Normal file
BIN
tests/ref/image-pixmap-rgba8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 854 B |
@ -65,6 +65,58 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
|||||||
caption: [Bilingual text]
|
caption: [Bilingual text]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-rgb8 ---
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes((
|
||||||
|
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
|
||||||
|
0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80,
|
||||||
|
0x80, 0x80, 0x00, 0x80, 0x80, 0x00, 0x80, 0x00, 0x80,
|
||||||
|
)),
|
||||||
|
pixel-width: 3,
|
||||||
|
pixel-height: 3,
|
||||||
|
),
|
||||||
|
format: "rgb8",
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-rgba8 ---
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: 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,
|
||||||
|
)),
|
||||||
|
pixel-width: 3,
|
||||||
|
pixel-height: 3,
|
||||||
|
),
|
||||||
|
format: "rgba8",
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-luma8 ---
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes(range(16).map(x => x * 16)),
|
||||||
|
pixel-width: 4,
|
||||||
|
pixel-height: 4,
|
||||||
|
),
|
||||||
|
format: "luma8",
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-lumaa8 ---
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes(range(16).map(x => (0x80, x * 16)).flatten()),
|
||||||
|
pixel-width: 4,
|
||||||
|
pixel-height: 4,
|
||||||
|
),
|
||||||
|
format: "lumaa8",
|
||||||
|
width: 1cm,
|
||||||
|
)
|
||||||
|
|
||||||
--- 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 +155,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.decode(
|
||||||
|
(
|
||||||
|
data: bytes(()),
|
||||||
|
pixel-width: 0,
|
||||||
|
pixel-height: 0,
|
||||||
|
),
|
||||||
|
format: "rgb8",
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-invalid-size ---
|
||||||
|
// Error: 1:2-8:2 provided pixel dimensions and pixmap data do not match
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes((0x00, 0x00, 0x00)),
|
||||||
|
pixel-width: 16,
|
||||||
|
pixel-height: 16,
|
||||||
|
),
|
||||||
|
format: "rgb8",
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-unknown-attribute ---
|
||||||
|
// Error: 2:3-7:4 unexpected key "stowaway", valid keys are "data", "pixel-width", "pixel-height", and "icc-profile"
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes((0x00, 0x00, 0x00)),
|
||||||
|
pixel-width: 1,
|
||||||
|
pixel-height: 1,
|
||||||
|
stowaway: "I do work here, promise",
|
||||||
|
),
|
||||||
|
format: "rgb8",
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-pixmap-but-png-format ---
|
||||||
|
// Error: 1:2-8:2 expected readable source for the given format (str or bytes)
|
||||||
|
#image.decode(
|
||||||
|
(
|
||||||
|
data: bytes((0x00, 0x00, 0x00)),
|
||||||
|
pixel-width: 1,
|
||||||
|
pixel-height: 1,
|
||||||
|
),
|
||||||
|
format: "png",
|
||||||
|
)
|
||||||
|
|
||||||
|
--- image-png-but-pixmap-format ---
|
||||||
|
// Error: 1:2-4:2 source must be pixmap
|
||||||
|
#image.decode(
|
||||||
|
read("/assets/images/tiger.jpg", encoding: none),
|
||||||
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user