summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2020-11-20 16:36:22 +0100
committerLaurenz <laurmaedje@gmail.com>2020-11-20 16:36:22 +0100
commitf105663037c44740b5aa02dea72a9b368bc003e0 (patch)
treeee97638bfba2be3cfba8c1191919447262f181d0
parent2e6e6244ccf73795d7d74cbc286fb0b43b404315 (diff)
Basic image support 🖼
- [image] function - Image rendering in tests - Supports JPEG and PNG - No PDF export so far
-rw-r--r--Cargo.toml3
-rw-r--r--src/export/pdf.rs17
-rw-r--r--src/geom/length.rs8
-rw-r--r--src/layout/graphics.rs62
-rw-r--r--src/layout/mod.rs15
-rw-r--r--src/library/graphics.rs42
-rw-r--r--src/library/mod.rs27
-rw-r--r--tests/ref/coma.pngbin125591 -> 77574 bytes
-rw-r--r--tests/ref/image.pngbin0 -> 957679 bytes
-rw-r--r--tests/res/tiger.jpgbin0 -> 116679 bytes
-rw-r--r--tests/typ/image.typ13
-rw-r--r--tests/typeset.rs57
12 files changed, 213 insertions, 31 deletions
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<FaceId, usize>, Vec<FaceId>) {
// 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<f64> 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<Linear>,
+ /// The fixed height, if any.
+ pub height: Option<Linear>,
+ /// 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<Image> 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<String>>(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
--- a/tests/ref/coma.png
+++ b/tests/ref/coma.png
Binary files differ
diff --git a/tests/ref/image.png b/tests/ref/image.png
new file mode 100644
index 00000000..d532d4e9
--- /dev/null
+++ b/tests/ref/image.png
Binary files differ
diff --git a/tests/res/tiger.jpg b/tests/res/tiger.jpg
new file mode 100644
index 00000000..74dc5e0b
--- /dev/null
+++ b/tests/res/tiger.jpg
Binary files 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 {