mirror of
https://github.com/typst/typst
synced 2025-06-28 00:03:17 +08:00
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]
|
#[fold]
|
||||||
pub outset: Sides<Option<Rel<Length>>>,
|
pub outset: Sides<Option<Rel<Length>>>,
|
||||||
|
|
||||||
|
/// Whether to clip the content inside the box.
|
||||||
|
#[default(false)]
|
||||||
|
pub clip: bool,
|
||||||
|
|
||||||
/// The contents of the box.
|
/// The contents of the box.
|
||||||
#[positional]
|
#[positional]
|
||||||
pub body: Option<Content>,
|
pub body: Option<Content>,
|
||||||
@ -133,6 +137,11 @@ impl Layout for BoxElem {
|
|||||||
frame.set_baseline(frame.baseline() - shift);
|
frame.set_baseline(frame.baseline() - shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clip the contents
|
||||||
|
if self.clip(styles) {
|
||||||
|
frame.clip();
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare fill and stroke.
|
// Prepare fill and stroke.
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
|
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()))]
|
#[default(VElem::block_spacing(Em::new(1.2).into()))]
|
||||||
pub below: VElem,
|
pub below: VElem,
|
||||||
|
|
||||||
|
/// Whether to clip the content inside the block.
|
||||||
|
#[default(false)]
|
||||||
|
pub clip: bool,
|
||||||
|
|
||||||
/// The contents of the block.
|
/// The contents of the block.
|
||||||
#[positional]
|
#[positional]
|
||||||
pub body: Option<Content>,
|
pub body: Option<Content>,
|
||||||
@ -369,6 +382,13 @@ impl Layout for BlockElem {
|
|||||||
body.layout(vt, styles, pod)?.into_frames()
|
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.
|
// Prepare fill and stroke.
|
||||||
let fill = self.fill(styles);
|
let fill = self.fill(styles);
|
||||||
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
|
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 image::{GenericImageView, Rgba};
|
||||||
use tiny_skia as sk;
|
use tiny_skia as sk;
|
||||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
use usvg::FitTo;
|
use usvg::{FitTo, NodeExt};
|
||||||
|
|
||||||
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
|
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
|
||||||
use crate::geom::{
|
use crate::geom::{
|
||||||
@ -134,7 +134,7 @@ fn render_text(
|
|||||||
fn render_svg_glyph(
|
fn render_svg_glyph(
|
||||||
canvas: &mut sk::Pixmap,
|
canvas: &mut sk::Pixmap,
|
||||||
ts: sk::Transform,
|
ts: sk::Transform,
|
||||||
_: Option<&sk::ClipMask>,
|
mask: Option<&sk::ClipMask>,
|
||||||
text: &TextItem,
|
text: &TextItem,
|
||||||
id: GlyphId,
|
id: GlyphId,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
@ -173,10 +173,41 @@ fn render_svg_glyph(
|
|||||||
height = view_box.height() as f32;
|
height = view_box.height() as f32;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This doesn't respect the clipping mask.
|
|
||||||
let size = text.size.to_f32();
|
let size = text.size.to_f32();
|
||||||
let ts = ts.pre_scale(size / width, size / height);
|
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.
|
/// Render a bitmap glyph into the canvas.
|
||||||
@ -239,45 +270,71 @@ fn render_outline_glyph(
|
|||||||
// doesn't exist, yet.
|
// doesn't exist, yet.
|
||||||
let glyph = pixglyph::Glyph::load(text.font.ttf(), id)?;
|
let glyph = pixglyph::Glyph::load(text.font.ttf(), id)?;
|
||||||
let bitmap = glyph.rasterize(ts.tx, ts.ty, ppem);
|
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.
|
// If we have a clip mask we first render to a pixmap that we then blend
|
||||||
let left = bitmap.left;
|
// with our canvas
|
||||||
let right = left + mw;
|
if mask.is_some() {
|
||||||
let top = bitmap.top;
|
let mw = bitmap.width;
|
||||||
let bottom = top + mh;
|
let mh = bitmap.height;
|
||||||
|
|
||||||
// Premultiply the text color.
|
let Paint::Solid(color) = text.fill;
|
||||||
let Paint::Solid(color) = text.fill;
|
let c = color.to_rgba();
|
||||||
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.
|
// Pad the pixmap with 1 pixel in each dimension so that we do
|
||||||
// FIXME: This doesn't respect the clipping mask.
|
// not get any problem with floating point errors along ther border
|
||||||
let pixels = bytemuck::cast_slice_mut::<u8, u32>(canvas.data_mut());
|
let mut pixmap = sk::Pixmap::new(mw+2, mh+2)?;
|
||||||
for x in left.clamp(0, cw)..right.clamp(0, cw) {
|
for x in 0..mw {
|
||||||
for y in top.clamp(0, ch)..bottom.clamp(0, ch) {
|
for y in 0..mh {
|
||||||
let ai = ((y - top) * mw + (x - left)) as usize;
|
let alpha = bitmap.coverage[(y * mw + x) as usize];
|
||||||
let cov = bitmap.coverage[ai];
|
let color = sk::ColorU8::from_rgba(c.r, c.g, c.b, alpha).premultiply();
|
||||||
if cov == 0 {
|
pixmap.pixels_mut()[((y+1) * (mw+2) + (x+1)) as usize] = color;
|
||||||
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(())
|
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.
|
/// 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…
x
Reference in New Issue
Block a user