diff --git a/Cargo.toml b/Cargo.toml index 50cfde1b..11a2ed57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ fs = ["fontdock/fs"] [dependencies] fontdock = { path = "../fontdock", default-features = false } pdf-writer = { path = "../pdf-writer" } +image = { version = "0.23", default-features = false, features = ["jpeg", "png"] } itoa = "0.4" ttf-parser = "0.8.2" unicode-xid = "0.2" @@ -24,7 +25,7 @@ criterion = "0.3" memmap = "0.7" raqote = { version = "0.8", default-features = false } -[profile.dev.package."*"] +[profile.dev] opt-level = 2 [profile.release] diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 763cea8a..cb05dee0 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -148,6 +148,10 @@ impl<'a> PdfExporter<'a> { text = text.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32); text = text.tj(&shaped.encode_glyphs_be()); } + + LayoutElement::Image(_image) => { + // TODO: Write image. + } } } @@ -280,12 +284,13 @@ fn remap_fonts(layouts: &[BoxLayout]) -> (HashMap, Vec) { // each text element to find out which face is uses. for layout in layouts { for (_, element) in &layout.elements { - let LayoutElement::Text(shaped) = element; - to_pdf.entry(shaped.face).or_insert_with(|| { - let next_id = to_layout.len(); - to_layout.push(shaped.face); - next_id - }); + if let LayoutElement::Text(shaped) = element { + to_pdf.entry(shaped.face).or_insert_with(|| { + let next_id = to_layout.len(); + to_layout.push(shaped.face); + next_id + }); + } } } diff --git a/src/geom/length.rs b/src/geom/length.rs index 60ccce2b..061510a1 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -142,6 +142,14 @@ impl Div for Length { } } +impl Div for Length { + type Output = f64; + + fn div(self, other: Self) -> f64 { + self.raw / other.raw + } +} + assign_impl!(Length += Length); assign_impl!(Length -= Length); assign_impl!(Length *= f64); diff --git a/src/layout/graphics.rs b/src/layout/graphics.rs new file mode 100644 index 00000000..1fa05605 --- /dev/null +++ b/src/layout/graphics.rs @@ -0,0 +1,62 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::*; + +/// An image node. +#[derive(Clone, PartialEq)] +pub struct Image { + /// The image. + pub buf: RgbaImage, + /// The fixed width, if any. + pub width: Option, + /// The fixed height, if any. + pub height: Option, + /// How to align this image node in its parent. + pub align: BoxAlign, +} + +impl Layout for Image { + fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Layouted { + let Area { rem, full } = areas.current; + let (pixel_width, pixel_height) = self.buf.dimensions(); + let pixel_ratio = (pixel_width as f64) / (pixel_height as f64); + + let width = self.width.map(|w| w.resolve(full.width)); + let height = self.height.map(|w| w.resolve(full.height)); + + let size = match (width, height) { + (Some(width), Some(height)) => Size::new(width, height), + (Some(width), None) => Size::new(width, width / pixel_ratio), + (None, Some(height)) => Size::new(height * pixel_ratio, height), + (None, None) => { + let ratio = rem.width / rem.height; + if ratio < pixel_ratio { + Size::new(rem.width, rem.width / pixel_ratio) + } else { + // TODO: Fix issue with line spacing. + Size::new(rem.height * pixel_ratio, rem.height) + } + } + }; + + let mut boxed = BoxLayout::new(size); + boxed.push( + Point::ZERO, + LayoutElement::Image(ImageElement { buf: self.buf.clone(), size }), + ); + + Layouted::Layout(boxed, self.align) + } +} + +impl Debug for Image { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Image") + } +} + +impl From for LayoutNode { + fn from(image: Image) -> Self { + Self::dynamic(image) + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 4dd6184f..5586a1fd 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -2,6 +2,7 @@ mod document; mod fixed; +mod graphics; mod node; mod pad; mod par; @@ -9,12 +10,15 @@ mod spacing; mod stack; mod text; +use image::RgbaImage; + use crate::font::SharedFontLoader; use crate::geom::*; use crate::shaping::Shaped; pub use document::*; pub use fixed::*; +pub use graphics::*; pub use node::*; pub use pad::*; pub use par::*; @@ -179,4 +183,15 @@ impl BoxLayout { pub enum LayoutElement { /// Shaped text. Text(Shaped), + /// An image. + Image(ImageElement), +} + +/// An image. +#[derive(Debug, Clone, PartialEq)] +pub struct ImageElement { + /// The image. + pub buf: RgbaImage, + /// The document size of the image. + pub size: Size, } diff --git a/src/library/graphics.rs b/src/library/graphics.rs new file mode 100644 index 00000000..779d78b5 --- /dev/null +++ b/src/library/graphics.rs @@ -0,0 +1,42 @@ +use std::fs::File; +use std::io::BufReader; + +use image::io::Reader; + +use crate::layout::Image; +use crate::prelude::*; + +/// `image`: Include an image. +/// +/// # Positional arguments +/// - The path to the image (string) +pub fn image(mut args: Args, ctx: &mut EvalContext) -> Value { + let path = args.need::<_, Spanned>(ctx, 0, "path"); + let width = args.get::<_, Linear>(ctx, "width"); + let height = args.get::<_, Linear>(ctx, "height"); + + if let Some(path) = path { + if let Ok(file) = File::open(path.v) { + match Reader::new(BufReader::new(file)) + .with_guessed_format() + .map_err(|err| err.into()) + .and_then(|reader| reader.decode()) + .map(|img| img.into_rgba8()) + { + Ok(buf) => { + ctx.push(Image { + buf, + width, + height, + align: ctx.state.align, + }); + } + Err(err) => ctx.diag(error!(path.span, "invalid image: {}", err)), + } + } else { + ctx.diag(error!(path.span, "failed to open image file")); + } + } + + Value::None +} diff --git a/src/library/mod.rs b/src/library/mod.rs index af23d050..e59201dc 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -4,6 +4,7 @@ mod align; mod boxed; mod color; mod font; +mod graphics; mod page; mod spacing; @@ -11,29 +12,35 @@ pub use align::*; pub use boxed::*; pub use color::*; pub use font::*; +pub use graphics::*; pub use page::*; pub use spacing::*; use crate::eval::{Scope, ValueFunc}; macro_rules! std { - ($($name:literal => $func:expr),* $(,)?) => { + ($($func:expr $(=> $name:expr)?),* $(,)?) => { /// Create a scope with all standard library functions. pub fn _std() -> Scope { let mut std = Scope::new(); - $(std.set($name, ValueFunc::new($func));)* + $( + let _name = stringify!($func); + $(let _name = $name;)? + std.set(_name, ValueFunc::new($func)); + )* std } }; } std! { - "align" => align, - "box" => boxed, - "font" => font, - "h" => h, - "page" => page, - "pagebreak" => pagebreak, - "rgb" => rgb, - "v" => v, + align, + boxed => "box", + font, + h, + image, + page, + pagebreak, + rgb, + v, } diff --git a/tests/ref/coma.png b/tests/ref/coma.png index 642759a8..f84423ee 100644 Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ diff --git a/tests/ref/image.png b/tests/ref/image.png new file mode 100644 index 00000000..d532d4e9 Binary files /dev/null and b/tests/ref/image.png differ diff --git a/tests/res/tiger.jpg b/tests/res/tiger.jpg new file mode 100644 index 00000000..74dc5e0b Binary files /dev/null and b/tests/res/tiger.jpg differ diff --git a/tests/typ/image.typ b/tests/typ/image.typ new file mode 100644 index 00000000..b0fd4b51 --- /dev/null +++ b/tests/typ/image.typ @@ -0,0 +1,13 @@ +[page: width=10cm, height=10cm, margins=1cm] + +[image: "res/tiger.jpg"] + +[pagebreak] + +[image: "res/tiger.jpg", width=3cm] +[image: "res/tiger.jpg", height=3cm] + +[pagebreak] + +[align: center] +[image: "res/tiger.jpg", width=6cm, height=6cm] diff --git a/tests/typeset.rs b/tests/typeset.rs index 90e0d4fb..7c628879 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -15,21 +15,23 @@ use typst::eval::State; use typst::export::pdf; use typst::font::{FontLoader, SharedFontLoader}; use typst::geom::{Length, Point}; -use typst::layout::{BoxLayout, LayoutElement}; +use typst::layout::{BoxLayout, ImageElement, LayoutElement}; use typst::parse::LineMap; use typst::shaping::Shaped; use typst::typeset; -const FONT_DIR: &str = "fonts"; -const TYP_DIR: &str = "tests/typ"; -const PDF_DIR: &str = "tests/pdf"; -const PNG_DIR: &str = "tests/png"; -const REF_DIR: &str = "tests/ref"; +const FONT_DIR: &str = "../fonts"; +const TYP_DIR: &str = "typ"; +const PDF_DIR: &str = "pdf"; +const PNG_DIR: &str = "png"; +const REF_DIR: &str = "ref"; const BLACK: SolidSource = SolidSource { r: 0, g: 0, b: 0, a: 255 }; const WHITE: SolidSource = SolidSource { r: 255, g: 255, b: 255, a: 255 }; fn main() { + env::set_current_dir(env::current_dir().unwrap().join("tests")).unwrap(); + let filter = TestFilter::new(env::args().skip(1)); let mut filtered = Vec::new(); @@ -131,7 +133,7 @@ fn test(src_path: &Path, pdf_path: &Path, png_path: &Path, loader: &SharedFontLo let loader = loader.borrow(); - let surface = render(&layouts, &loader, 3.0); + let surface = render(&layouts, &loader, 2.0); surface.write_png(png_path).unwrap(); let pdf_data = pdf::export(&layouts, &loader); @@ -197,14 +199,15 @@ fn render(layouts: &[BoxLayout], loader: &FontLoader, scale: f64) -> DrawTarget ); for &(pos, ref element) in &layout.elements { + let pos = scale * pos + offset; + match element { - LayoutElement::Text(shaped) => render_shaped( - &mut surface, - loader, - shaped, - scale * pos + offset, - scale, - ), + LayoutElement::Text(shaped) => { + render_shaped(&mut surface, loader, shaped, pos, scale) + } + LayoutElement::Image(image) => { + render_image(&mut surface, image, pos, scale) + } } } @@ -244,6 +247,32 @@ fn render_shaped( } } +fn render_image(surface: &mut DrawTarget, image: &ImageElement, pos: Point, scale: f64) { + let mut data = vec![]; + for pixel in image.buf.pixels() { + let [r, g, b, a] = pixel.0; + data.push( + ((a as u32) << 24) + | ((r as u32) << 16) + | ((g as u32) << 8) + | ((b as u32) << 0), + ); + } + + surface.draw_image_with_size_at( + (scale * image.size.width.to_pt()) as f32, + (scale * image.size.height.to_pt()) as f32, + pos.x.to_pt() as f32, + pos.y.to_pt() as f32, + &raqote::Image { + width: image.buf.dimensions().0 as i32, + height: image.buf.dimensions().1 as i32, + data: &data, + }, + &Default::default(), + ); +} + struct WrappedPathBuilder(PathBuilder); impl OutlineBuilder for WrappedPathBuilder {