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

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.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
});
}
}
}

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 *= 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 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,
}

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

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::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 {