Add support for cliping content in block
and box
(#431)
This commit is contained in:
parent
b2ba061fbb
commit
ed79ecbb44
@ -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));
|
||||
|
@ -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
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
42
tests/typ/layout/clip.typ
Normal 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.
|
||||
]
|
Loading…
Reference in New Issue
Block a user