Basic image support 🖼

- [image] function
- Image rendering in tests
- Supports JPEG and PNG
- No PDF export so far
This commit is contained in:
Laurenz 2020-11-20 16:36:22 +01:00
parent 2e6e6244cc
commit f105663037
12 changed files with 213 additions and 31 deletions

View File

@ -11,6 +11,7 @@ fs = ["fontdock/fs"]
[dependencies] [dependencies]
fontdock = { path = "../fontdock", default-features = false } fontdock = { path = "../fontdock", default-features = false }
pdf-writer = { path = "../pdf-writer" } pdf-writer = { path = "../pdf-writer" }
image = { version = "0.23", default-features = false, features = ["jpeg", "png"] }
itoa = "0.4" itoa = "0.4"
ttf-parser = "0.8.2" ttf-parser = "0.8.2"
unicode-xid = "0.2" unicode-xid = "0.2"
@ -24,7 +25,7 @@ criterion = "0.3"
memmap = "0.7" memmap = "0.7"
raqote = { version = "0.8", default-features = false } raqote = { version = "0.8", default-features = false }
[profile.dev.package."*"] [profile.dev]
opt-level = 2 opt-level = 2
[profile.release] [profile.release]

View File

@ -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.tm(1.0, 0.0, 0.0, 1.0, x as f32, y as f32);
text = text.tj(&shaped.encode_glyphs_be()); text = text.tj(&shaped.encode_glyphs_be());
} }
LayoutElement::Image(_image) => {
// TODO: Write image.
}
} }
} }
@ -280,7 +284,7 @@ fn remap_fonts(layouts: &[BoxLayout]) -> (HashMap<FaceId, usize>, Vec<FaceId>) {
// each text element to find out which face is uses. // each text element to find out which face is uses.
for layout in layouts { for layout in layouts {
for (_, element) in &layout.elements { for (_, element) in &layout.elements {
let LayoutElement::Text(shaped) = element; if let LayoutElement::Text(shaped) = element {
to_pdf.entry(shaped.face).or_insert_with(|| { to_pdf.entry(shaped.face).or_insert_with(|| {
let next_id = to_layout.len(); let next_id = to_layout.len();
to_layout.push(shaped.face); to_layout.push(shaped.face);
@ -288,6 +292,7 @@ fn remap_fonts(layouts: &[BoxLayout]) -> (HashMap<FaceId, usize>, Vec<FaceId>) {
}); });
} }
} }
}
(to_pdf, to_layout) (to_pdf, to_layout)
} }

View File

@ -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 -= Length); assign_impl!(Length -= Length);
assign_impl!(Length *= f64); assign_impl!(Length *= f64);

62
src/layout/graphics.rs Normal file
View File

@ -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)
}
}

View File

@ -2,6 +2,7 @@
mod document; mod document;
mod fixed; mod fixed;
mod graphics;
mod node; mod node;
mod pad; mod pad;
mod par; mod par;
@ -9,12 +10,15 @@ mod spacing;
mod stack; mod stack;
mod text; mod text;
use image::RgbaImage;
use crate::font::SharedFontLoader; use crate::font::SharedFontLoader;
use crate::geom::*; use crate::geom::*;
use crate::shaping::Shaped; use crate::shaping::Shaped;
pub use document::*; pub use document::*;
pub use fixed::*; pub use fixed::*;
pub use graphics::*;
pub use node::*; pub use node::*;
pub use pad::*; pub use pad::*;
pub use par::*; pub use par::*;
@ -179,4 +183,15 @@ impl BoxLayout {
pub enum LayoutElement { pub enum LayoutElement {
/// Shaped text. /// Shaped text.
Text(Shaped), 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,
} }

42
src/library/graphics.rs Normal file
View File

@ -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
}

View File

