summaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/typst-library/src/layout/container.rs26
-rw-r--r--crates/typst-library/src/visualize/image.rs4
-rw-r--r--crates/typst/src/doc.rs16
-rw-r--r--crates/typst/src/export/pdf/page.rs10
-rw-r--r--crates/typst/src/export/render.rs10
-rw-r--r--crates/typst/src/export/svg.rs60
-rw-r--r--crates/typst/src/geom/mod.rs2
-rw-r--r--crates/typst/src/geom/rect.rs164
-rw-r--r--crates/typst/src/geom/sides.rs10
9 files changed, 191 insertions, 111 deletions
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
index 28a56103..36b62864 100644
--- a/crates/typst-library/src/layout/container.rs
+++ b/crates/typst-library/src/layout/container.rs
@@ -146,15 +146,18 @@ 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(Stroke::unwrap_or_default));
+ // Clip the contents
+ if self.clip(styles) {
+ let outset = self.outset(styles).relative_to(frame.size());
+ let size = frame.size() + outset.sum_by_axis();
+ let radius = self.radius(styles);
+ frame.clip(path_rect(size, radius, &stroke));
+ }
+
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
let outset = self.outset(styles);
@@ -408,17 +411,20 @@ impl Layout for BlockElem {
frames
};
+ // Prepare fill and stroke.
+ let fill = self.fill(styles);
+ let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
+
// Clip the contents
if self.clip(styles) {
for frame in frames.iter_mut() {
- frame.clip();
+ let outset = self.outset(styles).relative_to(frame.size());
+ let size = frame.size() + outset.sum_by_axis();
+ let radius = self.radius(styles);
+ frame.clip(path_rect(size, radius, &stroke));
}
}
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
-
// Add fill and/or stroke.
if fill.is_some() || stroke.iter().any(Option::is_some) {
let mut skip = false;
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index b0c9d8ea..05a9c352 100644
--- a/crates/typst-library/src/visualize/image.rs
+++ b/crates/typst-library/src/visualize/image.rs
@@ -1,7 +1,7 @@
use std::ffi::OsStr;
use std::path::Path;
-use typst::geom::Smart;
+use typst::geom::{self, Smart};
use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use typst::util::option_eq;
@@ -212,7 +212,7 @@ impl Layout for ImageElem {
// Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) {
- frame.clip();
+ frame.clip(geom::Path::rect(frame.size()));
}
// Apply metadata.
diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs
index dd206044..6fddc281 100644
--- a/crates/typst/src/doc.rs
+++ b/crates/typst/src/doc.rs
@@ -13,7 +13,7 @@ use crate::export::PdfPageLabel;
use crate::font::Font;
use crate::geom::{
self, styled_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
- Geometry, Length, Numeric, Paint, Point, Rel, Shape, Sides, Size, Transform,
+ Geometry, Length, Numeric, Paint, Path, Point, Rel, Shape, Sides, Size, Transform,
};
use crate::image::Image;
use crate::model::{Content, Location, MetaElem, StyleChain};
@@ -351,10 +351,14 @@ impl Frame {
}
}
- /// Clip the contents of a frame to its size.
- pub fn clip(&mut self) {
+ /// Clip the contents of a frame to a clip path.
+ ///
+ /// The clip path can be the size of the frame in the case of a
+ /// rectangular frame. In the case of a frame with rounded corner,
+ /// this should be a path that matches the frame's outline.
+ pub fn clip(&mut self, clip_path: Path) {
if !self.is_empty() {
- self.group(|g| g.clips = true);
+ self.group(|g| g.clip_path = Some(clip_path));
}
}
@@ -505,7 +509,7 @@ pub struct GroupItem {
/// A transformation to apply to the group.
pub transform: Transform,
/// Whether the frame should be a clipping boundary.
- pub clips: bool,
+ pub clip_path: Option<Path>,
}
impl GroupItem {
@@ -514,7 +518,7 @@ impl GroupItem {
Self {
frame,
transform: Transform::identity(),
- clips: false,
+ clip_path: None,
}
}
}
diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs
index 412d753d..04470aad 100644
--- a/crates/typst/src/export/pdf/page.rs
+++ b/crates/typst/src/export/pdf/page.rs
@@ -462,14 +462,8 @@ fn write_group(ctx: &mut PageContext, pos: Point, group: &GroupItem) {
ctx.size(group.frame.size());
}
- if group.clips {
- let size = group.frame.size();
- let w = size.x.to_f32();
- let h = size.y.to_f32();
- ctx.content.move_to(0.0, 0.0);
- ctx.content.line_to(w, 0.0);
- ctx.content.line_to(w, h);
- ctx.content.line_to(0.0, h);
+ if let Some(clip_path) = &group.clip_path {
+ write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero();
ctx.content.end_path();
}
diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs
index 6fd47387..090c9756 100644
--- a/crates/typst/src/export/render.rs
+++ b/crates/typst/src/export/render.rs
@@ -183,13 +183,9 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, group: &GroupItem) {
let mut mask = state.mask;
let storage;
- if group.clips {
- let size: geom::Axes<Abs> = group.frame.size();
- let w = size.x.to_f32();
- let h = size.y.to_f32();
- if let Some(path) = sk::Rect::from_xywh(0.0, 0.0, w, h)
- .map(sk::PathBuilder::from_rect)
- .and_then(|path| path.transform(state.transform))
+ if let Some(clip_path) = group.clip_path.as_ref() {
+ if let Some(path) =
+ convert_path(clip_path).and_then(|path| path.transform(state.transform))
{
if let Some(mask) = mask {
let mut mask = mask.clone();
diff --git a/crates/typst/src/export/svg.rs b/crates/typst/src/export/svg.rs
index 25cb7519..6399f77d 100644
--- a/crates/typst/src/export/svg.rs
+++ b/crates/typst/src/export/svg.rs
@@ -12,8 +12,9 @@ use crate::doc::{Frame, FrameItem, FrameKind, GroupItem, TextItem};
use crate::eval::Repr;
use crate::font::Font;
use crate::geom::{
- Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin, Paint,
- PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size, Transform,
+ self, Abs, Angle, Axes, Color, FixedStroke, Geometry, Gradient, LineCap, LineJoin,
+ Paint, PathItem, Point, Quadrant, Ratio, RatioOrAngle, Relative, Shape, Size,
+ Transform,
};
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::util::hash128;
@@ -269,16 +270,9 @@ impl SVGRenderer {
self.xml.start_element("g");
self.xml.write_attribute("class", "typst-group");
- if group.clips {
+ if let Some(clip_path) = &group.clip_path {
let hash = hash128(&group);
- let size = group.frame.size();
- let x = size.x.to_pt();
- let y = size.y.to_pt();
- let id = self.clip_paths.insert_with(hash, || {
- let mut builder = SvgPathBuilder(EcoString::new());
- builder.rect(x as f32, y as f32);
- builder.0
- });
+ let id = self.clip_paths.insert_with(hash, || convert_path(clip_path));
self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})"));
}
@@ -1014,28 +1008,32 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString {
let y = rect.y.to_pt() as f32;
builder.rect(x, y);
}
- Geometry::Path(p) => {
- for item in &p.0 {
- match item {
- PathItem::MoveTo(m) => {
- builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
- }
- PathItem::LineTo(l) => {
- builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32)
- }
- PathItem::CubicTo(c1, c2, t) => builder.curve_to(
- c1.x.to_pt() as f32,
- c1.y.to_pt() as f32,
- c2.x.to_pt() as f32,
- c2.y.to_pt() as f32,
- t.x.to_pt() as f32,
- t.y.to_pt() as f32,
- ),
- PathItem::ClosePath => builder.close(),
- }
+ Geometry::Path(p) => return convert_path(p),
+ };
+ builder.0
+}
+
+fn convert_path(path: &geom::Path) -> EcoString {
+ let mut builder = SvgPathBuilder::default();
+ for item in &path.0 {
+ match item {
+ PathItem::MoveTo(m) => {
+ builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32)
+ }
+ PathItem::LineTo(l) => {
+ builder.line_to(l.x.to_pt() as f32, l.y.to_pt() as f32)
}
+ PathItem::CubicTo(c1, c2, t) => builder.curve_to(
+ c1.x.to_pt() as f32,
+ c1.y.to_pt() as f32,
+ c2.x.to_pt() as f32,
+ c2.y.to_pt() as f32,
+ t.x.to_pt() as f32,
+ t.y.to_pt() as f32,
+ ),
+ PathItem::ClosePath => builder.close(),
}
- };
+ }
builder.0
}
diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs
index 105ee5a3..8ad6cea0 100644
--- a/crates/typst/src/geom/mod.rs
+++ b/crates/typst/src/geom/mod.rs
@@ -46,7 +46,7 @@ pub use self::paint::Paint;
pub use self::path::{Path, PathItem};
pub use self::point::Point;
pub use self::ratio::Ratio;
-pub use self::rect::styled_rect;
+pub use self::rect::{path_rect, styled_rect};
pub use self::rel::Rel;
pub use self::scalar::Scalar;
pub use self::shape::{Geometry, Shape};
diff --git a/crates/typst/src/geom/rect.rs b/crates/typst/src/geom/rect.rs
index 108f5add..37b94527 100644
--- a/crates/typst/src/geom/rect.rs
+++ b/crates/typst/src/geom/rect.rs
@@ -42,6 +42,19 @@ impl PathExtension for Path {
}
}
+/// Creates a new rectangle as a path.
+pub fn path_rect(
+ size: Size,
+ radius: Corners<Rel<Abs>>,
+ stroke: &Sides<Option<FixedStroke>>,
+) -> Path {
+ if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
+ Path::rect(size)
+ } else {
+ segmented_path_rect(size, radius, stroke)
+ }
+}
+
/// Create a styled rectangle with shapes.
/// - use rect primitive for simple rectangles
/// - stroke sides if possible
@@ -68,24 +81,13 @@ fn simple_rect(
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
}
-/// Use stroke and fill for the rectangle
-fn segmented_rect(
+fn corners_control_points(
size: Size,
- radius: Corners<Rel<Abs>>,
- fill: Option<Paint>,
- strokes: Sides<Option<FixedStroke>>,
-) -> Vec<Shape> {
- let mut res = vec![];
- let stroke_widths = strokes
- .clone()
- .map(|s| s.map(|s| s.thickness / 2.0).unwrap_or(Abs::zero()));
-
- let max_radius = (size.x.min(size.y)) / 2.0
- + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
-
- let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
-
- let corners = Corners {
+ radius: Corners<Abs>,
+ strokes: &Sides<Option<FixedStroke>>,
+ stroke_widths: Sides<Abs>,
+) -> Corners<ControlPoints> {
+ Corners {
top_left: Corner::TopLeft,
top_right: Corner::TopRight,
bottom_right: Corner::BottomRight,
@@ -105,7 +107,67 @@ fn segmented_rect(
(None, None) => true,
_ => false,
},
- });
+ })
+}
+
+fn segmented_path_rect(
+ size: Size,
+ radius: Corners<Rel<Abs>>,
+ strokes: &Sides<Option<FixedStroke>>,
+) -> Path {
+ let stroke_widths = strokes
+ .as_ref()
+ .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
+
+ let max_radius = (size.x.min(size.y)) / 2.0
+ + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
+
+ let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
+
+ // insert stroked sides below filled sides
+ let mut path = Path::new();
+ let corners = corners_control_points(size, radius, strokes, stroke_widths);
+ let current = corners.iter().find(|c| !c.same).map(|c| c.corner);
+ if let Some(mut current) = current {
+ // multiple segments
+ // start at a corner with a change between sides and iterate clockwise all other corners
+ let mut last = current;
+ for _ in 0..4 {
+ current = current.next_cw();
+ if corners.get_ref(current).same {
+ continue;
+ }
+ // create segment
+ let start = last;
+ let end = current;
+ last = current;
+ path_segment(start, end, &corners, &mut path);
+ }
+ } else if strokes.top.is_some() {
+ // single segment
+ path_segment(Corner::TopLeft, Corner::TopLeft, &corners, &mut path);
+ }
+ path
+}
+
+/// Use stroke and fill for the rectangle
+fn segmented_rect(
+ size: Size,
+ radius: Corners<Rel<Abs>>,
+ fill: Option<Paint>,
+ strokes: Sides<Option<FixedStroke>>,
+) -> Vec<Shape> {
+ let mut res = vec![];
+ let stroke_widths = strokes
+ .as_ref()
+ .map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
+
+ let max_radius = (size.x.min(size.y)) / 2.0
+ + stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
+
+ let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
+
+ let corners = corners_control_points(size, radius, &strokes, stroke_widths);
// insert stroked sides below filled sides
let mut stroke_insert = 0;
@@ -171,6 +233,43 @@ fn segmented_rect(
res
}
+fn path_segment(
+ start: Corner,
+ end: Corner,
+ corners: &Corners<ControlPoints>,
+ path: &mut Path,
+) {
+ // create start corner
+ let c = corners.get_ref(start);
+ if start == end || !c.arc() {
+ path.move_to(c.end());
+ } else {
+ path.arc_move(c.mid(), c.center(), c.end());
+ }
+
+ // create corners between start and end
+ let mut current = start.next_cw();
+ while current != end {
+ let c = corners.get_ref(current);
+ if c.arc() {
+ path.arc_line(c.start(), c.center(), c.end());
+ } else {
+ path.line_to(c.end());
+ }
+ current = current.next_cw();
+ }
+
+ // create end corner
+ let c = corners.get_ref(end);
+ if !c.arc() {
+ path.line_to(c.start());
+ } else if start == end {
+ path.arc_line(c.start(), c.center(), c.end());
+ } else {
+ path.arc_line(c.start(), c.center(), c.mid());
+ }
+}
+
/// Returns the shape for the segment and whether the shape should be drawn on top.
fn segment(
start: Corner,
@@ -228,35 +327,8 @@ fn stroke_segment(
stroke: FixedStroke,
) -> Shape {
// create start corner
- let c = corners.get_ref(start);
let mut path = Path::new();
- if start == end || !c.arc() {
- path.move_to(c.end());
- } else {
- path.arc_move(c.mid(), c.center(), c.end());
- }
-
- // create corners between start and end
- let mut current = start.next_cw();
- while current != end {
- let c = corners.get_ref(current);
- if c.arc() {
- path.arc_line(c.start(), c.center(), c.end());
- } else {
- path.line_to(c.end());
- }
- current = current.next_cw();
- }
-
- // create end corner
- let c = corners.get_ref(end);
- if !c.arc() {
- path.line_to(c.start());
- } else if start == end {
- path.arc_line(c.start(), c.center(), c.end());
- } else {
- path.arc_line(c.start(), c.center(), c.mid());
- }
+ path_segment(start, end, corners, &mut path);
Shape {
geometry: Geometry::Path(path),
diff --git a/crates/typst/src/geom/sides.rs b/crates/typst/src/geom/sides.rs
index e21fe63f..38477f36 100644
--- a/crates/typst/src/geom/sides.rs
+++ b/crates/typst/src/geom/sides.rs
@@ -46,6 +46,16 @@ impl<T> Sides<T> {
}
}
+ /// Convert from `&Sides<T>` to `Sides<&T>`.
+ pub fn as_ref(&self) -> Sides<&T> {
+ Sides {
+ left: &self.left,
+ top: &self.top,
+ right: &self.right,
+ bottom: &self.bottom,
+ }
+ }
+
/// Zip two instances into one.
pub fn zip<U>(self, other: Sides<U>) -> Sides<(T, U)> {
Sides {