Add support for cliping content in block and box (#431)

This commit is contained in:
Birk Tjelmeland 2023-04-01 16:04:38 +02:00 committed by GitHub
parent b2ba061fbb
commit ed79ecbb44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 37 deletions

View File

@ -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));

View File

@ -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;
// 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;
// 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();
let Paint::Solid(color) = text.fill;
let c = color.to_rgba();
// 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;
// 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 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(())
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;
// 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(())
}
}
/// Render a geometrical shape into the canvas.

BIN
tests/ref/layout/clip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 34 KiB

42
tests/typ/layout/clip.typ Normal file
View File

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