mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 06:51:50 -06:00
vello_cpu 0.0.8's clip_color, shared by the nonseparable blend modes, divided 0 by 0 when a gray's equal components were rounded just below 0 or above 1 after set_lum. The NaN drew the pixel black at the source's alpha whatever the backdrop: nine gray levels under /BM /Color in an empty isolated group (0.96 g among them) rendered (0, 0, 0), and two more at half opacity came out wrong. Vendor vello_cpu 0.0.8 (Apache-2.0 OR MIT) with a backport of linebender/vello#2000, which clips only when the denominator is positive. No vello_cpu release carries the fix yet; drop the copy when one does. Tests in it::nonseparable_gray run every gray level. The two Color ramps fail with the unpatched blend.rs. The Luminosity ramp (black over each opaque gray, upstream's other case) passes either way here: pages use the u8 pipeline, and that case only failed in the f32 one; it stays as a guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
135 lines
6.3 KiB
Rust
135 lines
6.3 KiB
Rust
// Copyright 2025 the Vello Authors
|
|
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
|
|
//! Applying clip paths using Vello CPU.
|
|
|
|
use vello_cpu::Pixmap;
|
|
use vello_cpu::RenderContext;
|
|
use vello_cpu::Resources;
|
|
use vello_cpu::color::palette::css::{BLUE, RED, WHITE};
|
|
use vello_cpu::kurbo::{Circle, Rect, Shape};
|
|
|
|
fn main() {
|
|
// Clip-paths are a fundamental operation in 2D rendering and allow you
|
|
// to constrain the visible areas of all subsequently drawn paths to the
|
|
// shape of another area. Vello CPU has full support for them and actually
|
|
// provides 2 different ways of using them. Below, we will explore them
|
|
// and explain their difference and when to use which one.
|
|
|
|
let mut ctx = RenderContext::new(200, 200);
|
|
|
|
// Two example clip shapes. They have a small overlap in the center that forms
|
|
// an ellipse.
|
|
let clip_1 = Circle::new((75.0, 75.0), 50.0).to_path(0.1);
|
|
let clip_2 = Circle::new((125.0, 75.0), 50.0).to_path(0.1);
|
|
|
|
// Method 1: Non-isolated clipping using the `push_clip_path` and
|
|
// `pop_clip_path` methods:
|
|
{
|
|
// Let's first create a white background.
|
|
ctx.set_paint(WHITE);
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
|
|
// As mentioned, clip paths contain the area that will be affected by
|
|
// our drawing operations. By default, drawing operations will be
|
|
// visible on the whole width/height of the render context.
|
|
|
|
// After this first `push_clip_path`, all drawing operations will be
|
|
// constrained to the area of the first circle.
|
|
ctx.push_clip_path(&clip_1.to_path(0.1));
|
|
// Clip paths can be nested/stacked to arbitrary depths. By
|
|
// nesting clip paths, the drawing area will be further reduced to
|
|
// the _intersection_ of all clip paths that are currently in-place.
|
|
// Thus, after this second `push_clip_path` call, only the pixels
|
|
// that lie in the intersection of both circles will be painted.
|
|
ctx.push_clip_path(&clip_2.to_path(0.1));
|
|
ctx.set_paint(RED);
|
|
// Even though the rectangle covers the whole viewport, only the parts
|
|
// that lie in the intersection of both circles will be painted.
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
// By popping a clip path, the top clip-path on the element (in this case
|
|
// `clip_2`) will be removed. Thus, only `clip_1` remains in-place.
|
|
ctx.pop_clip_path();
|
|
ctx.set_paint(BLUE.with_alpha(0.2));
|
|
// This rectangle will only be constrained by the area of `clip_1`.
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
|
|
// This is optional. You don't strictly have to pop all clip paths
|
|
// currently in-place before rasterizing to the pixmap.
|
|
ctx.pop_clip_path();
|
|
|
|
ctx.flush();
|
|
|
|
save_pixmap(&ctx, "example_clipping1");
|
|
}
|
|
|
|
// Method 2: Isolated clipping using the `push_clip_layer` and `pop_layer`
|
|
// methods:
|
|
{
|
|
// Overall, this method works exactly the same as the previous
|
|
// one, just that the method calls are different. Instead of
|
|
// `push_clip_path`, we have `push_clip_layer`, and instead of
|
|
// `pop_clip_path`, we have `pop_clip_layer`.
|
|
|
|
ctx.set_paint(WHITE);
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
|
|
ctx.push_clip_layer(&clip_1.to_path(0.1));
|
|
ctx.push_clip_layer(&clip_2.to_path(0.1));
|
|
ctx.set_paint(RED);
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
ctx.pop_layer();
|
|
ctx.set_paint(BLUE.with_alpha(0.2));
|
|
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
|
|
|
|
// Unlike the first method, THIS PART IS NOT OPTIONAL! Before
|
|
// rasterizing, you need to make sure that all previously pushed layers
|
|
// have been popped. Otherwise, the renderer will panic.
|
|
ctx.pop_layer();
|
|
|
|
ctx.flush();
|
|
save_pixmap(&ctx, "example_clipping2");
|
|
}
|
|
|
|
// If you inspect the above results, you will see that they visually yield
|
|
// the same result. So what is their difference and when should you use
|
|
// which one? The answer should become clearer when explaining how they
|
|
// differ conceptually.
|
|
// When creating a clip path using `push_clip_path`, every subsequent drawing
|
|
// operation will conceptually be stencil-masked through the intersection
|
|
// of all currently active clip paths before being drawn onto the screen.
|
|
// On the other hand, doing `push_clip_layer` will actually push a whole
|
|
// new isolated layer, and once you call
|
|
// `pop_layer`, the layer _as a whole_ will be clipped to the bounds of the
|
|
// paths and composited back into the previous layer.
|
|
//
|
|
// Which one of these two methods you should use depend on the imaging model
|
|
// you are trying to reflect. For example, in SVG, each group with a clip-path
|
|
// automatically requires creating a new isolated layer. In this case, the
|
|
// isolated clipping method fits the imaging model better. On the other hand,
|
|
// in PDF for example, clip paths and layer isolation are two completely
|
|
// separate concepts. Therefore, it makes much more sense to use the
|
|
// `push_clip_path` method, since you don't want to introduce an isolated
|
|
// layer each time a new clip path is added.
|
|
//
|
|
// Finally, it is also worth mentioning that according to your experiments,
|
|
// non-isolated clipping is usually faster than isolated clipping, especially
|
|
// on the CPU. Therefore, if you are still in doubt, it is recommended
|
|
// that you simply use the non-isolated method. If necessary, you can easily
|
|
// just mix the two different methods as well.
|
|
//
|
|
// Another small note: Clip paths can actually be emulated using alpha
|
|
// masks (see the masking example), so strictly speaking you don't need
|
|
// to use the specialized clipping methods to create clip paths. However,
|
|
// the clipping methods are _much faster_ than masking, and you should
|
|
// therefore always prefer using those over masking.
|
|
}
|
|
|
|
fn save_pixmap(ctx: &RenderContext, filename: &str) {
|
|
let mut resources = Resources::new();
|
|
let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
|
|
ctx.render_to_pixmap(&mut resources, &mut pixmap);
|
|
let png = pixmap.into_png().unwrap();
|
|
std::fs::write(format!("{filename}.png"), png).unwrap();
|
|
}
|