diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 24944246d..05f73e52e 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -14,7 +14,7 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset;
use crate::font::{find_name, FaceId, FontStore};
-use crate::frame::{Element, Frame, Geometry};
+use crate::frame::{Element, Frame, Geometry, Text};
use crate::geom::{self, Color, Em, Length, Paint, Size};
use crate::image::{Image, ImageId, ImageStore};
use crate::Context;
@@ -36,16 +36,16 @@ struct PdfExporter<'a> {
frames: &'a [Rc],
fonts: &'a FontStore,
images: &'a ImageStore,
- glyphs: HashMap>,
font_map: Remapper,
image_map: Remapper,
+ glyphs: HashMap>,
}
impl<'a> PdfExporter<'a> {
fn new(ctx: &'a Context, frames: &'a [Rc]) -> Self {
- let mut glyphs = HashMap::>::new();
let mut font_map = Remapper::new();
let mut image_map = Remapper::new();
+ let mut glyphs = HashMap::>::new();
let mut alpha_masks = 0;
for frame in frames {
@@ -56,7 +56,6 @@ impl<'a> PdfExporter<'a> {
let set = glyphs.entry(text.face_id).or_default();
set.extend(text.glyphs.iter().map(|g| g.id));
}
- Element::Geometry(_, _) => {}
Element::Image(id, _) => {
let img = ctx.images.get(id);
if img.buf.color().has_alpha() {
@@ -64,7 +63,7 @@ impl<'a> PdfExporter<'a> {
}
image_map.insert(id);
}
- Element::Link(_, _) => {}
+ _ => {}
}
}
}
@@ -120,8 +119,8 @@ impl<'a> PdfExporter<'a> {
for ((page_id, content_id), page) in
self.refs.pages().zip(self.refs.contents()).zip(self.frames)
{
- let w = page.size.w.to_pt() as f32;
- let h = page.size.h.to_pt() as f32;
+ let w = page.size.w.to_f32();
+ let h = page.size.h.to_f32();
let mut page_writer = self.writer.page(page_id);
page_writer
@@ -131,10 +130,10 @@ impl<'a> PdfExporter<'a> {
let mut annotations = page_writer.annotations();
for (pos, element) in page.elements() {
if let Element::Link(href, size) = element {
- let x = pos.x.to_pt() as f32;
- let y = (page.size.h - pos.y).to_pt() as f32;
- let w = size.w.to_pt() as f32;
- let h = size.h.to_pt() as f32;
+ let x = pos.x.to_f32();
+ let y = (page.size.h - pos.y).to_f32();
+ let w = size.w.to_f32();
+ let h = size.h.to_f32();
annotations
.push()
@@ -158,148 +157,9 @@ impl<'a> PdfExporter<'a> {
}
fn write_page(&mut self, id: Ref, page: &'a Frame) {
- let mut content = Content::new();
-
- // We only write font switching actions when the used face changes. To
- // do that, we need to remember the active face.
- let mut face_id = None;
- let mut size = Length::zero();
- let mut fill: Option = None;
- let mut in_text_state = false;
-
- for (pos, element) in page.elements() {
- // Make sure the content stream is in the correct state.
- match element {
- Element::Text(_) if !in_text_state => {
- content.begin_text();
- in_text_state = true;
- }
-
- Element::Geometry(..) | Element::Image(..) if in_text_state => {
- content.end_text();
- in_text_state = false;
- }
-
- _ => {}
- }
-
- let x = pos.x.to_pt() as f32;
- let y = (page.size.h - pos.y).to_pt() as f32;
-
- match *element {
- Element::Text(ref text) => {
- if fill != Some(text.fill) {
- write_fill(&mut content, text.fill);
- fill = Some(text.fill);
- }
-
- // Then, also check if we need to issue a font switching
- // action.
- if face_id != Some(text.face_id) || text.size != size {
- face_id = Some(text.face_id);
- size = text.size;
-
- let name = format_eco!("F{}", self.font_map.map(text.face_id));
- content.set_font(Name(name.as_bytes()), size.to_pt() as f32);
- }
-
- let face = self.fonts.get(text.face_id);
-
- // Position the text.
- content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]);
-
- let mut positioned = content.show_positioned();
- let mut items = positioned.items();
- let mut adjustment = Em::zero();
- let mut encoded = vec![];
-
- // Write the glyphs with kerning adjustments.
- for glyph in &text.glyphs {
- adjustment += glyph.x_offset;
-
- if !adjustment.is_zero() {
- if !encoded.is_empty() {
- items.show(Str(&encoded));
- encoded.clear();
- }
-
- items.adjust(-adjustment.to_pdf());
- adjustment = Em::zero();
- }
-
- encoded.push((glyph.id >> 8) as u8);
- encoded.push((glyph.id & 0xff) as u8);
-
- if let Some(advance) = face.advance(glyph.id) {
- adjustment += glyph.x_advance - advance;
- }
-
- adjustment -= glyph.x_offset;
- }
-
- if !encoded.is_empty() {
- items.show(Str(&encoded));
- }
- }
-
- Element::Geometry(ref geometry, paint) => {
- content.save_state();
-
- match *geometry {
- Geometry::Rect(Size { w, h }) => {
- let w = w.to_pt() as f32;
- let h = h.to_pt() as f32;
- if w > 0.0 && h > 0.0 {
- write_fill(&mut content, paint);
- content.rect(x, y - h, w, h);
- content.fill_nonzero();
- }
- }
- Geometry::Ellipse(size) => {
- let path = geom::Path::ellipse(size);
- write_fill(&mut content, paint);
- write_path(&mut content, x, y, &path);
- }
- Geometry::Line(target, thickness) => {
- write_stroke(&mut content, paint, thickness.to_pt() as f32);
- content.move_to(x, y);
- content.line_to(
- x + target.x.to_pt() as f32,
- y - target.y.to_pt() as f32,
- );
- content.stroke();
- }
- Geometry::Path(ref path) => {
- write_fill(&mut content, paint);
- write_path(&mut content, x, y, path)
- }
- }
-
- content.restore_state();
- }
-
- Element::Image(id, Size { w, h }) => {
- let name = format!("Im{}", self.image_map.map(id));
- let w = w.to_pt() as f32;
- let h = h.to_pt() as f32;
-
- content.save_state();
- content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
- content.x_object(Name(name.as_bytes()));
- content.restore_state();
- }
-
- Element::Link(_, _) => {}
- }
- }
-
- if in_text_state {
- content.end_text();
- }
-
- self.writer
- .stream(id, &deflate(&content.finish()))
- .filter(Filter::FlateDecode);
+ let writer = PageExporter::new(self);
+ let content = writer.write(page);
+ self.writer.stream(id, &deflate(&content)).filter(Filter::FlateDecode);
}
fn write_fonts(&mut self) {
@@ -350,7 +210,7 @@ impl<'a> PdfExporter<'a> {
let num_glyphs = ttf.number_of_glyphs();
(0 .. num_glyphs).map(|g| {
let x = ttf.glyph_hor_advance(GlyphId(g)).unwrap_or(0);
- face.to_em(x).to_pdf()
+ face.to_em(x).to_font_units()
})
});
@@ -363,16 +223,16 @@ impl<'a> PdfExporter<'a> {
let global_bbox = ttf.global_bounding_box();
let bbox = Rect::new(
- face.to_em(global_bbox.x_min).to_pdf(),
- face.to_em(global_bbox.y_min).to_pdf(),
- face.to_em(global_bbox.x_max).to_pdf(),
- face.to_em(global_bbox.y_max).to_pdf(),
+ face.to_em(global_bbox.x_min).to_font_units(),
+ face.to_em(global_bbox.y_min).to_font_units(),
+ face.to_em(global_bbox.x_max).to_font_units(),
+ face.to_em(global_bbox.y_max).to_font_units(),
);
let italic_angle = ttf.italic_angle().unwrap_or(0.0);
- let ascender = face.ascender.to_pdf();
- let descender = face.descender.to_pdf();
- let cap_height = face.cap_height.to_pdf();
+ let ascender = face.ascender.to_font_units();
+ let descender = face.descender.to_font_units();
+ let cap_height = face.cap_height.to_font_units();
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
// Write the font descriptor (contains metrics about the font).
@@ -474,45 +334,244 @@ impl<'a> PdfExporter<'a> {
}
}
-/// Write a fill change into a content stream.
-fn write_fill(content: &mut Content, fill: Paint) {
- let Paint::Color(Color::Rgba(c)) = fill;
- content.set_fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0);
+/// A writer for the contents of a single page.
+struct PageExporter<'a> {
+ fonts: &'a FontStore,
+ font_map: &'a Remapper,
+ image_map: &'a Remapper,
+ content: Content,
+ in_text_state: bool,
+ face_id: Option,
+ font_size: Length,
+ font_fill: Option,
}
-/// Write a stroke change into a content stream.
-fn write_stroke(content: &mut Content, stroke: Paint, thickness: f32) {
- match stroke {
- Paint::Color(Color::Rgba(c)) => {
- content.set_stroke_rgb(
- c.r as f32 / 255.0,
- c.g as f32 / 255.0,
- c.b as f32 / 255.0,
- );
+impl<'a> PageExporter<'a> {
+ /// Create a new page exporter.
+ fn new(exporter: &'a PdfExporter) -> Self {
+ Self {
+ fonts: exporter.fonts,
+ font_map: &exporter.font_map,
+ image_map: &exporter.image_map,
+ content: Content::new(),
+ in_text_state: false,
+ face_id: None,
+ font_size: Length::zero(),
+ font_fill: None,
}
}
- content.set_line_width(thickness);
-}
-/// Write a path into a content stream.
-fn write_path(content: &mut Content, x: f32, y: f32, path: &geom::Path) {
- let f = |length: Length| length.to_pt() as f32;
- for elem in &path.0 {
- match elem {
- geom::PathElement::MoveTo(p) => content.move_to(x + f(p.x), y + f(p.y)),
- geom::PathElement::LineTo(p) => content.line_to(x + f(p.x), y + f(p.y)),
- geom::PathElement::CubicTo(p1, p2, p3) => content.cubic_to(
- x + f(p1.x),
- y + f(p1.y),
- x + f(p2.x),
- y + f(p2.y),
- x + f(p3.x),
- y + f(p3.y),
- ),
- geom::PathElement::ClosePath => content.close_path(),
- };
+ /// Write the page frame into the content stream.
+ fn write(mut self, frame: &Frame) -> Vec {
+ self.write_frame(0.0, frame.size.h.to_f32(), frame);
+ self.content.finish()
+ }
+
+ /// Write a frame into the content stream.
+ fn write_frame(&mut self, x: f32, y: f32, frame: &Frame) {
+ if frame.clips {
+ let w = frame.size.w.to_f32();
+ let h = frame.size.h.to_f32();
+ self.content.save_state();
+ self.content.move_to(x, y);
+ self.content.line_to(x + w, y);
+ self.content.line_to(x + w, y - h);
+ self.content.line_to(x, y - h);
+ self.content.clip_nonzero();
+ self.content.end_path();
+ }
+
+ for (offset, element) in &frame.elements {
+ // Make sure the content stream is in the correct state.
+ match element {
+ Element::Text(_) if !self.in_text_state => {
+ self.content.begin_text();
+ self.in_text_state = true;
+ }
+
+ Element::Geometry(..) | Element::Image(..) if self.in_text_state => {
+ self.content.end_text();
+ self.in_text_state = false;
+ }
+
+ _ => {}
+ }
+
+ let x = x + offset.x.to_f32();
+ let y = y - offset.y.to_f32();
+
+ match *element {
+ Element::Text(ref text) => {
+ self.write_text(x, y, text);
+ }
+ Element::Geometry(ref geometry, paint) => {
+ self.write_geometry(x, y, geometry, paint);
+ }
+ Element::Image(id, size) => {
+ self.write_image(x, y, id, size);
+ }
+ Element::Link(_, _) => {}
+ Element::Frame(ref frame) => {
+ self.write_frame(x, y, frame);
+ }
+ }
+ }
+
+ if self.in_text_state {
+ self.content.end_text();
+ }
+
+ if frame.clips {
+ self.content.restore_state();
+ }
+ }
+
+ /// Write a glyph run into the content stream.
+ fn write_text(&mut self, x: f32, y: f32, text: &Text) {
+ if self.font_fill != Some(text.fill) {
+ self.write_fill(text.fill);
+ self.font_fill = Some(text.fill);
+ }
+
+ // Then, also check if we need to issue a font switching
+ // action.
+ if self.face_id != Some(text.face_id) || self.font_size != text.size {
+ self.face_id = Some(text.face_id);
+ self.font_size = text.size;
+
+ let name = format_eco!("F{}", self.font_map.map(text.face_id));
+ self.content.set_font(Name(name.as_bytes()), text.size.to_f32());
+ }
+
+ let face = self.fonts.get(text.face_id);
+
+ // Position the text.
+ self.content.set_text_matrix([1.0, 0.0, 0.0, 1.0, x, y]);
+
+ let mut positioned = self.content.show_positioned();
+ let mut items = positioned.items();
+ let mut adjustment = Em::zero();
+ let mut encoded = vec![];
+
+ // Write the glyphs with kerning adjustments.
+ for glyph in &text.glyphs {
+ adjustment += glyph.x_offset;
+
+ if !adjustment.is_zero() {
+ if !encoded.is_empty() {
+ items.show(Str(&encoded));
+ encoded.clear();
+ }
+
+ items.adjust(-adjustment.to_font_units());
+ adjustment = Em::zero();
+ }
+
+ encoded.push((glyph.id >> 8) as u8);
+ encoded.push((glyph.id & 0xff) as u8);
+
+ if let Some(advance) = face.advance(glyph.id) {
+ adjustment += glyph.x_advance - advance;
+ }
+
+ adjustment -= glyph.x_offset;
+ }
+
+ if !encoded.is_empty() {
+ items.show(Str(&encoded));
+ }
+ }
+
+ /// Write an image into the content stream.
+ fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
+ let name = format!("Im{}", self.image_map.map(id));
+ let w = size.w.to_f32();
+ let h = size.h.to_f32();
+
+ self.content.save_state();
+ self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]);
+ self.content.x_object(Name(name.as_bytes()));
+ self.content.restore_state();
+ }
+
+ /// Write a geometrical shape into the content stream.
+ fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) {
+ self.content.save_state();
+
+ match *geometry {
+ Geometry::Rect(Size { w, h }) => {
+ let w = w.to_f32();
+ let h = h.to_f32();
+ if w > 0.0 && h > 0.0 {
+ self.write_fill(paint);
+ self.content.rect(x, y - h, w, h);
+ self.content.fill_nonzero();
+ }
+ }
+ Geometry::Ellipse(size) => {
+ let path = geom::Path::ellipse(size);
+ self.write_fill(paint);
+ self.write_filled_path(x, y, &path);
+ }
+ Geometry::Line(target, thickness) => {
+ self.write_stroke(paint, thickness.to_f32());
+ self.content.move_to(x, y);
+ self.content.line_to(x + target.x.to_f32(), y - target.y.to_f32());
+ self.content.stroke();
+ }
+ Geometry::Path(ref path) => {
+ self.write_fill(paint);
+ self.write_filled_path(x, y, path)
+ }
+ }
+
+ self.content.restore_state();
+ }
+
+ /// Write and fill path into a content stream.
+ fn write_filled_path(&mut self, x: f32, y: f32, path: &geom::Path) {
+ for elem in &path.0 {
+ match elem {
+ geom::PathElement::MoveTo(p) => {
+ self.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
+ }
+ geom::PathElement::LineTo(p) => {
+ self.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
+ }
+ geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to(
+ x + p1.x.to_f32(),
+ y + p1.y.to_f32(),
+ x + p2.x.to_f32(),
+ y + p2.y.to_f32(),
+ x + p3.x.to_f32(),
+ y + p3.y.to_f32(),
+ ),
+ geom::PathElement::ClosePath => self.content.close_path(),
+ };
+ }
+ self.content.fill_nonzero();
+ }
+
+ /// Write a fill change into a content stream.
+ fn write_fill(&mut self, fill: Paint) {
+ let Paint::Color(Color::Rgba(c)) = fill;
+ self.content.set_fill_rgb(
+ c.r as f32 / 255.0,
+ c.g as f32 / 255.0,
+ c.b as f32 / 255.0,
+ );
+ }
+
+ /// Write a stroke change into a content stream.
+ fn write_stroke(&mut self, stroke: Paint, thickness: f32) {
+ let Paint::Color(Color::Rgba(c)) = stroke;
+ self.content.set_stroke_rgb(
+ c.r as f32 / 255.0,
+ c.g as f32 / 255.0,
+ c.b as f32 / 255.0,
+ );
+ self.content.set_line_width(thickness);
}
- content.fill_nonzero();
}
/// The compression level for the deflating.
@@ -693,14 +752,26 @@ where
}
}
+/// Additional methods for [`Length`].
+trait LengthExt {
+ /// Convert an em length to a number of points.
+ fn to_f32(self) -> f32;
+}
+
+impl LengthExt for Length {
+ fn to_f32(self) -> f32 {
+ self.to_pt() as f32
+ }
+}
+
/// Additional methods for [`Em`].
trait EmExt {
/// Convert an em length to a number of PDF font units.
- fn to_pdf(self) -> f32;
+ fn to_font_units(self) -> f32;
}
impl EmExt for Em {
- fn to_pdf(self) -> f32 {
+ fn to_font_units(self) -> f32 {
1000.0 * self.get() as f32
}
}
diff --git a/src/frame.rs b/src/frame.rs
index 68aa2e9cf..94234ae9c 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -16,8 +16,10 @@ pub struct Frame {
pub size: Size,
/// The baseline of the frame measured from the top.
pub baseline: Length,
+ /// Whether this frame should be a clipping boundary.
+ pub clips: bool,
/// The elements composing this layout.
- pub children: Vec<(Point, FrameChild)>,
+ pub elements: Vec<(Point, Element)>,
}
impl Frame {
@@ -25,37 +27,44 @@ impl Frame {
#[track_caller]
pub fn new(size: Size, baseline: Length) -> Self {
assert!(size.is_finite());
- Self { size, baseline, children: vec![] }
- }
-
- /// Add an element at a position in the foreground.
- pub fn push(&mut self, pos: Point, element: Element) {
- self.children.push((pos, FrameChild::Element(element)));
+ Self {
+ size,
+ baseline,
+ elements: vec![],
+ clips: false,
+ }
}
/// Add an element at a position in the background.
pub fn prepend(&mut self, pos: Point, element: Element) {
- self.children.insert(0, (pos, FrameChild::Element(element)));
+ self.elements.insert(0, (pos, element));
+ }
+
+ /// Add an element at a position in the foreground.
+ pub fn push(&mut self, pos: Point, element: Element) {
+ self.elements.push((pos, element));
}
/// Add a frame element.
pub fn push_frame(&mut self, pos: Point, subframe: Rc) {
- self.children.push((pos, FrameChild::Group(subframe)))
+ self.elements.push((pos, Element::Frame(subframe)))
}
/// Add all elements of another frame, placing them relative to the given
/// position.
pub fn merge_frame(&mut self, pos: Point, subframe: Self) {
- if pos == Point::zero() && self.children.is_empty() {
- self.children = subframe.children;
+ if subframe.clips {
+ self.push_frame(pos, Rc::new(subframe));
+ } else if pos == Point::zero() && self.elements.is_empty() {
+ self.elements = subframe.elements;
} else {
- for (subpos, child) in subframe.children {
- self.children.push((pos + subpos, child));
+ for (subpos, child) in subframe.elements {
+ self.elements.push((pos + subpos, child));
}
}
}
- /// An iterator over all elements in the frame and its children.
+ /// An iterator over all non-frame elements in this and nested frames.
pub fn elements(&self) -> Elements {
Elements { stack: vec![(0, Point::zero(), self)] }
}
@@ -63,7 +72,7 @@ impl Frame {
impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- struct Children<'a>(&'a [(Point, FrameChild)]);
+ struct Children<'a>(&'a [(Point, Element)]);
impl Debug for Children<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
@@ -74,30 +83,12 @@ impl Debug for Frame {
f.debug_struct("Frame")
.field("size", &self.size)
.field("baseline", &self.baseline)
- .field("children", &Children(&self.children))
+ .field("clips", &self.clips)
+ .field("children", &Children(&self.elements))
.finish()
}
}
-/// A frame can contain two different kinds of children: a leaf element or a
-/// nested frame.
-#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
-pub enum FrameChild {
- /// A leaf node in the frame tree.
- Element(Element),
- /// An interior group.
- Group(Rc),
-}
-
-impl Debug for FrameChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Element(element) => element.fmt(f),
- Self::Group(frame) => frame.fmt(f),
- }
- }
-}
-
/// An iterator over all elements in a frame, alongside with their positions.
pub struct Elements<'a> {
stack: Vec<(usize, Point, &'a Frame)>,
@@ -108,23 +99,21 @@ impl<'a> Iterator for Elements<'a> {
fn next(&mut self) -> Option {
let (cursor, offset, frame) = self.stack.last_mut()?;
- match frame.children.get(*cursor) {
- Some((pos, FrameChild::Group(f))) => {
+ if let Some((pos, e)) = frame.elements.get(*cursor) {
+ if let Element::Frame(f) = e {
let new_offset = *offset + *pos;
self.stack.push((0, new_offset, f.as_ref()));
self.next()
- }
- Some((pos, FrameChild::Element(e))) => {
+ } else {
*cursor += 1;
Some((*offset + *pos, e))
}
- None => {
- self.stack.pop();
- if let Some((cursor, _, _)) = self.stack.last_mut() {
- *cursor += 1;
- }
- self.next()
+ } else {
+ self.stack.pop();
+ if let Some((cursor, _, _)) = self.stack.last_mut() {
+ *cursor += 1;
}
+ self.next()
}
}
}
@@ -141,6 +130,8 @@ pub enum Element {
Image(ImageId, Size),
/// A link to an external resource.
Link(String, Size),
+ /// A subframe, which can be a clipping boundary.
+ Frame(Rc),
}
/// A run of shaped text.
diff --git a/src/geom/length.rs b/src/geom/length.rs
index de184e6ce..68261d5f3 100644
--- a/src/geom/length.rs
+++ b/src/geom/length.rs
@@ -127,11 +127,15 @@ impl Length {
self == other || (self - other).to_raw().abs() < 1e-6
}
- /// Perform a checked division by a number, returning `None` if the result
+ /// Perform a checked division by a number, returning zero if the result
/// is not finite.
- pub fn div_finite(self, number: f64) -> Option {
+ pub fn safe_div(self, number: f64) -> Self {
let result = self.to_raw() / number;
- result.is_finite().then(|| Self::raw(result))
+ if result.is_finite() {
+ Self::raw(result)
+ } else {
+ Self::zero()
+ }
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 7d2837de2..949fff642 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -16,8 +16,9 @@ use std::rc::Rc;
use crate::font::FontStore;
use crate::frame::Frame;
+use crate::geom::{Linear, Spec};
use crate::image::ImageStore;
-use crate::library::DocumentNode;
+use crate::library::{DocumentNode, SizedNode};
use crate::Context;
/// Layout a document node into a collection of frames.
@@ -93,6 +94,23 @@ pub struct PackedNode {
hash: u64,
}
+impl PackedNode {
+ /// Force a size for this node.
+ ///
+ /// If at least one of `width` and `height` is `Some`, this wraps the node
+ /// in a [`SizedNode`]. Otherwise, it returns the node unchanged.
+ pub fn sized(self, width: Option, height: Option) -> PackedNode {
+ if width.is_some() || height.is_some() {
+ Layout::pack(SizedNode {
+ sizing: Spec::new(width, height),
+ child: self,
+ })
+ } else {
+ self
+ }
+ }
+}
+
impl Layout for PackedNode {
fn layout(
&self,
diff --git a/src/library/deco.rs b/src/library/deco.rs
index 6ef5a97bc..2722fd687 100644
--- a/src/library/deco.rs
+++ b/src/library/deco.rs
@@ -102,9 +102,9 @@ pub enum LineKind {
impl LineDecoration {
/// Apply a line decoration to a all text elements in a frame.
pub fn apply(&self, ctx: &LayoutContext, frame: &mut Frame) {
- for i in 0 .. frame.children.len() {
- let (pos, child) = &frame.children[i];
- if let FrameChild::Element(Element::Text(text)) = child {
+ for i in 0 .. frame.elements.len() {
+ let (pos, child) = &frame.elements[i];
+ if let Element::Text(text) = child {
let face = ctx.fonts.get(text.face_id);
let metrics = match self.kind {
LineKind::Underline => face.underline,
diff --git a/src/library/grid.rs b/src/library/grid.rs
index c9accffb2..f247c7f45 100644
--- a/src/library/grid.rs
+++ b/src/library/grid.rs
@@ -167,7 +167,8 @@ impl<'a> GridLayouter<'a> {
cols.pop();
rows.pop();
- // We use the regions only for auto row measurement and constraints.
+ // We use the regions for auto row measurement. Since at that moment,
+ // columns are already sized, we can enable horizontal expansion.
let expand = regions.expand;
regions.expand = Spec::new(true, false);
diff --git a/src/library/image.rs b/src/library/image.rs
index f93d4b548..ea9a050c3 100644
--- a/src/library/image.rs
+++ b/src/library/image.rs
@@ -9,7 +9,9 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult {
let path = args.expect::>("path to image file")?;
let width = args.named("width")?;
let height = args.named("height")?;
+ let fit = args.named("fit")?.unwrap_or_default();
+ // Load the image.
let full = ctx.make_path(&path.v);
let id = ctx.images.load(&full).map_err(|err| {
Error::boxed(path.span, match err.kind() {
@@ -18,10 +20,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult {
})
})?;
- Ok(Value::Template(Template::from_inline(move |_| ImageNode {
- id,
- width,
- height,
+ Ok(Value::Template(Template::from_inline(move |_| {
+ ImageNode { id, fit }.pack().sized(width, height)
})))
}
@@ -30,10 +30,8 @@ pub fn image(ctx: &mut EvalContext, args: &mut Args) -> TypResult {
pub struct ImageNode {
/// The id of the image file.
pub id: ImageId,
- /// The fixed width, if any.
- pub width: Option,
- /// The fixed height, if any.
- pub height: Option,
+ /// How the image should adjust itself to a given area.
+ pub fit: ImageFit,
}
impl Layout for ImageNode {
@@ -42,36 +40,77 @@ impl Layout for ImageNode {
ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec>> {
+ let &Regions { current, expand, .. } = regions;
+
let img = ctx.images.get(self.id);
- let pixel_size = Spec::new(img.width() as f64, img.height() as f64);
- let pixel_ratio = pixel_size.x / pixel_size.y;
+ let pixel_w = img.width() as f64;
+ let pixel_h = img.height() as f64;
- let width = self.width.map(|w| w.resolve(regions.base.w));
- let height = self.height.map(|w| w.resolve(regions.base.h));
+ let region_ratio = current.w / current.h;
+ let pixel_ratio = pixel_w / pixel_h;
+ let wide = region_ratio < pixel_ratio;
- let mut cts = Constraints::new(regions.expand);
- cts.set_base_if_linear(regions.base, Spec::new(self.width, self.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) => {
- cts.exact.x = Some(regions.current.w);
- if regions.current.w.is_finite() {
- // Fit to width.
- Size::new(regions.current.w, regions.current.w / pixel_ratio)
- } else {
- // Unbounded width, we have to make up something,
- // so it is 1pt per pixel.
- pixel_size.map(Length::pt).to_size()
- }
- }
+ // The space into which the image will be placed according to its fit.
+ let canvas = if expand.x && expand.y {
+ current
+ } else if expand.x || (wide && current.w.is_finite()) {
+ Size::new(current.w, current.w.safe_div(pixel_ratio))
+ } else if current.h.is_finite() {
+ Size::new(current.h * pixel_ratio, current.h)
+ } else {
+ Size::new(Length::pt(pixel_w), Length::pt(pixel_h))
};
- let mut frame = Frame::new(size, size.h);
- frame.push(Point::zero(), Element::Image(self.id, size));
+ // The actual size of the fitted image.
+ let size = match self.fit {
+ ImageFit::Contain | ImageFit::Cover => {
+ if wide == (self.fit == ImageFit::Contain) {
+ Size::new(canvas.w, canvas.w / pixel_ratio)
+ } else {
+ Size::new(canvas.h * pixel_ratio, canvas.h)
+ }
+ }
+ ImageFit::Stretch => canvas,
+ };
+ // The position of the image so that it is centered in the canvas.
+ let mut frame = Frame::new(canvas, canvas.h);
+ frame.clips = self.fit == ImageFit::Cover;
+ frame.push(
+ Point::new((canvas.w - size.w) / 2.0, (canvas.h - size.h) / 2.0),
+ Element::Image(self.id, size),
+ );
+
+ let mut cts = Constraints::new(regions.expand);
+ cts.exact = regions.current.to_spec().map(Some);
vec![frame.constrain(cts)]
}
}
+
+/// How an image should adjust itself to a given area.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum ImageFit {
+ /// The image should be fully contained in the area.
+ Contain,
+ /// The image should completely cover the area.
+ Cover,
+ /// The image should be stretched so that it exactly fills the area.
+ Stretch,
+}
+
+castable! {
+ ImageFit,
+ Expected: "string",
+ Value::Str(string) => match string.as_str() {
+ "contain" => Self::Contain,
+ "cover" => Self::Cover,
+ "stretch" => Self::Stretch,
+ _ => Err(r#"expected "contain", "cover" or "stretch""#)?,
+ },
+}
+
+impl Default for ImageFit {
+ fn default() -> Self {
+ Self::Contain
+ }
+}
diff --git a/src/library/pad.rs b/src/library/pad.rs
index 1ec1b4a23..a1c8c6f90 100644
--- a/src/library/pad.rs
+++ b/src/library/pad.rs
@@ -46,9 +46,7 @@ impl Layout for PadNode {
frames.iter_mut().zip(regions.iter())
{
fn solve_axis(length: Length, padding: Linear) -> Length {
- (length + padding.abs)
- .div_finite(1.0 - padding.rel.get())
- .unwrap_or_default()
+ (length + padding.abs).safe_div(1.0 - padding.rel.get())
}
// Solve for the size `padded` that satisfies (approximately):
diff --git a/src/library/shape.rs b/src/library/shape.rs
index 407d5974d..dbd6eea71 100644
--- a/src/library/shape.rs
+++ b/src/library/shape.rs
@@ -1,7 +1,7 @@
use std::f64::consts::SQRT_2;
use super::prelude::*;
-use super::{PadNode, SizedNode};
+use super::PadNode;
use crate::util::RcExt;
/// `rect`: A rectangle with optional content.
@@ -65,20 +65,13 @@ fn shape_impl(
let fill = fill.unwrap_or(Color::Rgba(RgbaColor::gray(175)));
Value::Template(Template::from_inline(move |style| {
- let shape = Layout::pack(ShapeNode {
+ ShapeNode {
kind,
fill: Some(Paint::Color(fill)),
child: body.as_ref().map(|body| body.pack(style)),
- });
-
- if width.is_some() || height.is_some() {
- Layout::pack(SizedNode {
- sizing: Spec::new(width, height),
- child: shape,
- })
- } else {
- shape
}
+ .pack()
+ .sized(width, height)
}))
}
@@ -112,7 +105,7 @@ impl Layout for ShapeNode {
ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec>> {
- // Layout.
+ // Layout, either with or without child.
let mut frame = if let Some(child) = &self.child {
let mut node: &dyn Layout = child;
@@ -141,15 +134,18 @@ impl Layout for ShapeNode {
frames = node.layout(ctx, &pod);
}
- // Validate and set constraints.
- assert_eq!(frames.len(), 1);
+ // Extract the frame.
Rc::take(frames.into_iter().next().unwrap().item)
} else {
+ // When there's no child, fill the area if expansion is on,
+ // otherwise fall back to a default size.
let default = Length::pt(30.0);
let size = Size::new(
if regions.expand.x {
regions.current.w
} else {
+ // For rectangle and ellipse, the default shape is a bit
+ // wider than high.
match self.kind {
ShapeKind::Square | ShapeKind::Circle => default,
ShapeKind::Rect | ShapeKind::Ellipse => 1.5 * default,
@@ -161,7 +157,7 @@ impl Layout for ShapeNode {
Frame::new(size, size.h)
};
- // Add background shape if desired.
+ // Add background fill if desired.
if let Some(fill) = self.fill {
let (pos, geometry) = match self.kind {
ShapeKind::Square | ShapeKind::Rect => {
@@ -175,11 +171,10 @@ impl Layout for ShapeNode {
frame.prepend(pos, Element::Geometry(geometry, fill));
}
- // Generate tight constraints for now.
+ // Return tight constraints for now.
let mut cts = Constraints::new(regions.expand);
cts.exact = regions.current.to_spec().map(Some);
cts.base = regions.base.to_spec().map(Some);
-
vec![frame.constrain(cts)]
}
}
diff --git a/src/library/sized.rs b/src/library/sized.rs
index 686d79b90..6394b0f4e 100644
--- a/src/library/sized.rs
+++ b/src/library/sized.rs
@@ -6,12 +6,7 @@ pub fn box_(_: &mut EvalContext, args: &mut Args) -> TypResult {
let height = args.named("height")?;
let body: Template = args.find().unwrap_or_default();
Ok(Value::Template(Template::from_inline(move |style| {
- let child = body.pack(style);
- if width.is_some() || height.is_some() {
- Layout::pack(SizedNode { sizing: Spec::new(width, height), child })
- } else {
- child
- }
+ body.pack(style).sized(width, height)
})))
}
@@ -21,12 +16,7 @@ pub fn block(_: &mut EvalContext, args: &mut Args) -> TypResult {
let height = args.named("height")?;
let body: Template = args.find().unwrap_or_default();
Ok(Value::Template(Template::from_block(move |style| {
- let child = body.pack(style);
- if width.is_some() || height.is_some() {
- Layout::pack(SizedNode { sizing: Spec::new(width, height), child })
- } else {
- child
- }
+ body.pack(style).sized(width, height)
})))
}
diff --git a/src/library/transform.rs b/src/library/transform.rs
index 85d65703d..d0a276229 100644
--- a/src/library/transform.rs
+++ b/src/library/transform.rs
@@ -36,7 +36,7 @@ impl Layout for MoveNode {
self.offset.y.map(|y| y.resolve(base.h)).unwrap_or_default(),
);
- for (point, _) in &mut Rc::make_mut(frame).children {
+ for (point, _) in &mut Rc::make_mut(frame).elements {
*point += offset;
}
}
diff --git a/tests/ref/coma.png b/tests/ref/coma.png
index d4c6c3def..04356991b 100644
Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ
diff --git a/tests/ref/elements/circle.png b/tests/ref/elements/circle.png
index 2b938c89b..efc0b8ece 100644
Binary files a/tests/ref/elements/circle.png and b/tests/ref/elements/circle.png differ
diff --git a/tests/ref/elements/image.png b/tests/ref/elements/image.png
index 943d43b1e..2385d8200 100644
Binary files a/tests/ref/elements/image.png and b/tests/ref/elements/image.png differ
diff --git a/tests/ref/layout/grid-3.png b/tests/ref/layout/grid-3.png
index f4d450f8e..7ef5df73a 100644
Binary files a/tests/ref/layout/grid-3.png and b/tests/ref/layout/grid-3.png differ
diff --git a/tests/ref/layout/grid-5.png b/tests/ref/layout/grid-5.png
index 1b29e0bd0..51703b11a 100644
Binary files a/tests/ref/layout/grid-5.png and b/tests/ref/layout/grid-5.png differ
diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png
index 71c3f18af..75ad00d12 100644
Binary files a/tests/ref/layout/page.png and b/tests/ref/layout/page.png differ
diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png
index 78f7ed773..535632c4c 100644
Binary files a/tests/ref/layout/stack-1.png and b/tests/ref/layout/stack-1.png differ
diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png
index 3e503e653..470b57ece 100644
Binary files a/tests/ref/layout/stack-2.png and b/tests/ref/layout/stack-2.png differ
diff --git a/tests/ref/markup/enums.png b/tests/ref/markup/enums.png
index f1d3855bb..a201131f2 100644
Binary files a/tests/ref/markup/enums.png and b/tests/ref/markup/enums.png differ
diff --git a/tests/ref/markup/escape.png b/tests/ref/markup/escape.png
index 3f41a5164..41b8c4d6a 100644
Binary files a/tests/ref/markup/escape.png and b/tests/ref/markup/escape.png differ
diff --git a/tests/ref/markup/lists.png b/tests/ref/markup/lists.png
index fef578ae1..1405b95e6 100644
Binary files a/tests/ref/markup/lists.png and b/tests/ref/markup/lists.png differ
diff --git a/tests/ref/text/basic.png b/tests/ref/text/basic.png
index 1a5a13091..88d3059c5 100644
Binary files a/tests/ref/text/basic.png and b/tests/ref/text/basic.png differ
diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png
index 38abf0c59..43b77c87b 100644
Binary files a/tests/ref/text/links.png and b/tests/ref/text/links.png differ
diff --git a/tests/ref/text/whitespace.png b/tests/ref/text/whitespace.png
index f1421bfe3..7e79c1772 100644
Binary files a/tests/ref/text/whitespace.png and b/tests/ref/text/whitespace.png differ
diff --git a/tests/typ/elements/image.typ b/tests/typ/elements/image.typ
index c0e6a3efc..43f93baf8 100644
--- a/tests/typ/elements/image.typ
+++ b/tests/typ/elements/image.typ
@@ -5,30 +5,42 @@
// Load an RGBA PNG image.
#image("../../res/rhino.png")
-#pagebreak()
// Load an RGB JPEG image.
+#page(height: 60pt)
#image("../../res/tiger.jpg")
---
// Test configuring the size and fitting behaviour of images.
-// Set width explicitly.
-#image("../../res/rhino.png", width: 50pt)
-
-// Set height explicitly.
-#image("../../res/rhino.png", height: 50pt)
+// Set width and height explicitly.
+#image("../../res/rhino.png", width: 30pt)
+#image("../../res/rhino.png", height: 30pt)
// Set width and height explicitly and force stretching.
-#image("../../res/rhino.png", width: 25pt, height: 50pt)
+#image("../../res/tiger.jpg", width: 100%, height: 20pt, fit: "stretch")
// Make sure the bounding-box of the image is correct.
#align(bottom, right)
-#image("../../res/tiger.jpg", width: 60pt)
+#image("../../res/tiger.jpg", width: 40pt)
---
-// Does not fit to height of page.
+// Test all three fit modes.
+#page(height: 50pt, margins: 0pt)
+#grid(
+ columns: 3,
+ rows: 100%,
+ gutter: 3pt,
+ image("../../res/tiger.jpg", fit: "contain"),
+ image("../../res/tiger.jpg", fit: "cover"),
+ image("../../res/tiger.jpg", fit: "stretch"),
+)
+
+---
+// Does not fit to remaining height of page.
#page(height: 60pt)
+Stuff \
+Stuff
#image("../../res/rhino.png")
---
diff --git a/tests/typ/layout/grid-3.typ b/tests/typ/layout/grid-3.typ
index af9718750..82472e554 100644
--- a/tests/typ/layout/grid-3.typ
+++ b/tests/typ/layout/grid-3.typ
@@ -23,7 +23,7 @@
columns: 4 * (1fr,),
row-gutter: 10pt,
column-gutter: (0pt, 10%),
- image("../../res/rhino.png"),
+ align(top, image("../../res/rhino.png")),
align(right, rect(width: 100%, fill: eastern)[LoL]),
[rofl],
[\ A] * 3,
diff --git a/tests/typ/layout/stack-1.typ b/tests/typ/layout/stack-1.typ
index 42a0137dc..2fbe22e3d 100644
--- a/tests/typ/layout/stack-1.typ
+++ b/tests/typ/layout/stack-1.typ
@@ -17,7 +17,6 @@
#page(width: 50pt, margins: 0pt)
#stack(dir: btt, ..items)
-#pagebreak()
---
// Test spacing.
diff --git a/tests/typeset.rs b/tests/typeset.rs
index aa4d250bb..a287638a6 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -13,9 +13,7 @@ use typst::diag::Error;
use typst::eval::Value;
use typst::font::Face;
use typst::frame::{Element, Frame, Geometry, Text};
-use typst::geom::{
- self, Color, Length, Paint, PathElement, Point, RgbaColor, Sides, Size,
-};
+use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size};
use typst::image::Image;
use typst::layout::layout;
#[cfg(feature = "layout-cache")]
@@ -390,66 +388,96 @@ fn draw(ctx: &Context, frames: &[Rc], dpp: f32) -> sk::Pixmap {
let width = 2.0 * pad + frames.iter().map(|l| l.size.w).max().unwrap_or_default();
let height = pad + frames.iter().map(|l| l.size.h + pad).sum::();
- let pixel_width = (dpp * width.to_pt() as f32) as u32;
- let pixel_height = (dpp * height.to_pt() as f32) as u32;
- if pixel_width > 4000 || pixel_height > 4000 {
+ let pxw = (dpp * width.to_pt() as f32) as u32;
+ let pxh = (dpp * height.to_pt() as f32) as u32;
+ if pxw > 4000 || pxh > 4000 {
panic!(
"overlarge image: {} by {} ({:?} x {:?})",
- pixel_width, pixel_height, width, height,
+ pxw, pxh, width, height,
);
}
- let mut canvas = sk::Pixmap::new(pixel_width, pixel_height).unwrap();
- let ts = sk::Transform::from_scale(dpp, dpp);
+ let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
canvas.fill(sk::Color::BLACK);
- let mut origin = Point::splat(pad);
+ let mut mask = sk::ClipMask::new();
+ let rect = sk::Rect::from_xywh(0.0, 0.0, pxw as f32, pxh as f32).unwrap();
+ let path = sk::PathBuilder::from_rect(rect);
+ mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false);
+
+ let mut ts = sk::Transform::from_scale(dpp, dpp)
+ .pre_translate(pad.to_pt() as f32, pad.to_pt() as f32);
+
for frame in frames {
- let mut paint = sk::Paint::default();
- paint.set_color(sk::Color::WHITE);
- canvas.fill_rect(
- sk::Rect::from_xywh(
- origin.x.to_pt() as f32,
- origin.y.to_pt() as f32,
- frame.size.w.to_pt() as f32,
- frame.size.h.to_pt() as f32,
- )
- .unwrap(),
- &paint,
- ts,
- None,
- );
+ let mut background = sk::Paint::default();
+ background.set_color(sk::Color::WHITE);
- for (pos, element) in frame.elements() {
- let global = origin + pos;
- let x = global.x.to_pt() as f32;
- let y = global.y.to_pt() as f32;
- let ts = ts.pre_translate(x, y);
- match *element {
- Element::Text(ref text) => {
- draw_text(&mut canvas, ts, ctx.fonts.get(text.face_id), text);
- }
- Element::Geometry(ref geometry, paint) => {
- draw_geometry(&mut canvas, ts, geometry, paint);
- }
- Element::Image(id, size) => {
- draw_image(&mut canvas, ts, ctx.images.get(id), size);
- }
- Element::Link(_, s) => {
- let outline = Geometry::Rect(s);
- let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
- draw_geometry(&mut canvas, ts, &outline, paint);
- }
- }
- }
+ let w = frame.size.w.to_pt() as f32;
+ let h = frame.size.h.to_pt() as f32;
+ let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
+ canvas.fill_rect(rect, &background, ts, None);
- origin.y += frame.size.h + pad;
+ draw_frame(&mut canvas, ts, &mask, ctx, frame);
+ ts = ts.pre_translate(0.0, (frame.size.h + pad).to_pt() as f32);
}
canvas
}
-fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Text) {
+fn draw_frame(
+ canvas: &mut sk::Pixmap,
+ ts: sk::Transform,
+ mask: &sk::ClipMask,
+ ctx: &Context,
+ frame: &Frame,
+) {
+ let mut storage;
+ let mask = if frame.clips {
+ let w = frame.size.w.to_pt() as f32;
+ let h = frame.size.h.to_pt() as f32;
+ let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
+ let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap();
+ storage = mask.clone();
+ storage.intersect_path(&path, sk::FillRule::default(), false);
+ &storage
+ } else {
+ mask
+ };
+
+ for (pos, element) in &frame.elements {
+ let x = pos.x.to_pt() as f32;
+ let y = pos.y.to_pt() as f32;
+ let ts = ts.pre_translate(x, y);
+
+ match *element {
+ Element::Text(ref text) => {
+ draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text);
+ }
+ Element::Geometry(ref geometry, paint) => {
+ draw_geometry(canvas, ts, mask, geometry, paint);
+ }
+ Element::Image(id, size) => {
+ draw_image(canvas, ts, mask, ctx.images.get(id), size);
+ }
+ Element::Link(_, s) => {
+ let outline = Geometry::Rect(s);
+ let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40)));
+ draw_geometry(canvas, ts, mask, &outline, paint);
+ }
+ Element::Frame(ref frame) => {
+ draw_frame(canvas, ts, mask, ctx, frame);
+ }
+ }
+ }
+}
+
+fn draw_text(
+ canvas: &mut sk::Pixmap,
+ ts: sk::Transform,
+ mask: &sk::ClipMask,
+ face: &Face,
+ text: &Text,
+) {
let ttf = face.ttf();
let size = text.size.to_pt() as f32;
let units_per_em = ttf.units_per_em() as f32;
@@ -481,7 +509,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
if let Some(fill) = &node.fill {
let path = convert_usvg_path(&node.data);
let (paint, fill_rule) = convert_usvg_fill(fill);
- canvas.fill_path(&path, &paint, fill_rule, ts, None);
+ canvas.fill_path(&path, &paint, fill_rule, ts, Some(mask));
}
}
}
@@ -497,7 +525,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
let dx = (raster.x as f32) / (img.width() as f32) * size;
let dy = (raster.y as f32) / (img.height() as f32) * size;
let ts = ts.pre_translate(dx, -size - dy);
- draw_image(canvas, ts, &img, Size::new(w, h));
+ draw_image(canvas, ts, mask, &img, Size::new(w, h));
} else {
// Otherwise, draw normal outline.
let mut builder = WrappedPathBuilder(sk::PathBuilder::new());
@@ -507,7 +535,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
let path = builder.0.finish().unwrap();
let mut paint = convert_typst_paint(text.fill);
paint.anti_alias = true;
- canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, None);
+ canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask));
}
}
@@ -518,6 +546,7 @@ fn draw_text(canvas: &mut sk::Pixmap, ts: sk::Transform, face: &Face, text: &Tex
fn draw_geometry(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
+ mask: &sk::ClipMask,
geometry: &Geometry,
paint: Paint,
) {
@@ -529,11 +558,11 @@ fn draw_geometry(
let w = width.to_pt() as f32;
let h = height.to_pt() as f32;
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
- canvas.fill_rect(rect, &paint, ts, None);
+ canvas.fill_rect(rect, &paint, ts, Some(mask));
}
Geometry::Ellipse(size) => {
let path = convert_typst_path(&geom::Path::ellipse(size));
- canvas.fill_path(&path, &paint, rule, ts, None);
+ canvas.fill_path(&path, &paint, rule, ts, Some(mask));
}
Geometry::Line(target, thickness) => {
let path = {
@@ -544,16 +573,22 @@ fn draw_geometry(
let mut stroke = sk::Stroke::default();
stroke.width = thickness.to_pt() as f32;
- canvas.stroke_path(&path, &paint, &stroke, ts, None);
+ canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask));
}
Geometry::Path(ref path) => {
let path = convert_typst_path(path);
- canvas.fill_path(&path, &paint, rule, ts, None);
+ canvas.fill_path(&path, &paint, rule, ts, Some(mask));
}
};
}
-fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Size) {
+fn draw_image(
+ canvas: &mut sk::Pixmap,
+ ts: sk::Transform,
+ mask: &sk::ClipMask,
+ img: &Image,
+ size: Size,
+) {
let mut pixmap = sk::Pixmap::new(img.buf.width(), img.buf.height()).unwrap();
for ((_, _, src), dest) in img.buf.pixels().zip(pixmap.pixels_mut()) {
let Rgba([r, g, b, a]) = src;
@@ -575,7 +610,7 @@ fn draw_image(canvas: &mut sk::Pixmap, ts: sk::Transform, img: &Image, size: Siz
);
let rect = sk::Rect::from_xywh(0.0, 0.0, view_width, view_height).unwrap();
- canvas.fill_rect(rect, &paint, ts, None);
+ canvas.fill_rect(rect, &paint, ts, Some(mask));
}
fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> {