diff --git a/src/export/pdf.rs b/src/export/pdf.rs index b807d0598..b12ee7716 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, Shape, Stroke, Text}; +use crate::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use crate::geom::{self, Color, Em, Length, Paint, Size}; use crate::image::{Image, ImageId, ImageStore}; use crate::Context; @@ -369,18 +369,6 @@ impl<'a> PageExporter<'a> { /// 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 { @@ -401,10 +389,10 @@ impl<'a> PageExporter<'a> { let y = y - offset.y.to_f32(); match *element { + Element::Group(ref group) => self.write_group(x, y, group), Element::Text(ref text) => self.write_text(x, y, text), Element::Shape(ref shape) => self.write_shape(x, y, shape), Element::Image(id, size) => self.write_image(x, y, id, size), - Element::Frame(ref frame) => self.write_frame(x, y, frame), Element::Link(_, _) => {} } } @@ -412,8 +400,24 @@ impl<'a> PageExporter<'a> { if self.in_text_state { self.content.end_text(); } + } - if frame.clips { + fn write_group(&mut self, x: f32, y: f32, group: &Group) { + if group.clips { + let w = group.frame.size.w.to_f32(); + let h = group.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(); + } + + self.write_frame(x, y, &group.frame); + + if group.clips { self.content.restore_state(); } } diff --git a/src/frame.rs b/src/frame.rs index 9f1b1c284..7862e0054 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -16,8 +16,6 @@ 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 elements: Vec<(Point, Element)>, } @@ -27,12 +25,7 @@ impl Frame { #[track_caller] pub fn new(size: Size, baseline: Length) -> Self { assert!(size.is_finite()); - Self { - size, - baseline, - elements: vec![], - clips: false, - } + Self { size, baseline, elements: vec![] } } /// Add an element at a position in the background. @@ -45,17 +38,16 @@ impl Frame { self.elements.push((pos, element)); } - /// Add a frame element. - pub fn push_frame(&mut self, pos: Point, subframe: Rc) { - self.elements.push((pos, Element::Frame(subframe))) + /// Add a group element. + pub fn push_frame(&mut self, pos: Point, frame: Rc) { + self.elements + .push((pos, Element::Group(Group { frame, clips: false }))) } /// Add all elements of another frame, placing them relative to the given /// position. pub fn merge_frame(&mut self, pos: Point, subframe: Self) { - if subframe.clips { - self.push_frame(pos, Rc::new(subframe)); - } else if pos == Point::zero() && self.elements.is_empty() { + if pos == Point::zero() && self.elements.is_empty() { self.elements = subframe.elements; } else { for (subpos, child) in subframe.elements { @@ -90,7 +82,6 @@ impl Debug for Frame { f.debug_struct("Frame") .field("size", &self.size) .field("baseline", &self.baseline) - .field("clips", &self.clips) .field("children", &Children(&self.elements)) .finish() } @@ -107,9 +98,9 @@ impl<'a> Iterator for Elements<'a> { fn next(&mut self) -> Option { let (cursor, offset, frame) = self.stack.last_mut()?; if let Some((pos, e)) = frame.elements.get(*cursor) { - if let Element::Frame(f) = e { + if let Element::Group(g) = e { let new_offset = *offset + *pos; - self.stack.push((0, new_offset, f.as_ref())); + self.stack.push((0, new_offset, g.frame.as_ref())); self.next() } else { *cursor += 1; @@ -128,6 +119,8 @@ impl<'a> Iterator for Elements<'a> { /// The building block frames are composed of. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum Element { + /// A group of elements. + Group(Group), /// A run of shaped text. Text(Text), /// A geometric shape with optional fill and stroke. @@ -136,8 +129,15 @@ pub enum Element { Image(ImageId, Size), /// A link to an external resource and its trigger region. Link(String, Size), - /// A subframe, which can be a clipping boundary. - Frame(Rc), +} + +/// A group of elements with optional clipping. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Group { + /// The group's frame. + pub frame: Rc, + /// Whether the frame should be a clipping boundary. + pub clips: bool, } /// A run of shaped text. diff --git a/src/library/image.rs b/src/library/image.rs index 8b85a53fd..b0d66a636 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -75,12 +75,22 @@ impl Layout for ImageNode { // 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( (canvas - size).to_point() / 2.0, Element::Image(self.id, size), ); + // Create a clipping group if the image mode is `cover`. + if self.fit == ImageFit::Cover { + let group = Group { + frame: Rc::new(frame), + clips: self.fit == ImageFit::Cover, + }; + + frame = Frame::new(canvas, canvas.h); + frame.push(Point::zero(), Element::Group(group)); + } + let mut cts = Constraints::new(regions.expand); cts.exact = regions.current.to_spec().map(Some); vec![frame.constrain(cts)] diff --git a/tests/ref/layout/background.png b/tests/ref/layout/background.png new file mode 100644 index 000000000..805139f54 Binary files /dev/null and b/tests/ref/layout/background.png differ diff --git a/tests/typ/layout/background.typ b/tests/typ/layout/background.typ new file mode 100644 index 000000000..81c8c99dc --- /dev/null +++ b/tests/typ/layout/background.typ @@ -0,0 +1,18 @@ +// Test placing a background image on a page. + +--- +#page(paper: "a10", flip: true) +#font(fill: white) +#place( + dx: -10pt, + dy: -10pt, + image( + "../../res/tiger.jpg", + fit: "cover", + width: 100% + 20pt, + height: 100% + 20pt, + ) +) +#align(bottom, right)[ + _Welcome to_ #underline[*Tigerland*] +] diff --git a/tests/typeset.rs b/tests/typeset.rs index 6a21cf7b4..6f85125ef 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -12,7 +12,7 @@ use walkdir::WalkDir; use typst::diag::Error; use typst::eval::{Smart, Value}; use typst::font::Face; -use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text}; +use typst::frame::{Element, Frame, Geometry, Group, Shape, Stroke, Text}; use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size}; use typst::image::Image; use typst::layout::layout; @@ -431,30 +431,15 @@ fn draw_frame( ctx: &Context, frame: &Frame, ) { - let mut storage; - let mask = if frame.clips { - let w = frame.size.w.to_f32(); - let h = frame.size.h.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); - let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap(); - let rule = sk::FillRule::default(); - storage = mask.clone(); - if storage.intersect_path(&path, rule, false).is_none() { - // Fails if clipping rect is empty. In that case we just clip everything - // by returning. - return; - } - &storage - } else { - mask - }; - for (pos, element) in &frame.elements { let x = pos.x.to_f32(); let y = pos.y.to_f32(); let ts = ts.pre_translate(x, y); match *element { + Element::Group(ref group) => { + draw_group(canvas, ts, mask, ctx, group); + } Element::Text(ref text) => { draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text); } @@ -469,13 +454,35 @@ fn draw_frame( let shape = Shape::filled(Geometry::Rect(s), fill); draw_shape(canvas, ts, mask, &shape); } - Element::Frame(ref frame) => { - draw_frame(canvas, ts, mask, ctx, frame); - } } } } +fn draw_group( + canvas: &mut sk::Pixmap, + ts: sk::Transform, + mask: &sk::ClipMask, + ctx: &Context, + group: &Group, +) { + if group.clips { + let w = group.frame.size.w.to_f32(); + let h = group.frame.size.h.to_f32(); + let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); + let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap(); + let rule = sk::FillRule::default(); + let mut mask = mask.clone(); + if mask.intersect_path(&path, rule, false).is_none() { + // Fails if clipping rect is empty. In that case we just clip everything + // by returning. + return; + } + draw_frame(canvas, ts, &mask, ctx, &group.frame); + } else { + draw_frame(canvas, ts, mask, ctx, &group.frame); + }; +} + fn draw_text( canvas: &mut sk::Pixmap, ts: sk::Transform,