@ -4,6 +4,7 @@ mod align;
mod boxed; mod boxed;
mod color; mod color;
mod font; mod font;
mod graphics;
mod page; mod page;
mod spacing; mod spacing;
@ -11,29 +12,35 @@ pub use align::*;
pub use boxed::*; pub use boxed::*;
pub use color::*; pub use color::*;
pub use font::*; pub use font::*;
pub use graphics::*;
pub use page::*; pub use page::*;
pub use spacing::*; pub use spacing::*;
use crate::eval::{Scope, ValueFunc}; use crate::eval::{Scope, ValueFunc};
macro_rules! std { macro_rules! std {
($($name:literal => $func:expr),* $(,)?) => { ($($func:expr $(=> $name:expr)?),* $(,)?) => {
/// Create a scope with all standard library functions. /// Create a scope with all standard library functions.
pub fn _std() -> Scope { pub fn _std() -> Scope {
let mut std = Scope::new(); 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
} }
}; };
} }
std! { std! {
"align" => align, align,
"box" => boxed, boxed => "box",
"font" => font, font,
"h" => h, h,
"page" => page, image,
"pagebreak" => pagebreak, page,
"rgb" => rgb, pagebreak,
"v" => v, rgb,
v,
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 76 KiB

BIN
tests/ref/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 KiB

BIN
tests/res/tiger.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

13
tests/typ/image.typ Normal file
View File

@ -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]

View File

@ -15,21 +15,23 @@ use typst::eval::State;
use typst::export::pdf; use typst::export::pdf;
use typst::font::{FontLoader, SharedFontLoader}; use typst::font::{FontLoader, SharedFontLoader};
use typst::geom::{Length, Point}; use typst::geom::{Length, Point};
use typst::layout::{BoxLayout, LayoutElement}; use typst::layout::{BoxLayout, ImageElement, LayoutElement};
use typst::parse::LineMap; use typst::parse::LineMap;
use typst::shaping::Shaped; use typst::shaping::Shaped;
use typst::typeset; use typst::typeset;
const FONT_DIR: &str = "fonts"; const FONT_DIR: &str = "../fonts";
const TYP_DIR: &str = "tests/typ"; const TYP_DIR: &str = "typ";
const PDF_DIR: &str = "tests/pdf"; const PDF_DIR: &str = "pdf";
const PNG_DIR: &str = "tests/png"; const PNG_DIR: &str = "png";
const REF_DIR: &str = "tests/ref"; const REF_DIR: &str = "ref";
const BLACK: SolidSource = SolidSource { r: 0, g: 0, b: 0, a: 255 }; const BLACK: SolidSource = SolidSource { r: 0, g: 0, b: 0, a: 255 };
const WHITE: SolidSource = SolidSource { r: 255, g: 255, b: 255, a: 255 }; const WHITE: SolidSource = SolidSource { r: 255, g: 255, b: 255, a: 255 };
fn main() { fn main() {
env::set_current_dir(env::current_dir().unwrap().join("tests")).unwrap();
let filter = TestFilter::new(env::args().skip(1)); let filter = TestFilter::new(env::args().skip(1));
let mut filtered = Vec::new(); 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 loader = loader.borrow();
let surface = render(&layouts, &loader, 3.0); let surface = render(&layouts, &loader, 2.0);
surface.write_png(png_path).unwrap(); surface.write_png(png_path).unwrap();
let pdf_data = pdf::export(&layouts, &loader); 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 { for &(pos, ref element) in &layout.elements {
let pos = scale * pos + offset;
match element { match element {
LayoutElement::Text(shaped) => render_shaped( LayoutElement::Text(shaped) => {
&mut surface, render_shaped(&mut surface, loader, shaped, pos, scale)
loader, }
shaped, LayoutElement::Image(image) => {
scale * pos + offset, render_image(&mut surface, image, pos, scale)
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); struct WrappedPathBuilder(PathBuilder);
impl OutlineBuilder for WrappedPathBuilder { impl OutlineBuilder for WrappedPathBuilder {