mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Basic image support 🖼
- [image] function - Image rendering in tests - Supports JPEG and PNG - No PDF export so far
This commit is contained in:
parent
2e6e6244cc
commit
f105663037
@ -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]
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
62
src/layout/graphics.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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
42
src/library/graphics.rs
Normal 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
|
||||
}
|
@ -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
BIN
tests/ref/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 935 KiB |
BIN
tests/res/tiger.jpg
Normal file
BIN
tests/res/tiger.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
13
tests/typ/image.typ
Normal file
13
tests/typ/image.typ
Normal 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]
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user