diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 590ee9057..aa3c92cc5 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -827,7 +827,7 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) { Geometry::Rect(size) => { let w = size.x.to_f32(); let h = size.y.to_f32(); - if w > 0.0 && h > 0.0 { + if w.abs() > f32::EPSILON && h.abs() > f32::EPSILON { ctx.content.rect(x, y, w, h); } } diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index e9dd3fbf0..fdacb5976 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -570,7 +570,18 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option< Geometry::Rect(size) => { let w = size.x.to_f32(); let h = size.y.to_f32(); - let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?; + let rect = if w < 0.0 || h < 0.0 { + // Skia doesn't normally allow for negative dimensions, but + // Typst supports them, so we apply a transform if needed + // Because this operation is expensive according to tiny-skia's + // docs, we prefer to not apply it if not needed + let transform = sk::Transform::from_scale(w.signum(), h.signum()); + let rect = sk::Rect::from_xywh(0.0, 0.0, w.abs(), h.abs())?; + rect.transform(transform)? + } else { + sk::Rect::from_xywh(0.0, 0.0, w, h)? + }; + sk::PathBuilder::from_rect(rect) } Geometry::Path(ref path) => convert_path(path)?, @@ -941,8 +952,10 @@ fn to_sk_paint<'a>( .container_transform .post_concat(state.transform.invert().unwrap()), }; - let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32; - let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32; + let width = + (container_size.x.to_f32().abs() * state.pixel_per_pt).ceil() as u32; + let height = + (container_size.y.to_f32().abs() * state.pixel_per_pt).ceil() as u32; *pixmap = Some(cached( gradient, @@ -958,8 +971,10 @@ fn to_sk_paint<'a>( sk::SpreadMode::Pad, sk::FilterQuality::Nearest, 1.0, - fill_transform - .pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt), + fill_transform.pre_scale( + container_size.x.signum() as f32 / state.pixel_per_pt, + container_size.y.signum() as f32 / state.pixel_per_pt, + ), ); sk_paint.anti_alias = gradient.anti_alias(); diff --git a/crates/typst/src/layout/abs.rs b/crates/typst/src/layout/abs.rs index b19167954..5c07c5a06 100644 --- a/crates/typst/src/layout/abs.rs +++ b/crates/typst/src/layout/abs.rs @@ -117,6 +117,11 @@ impl Abs { pub fn approx_eq(self, other: Self) -> bool { self == other || (self - other).to_raw().abs() < 1e-6 } + + /// Returns a number that represent the sign of this length + pub fn signum(self) -> f64 { + self.0.get().signum() + } } impl Numeric for Abs { diff --git a/tests/ref/visualize/shape-rect.png b/tests/ref/visualize/shape-rect.png index 3eda642f8..a279341ec 100644 Binary files a/tests/ref/visualize/shape-rect.png and b/tests/ref/visualize/shape-rect.png differ diff --git a/tests/typ/visualize/shape-rect.typ b/tests/typ/visualize/shape-rect.typ index ea0e66b0e..bd9cf1ae4 100644 --- a/tests/typ/visualize/shape-rect.typ +++ b/tests/typ/visualize/shape-rect.typ @@ -61,3 +61,18 @@ #set par(justify: true) #lorem(100) #rect(lorem(100)) + +--- +// Negative dimensions +#rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse left] + +#rect(width: 1cm, fill: gradient.linear(red, blue))[Left] + +#align(center, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse center]) + +#align(center, rect(width: 1cm, fill: gradient.linear(red, blue))[Center]) + +#align(right, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse right]) + +#align(right, rect(width: 1cm, fill: gradient.linear(red, blue))[Right]) +