summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBirk Tjelmeland <web-github@birktj.no>2023-04-01 16:04:38 +0200
committerGitHub <noreply@github.com>2023-04-01 16:04:38 +0200
commited79ecbb44a3132f23e3bd4d797009b5c3282fe3 (patch)
tree23a3ed7dbe18b3abb7a73112ba9038bb0264a411
parentb2ba061fbb62478017c0a2e9ceced48710cdd291 (diff)
Add support for cliping content in `block` and `box` (#431)
-rw-r--r--library/src/layout/container.rs20
-rw-r--r--src/export/render.rs133
-rw-r--r--tests/ref/layout/clip.pngbin0 -> 44908 bytes
-rw-r--r--tests/ref/text/font.pngbin20402 -> 34487 bytes
-rw-r--r--tests/typ/layout/clip.typ42
5 files changed, 157 insertions, 38 deletions
diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs
index bac0cb7f..0f81a1df 100644
--- a/library/src/layout/container.rs
+++ b/library/src/layout/container.rs
@@ -88,6 +88,10 @@ pub struct BoxElem {
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
+ /// Whether to clip the content inside the box.
+ #[default(false)]
+ pub clip: bool,
+
/// The contents of the box.
#[positional]
pub body: Option<Content>,
@@ -133,6 +137,11 @@ impl Layout for BoxElem {
frame.set_baseline(frame.baseline() - shift);
}
+ // Clip the contents
+ if self.clip(styles) {
+ frame.clip();
+ }
+
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
@@ -296,6 +305,10 @@ pub struct BlockElem {
#[default(VElem::block_spacing(Em::new(1.2).into()))]
pub below: VElem,
+ /// Whether to clip the content inside the block.
+ #[default(false)]
+ pub clip: bool,
+
/// The contents of the block.
#[positional]
pub body: Option<Content>,
@@ -369,6 +382,13 @@ impl Layout for BlockElem {
body.layout(vt, styles, pod)?.into_frames()
};
+ // Clip the contents
+ if self.clip(styles) {
+ for frame in frames.iter_mut() {
+ frame.clip();
+ }
+ }
+
// Prepare fill and stroke.
let fill = self.fill(styles);
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
diff --git a/src/export/render.rs b/src/export/render.rs
index dc87f447..7dd78c5f 100644
--- a/src/export/render.rs
+++ b/src/export/render.rs
@@ -7,7 +7,7 @@ use image::imageops::FilterType;
use image::{GenericImageView, Rgba};
use tiny_skia as sk;
use ttf_parser::{GlyphId, OutlineBuilder};
-use usvg::FitTo;
+use usvg::{FitTo, NodeExt};
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::geom::{
@@ -134,7 +134,7 @@ fn render_text(
fn render_svg_glyph(
canvas: &mut sk::Pixmap,
ts: sk::Transform,
- _: Option<&sk::ClipMask>,
+ mask: Option<&sk::ClipMask>,
text: &TextItem,
id: GlyphId,
) -> Option<()> {
@@ -173,10 +173,41 @@ fn render_svg_glyph(
height = view_box.height() as f32;
}
- // FIXME: This doesn't respect the clipping mask.
let size = text.size.to_f32();
let ts = ts.pre_scale(size / width, size / height);
- resvg::render(&tree, FitTo::Original, ts, canvas.as_mut())
+
+ // Compute the space we need to draw our glyph.
+ // See https://github.com/RazrFalcon/resvg/issues/602 for why
+ // using the svg size is problematic here.
+ let mut bbox = usvg::Rect::new_bbox();
+ for node in tree.root().descendants() {
+ if let Some(rect) = node.calculate_bbox().and_then(|b| b.to_rect()) {
+ bbox = bbox.expand(rect);
+ }
+ }
+
+ let canvas_rect = usvg::ScreenRect::new(0, 0, canvas.width(), canvas.height())?;
+
+ // Compute the bbox after the transform is applied.
+ // We add a nice 5px border along the bounding box to
+ // be on the safe size. We also compute the intersection
+ // with the canvas rectangle
+ let svg_ts = usvg::Transform::new(
+ ts.sx.into(), ts.kx.into(),
+ ts.ky.into(), ts.sy.into(),
+ ts.tx.into(), ts.ty.into());
+ let bbox = bbox.transform(&svg_ts)?
+ .to_screen_rect();
+ let bbox = usvg::ScreenRect::new(bbox.left()-5, bbox.y()-5, bbox.width()+10, bbox.height()+10)?
+ .fit_to_rect(canvas_rect);
+
+ let mut pixmap = sk::Pixmap::new(bbox.width(), bbox.height())?;
+
+ // We offset our transform so that the pixmap starts at the edge of the bbox.
+ let ts = ts.post_translate(-bbox.left() as f32, -bbox.top() as f32);
+ resvg::render(&tree, FitTo::Original, ts, pixmap.as_mut())?;
+
+ canvas.draw_pixmap(bbox.left(), bbox.top(), pixmap.as_ref(), &sk::PixmapPaint::default(), sk::Transform::identity(), mask)
}
/// Render a bitmap glyph into the canvas.
@@ -239,45 +270,71 @@ fn render_outline_glyph(
// doesn't exist, yet.
let glyph = pixglyph::Glyph::load(text.font.ttf(), id)?;
let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem);
- let cw = canvas.width() as i32;
- let ch = canvas.height() as i32;
- let mw = bitmap.width as i32;
- let mh = bitmap.height as i32;
-
- // Determine the pixel bounding box that we actually need to draw.
- let left = bitmap.left;
- let right = left + mw;
- let top = bitmap.top;
- let bottom = top + mh;
-
- // Premultiply the text color.
- let Paint::Solid(color) = text.fill;
- let c = color.to_rgba();
- let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get();
-
- // Blend the glyph bitmap with the existing pixels on the canvas.
- // FIXME: This doesn't respect the clipping mask.
- let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
- for x in left.clamp(0, cw)..right.clamp(0, cw) {
- for y in top.clamp(0, ch)..bottom.clamp(0, ch) {
- let ai = ((y - top) * mw + (x - left)) as usize;
- let cov = bitmap.coverage[ai];
- if cov == 0 {
- continue;
- }
- let pi = (y * cw + x) as usize;
- if cov == 255 {
- pixels[pi] = color;
- continue;
+ // If we have a clip mask we first render to a pixmap that we then blend
+ // with our canvas
+ if mask.is_some() {
+ let mw = bitmap.width;
+ let mh = bitmap.height;
+
+ let Paint::Solid(color) = text.fill;
+ let c = color.to_rgba();
+
+ // Pad the pixmap with 1 pixel in each dimension so that we do
+ // not get any problem with floating point errors along ther border
+ let mut pixmap = sk::Pixmap::new(mw+2, mh+2)?;
+ for x in 0..mw {
+ for y in 0..mh {
+ let alpha = bitmap.coverage[(y * mw + x) as usize];
+ let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply();
+ pixmap.pixels_mut()[((y+1) * (mw+2) + (x+1)) as usize] = color;
}
+ }
+
+ let left = bitmap.left;
+ let top = bitmap.top;
+
+ canvas.draw_pixmap(left-1, top-1, pixmap.as_ref(), &sk::PixmapPaint::default(), sk::Transform::identity(), mask)
+ } else {
+ let cw = canvas.width() as i32;
+ let ch = canvas.height() as i32;
+ let mw = bitmap.width as i32;
+ let mh = bitmap.height as i32;
+
+ // Determine the pixel bounding box that we actually need to draw.
+ let left = bitmap.left;
+ let right = left + mw;
+ let top = bitmap.top;
+ let bottom = top + mh;
- let applied = alpha_mul(color, cov as u32);
- pixels[pi] = blend_src_over(applied, pixels[pi]);
+ // Premultiply the text color.
+ let Paint::Solid(color) = text.fill;
+ let c = color.to_rgba();
+ let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, 255).premultiply().get();
+
+ // Blend the glyph bitmap with the existing pixels on the canvas.
+ let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
+ for x in left.clamp(0, cw)..right.clamp(0, cw) {
+ for y in top.clamp(0, ch)..bottom.clamp(0, ch) {
+ let ai = ((y - top) * mw + (x - left)) as usize;
+ let cov = bitmap.coverage[ai];
+ if cov == 0 {
+ continue;
+ }
+
+ let pi = (y * cw + x) as usize;
+ if cov == 255 {
+ pixels[pi] = color;
+ continue;
+ }
+
+ let applied = alpha_mul(color, cov as u32);
+ pixels[pi] = blend_src_over(applied, pixels[pi]);
+ }
}
- }
- Some(())
+ Some(())
+ }
}
/// Render a geometrical shape into the canvas.
diff --git a/tests/ref/layout/clip.png b/tests/ref/layout/clip.png
new file mode 100644
index 00000000..f6ec0ba9
--- /dev/null
+++ b/tests/ref/layout/clip.png
Binary files differ
diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png
index 7cf8beb5..856d556b 100644
--- a/tests/ref/text/font.png
+++ b/tests/ref/text/font.png
Binary files differ
diff --git a/tests/typ/layout/clip.typ b/tests/typ/layout/clip.typ
new file mode 100644
index 00000000..3baa8b80
--- /dev/null
+++ b/tests/typ/layout/clip.typ
@@ -0,0 +1,42 @@
+// Test clipping with the `box` and `block` containers.
+
+---
+// Test box clipping with a rectangle
+Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]
+world 1
+
+Space
+
+Hello #box(width: 1em, height: 1em, clip: true)[#rect(width: 3em, height: 3em, fill: red)]
+world 2
+
+---
+// Test cliping text
+#block(width: 5em, height: 2em, clip: false, stroke: 1pt + black)[
+ But, soft! what light through
+]
+
+#v(2em)
+
+#block(width: 5em, height: 2em, clip: true, stroke: 1pt + black)[
+ But, soft! what light through yonder window breaks? It is the east, and Juliet
+ is the sun.
+]
+
+---
+// Test cliping svg glyphs
+Emoji: #box(height: 0.5em, stroke: 1pt + black)[🐪, 🌋, 🏞]
+
+Emoji: #box(height: 0.5em, clip: true, stroke: 1pt + black)[🐪, 🌋, 🏞]
+
+---
+// Test block clipping over multiple pages.
+
+#set page(height: 60pt)
+
+First!
+
+#block(height: 4em, clip: true, stroke: 1pt + black)[
+ But, soft! what light through yonder window breaks? It is the east, and Juliet
+ is the sun.
+]