mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
127 lines
3.9 KiB
Rust
127 lines
3.9 KiB
Rust
use std::io;
|
|
|
|
use super::prelude::*;
|
|
use crate::diag::Error;
|
|
use crate::image::ImageId;
|
|
|
|
/// `image`: An image.
|
|
pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> {
|
|
let path = args.expect::<Spanned<EcoString>>("path to image file")?;
|
|
let width = args.named("width")?;
|
|
let height = args.named("height")?;
|
|
let fit = args.named("fit")?.unwrap_or_default();
|
|
|
|
// Load the image.
|
|
let full = ctx.make_path(&path.v);
|
|
let id = ctx.images.load(&full).map_err(|err| {
|
|
Error::boxed(path.span, match err.kind() {
|
|
io::ErrorKind::NotFound => "file not found".into(),
|
|
_ => format!("failed to load image ({})", err),
|
|
})
|
|
})?;
|
|
|
|
Ok(Value::Template(Template::from_inline(move |_| {
|
|
ImageNode { id, fit }.pack().sized(width, height)
|
|
})))
|
|
}
|
|
|
|
/// An image node.
|
|
#[derive(Debug, Hash)]
|
|
pub struct ImageNode {
|
|
/// The id of the image file.
|
|
pub id: ImageId,
|
|
/// How the image should adjust itself to a given area.
|
|
pub fit: ImageFit,
|
|
}
|
|
|
|
impl Layout for ImageNode {
|
|
fn layout(
|
|
&self,
|
|
ctx: &mut LayoutContext,
|
|
regions: &Regions,
|
|
) -> Vec<Constrained<Rc<Frame>>> {
|
|
let &Regions { current, expand, .. } = regions;
|
|
|
|
let img = ctx.images.get(self.id);
|
|
let pixel_w = img.width() as f64;
|
|
let pixel_h = img.height() as f64;
|
|
|
|
let region_ratio = current.w / current.h;
|
|
let pixel_ratio = pixel_w / pixel_h;
|
|
let wide = region_ratio < pixel_ratio;
|
|
|
|
// The space into which the image will be placed according to its fit.
|
|
let canvas = if expand.x && expand.y {
|
|
current
|
|
} else if expand.x || (wide && current.w.is_finite()) {
|
|
Size::new(current.w, current.h.min(current.w.safe_div(pixel_ratio)))
|
|
} else if current.h.is_finite() {
|
|
Size::new(current.w.min(current.h * pixel_ratio), current.h)
|
|
} else {
|
|
Size::new(Length::pt(pixel_w), Length::pt(pixel_h))
|
|
};
|
|
|
|
// The actual size of the fitted image.
|
|
let size = match self.fit {
|
|
ImageFit::Contain | ImageFit::Cover => {
|
|
if wide == (self.fit == ImageFit::Contain) {
|
|
Size::new(canvas.w, canvas.w / pixel_ratio)
|
|
} else {
|
|
Size::new(canvas.h * pixel_ratio, canvas.h)
|
|
}
|
|
}
|
|
ImageFit::Stretch => canvas,
|
|
};
|
|
|
|
// The position of the image so that it is centered in the canvas.
|
|
let mut frame = Frame::new(canvas, canvas.h);
|
|
frame.push(
|
|
(canvas - size).to_point() / 2.0,
|
|
Element::Image(self.id, size),
|
|
);
|
|
|
|
// Create a clipping group if the image mode is `cover`.
|
|
if self.fit == ImageFit::Cover {
|
|
let group = Group {
|
|
frame: Rc::new(frame),
|
|
clips: self.fit == ImageFit::Cover,
|
|
};
|
|
|
|
frame = Frame::new(canvas, canvas.h);
|
|
frame.push(Point::zero(), Element::Group(group));
|
|
}
|
|
|
|
let mut cts = Constraints::new(regions.expand);
|
|
cts.exact = regions.current.to_spec().map(Some);
|
|
vec![frame.constrain(cts)]
|
|
}
|
|
}
|
|
|
|
/// How an image should adjust itself to a given area.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum ImageFit {
|
|
/// The image should be fully contained in the area.
|
|
Contain,
|
|
/// The image should completely cover the area.
|
|
Cover,
|
|
/// The image should be stretched so that it exactly fills the area.
|
|
Stretch,
|
|
}
|
|
|
|
castable! {
|
|
ImageFit,
|
|
Expected: "string",
|
|
Value::Str(string) => match string.as_str() {
|
|
"contain" => Self::Contain,
|
|
"cover" => Self::Cover,
|
|
"stretch" => Self::Stretch,
|
|
_ => Err(r#"expected "contain", "cover" or "stretch""#)?,
|
|
},
|
|
}
|
|
|
|
impl Default for ImageFit {
|
|
fn default() -> Self {
|
|
Self::Contain
|
|
}
|
|
}
|