mirror of
https://github.com/typst/typst
synced 2025-08-10 13:17:55 +08:00
Compare commits
7 Commits
574952f578
...
8f884f9f1d
Author | SHA1 | Date | |
---|---|---|---|
|
8f884f9f1d | ||
|
1e641be5b2 | ||
|
776c6d1923 | ||
|
b68971d01e | ||
|
e00508f22d | ||
|
19e719ffe1 | ||
|
1e5b82f95c |
@ -1,6 +1,6 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
|
||||
use typst_library::diag::{warning, At, SourceResult, StrResult};
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
|
||||
use typst_library::introspection::Locator;
|
||||
@ -10,8 +10,8 @@ use typst_library::layout::{
|
||||
use typst_library::loading::DataSource;
|
||||
use typst_library::text::families;
|
||||
use typst_library::visualize::{
|
||||
Curve, Image, ImageElem, ImageFit, ImageFormat, ImageKind, ImageSource, PixmapImage,
|
||||
RasterFormat, RasterImage, SvgImage, VectorFormat,
|
||||
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
|
||||
RasterImage, SvgImage, VectorFormat,
|
||||
};
|
||||
|
||||
/// Layout the image.
|
||||
@ -50,14 +50,16 @@ pub fn layout_image(
|
||||
}
|
||||
|
||||
// Construct the image itself.
|
||||
let kind = match (format, source) {
|
||||
(ImageFormat::Pixmap(format), ImageSource::Pixmap(source)) => {
|
||||
ImageKind::Pixmap(PixmapImage::new(source.clone(), format).at(span)?)
|
||||
}
|
||||
(ImageFormat::Raster(format), ImageSource::Data(_)) => {
|
||||
ImageKind::Raster(RasterImage::new(data.clone(), format).at(span)?)
|
||||
}
|
||||
(ImageFormat::Vector(VectorFormat::Svg), ImageSource::Data(_)) => ImageKind::Svg(
|
||||
let kind = match format {
|
||||
ImageFormat::Raster(format) => ImageKind::Raster(
|
||||
RasterImage::new(
|
||||
data.clone(),
|
||||
format,
|
||||
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
|
||||
)
|
||||
.at(span)?,
|
||||
),
|
||||
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
|
||||
SvgImage::with_fonts(
|
||||
data.clone(),
|
||||
engine.world,
|
||||
@ -66,10 +68,6 @@ pub fn layout_image(
|
||||
)
|
||||
.at(span)?,
|
||||
),
|
||||
(ImageFormat::Pixmap(_), _) => bail!(span, "source must be a pixmap"),
|
||||
(ImageFormat::Raster(_) | ImageFormat::Vector(_), _) => {
|
||||
bail!(span, "expected readable source for the given format (str or bytes)")
|
||||
}
|
||||
};
|
||||
|
||||
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
|
||||
@ -135,26 +133,20 @@ pub fn layout_image(
|
||||
}
|
||||
|
||||
/// Try to determine the image format based on the data.
|
||||
fn determine_format(source: &ImageSource, data: &Bytes) -> StrResult<ImageFormat> {
|
||||
match source {
|
||||
ImageSource::Data(DataSource::Path(path)) => {
|
||||
let ext = std::path::Path::new(path.as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
|
||||
if let DataSource::Path(path) = source {
|
||||
let ext = std::path::Path::new(path.as_str())
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or_default()
|
||||
.to_lowercase();
|
||||
|
||||
match ext.as_str() {
|
||||
"png" => return Ok(ImageFormat::Raster(RasterFormat::Png)),
|
||||
"jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)),
|
||||
"gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)),
|
||||
"svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ImageSource::Data(DataSource::Bytes(_)) => {}
|
||||
ImageSource::Pixmap(_) => {
|
||||
bail!("pixmaps require an explicit image format to be given")
|
||||
match ext.as_str() {
|
||||
"png" => return Ok(ExchangeFormat::Png.into()),
|
||||
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
|
||||
"gif" => return Ok(ExchangeFormat::Gif.into()),
|
||||
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,11 @@ use typst_syntax::Span;
|
||||
use usvg::tiny_skia_path;
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
use crate::foundations::{Bytes, Smart};
|
||||
use crate::foundations::Bytes;
|
||||
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||
use crate::text::{Font, Glyph};
|
||||
use crate::visualize::{
|
||||
FixedStroke, Geometry, Image, RasterFormat, RasterImage, SvgImage,
|
||||
ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage,
|
||||
};
|
||||
|
||||
/// Whether this glyph should be rendered via simple outlining instead of via
|
||||
@ -105,8 +105,7 @@ fn draw_raster_glyph(
|
||||
raster_image: ttf_parser::RasterGlyphImage,
|
||||
) -> Option<()> {
|
||||
let data = Bytes::new(raster_image.data.to_vec());
|
||||
let image =
|
||||
Image::new(RasterImage::new(data, RasterFormat::Png).ok()?, None, Smart::Auto);
|
||||
let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?);
|
||||
|
||||
// Apple Color emoji doesn't provide offset information (or at least
|
||||
// not in a way ttf-parser understands), so we artificially shift their
|
||||
@ -178,7 +177,7 @@ fn draw_colr_glyph(
|
||||
svg.end_element();
|
||||
|
||||
let data = Bytes::from_string(svg.end_document());
|
||||
let image = Image::new(SvgImage::new(data).ok()?, None, Smart::Auto);
|
||||
let image = Image::plain(SvgImage::new(data).ok()?);
|
||||
|
||||
let y_shift = Abs::pt(upem.to_pt() - y_max);
|
||||
let position = Point::new(Abs::pt(x_min), y_shift);
|
||||
@ -254,7 +253,7 @@ fn draw_svg_glyph(
|
||||
);
|
||||
|
||||
let data = Bytes::from_string(wrapper_svg);
|
||||
let image = Image::new(SvgImage::new(data).ok()?, None, Smart::Auto);
|
||||
let image = Image::plain(SvgImage::new(data).ok()?);
|
||||
|
||||
let position = Point::new(Abs::pt(left), Abs::pt(top) + upem);
|
||||
let size = Size::new(Abs::pt(width), Abs::pt(height));
|
||||
|
@ -1,17 +1,16 @@
|
||||
//! Image handling.
|
||||
|
||||
mod pixmap;
|
||||
mod raster;
|
||||
mod svg;
|
||||
|
||||
pub use self::pixmap::{PixmapFormat, PixmapImage, PixmapSource};
|
||||
pub use self::raster::{RasterFormat, RasterImage};
|
||||
pub use self::raster::{
|
||||
ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
|
||||
};
|
||||
pub use self::svg::SvgImage;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use comemo::Tracked;
|
||||
use ecow::EcoString;
|
||||
use typst_syntax::{Span, Spanned};
|
||||
use typst_utils::LazyHash;
|
||||
@ -26,7 +25,6 @@ use crate::layout::{BlockElem, Length, Rel, Sizing};
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
use crate::model::Figurable;
|
||||
use crate::text::LocalName;
|
||||
use crate::World;
|
||||
|
||||
/// A raster or vector graphic.
|
||||
///
|
||||
@ -48,34 +46,62 @@ use crate::World;
|
||||
/// ```
|
||||
#[elem(scope, Show, LocalName, Figurable)]
|
||||
pub struct ImageElem {
|
||||
/// The source to load the image from. Either of:
|
||||
/// A path to an image file or raw bytes making up an image in one of the
|
||||
/// supported [formats]($image.format).
|
||||
///
|
||||
/// - A path to an image file. For more details about paths, see the [Paths
|
||||
/// section]($syntax/#paths).
|
||||
/// - Raw bytes making up an encoded image.
|
||||
/// - A dictionary with the following keys:
|
||||
/// - `data` ([bytes]): Raw pixel data in the specified [`format`]($image.format).
|
||||
/// - `pixel-width` ([int]): The width in pixels.
|
||||
/// - `pixel-height` ([int]): The height in pixels.
|
||||
/// - `icc-profile` ([bytes], optional): An ICC profile for the image.
|
||||
///
|
||||
/// The width multiplied by the height multiplied by the channel count for
|
||||
/// the specified format must match the data length.
|
||||
/// For more details about paths, see the [Paths section]($syntax/#paths).
|
||||
#[required]
|
||||
#[parse(
|
||||
let source = args.expect::<Spanned<ImageSource>>("source")?;
|
||||
let source = args.expect::<Spanned<DataSource>>("source")?;
|
||||
let data = source.load(engine.world)?;
|
||||
Derived::new(source.v, data)
|
||||
)]
|
||||
pub source: Derived<ImageSource, Bytes>,
|
||||
pub source: Derived<DataSource, Bytes>,
|
||||
|
||||
/// The image's format. Detected automatically by default.
|
||||
/// The image's format.
|
||||
///
|
||||
/// Supported image formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image
|
||||
/// is [not currently supported](https://github.com/typst/typst/issues/145).
|
||||
/// By default, the format is detected automatically. Typically, you thus
|
||||
/// 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).
|
||||
///
|
||||
/// Aside from these encoded image formats, Typst also lets you provide raw
|
||||
/// image data as the source. In this case, providing a format is mandatory.
|
||||
/// 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: brightness.)
|
||||
/// - `{"lumaa8"}` (two 8-bit channels: brightness and alpha.)
|
||||
/// - `width` ([int]): The pixel width of the image.
|
||||
/// - `height` ([int]): The pixel height of the image.
|
||||
///
|
||||
/// 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>,
|
||||
|
||||
/// The width of the image.
|
||||
@ -110,6 +136,21 @@ pub struct ImageElem {
|
||||
/// _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
|
||||
/// embedding. This will result in the text becoming unselectable in the
|
||||
/// output.
|
||||
@ -164,8 +205,7 @@ impl ImageElem {
|
||||
flatten_text: Option<bool>,
|
||||
) -> StrResult<Content> {
|
||||
let bytes = data.into_bytes();
|
||||
let source =
|
||||
Derived::new(ImageSource::Data(DataSource::Bytes(bytes.clone())), bytes);
|
||||
let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes);
|
||||
let mut elem = ImageElem::new(source);
|
||||
if let Some(format) = format {
|
||||
elem.push_format(format);
|
||||
@ -182,15 +222,12 @@ impl ImageElem {
|
||||
if let Some(fit) = fit {
|
||||
elem.push_fit(fit);
|
||||
}
|
||||
if let Some(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);
|
||||
}
|
||||
if let Some(scaling) = scaling {
|
||||
elem.push_scaling(scaling);
|
||||
}
|
||||
Ok(elem.pack().spanned(span))
|
||||
}
|
||||
}
|
||||
@ -253,7 +290,7 @@ impl Image {
|
||||
/// Should always be the same as the default DPI used by usvg.
|
||||
pub const USVG_DEFAULT_DPI: f64 = 96.0;
|
||||
|
||||
/// Create an image from a kind.
|
||||
/// Create an image from a `RasterImage` or `SvgImage`.
|
||||
pub fn new(
|
||||
kind: impl Into<ImageKind>,
|
||||
alt: Option<EcoString>,
|
||||
@ -262,6 +299,11 @@ impl Image {
|
||||
Self::new_impl(kind.into(), alt, scaling)
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@ -278,7 +320,6 @@ impl Image {
|
||||
match &self.0.kind {
|
||||
ImageKind::Raster(raster) => raster.format().into(),
|
||||
ImageKind::Svg(_) => VectorFormat::Svg.into(),
|
||||
ImageKind::Pixmap(pixmap) => pixmap.format().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,7 +328,6 @@ impl Image {
|
||||
match &self.0.kind {
|
||||
ImageKind::Raster(raster) => raster.width() as f64,
|
||||
ImageKind::Svg(svg) => svg.width(),
|
||||
ImageKind::Pixmap(pixmap) => pixmap.width() as f64,
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,7 +336,6 @@ impl Image {
|
||||
match &self.0.kind {
|
||||
ImageKind::Raster(raster) => raster.height() as f64,
|
||||
ImageKind::Svg(svg) => svg.height(),
|
||||
ImageKind::Pixmap(pixmap) => pixmap.height() as f64,
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +344,6 @@ impl Image {
|
||||
match &self.0.kind {
|
||||
ImageKind::Raster(raster) => raster.dpi(),
|
||||
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
|
||||
ImageKind::Pixmap(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,40 +375,6 @@ impl Debug for Image {
|
||||
}
|
||||
}
|
||||
|
||||
/// Information specifying the source of an image's byte data.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum ImageSource {
|
||||
Data(DataSource),
|
||||
Pixmap(PixmapSource),
|
||||
}
|
||||
|
||||
impl From<Bytes> for ImageSource {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
ImageSource::Data(DataSource::Bytes(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Load for Spanned<ImageSource> {
|
||||
type Output = Bytes;
|
||||
|
||||
fn load(&self, world: Tracked<dyn World + '_>) -> SourceResult<Self::Output> {
|
||||
match &self.v {
|
||||
ImageSource::Data(data) => Spanned::new(data, self.span).load(world),
|
||||
ImageSource::Pixmap(pixmap) => Ok(pixmap.data.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
ImageSource,
|
||||
self => match self {
|
||||
Self::Data(data) => data.into_value(),
|
||||
Self::Pixmap(pixmap) => pixmap.into_value(),
|
||||
},
|
||||
data: DataSource => Self::Data(data),
|
||||
pixmap: PixmapSource => Self::Pixmap(pixmap),
|
||||
}
|
||||
|
||||
/// A kind of image.
|
||||
#[derive(Clone, Hash)]
|
||||
pub enum ImageKind {
|
||||
@ -378,8 +382,6 @@ pub enum ImageKind {
|
||||
Raster(RasterImage),
|
||||
/// An SVG image.
|
||||
Svg(SvgImage),
|
||||
/// An image constructed from a pixmap.
|
||||
Pixmap(PixmapImage),
|
||||
}
|
||||
|
||||
impl From<RasterImage> for ImageKind {
|
||||
@ -394,12 +396,6 @@ impl From<SvgImage> for ImageKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PixmapImage> for ImageKind {
|
||||
fn from(image: PixmapImage) -> Self {
|
||||
Self::Pixmap(image)
|
||||
}
|
||||
}
|
||||
|
||||
/// A raster or vector image format.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ImageFormat {
|
||||
@ -407,15 +403,13 @@ pub enum ImageFormat {
|
||||
Raster(RasterFormat),
|
||||
/// A vector graphics format.
|
||||
Vector(VectorFormat),
|
||||
/// A format made up of flat pixels without metadata or compression.
|
||||
Pixmap(PixmapFormat),
|
||||
}
|
||||
|
||||
impl ImageFormat {
|
||||
/// Try to detect the format of an image from data.
|
||||
pub fn detect(data: &[u8]) -> Option<Self> {
|
||||
if let Some(format) = RasterFormat::detect(data) {
|
||||
return Some(Self::Raster(format));
|
||||
if let Some(format) = ExchangeFormat::detect(data) {
|
||||
return Some(Self::Raster(RasterFormat::Exchange(format)));
|
||||
}
|
||||
|
||||
// SVG or compressed SVG.
|
||||
@ -434,9 +428,12 @@ pub enum VectorFormat {
|
||||
Svg,
|
||||
}
|
||||
|
||||
impl From<RasterFormat> for ImageFormat {
|
||||
fn from(format: RasterFormat) -> Self {
|
||||
Self::Raster(format)
|
||||
impl<R> From<R> for ImageFormat
|
||||
where
|
||||
R: Into<RasterFormat>,
|
||||
{
|
||||
fn from(format: R) -> Self {
|
||||
Self::Raster(format.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,22 +443,14 @@ impl From<VectorFormat> for ImageFormat {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PixmapFormat> for ImageFormat {
|
||||
fn from(format: PixmapFormat) -> Self {
|
||||
Self::Pixmap(format)
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
ImageFormat,
|
||||
self => match self {
|
||||
Self::Raster(v) => v.into_value(),
|
||||
Self::Vector(v) => v.into_value(),
|
||||
Self::Pixmap(v) => v.into_value(),
|
||||
},
|
||||
v: RasterFormat => Self::Raster(v),
|
||||
v: VectorFormat => Self::Vector(v),
|
||||
v: PixmapFormat => Self::Pixmap(v),
|
||||
}
|
||||
|
||||
/// The image scaling algorithm a viewer should use.
|
||||
|
@ -1,138 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use image::{DynamicImage, ImageBuffer, Pixel};
|
||||
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{cast, dict, Bytes, Cast, Dict};
|
||||
|
||||
/// A raster image based on a flat pixmap.
|
||||
#[derive(Clone, Hash)]
|
||||
pub struct PixmapImage(Arc<Repr>);
|
||||
|
||||
/// The internal representation.
|
||||
#[derive(Hash)]
|
||||
struct Repr {
|
||||
source: PixmapSource,
|
||||
format: PixmapFormat,
|
||||
}
|
||||
|
||||
impl PixmapImage {
|
||||
/// Builds a new [`PixmapImage`] from a flat, uncompressed byte sequence.
|
||||
#[comemo::memoize]
|
||||
#[typst_macros::time(name = "load pixmap")]
|
||||
pub fn new(source: PixmapSource, format: PixmapFormat) -> StrResult<PixmapImage> {
|
||||
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!("pixel dimensions are too large");
|
||||
};
|
||||
|
||||
if expected_size as usize != source.data.len() {
|
||||
bail!("pixel dimensions and pixel 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) -> &Bytes {
|
||||
&self.0.source.data
|
||||
}
|
||||
|
||||
/// Transform the image data to a [`DynamicImage`].
|
||||
#[comemo::memoize]
|
||||
pub fn to_dynamic(&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 {
|
||||
/// Raw image data with three 8-bit channels: Red, green, blue.
|
||||
Rgb8,
|
||||
/// Raw image data with four 8-bit channels: Red, green, blue, alpha.
|
||||
Rgba8,
|
||||
/// Raw image data with one 8-bit channel: Brightness.
|
||||
Luma8,
|
||||
/// Raw image data with two 8-bit channels: Brightness and alpha.
|
||||
Lumaa8,
|
||||
}
|
||||
|
||||
/// Raw pixmap data and relevant metadata.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct PixmapSource {
|
||||
pub data: Bytes,
|
||||
pub pixel_width: u32,
|
||||
pub pixel_height: u32,
|
||||
pub icc_profile: Option<Bytes>,
|
||||
}
|
||||
|
||||
cast! {
|
||||
PixmapSource,
|
||||
self => dict! {
|
||||
"data" => self.data.clone(),
|
||||
"pixel-width" => self.pixel_width,
|
||||
"pixel-height" => self.pixel_height,
|
||||
"icc-profile" => self.icc_profile.clone()
|
||||
}.into_value(),
|
||||
mut dict: Dict => {
|
||||
let source = 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(|v| v.cast()).transpose()?,
|
||||
};
|
||||
dict.finish(&["data", "pixel-width", "pixel-height", "icc-profile"])?;
|
||||
source
|
||||
}
|
||||
}
|
@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString};
|
||||
use image::codecs::gif::GifDecoder;
|
||||
use image::codecs::jpeg::JpegDecoder;
|
||||
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::foundations::{Bytes, Cast};
|
||||
use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value};
|
||||
|
||||
/// A decoded raster image.
|
||||
#[derive(Clone, Hash)]
|
||||
@ -21,46 +23,120 @@ struct Repr {
|
||||
data: Bytes,
|
||||
format: RasterFormat,
|
||||
dynamic: image::DynamicImage,
|
||||
icc_profile: Option<Vec<u8>>,
|
||||
icc: Option<Bytes>,
|
||||
dpi: Option<f64>,
|
||||
}
|
||||
|
||||
impl RasterImage {
|
||||
/// 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]
|
||||
#[typst_macros::time(name = "load raster image")]
|
||||
pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> {
|
||||
fn decode_with<T: ImageDecoder>(
|
||||
decoder: ImageResult<T>,
|
||||
) -> ImageResult<(image::DynamicImage, Option<Vec<u8>>)> {
|
||||
let mut decoder = decoder?;
|
||||
let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty());
|
||||
decoder.set_limits(Limits::default())?;
|
||||
let dynamic = image::DynamicImage::from_decoder(decoder)?;
|
||||
Ok((dynamic, icc))
|
||||
}
|
||||
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>,
|
||||
icc: Smart<Bytes>,
|
||||
) -> ImageResult<(image::DynamicImage, Option<Bytes>)> {
|
||||
let mut decoder = decoder?;
|
||||
let icc = icc.custom().or_else(|| {
|
||||
decoder
|
||||
.icc_profile()
|
||||
.ok()
|
||||
.flatten()
|
||||
.filter(|icc| !icc.is_empty())
|
||||
.map(Bytes::new)
|
||||
});
|
||||
decoder.set_limits(Limits::default())?;
|
||||
let dynamic = image::DynamicImage::from_decoder(decoder)?;
|
||||
Ok((dynamic, icc))
|
||||
}
|
||||
|
||||
let cursor = io::Cursor::new(&data);
|
||||
let (mut dynamic, icc_profile) = match format {
|
||||
RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)),
|
||||
RasterFormat::Png => decode_with(PngDecoder::new(cursor)),
|
||||
RasterFormat::Gif => decode_with(GifDecoder::new(cursor)),
|
||||
}
|
||||
.map_err(format_image_error)?;
|
||||
let cursor = io::Cursor::new(&data);
|
||||
let (mut dynamic, icc) = match format {
|
||||
ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc),
|
||||
ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc),
|
||||
ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc),
|
||||
}
|
||||
.map_err(format_image_error)?;
|
||||
|
||||
let exif = exif::Reader::new()
|
||||
.read_from_container(&mut std::io::Cursor::new(&data))
|
||||
.ok();
|
||||
let exif = exif::Reader::new()
|
||||
.read_from_container(&mut std::io::Cursor::new(&data))
|
||||
.ok();
|
||||
|
||||
// Apply rotation from EXIF metadata.
|
||||
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
|
||||
apply_rotation(&mut dynamic, rotation);
|
||||
}
|
||||
// Apply rotation from EXIF metadata.
|
||||
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
|
||||
apply_rotation(&mut dynamic, rotation);
|
||||
}
|
||||
|
||||
// Extract pixel density.
|
||||
let dpi = determine_dpi(&data, exif.as_ref());
|
||||
// Extract pixel density.
|
||||
let dpi = determine_dpi(&data, exif.as_ref());
|
||||
|
||||
Ok(Self(Arc::new(Repr { data, format, dynamic, icc_profile, dpi })))
|
||||
(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 })))
|
||||
}
|
||||
|
||||
/// The raw image data.
|
||||
@ -94,60 +170,141 @@ impl RasterImage {
|
||||
}
|
||||
|
||||
/// Access the ICC profile, if any.
|
||||
pub fn icc_profile(&self) -> Option<&[u8]> {
|
||||
self.0.icc_profile.as_deref()
|
||||
pub fn icc(&self) -> Option<&Bytes> {
|
||||
self.0.icc.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Repr {
|
||||
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.format.hash(state);
|
||||
self.icc.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// A raster graphics format.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
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.
|
||||
Png,
|
||||
/// Lossy raster format suitable for photos.
|
||||
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,
|
||||
}
|
||||
|
||||
impl RasterFormat {
|
||||
impl ExchangeFormat {
|
||||
/// Try to detect the format of data in a buffer.
|
||||
pub fn detect(data: &[u8]) -> Option<Self> {
|
||||
guess_format(data).ok().and_then(|format| format.try_into().ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RasterFormat> for image::ImageFormat {
|
||||
fn from(format: RasterFormat) -> Self {
|
||||
impl From<ExchangeFormat> for image::ImageFormat {
|
||||
fn from(format: ExchangeFormat) -> Self {
|
||||
match format {
|
||||
RasterFormat::Png => image::ImageFormat::Png,
|
||||
RasterFormat::Jpg => image::ImageFormat::Jpeg,
|
||||
RasterFormat::Gif => image::ImageFormat::Gif,
|
||||
ExchangeFormat::Png => image::ImageFormat::Png,
|
||||
ExchangeFormat::Jpg => image::ImageFormat::Jpeg,
|
||||
ExchangeFormat::Gif => image::ImageFormat::Gif,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<image::ImageFormat> for RasterFormat {
|
||||
impl TryFrom<image::ImageFormat> for ExchangeFormat {
|
||||
type Error = EcoString;
|
||||
|
||||
fn try_from(format: image::ImageFormat) -> StrResult<Self> {
|
||||
Ok(match format {
|
||||
image::ImageFormat::Png => RasterFormat::Png,
|
||||
image::ImageFormat::Jpeg => RasterFormat::Jpg,
|
||||
image::ImageFormat::Gif => RasterFormat::Gif,
|
||||
_ => bail!("Format not yet supported."),
|
||||
image::ImageFormat::Png => ExchangeFormat::Png,
|
||||
image::ImageFormat::Jpeg => ExchangeFormat::Jpg,
|
||||
image::ImageFormat::Gif => ExchangeFormat::Gif,
|
||||
_ => 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: Brightness.
|
||||
Luma8,
|
||||
/// Two 8-bit channels: Brightness 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.
|
||||
fn exif_rotation(exif: &exif::Exif) -> Option<u32> {
|
||||
exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)?
|
||||
@ -267,21 +424,20 @@ fn format_image_error(error: image::ImageError) -> EcoString {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RasterFormat, RasterImage};
|
||||
use crate::foundations::Bytes;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_image_dpi() {
|
||||
#[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 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));
|
||||
}
|
||||
|
||||
test("images/f2t.jpg", RasterFormat::Jpg, 220.0);
|
||||
test("images/tiger.jpg", RasterFormat::Jpg, 72.0);
|
||||
test("images/graph.png", RasterFormat::Png, 144.0);
|
||||
test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0);
|
||||
test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0);
|
||||
test("images/graph.png", ExchangeFormat::Png, 144.0);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,8 @@ use pdf_writer::{Chunk, Filter, Finish, Ref};
|
||||
use typst_library::diag::{At, SourceResult, StrResult};
|
||||
use typst_library::foundations::Smart;
|
||||
use typst_library::visualize::{
|
||||
ColorSpace, Image, ImageKind, ImageScaling, RasterFormat, SvgImage,
|
||||
ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||
RasterImage, SvgImage,
|
||||
};
|
||||
use typst_utils::Deferred;
|
||||
|
||||
@ -126,12 +127,9 @@ pub fn deferred_image(
|
||||
pdfa: bool,
|
||||
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
|
||||
let color_space = match image.kind() {
|
||||
ImageKind::Raster(raster) if raster.icc_profile().is_none() => {
|
||||
ImageKind::Raster(raster) if raster.icc().is_none() => {
|
||||
Some(to_color_space(raster.dynamic().color()))
|
||||
}
|
||||
ImageKind::Pixmap(pixmap) if pixmap.icc_profile().is_none() => {
|
||||
Some(to_color_space(pixmap.to_dynamic().color()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@ -140,30 +138,12 @@ pub fn deferred_image(
|
||||
let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
|
||||
|
||||
let deferred = Deferred::new(move || match image.kind() {
|
||||
ImageKind::Raster(raster) => {
|
||||
let format = if raster.format() == RasterFormat::Jpg {
|
||||
EncodeFormat::DctDecode
|
||||
} else {
|
||||
EncodeFormat::Flate
|
||||
};
|
||||
Ok(encode_raster_image(
|
||||
raster.dynamic(),
|
||||
raster.icc_profile(),
|
||||
format,
|
||||
interpolate,
|
||||
))
|
||||
}
|
||||
ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)),
|
||||
ImageKind::Svg(svg) => {
|
||||
let (chunk, id) = encode_svg(svg, pdfa)
|
||||
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
|
||||
Ok(EncodedImage::Svg(chunk, id))
|
||||
}
|
||||
ImageKind::Pixmap(pixmap) => Ok(encode_raster_image(
|
||||
&pixmap.to_dynamic(),
|
||||
pixmap.icc_profile(),
|
||||
EncodeFormat::Flate,
|
||||
interpolate,
|
||||
)),
|
||||
});
|
||||
|
||||
(deferred, color_space)
|
||||
@ -171,36 +151,31 @@ pub fn deferred_image(
|
||||
|
||||
/// Encode an image with a suitable filter.
|
||||
#[typst_macros::time(name = "encode raster image")]
|
||||
fn encode_raster_image(
|
||||
image: &DynamicImage,
|
||||
icc_profile: Option<&[u8]>,
|
||||
format: EncodeFormat,
|
||||
interpolate: bool,
|
||||
) -> EncodedImage {
|
||||
let color_space = to_color_space(image.color());
|
||||
fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
|
||||
let dynamic = image.dynamic();
|
||||
let color_space = to_color_space(dynamic.color());
|
||||
|
||||
let (filter, data, bits_per_component) = match format {
|
||||
EncodeFormat::DctDecode => {
|
||||
let (filter, data, bits_per_component) =
|
||||
if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
|
||||
let mut data = Cursor::new(vec![]);
|
||||
image.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
||||
dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
||||
(Filter::DctDecode, data.into_inner(), 8)
|
||||
}
|
||||
EncodeFormat::Flate => {
|
||||
} else {
|
||||
// TODO: Encode flate streams with PNG-predictor?
|
||||
let (data, bits_per_component) = match (image, color_space) {
|
||||
let (data, bits_per_component) = match (dynamic, color_space) {
|
||||
// RGB image.
|
||||
(DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
|
||||
// Grayscale image
|
||||
(DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
|
||||
(_, ColorSpace::D65Gray) => (deflate(image.to_luma8().as_raw()), 8),
|
||||
(_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8),
|
||||
// Anything else
|
||||
_ => (deflate(image.to_rgb8().as_raw()), 8),
|
||||
_ => (deflate(dynamic.to_rgb8().as_raw()), 8),
|
||||
};
|
||||
(Filter::FlateDecode, data, bits_per_component)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let compressed_icc = icc_profile.map(deflate);
|
||||
let alpha = image.color().has_alpha().then(|| encode_alpha(image));
|
||||
let compressed_icc = image.icc().map(|data| deflate(data));
|
||||
let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic));
|
||||
|
||||
EncodedImage::Raster {
|
||||
data,
|
||||
@ -267,12 +242,6 @@ pub enum EncodedImage {
|
||||
Svg(Chunk, Ref),
|
||||
}
|
||||
|
||||
/// How the raster image should be encoded.
|
||||
enum EncodeFormat {
|
||||
DctDecode,
|
||||
Flate,
|
||||
}
|
||||
|
||||
/// Matches an [`image::ColorType`] to [`ColorSpace`].
|
||||
fn to_color_space(color: image::ColorType) -> ColorSpace {
|
||||
use image::ColorType::*;
|
||||
|
@ -62,10 +62,29 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||
let mut texture = sk::Pixmap::new(w, h)?;
|
||||
match image.kind() {
|
||||
ImageKind::Raster(raster) => {
|
||||
scale_image(&mut texture, raster.dynamic(), image.scaling())
|
||||
}
|
||||
ImageKind::Pixmap(pixmap) => {
|
||||
scale_image(&mut texture, &pixmap.to_dynamic(), image.scaling())
|
||||
let w = texture.width();
|
||||
let h = texture.height();
|
||||
|
||||
let buf;
|
||||
let dynamic = raster.dynamic();
|
||||
let resized = if (w, h) == (dynamic.width(), dynamic.height()) {
|
||||
// Small optimization to not allocate in case image is not resized.
|
||||
dynamic
|
||||
} else {
|
||||
let upscale = w > dynamic.width();
|
||||
let filter = match image.scaling() {
|
||||
Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest,
|
||||
_ if upscale => FilterType::CatmullRom,
|
||||
_ => FilterType::Lanczos3, // downscale
|
||||
};
|
||||
buf = dynamic.resize_exact(w, h, filter);
|
||||
&buf
|
||||
};
|
||||
|
||||
for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) {
|
||||
let Rgba([r, g, b, a]) = src;
|
||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||
}
|
||||
}
|
||||
ImageKind::Svg(svg) => {
|
||||
let tree = svg.tree();
|
||||
@ -78,33 +97,3 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
|
||||
}
|
||||
Some(Arc::new(texture))
|
||||
}
|
||||
|
||||
/// Scale a rastered image to a given size and write it into the `texture`.
|
||||
fn scale_image(
|
||||
texture: &mut sk::Pixmap,
|
||||
image: &image::DynamicImage,
|
||||
scaling: Smart<ImageScaling>,
|
||||
) {
|
||||
let w = texture.width();
|
||||
let h = texture.height();
|
||||
|
||||
let buf;
|
||||
let resized = if (w, h) == (image.width(), image.height()) {
|
||||
// Small optimization to not allocate in case image is not resized.
|
||||
image
|
||||
} else {
|
||||
let upscale = w > image.width();
|
||||
let filter = match scaling {
|
||||
Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest,
|
||||
_ if upscale => FilterType::CatmullRom,
|
||||
_ => FilterType::Lanczos3, // downscale
|
||||
};
|
||||
buf = image.resize_exact(w, h, filter);
|
||||
&buf
|
||||
};
|
||||
|
||||
for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) {
|
||||
let Rgba([r, g, b, a]) = src;
|
||||
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use image::{codecs::png::PngEncoder, ImageEncoder};
|
||||
use typst_library::foundations::Smart;
|
||||
use typst_library::layout::{Abs, Axes};
|
||||
use typst_library::visualize::{
|
||||
Image, ImageFormat, ImageKind, ImageScaling, RasterFormat, VectorFormat,
|
||||
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat,
|
||||
};
|
||||
|
||||
use crate::SVGRenderer;
|
||||
@ -37,31 +37,28 @@ impl SVGRenderer {
|
||||
/// `data:image/{format};base64,`.
|
||||
#[comemo::memoize]
|
||||
pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
|
||||
let format = match image.format() {
|
||||
ImageFormat::Raster(f) => match f {
|
||||
RasterFormat::Png => "png",
|
||||
RasterFormat::Jpg => "jpeg",
|
||||
RasterFormat::Gif => "gif",
|
||||
},
|
||||
ImageFormat::Vector(f) => match f {
|
||||
VectorFormat::Svg => "svg+xml",
|
||||
},
|
||||
ImageFormat::Pixmap(_) => "png",
|
||||
};
|
||||
|
||||
let mut buf;
|
||||
let data = match image.kind() {
|
||||
ImageKind::Raster(raster) => raster.data(),
|
||||
ImageKind::Svg(svg) => svg.data(),
|
||||
ImageKind::Pixmap(pixmap) => {
|
||||
buf = vec![];
|
||||
let mut encoder = PngEncoder::new(&mut buf);
|
||||
if let Some(icc_profile) = pixmap.icc_profile() {
|
||||
encoder.set_icc_profile(icc_profile.to_vec()).ok();
|
||||
}
|
||||
pixmap.to_dynamic().write_with_encoder(encoder).unwrap();
|
||||
buf.as_slice()
|
||||
}
|
||||
let (format, data): (&str, &[u8]) = match image.kind() {
|
||||
ImageKind::Raster(raster) => match raster.format() {
|
||||
RasterFormat::Exchange(format) => (
|
||||
match format {
|
||||
ExchangeFormat::Png => "png",
|
||||
ExchangeFormat::Jpg => "jpeg",
|
||||
ExchangeFormat::Gif => "gif",
|
||||
},
|
||||
raster.data(),
|
||||
),
|
||||
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,");
|
||||
|
@ -3,11 +3,11 @@ use std::io::Read;
|
||||
use base64::Engine;
|
||||
use ecow::EcoString;
|
||||
use ttf_parser::GlyphId;
|
||||
use typst_library::foundations::{Bytes, Smart};
|
||||
use typst_library::foundations::Bytes;
|
||||
use typst_library::layout::{Abs, Point, Ratio, Size, Transform};
|
||||
use typst_library::text::{Font, TextItem};
|
||||
use typst_library::visualize::{
|
||||
FillRule, Image, Paint, RasterFormat, RasterImage, RelativeTo,
|
||||
ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo,
|
||||
};
|
||||
use typst_utils::hash128;
|
||||
|
||||
@ -246,10 +246,8 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64
|
||||
if raster.format != ttf_parser::RasterImageFormat::PNG {
|
||||
return None;
|
||||
}
|
||||
let image = Image::new(
|
||||
RasterImage::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png).ok()?,
|
||||
None,
|
||||
Smart::Auto,
|
||||
let image = Image::plain(
|
||||
RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?,
|
||||
);
|
||||
Some((image, raster.x as f64, raster.y as f64))
|
||||
}
|
||||
|
@ -67,71 +67,72 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
||||
|
||||
--- image-pixmap-rgb8 ---
|
||||
#image(
|
||||
(
|
||||
data: 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,
|
||||
)),
|
||||
pixel-width: 3,
|
||||
pixel-height: 3,
|
||||
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,
|
||||
),
|
||||
format: "rgb8",
|
||||
width: 1cm,
|
||||
)
|
||||
|
||||
--- image-pixmap-rgba8 ---
|
||||
#image(
|
||||
(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
format: "rgba8",
|
||||
width: 1cm,
|
||||
)
|
||||
|
||||
--- image-pixmap-luma8 ---
|
||||
#image(
|
||||
(
|
||||
data: bytes(range(16).map(x => x * 16)),
|
||||
pixel-width: 4,
|
||||
pixel-height: 4,
|
||||
bytes(range(16).map(x => x * 16)),
|
||||
format: (
|
||||
encoding: "luma8",
|
||||
width: 4,
|
||||
height: 4,
|
||||
),
|
||||
format: "luma8",
|
||||
width: 1cm,
|
||||
)
|
||||
|
||||
--- image-pixmap-lumaa8 ---
|
||||
#image(
|
||||
(
|
||||
data: bytes(range(16).map(x => (0x80, x * 16)).flatten()),
|
||||
pixel-width: 4,
|
||||
pixel-height: 4,
|
||||
bytes(range(16).map(x => (0x80, x * 16)).flatten()),
|
||||
format: (
|
||||
encoding: "lumaa8",
|
||||
width: 4,
|
||||
height: 4,
|
||||
),
|
||||
format: "lumaa8",
|
||||
width: 1cm,
|
||||
)
|
||||
|
||||
--- image-scaling-methods ---
|
||||
#let img(scaling) = image(
|
||||
(
|
||||
data: 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,
|
||||
)),
|
||||
pixel-width: 3,
|
||||
pixel-height: 3,
|
||||
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,
|
||||
),
|
||||
format: "rgb8",
|
||||
width: 1cm,
|
||||
scaling: scaling,
|
||||
)
|
||||
|
||||
#stack(
|
||||
dir: ltr,
|
||||
spacing: 4pt,
|
||||
@ -181,52 +182,52 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
|
||||
--- image-pixmap-empty ---
|
||||
// Error: 1:2-8:2 zero-sized images are not allowed
|
||||
#image(
|
||||
(
|
||||
data: bytes(()),
|
||||
pixel-width: 0,
|
||||
pixel-height: 0,
|
||||
bytes(()),
|
||||
format: (
|
||||
encoding: "rgb8",
|
||||
width: 0,
|
||||
height: 0,
|
||||
),
|
||||
format: "rgb8",
|
||||
)
|
||||
|
||||
--- image-pixmap-invalid-size ---
|
||||
// Error: 1:2-8:2 pixel dimensions and pixel data do not match
|
||||
#image(
|
||||
(
|
||||
data: bytes((0x00, 0x00, 0x00)),
|
||||
pixel-width: 16,
|
||||
pixel-height: 16,
|
||||
bytes((0x00, 0x00, 0x00)),
|
||||
format: (
|
||||
encoding: "rgb8",
|
||||
width: 16,
|
||||
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(
|
||||
(
|
||||
data: bytes((0x00, 0x00, 0x00)),
|
||||
pixel-width: 1,
|
||||
pixel-height: 1,
|
||||
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",
|
||||
),
|
||||
format: "rgb8",
|
||||
)
|
||||
|
||||
--- image-pixmap-but-png-format ---
|
||||
// Error: 1:2-8:2 expected readable source for the given format (str or bytes)
|
||||
#image(
|
||||
(
|
||||
data: bytes((0x00, 0x00, 0x00)),
|
||||
pixel-width: 1,
|
||||
pixel-height: 1,
|
||||
bytes((0x00, 0x00, 0x00)),
|
||||
// Error: 1:11-5:4 expected "rgb8", "rgba8", "luma8", or "lumaa8"
|
||||
format: (
|
||||
encoding: "png",
|
||||
width: 1,
|
||||
height: 1,
|
||||
),
|
||||
format: "png",
|
||||
)
|
||||
|
||||
--- image-png-but-pixmap-format ---
|
||||
// Error: 1:2-4:2 source must be a pixmap
|
||||
#image(
|
||||
read("/assets/images/tiger.jpg", encoding: none),
|
||||
// Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto
|
||||
format: "rgba8",
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user