mod raster;
mod svg;
pub use self::raster::{RasterFormat, RasterImage};
pub use self::svg::SvgImage;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use comemo::Tracked;
use ecow::EcoString;
use crate::diag::{bail, warning, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart,
StyleChain,
};
use crate::introspection::Locator;
use crate::layout::{
Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel,
Size, Sizing,
};
use crate::loading::Readable;
use crate::model::Figurable;
use crate::syntax::{Span, Spanned};
use crate::text::{families, LocalName};
use crate::utils::LazyHash;
use crate::visualize::Path;
use crate::World;
#[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem {
#[required]
#[parse(
let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
path
)]
#[borrowed]
pub path: EcoString,
#[internal]
#[required]
#[parse(Readable::Bytes(data))]
pub data: Readable,
pub format: Smart<ImageFormat>,
pub width: Smart<Rel<Length>>,
pub height: Sizing,
pub alt: Option<EcoString>,
#[default(ImageFit::Cover)]
pub fit: ImageFit,
}
#[scope]
impl ImageElem {
#[func(title = "Decode Image")]
pub fn decode(
span: Span,
data: 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>,
) -> StrResult<Content> {
let mut elem = ImageElem::new(EcoString::new(), data);
if let Some(format) = format {
elem.push_format(format);
}
if let Some(width) = width {
elem.push_width(width);
}
if let Some(height) = height {
elem.push_height(height);
}
if let Some(alt) = alt {
elem.push_alt(alt);
}
if let Some(fit) = fit {
elem.push_fit(fit);
}
Ok(elem.pack().spanned(span))
}
}
impl Show for Packed<ImageElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_image)
.with_width(self.width(styles))
.with_height(self.height(styles))
.pack()
.spanned(self.span()))
}
}
impl LocalName for Packed<ImageElem> {
const KEY: &'static str = "figure";
}
impl Figurable for Packed<ImageElem> {}
#[typst_macros::time(span = elem.span())]
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let span = elem.span();
let data = elem.data();
let format = match elem.format(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
};
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
if has_foreign_object {
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 image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
)
.at(span)?;
let pxw = image.width();
let pxh = image.height();
let px_ratio = pxw / pxh;
let region_ratio = region.size.x / region.size.y;
let wide = px_ratio > region_ratio;
let target = if region.expand.x && region.expand.y {
region.size
} else if region.expand.x {
Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
} else if region.expand.y {
Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
} else {
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new(
natural.x.min(region.size.x).min(region.size.y * px_ratio),
natural.y.min(region.size.y).min(region.size.x / px_ratio),
)
};
let fit = elem.fit(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {
Size::new(target.x, target.x / px_ratio)
} else {
Size::new(target.y * px_ratio, target.y)
}
}
ImageFit::Stretch => target,
};
let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
frame.resize(target, Axes::splat(FixedAlignment::Center));
if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size()));
}
Ok(frame)
}
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
"gif" => ImageFormat::Raster(RasterFormat::Gif),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
_ => match &data {
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
Some(f) => ImageFormat::Raster(f),
None => bail!("unknown image format"),
},
},
})
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum ImageFit {
Cover,
Contain,
Stretch,
}
#[derive(Clone, Hash, Eq, PartialEq)]
pub struct Image(Arc<LazyHash<Repr>>);
#[derive(Hash)]
struct Repr {
kind: ImageKind,
alt: Option<EcoString>,
}
#[derive(Hash)]
pub enum ImageKind {
Raster(RasterImage),
Svg(SvgImage),
}
impl Image {
pub const DEFAULT_DPI: f64 = 72.0;
pub const USVG_DEFAULT_DPI: f64 = 96.0;
#[comemo::memoize]
#[typst_macros::time(name = "load image")]
pub fn new(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(data, format)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::new(data)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
#[comemo::memoize]
#[typst_macros::time(name = "load image")]
pub fn with_fonts(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
world: Tracked<dyn World + '_>,
families: &[&str],
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(data, format)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
pub fn data(&self) -> &Bytes {
match &self.0.kind {
ImageKind::Raster(raster) => raster.data(),
ImageKind::Svg(svg) => svg.data(),
}
}
pub fn format(&self) -> ImageFormat {
match &self.0.kind {
ImageKind::Raster(raster) => raster.format().into(),
ImageKind::Svg(_) => VectorFormat::Svg.into(),
}
}
pub fn width(&self) -> f64 {
match &self.0.kind {
ImageKind::Raster(raster) => raster.width() as f64,
ImageKind::Svg(svg) => svg.width(),
}
}
pub fn height(&self) -> f64 {
match &self.0.kind {
ImageKind::Raster(raster) => raster.height() as f64,
ImageKind::Svg(svg) => svg.height(),
}
}
pub fn dpi(&self) -> Option<f64> {
match &self.0.kind {
ImageKind::Raster(raster) => raster.dpi(),
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
}
}
pub fn alt(&self) -> Option<&str> {
self.0.alt.as_deref()
}
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())
.finish()
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ImageFormat {
Raster(RasterFormat),
Vector(VectorFormat),
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VectorFormat {
Svg,
}
impl From<RasterFormat> for ImageFormat {
fn from(format: RasterFormat) -> Self {
Self::Raster(format)
}
}
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),
}