mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
svg: bound raster surfaces and handle duplicate gradient stops (#2190)
This commit is contained in:
+112
-29
@@ -20,12 +20,47 @@ pub fn rasterize(doc: &Doc, w: u32, h: u32, scale: f64) -> Image {
|
||||
|
||||
/// Rasterise `doc` with `m` mapping document pixels to image pixels.
|
||||
pub fn rasterize_with(doc: &Doc, w: u32, h: u32, m: Affine) -> Image {
|
||||
let mut img = Image::new(w, h);
|
||||
draw_group(&doc.root, m, 1.0, &mut img);
|
||||
let mut budget = SurfaceBudget { live_bytes: 0, limit_bytes: MAX_LIVE_SURFACE_BYTES };
|
||||
let Some(mut img) = budget.image(w, h, None) else { return Image::default() };
|
||||
draw_group(&doc.root, m, 1.0, &mut img, &mut budget);
|
||||
img
|
||||
}
|
||||
|
||||
fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
/// Bound the simultaneously live offscreen image surfaces created by nested groups.
|
||||
const MAX_LIVE_SURFACE_BYTES: usize = 512 * 1024 * 1024;
|
||||
|
||||
struct SurfaceBudget {
|
||||
live_bytes: usize,
|
||||
limit_bytes: usize,
|
||||
}
|
||||
|
||||
impl SurfaceBudget {
|
||||
fn image(&mut self, width: u32, height: u32, copy: Option<&Image>) -> Option<Image> {
|
||||
let pixels = (width as usize).checked_mul(height as usize)?;
|
||||
let bytes = pixels.checked_mul(std::mem::size_of::<[f32; 4]>())?;
|
||||
let new_live = self.live_bytes.checked_add(bytes)?;
|
||||
if new_live > self.limit_bytes {
|
||||
return None;
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.try_reserve_exact(pixels).ok()?;
|
||||
data.resize(pixels, [0.0; 4]);
|
||||
if let Some(source) = copy {
|
||||
if source.width != width || source.height != height || source.data.len() != pixels {
|
||||
return None;
|
||||
}
|
||||
data.copy_from_slice(&source.data);
|
||||
}
|
||||
self.live_bytes = new_live;
|
||||
Some(Image { width, height, data })
|
||||
}
|
||||
|
||||
fn release(&mut self, image: &Image) {
|
||||
self.live_bytes = self.live_bytes.saturating_sub(image.data.len().saturating_mul(std::mem::size_of::<[f32; 4]>()));
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image, budget: &mut SurfaceBudget) {
|
||||
let m = m * g.transform;
|
||||
let o = opacity * g.opacity;
|
||||
if o <= 0.0 {
|
||||
@@ -38,7 +73,7 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
|| g.knockout;
|
||||
if !offscreen {
|
||||
for c in &g.children {
|
||||
draw_node(c, m, o, dst);
|
||||
draw_node(c, m, o, dst, budget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -46,12 +81,24 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
// Draw the children on their own (isolated) or over a copy of the backdrop, keep what
|
||||
// every clip path covers and the soft mask lets through, then composite with the group's
|
||||
// opacity and blend mode.
|
||||
let backdrop = if g.isolated { None } else { Some(dst.clone()) };
|
||||
let backdrop = if g.isolated { None } else { budget.image(dst.width, dst.height, Some(dst)) };
|
||||
if !g.isolated && backdrop.is_none() {
|
||||
return;
|
||||
}
|
||||
let mut tmp = match &backdrop {
|
||||
Some(b) => b.clone(),
|
||||
None => Image::new(dst.width, dst.height),
|
||||
Some(b) => match budget.image(dst.width, dst.height, Some(b)) {
|
||||
Some(image) => image,
|
||||
None => {
|
||||
budget.release(b);
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => match budget.image(dst.width, dst.height, None) {
|
||||
Some(image) => image,
|
||||
None => return,
|
||||
},
|
||||
};
|
||||
draw_children(g, m, &mut tmp, backdrop.as_ref());
|
||||
draw_children(g, m, &mut tmp, backdrop.as_ref(), budget);
|
||||
if let Some(b) = &backdrop {
|
||||
if only_clip {
|
||||
// A non-isolated clipping group: the backdrop where the clip excludes the children.
|
||||
@@ -61,12 +108,24 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
d[i] = d[i] * (1.0 - c) + t[i] * c;
|
||||
}
|
||||
});
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Remove the backdrop's contribution (ISO 32000-1 §11.4.8): with the group's own
|
||||
// alpha αg (its children alone), C = Cn + (Cn − C0)·(α0/αg − α0).
|
||||
let mut solo = Image::new(dst.width, dst.height);
|
||||
draw_children(g, m, &mut solo, None);
|
||||
let Some(mut solo) = budget.image(dst.width, dst.height, None) else {
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
return;
|
||||
};
|
||||
draw_children(g, m, &mut solo, None, budget);
|
||||
tmp.data.par_iter_mut().zip(b.data.par_iter()).zip(solo.data.par_iter()).for_each(|((t, b0), s)| {
|
||||
let ag = s[3];
|
||||
if ag <= 0.0 {
|
||||
@@ -83,6 +142,8 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
out[3] = ag;
|
||||
*t = out;
|
||||
});
|
||||
budget.release(&solo);
|
||||
drop(solo);
|
||||
}
|
||||
if !g.clip.is_empty() {
|
||||
let cov = clip_coverage(g, m, dst.width, dst.height);
|
||||
@@ -93,8 +154,15 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
});
|
||||
}
|
||||
if let Some(sm) = &g.mask {
|
||||
let mut mk = Image::new(dst.width, dst.height);
|
||||
draw_group(&sm.content, m, 1.0, &mut mk);
|
||||
let Some(mut mk) = budget.image(dst.width, dst.height, None) else {
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
return;
|
||||
};
|
||||
draw_group(&sm.content, m, 1.0, &mut mk, budget);
|
||||
let bc = sm.backdrop.map(|v| v as f32);
|
||||
let lum = sm.luminosity;
|
||||
tmp.data.par_iter_mut().zip(mk.data.par_iter()).for_each(|(p, q)| {
|
||||
@@ -110,6 +178,8 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
*v *= k;
|
||||
}
|
||||
});
|
||||
budget.release(&mk);
|
||||
drop(mk);
|
||||
}
|
||||
if g.blend == BlendMode::Normal {
|
||||
over(dst, &tmp, o as f32);
|
||||
@@ -123,6 +193,11 @@ fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
}
|
||||
});
|
||||
}
|
||||
budget.release(&tmp);
|
||||
drop(tmp);
|
||||
if let Some(b) = &backdrop {
|
||||
budget.release(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// The product of a group's clip-path coverages.
|
||||
@@ -142,19 +217,22 @@ fn clip_coverage(g: &Group, m: Affine, w: u32, h: u32) -> Vec<f32> {
|
||||
/// Draw a group's children into `dst`. Knockout groups composite each child with the
|
||||
/// group's initial backdrop (`backdrop`, or transparent) instead of with the children below:
|
||||
/// where the child has shape `f`, `dst = dst·(1 − f) + child + backdrop·(f − αchild)`.
|
||||
fn draw_children(g: &Group, m: Affine, dst: &mut Image, backdrop: Option<&Image>) {
|
||||
fn draw_children(g: &Group, m: Affine, dst: &mut Image, backdrop: Option<&Image>, budget: &mut SurfaceBudget) {
|
||||
if !g.knockout {
|
||||
for c in &g.children {
|
||||
draw_node(c, m, 1.0, dst);
|
||||
draw_node(c, m, 1.0, dst, budget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for c in &g.children {
|
||||
let mut ci = Image::new(dst.width, dst.height);
|
||||
draw_node(c, m, 1.0, &mut ci);
|
||||
let Some(mut ci) = budget.image(dst.width, dst.height, None) else { return };
|
||||
draw_node(c, m, 1.0, &mut ci, budget);
|
||||
// Shape: the child drawn fully opaque (constant opacities to 1).
|
||||
let mut si = Image::new(dst.width, dst.height);
|
||||
draw_node(&opaque(c), m, 1.0, &mut si);
|
||||
let Some(mut si) = budget.image(dst.width, dst.height, None) else {
|
||||
budget.release(&ci);
|
||||
return;
|
||||
};
|
||||
draw_node(&opaque(c), m, 1.0, &mut si, budget);
|
||||
let zero = [0.0f32; 4];
|
||||
dst.data.par_iter_mut().enumerate().for_each(|(i, d)| {
|
||||
let (cp, f) = (ci.data[i], si.data[i][3].max(ci.data[i][3]));
|
||||
@@ -166,6 +244,10 @@ fn draw_children(g: &Group, m: Affine, dst: &mut Image, backdrop: Option<&Image>
|
||||
d[k] = d[k] * (1.0 - f) + cp[k] + b[k] * (f - cp[3]);
|
||||
}
|
||||
});
|
||||
budget.release(&ci);
|
||||
budget.release(&si);
|
||||
drop(ci);
|
||||
drop(si);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,14 +381,15 @@ fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
|
||||
c.map(|v| (v - mn) * s / (mx - mn))
|
||||
}
|
||||
|
||||
fn draw_node(n: &Node, m: Affine, opacity: f64, dst: &mut Image) {
|
||||
fn draw_node(n: &Node, m: Affine, opacity: f64, dst: &mut Image, budget: &mut SurfaceBudget) {
|
||||
match n {
|
||||
Node::Group(g) => draw_group(g, m, opacity, dst),
|
||||
Node::Group(g) => draw_group(g, m, opacity, dst, budget),
|
||||
Node::Shape(s) => {
|
||||
if s.opacity < 1.0 && s.fill.is_some() && s.stroke.is_some() {
|
||||
let mut tmp = Image::new(dst.width, dst.height);
|
||||
let Some(mut tmp) = budget.image(dst.width, dst.height, None) else { return };
|
||||
draw_shape(s, m, 1.0, &mut tmp);
|
||||
over(dst, &tmp, (opacity * s.opacity) as f32);
|
||||
budget.release(&tmp);
|
||||
} else {
|
||||
draw_shape(s, m, opacity * s.opacity, dst);
|
||||
}
|
||||
@@ -514,12 +597,12 @@ pub(crate) fn eval_stops(stops: &[(f64, [f64; 4])], t: f64) -> [f32; 4] {
|
||||
if t <= first.0 {
|
||||
return first.1.map(|v| v as f32);
|
||||
}
|
||||
for w in stops.windows(2) {
|
||||
let (a, b) = (w[0], w[1]);
|
||||
if t <= b.0 {
|
||||
let k = if b.0 > a.0 { (t - a.0) / (b.0 - a.0) } else { 1.0 };
|
||||
return [0, 1, 2, 3].map(|i| (a.1[i] + (b.1[i] - a.1[i]) * k) as f32);
|
||||
}
|
||||
}
|
||||
stops.last().map_or([0.0; 4], |s| s.1.map(|v| v as f32))
|
||||
// Find the first upper stop (not the last duplicate): equal-offset segments retain the
|
||||
// reference algorithm's exact-boundary behavior, while nearby values are interpolated from
|
||||
// the preceding stop as before.
|
||||
let upper = stops.partition_point(|stop| stop.0 < t);
|
||||
let Some(b) = stops.get(upper) else { return stops.last().map_or([0.0; 4], |s| s.1.map(|v| v as f32)) };
|
||||
let Some(a) = stops.get(upper.saturating_sub(1)) else { return first.1.map(|v| v as f32) };
|
||||
let k = if b.0 > a.0 { (t - a.0) / (b.0 - a.0) } else { 1.0 };
|
||||
[0, 1, 2, 3].map(|i| (a.1[i] + (b.1[i] - a.1[i]) * k) as f32)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,25 @@ fn near(a: [f32; 4], b: [f32; 4], tol: f32) -> bool {
|
||||
(0..4).all(|i| (a[i] - b[i]).abs() <= tol)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradient_stop_lookup_handles_duplicate_offsets() {
|
||||
use crate::raster::eval_stops;
|
||||
let stops = [(0.0, [0.0, 0.0, 0.0, 1.0]), (0.5, [1.0, 0.0, 0.0, 1.0]), (0.5, [0.0, 1.0, 0.0, 1.0]), (1.0, [0.0, 0.0, 1.0, 1.0])];
|
||||
assert_eq!(eval_stops(&stops, 0.5), [1.0, 0.0, 0.0, 1.0]);
|
||||
assert!(near(eval_stops(&stops, 0.5001), [0.0, 1.0, 0.0, 1.0], 3e-4));
|
||||
assert_eq!(eval_stops(&stops, 0.75), [0.0, 0.5, 0.5, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raster_surface_budget_rejects_oversized_destination_before_allocation() {
|
||||
let doc = parse(br#"<svg xmlns="http://www.w3.org/2000/svg"/>"#).unwrap();
|
||||
// 16,384² RGBA-f32 pixels exceed the bounded destination-surface budget; no giant
|
||||
// allocation should be attempted and the caller receives an empty image.
|
||||
let img = rasterize(&doc, 16_384, 16_384, 1.0);
|
||||
assert!(img.data.is_empty());
|
||||
assert_eq!((img.width, img.height), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_shapes_structure() {
|
||||
let d = parse(BASIC).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user