mod pdf;
mod raster;
mod svg;
pub use self::pdf::PdfImage;
pub use self::raster::{
ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
};
pub use self::svg::SvgImage;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::sync::Arc;
use ecow::EcoString;
use hayro_syntax::LoadPdfError;
use typst_syntax::{Span, Spanned};
use typst_utils::{LazyHash, NonZeroExt};
use crate::diag::{At, LoadedWithin, SourceResult, StrResult, bail, warning};
use crate::engine::Engine;
use crate::foundations::{
Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, StyleChain, Synthesize,
cast, elem, func, scope,
};
use crate::introspection::{Locatable, Tagged};
use crate::layout::{Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable;
use crate::text::{LocalName, Locale, families};
use crate::visualize::image::pdf::PdfDocument;
#[elem(scope, Locatable, Tagged, Synthesize, LocalName, Figurable)]
pub struct ImageElem {
#[required]
#[parse(
let source = args.expect::<Spanned<DataSource>>("source")?;
let loaded = source.load(engine.world)?;
Derived::new(source.v, loaded)
)]
pub source: Derived<DataSource, Loaded>,
pub format: Smart<ImageFormat>,
pub width: Smart<Rel<Length>>,
pub height: Sizing,
pub alt: Option<EcoString>,
#[default(NonZeroUsize::ONE)]
pub page: NonZeroUsize,
#[default(ImageFit::Cover)]
pub fit: ImageFit,
pub scaling: Smart<ImageScaling>,
#[parse(match args.named::<Spanned<Smart<DataSource>>>("icc")? {
Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({
let loaded = Spanned::new(&source, span).load(engine.world)?;
Derived::new(source, loaded.data)
})),
Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
None => None,
})]
pub icc: Smart<Derived<DataSource, Bytes>>,
#[internal]
#[synthesized]
pub locale: Locale,
}
impl Synthesize for Packed<ImageElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
self.locale = Some(Locale::get_in(styles));
Ok(())
}
}
#[scope]
#[allow(clippy::too_many_arguments)]
impl ImageElem {
#[func(title = "Decode Image")]
#[deprecated(
message = "`image.decode` is deprecated, directly pass bytes to `image` instead",
until = "0.15.0"
)]
pub fn decode(
span: Span,
data: Spanned<Readable>,
#[named]
format: Option<Smart<ImageFormat>>,
#[named]
width: Option<Smart<Rel<Length>>>,
#[named]
height: Option<Sizing>,
#[named]
alt: Option<Option<EcoString>>,
#[named]
fit: Option<ImageFit>,
#[named]
scaling: Option<Smart<ImageScaling>>,
) -> StrResult<Content> {
let bytes = data.v.into_bytes();
let loaded =
Loaded::new(Spanned::new(LoadSource::Bytes, data.span), bytes.clone());
let source = Derived::new(DataSource::Bytes(bytes), loaded);
let mut elem = ImageElem::new(source);
if let Some(format) = format {
elem.format.set(format);
}
if let Some(width) = width {
elem.width.set(width);
}
if let Some(height) = height {
elem.height.set(height);
}
if let Some(alt) = alt {
elem.alt.set(alt);
}
if let Some(fit) = fit {
elem.fit.set(fit);
}
if let Some(scaling) = scaling {
elem.scaling.set(scaling);
}
Ok(elem.pack().spanned(span))
}
}
impl Packed<ImageElem> {
pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Image> {
let span = self.span();
let loaded = &self.source.derived;
let format = self.determine_format(styles).at(span)?;
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
loaded.data.clone(),
format,
self.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => {
if memchr::memmem::find(&loaded.data, b"<foreignObject").is_some() {
engine.sink.warn(warning!(
span,
"image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in Typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information"
));
}
let svg_file = match self.source.source {
DataSource::Path(ref path) => span.resolve_path(path).ok(),
DataSource::Bytes(_) => span.id(),
};
ImageKind::Svg(
SvgImage::with_fonts_images(
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
svg_file,
)
.within(loaded)?,
)
}
ImageFormat::Vector(VectorFormat::Pdf) => {
let document = match PdfDocument::new(loaded.data.clone()) {
Ok(doc) => doc,
Err(e) => match e {
LoadPdfError::Decryption(_) => {
bail!(
span,
"the PDF is encrypted or password-protected";
hint: "such PDFs are currently not supported";
hint: "preprocess the PDF to remove the encryption"
);
}
LoadPdfError::Invalid => {
bail!(
span,
"the PDF could not be loaded";
hint: "perhaps the PDF file is malformed"
);
}
},
};
if document.pdf().xref().has_optional_content_groups() {
engine.sink.warn(warning!(
span,
"PDF contains optional content groups";
hint: "the image might display incorrectly in PDF export";
hint: "preprocess the PDF to flatten or remove optional content groups"
));
}
let page_num = self.page.get(styles).get();
let page_idx = page_num - 1;
let num_pages = document.num_pages();
let Some(pdf_image) = PdfImage::new(document, page_idx) else {
let s = if num_pages == 1 { "" } else { "s" };
bail!(
span,
"page {page_num} does not exist";
hint: "the document only has {num_pages} page{s}"
);
};
ImageKind::Pdf(pdf_image)
}
};
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
}
fn determine_format(&self, styles: StyleChain) -> StrResult<ImageFormat> {
if let Smart::Custom(v) = self.format.get(styles) {
return Ok(v);
};
let Derived { source, derived: loaded } = &self.source;
if let DataSource::Path(path) = source
&& let Some(format) = determine_format_from_path(path.as_str())
{
return Ok(format);
}
Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?)
}
}
fn determine_format_from_path(path: &str) -> Option<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
match ext.as_str() {
"png" => Some(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => Some(ExchangeFormat::Jpg.into()),
"gif" => Some(ExchangeFormat::Gif.into()),
"webp" => Some(ExchangeFormat::Webp.into()),
"svg" | "svgz" => Some(VectorFormat::Svg.into()),
"pdf" => Some(VectorFormat::Pdf.into()),
_ => None,
}
}
impl LocalName for Packed<ImageElem> {
const KEY: &'static str = "figure";
}
impl Figurable for Packed<ImageElem> {}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageFit {
Cover,
Contain,
Stretch,
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Image(Arc<LazyHash<Repr>>);
#[derive(Hash)]
struct Repr {
kind: ImageKind,
alt: Option<EcoString>,
scaling: Smart<ImageScaling>,
}
impl Image {
pub const DEFAULT_DPI: f64 = 72.0;
pub const USVG_DEFAULT_DPI: f64 = 96.0;
pub fn new(
kind: impl Into<ImageKind>,
alt: Option<EcoString>,
scaling: Smart<ImageScaling>,
) -> Self {
Self::new_impl(kind.into(), alt, scaling)
}
pub fn plain(kind: impl Into<ImageKind>) -> Self {
Self::new(kind, None, Smart::Auto)
}
#[comemo::memoize]
fn new_impl(
kind: ImageKind,
alt: Option<EcoString>,
scaling: Smart<ImageScaling>,
) -> Image {
Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling })))
}
pub fn format(&self) -> ImageFormat {
match &self.0.kind {
ImageKind::Raster(raster) => raster.format().into(),
ImageKind::Svg(_) => VectorFormat::Svg.into(),
ImageKind::Pdf(_) => VectorFormat::Pdf.into(),
}
}
pub fn width(&self) -> f64 {
match &self.0.kind {
ImageKind::Raster(raster) => raster.width() as f64,
ImageKind::Svg(svg) => svg.width(),
ImageKind::Pdf(pdf) => pdf.width() as f64,
}
}
pub fn height(&self) -> f64 {
match &self.0.kind {
ImageKind::Raster(raster) => raster.height() as f64,
ImageKind::Svg(svg) => svg.height(),
ImageKind::Pdf(pdf) => pdf.height() as f64,
}
}
pub fn dpi(&self) -> Option<f64> {
match &self.0.kind {
ImageKind::Raster(raster) => raster.dpi(),
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
ImageKind::Pdf(_) => Some(Image::DEFAULT_DPI),
}
}
pub fn alt(&self) -> Option<&str> {
self.0.alt.as_deref()
}
pub fn scaling(&self) -> Smart<ImageScaling> {
self.0.scaling
}
pub fn kind(&self) -> &ImageKind {
&self.0.kind
}
}
impl Debug for Image {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_struct("Image")
.field("format", &self.format())
.field("width", &self.width())
.field("height", &self.height())
.field("alt", &self.alt())
.field("scaling", &self.scaling())
.finish()
}
}
#[derive(Clone, Hash)]
pub enum ImageKind {
Raster(RasterImage),
Svg(SvgImage),
Pdf(PdfImage),
}
impl From<RasterImage> for ImageKind {
fn from(image: RasterImage) -> Self {
Self::Raster(image)
}
}
impl From<SvgImage> for ImageKind {
fn from(image: SvgImage) -> Self {
Self::Svg(image)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ImageFormat {
Raster(RasterFormat),
Vector(VectorFormat),
}
impl ImageFormat {
pub fn detect(data: &[u8]) -> Option<Self> {
if let Some(format) = ExchangeFormat::detect(data) {
return Some(Self::Raster(RasterFormat::Exchange(format)));
}
if is_svg(data) {
return Some(Self::Vector(VectorFormat::Svg));
}
if is_pdf(data) {
return Some(Self::Vector(VectorFormat::Pdf));
}
None
}
}
fn is_pdf(data: &[u8]) -> bool {
let head = &data[..data.len().min(2048)];
memchr::memmem::find(head, b"%PDF-").is_some()
}
fn is_svg(data: &[u8]) -> bool {
if data.starts_with(&[0x1f, 0x8b]) {
return true;
}
let head = &data[..data.len().min(2048)];
memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some()
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VectorFormat {
Svg,
Pdf,
}
impl<R> From<R> for ImageFormat
where
R: Into<RasterFormat>,
{
fn from(format: R) -> Self {
Self::Raster(format.into())
}
}
impl From<VectorFormat> for ImageFormat {
fn from(format: VectorFormat) -> Self {
Self::Vector(format)
}
}
cast! {
ImageFormat,
self => match self {
Self::Raster(v) => v.into_value(),
Self::Vector(v) => v.into_value(),
},
v: RasterFormat => Self::Raster(v),
v: VectorFormat => Self::Vector(v),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageScaling {
Smooth,
Pixelated,
}