Files
effectcraft/crates/svg/src/raster.rs
T

542 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Rasterisation of a parsed SVG into a premultiplied `f32` image (anti-aliased coverage from
//! `effectcraft-path`; group opacity composites the group as a whole, as SVG specifies).
use effectcraft_geom::Mat3;
use effectcraft_raster::{Image, Mask};
use kurbo::{Affine, Point, Shape as _};
use rayon::prelude::*;
use crate::{BlendMode, Doc, FillRule, Gradient, GradientKind, Group, Node, Paint, Shape, Spread};
fn mat3(a: Affine) -> Mat3 {
let c = a.as_coeffs();
Mat3([[c[0], c[2], c[4]], [c[1], c[3], c[5]], [0.0, 0.0, 1.0]])
}
/// Rasterise `doc` into a `w`×`h` image, scaled by `scale` (1 = the document's pixel size).
pub fn rasterize(doc: &Doc, w: u32, h: u32, scale: f64) -> Image {
rasterize_with(doc, w, h, Affine::scale(scale))
}
/// 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);
img
}
fn draw_group(g: &Group, m: Affine, opacity: f64, dst: &mut Image) {
let m = m * g.transform;
let o = opacity * g.opacity;
if o <= 0.0 {
return;
}
let offscreen = !g.clip.is_empty()
|| g.mask.is_some()
|| g.blend != BlendMode::Normal
|| (g.opacity < 1.0 && !matches!(g.children.as_slice(), [Node::Shape(_) | Node::Image(_)]))
|| g.knockout;
if !offscreen {
for c in &g.children {
draw_node(c, m, o, dst);
}
return;
}
let only_clip = g.mask.is_none() && g.blend == BlendMode::Normal && o >= 1.0 && !g.knockout;
// 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 mut tmp = match &backdrop {
Some(b) => b.clone(),
None => Image::new(dst.width, dst.height),
};
draw_children(g, m, &mut tmp, backdrop.as_ref());
if let Some(b) = &backdrop {
if only_clip {
// A non-isolated clipping group: the backdrop where the clip excludes the children.
let cov = clip_coverage(g, m, dst.width, dst.height);
dst.data.par_iter_mut().zip(tmp.data.par_iter()).zip(cov.par_iter()).for_each(|((d, t), c)| {
for i in 0..4 {
d[i] = d[i] * (1.0 - c) + t[i] * c;
}
});
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);
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 {
*t = [0.0; 4];
return;
}
let un = |c: &[f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
let (cn, c0, a0) = (un(t), un(b0), b0[3]);
let k = a0 / ag - a0;
let mut out = [0.0; 4];
for i in 0..3 {
out[i] = (cn[i] + (cn[i] - c0[i]) * k).clamp(0.0, 1.0) * ag;
}
out[3] = ag;
*t = out;
});
}
if !g.clip.is_empty() {
let cov = clip_coverage(g, m, dst.width, dst.height);
tmp.data.par_iter_mut().zip(cov.par_iter()).for_each(|(p, c)| {
for v in p.iter_mut() {
*v *= c;
}
});
}
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 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)| {
let k = if lum {
// The mask group composited over its backdrop colour, then its luminosity.
let c = [0, 1, 2].map(|i| q[i] + bc[i] * (1.0 - q[3]));
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
} else {
q[3]
};
let k = k.clamp(0.0, 1.0);
for v in p.iter_mut() {
*v *= k;
}
});
}
if g.blend == BlendMode::Normal {
over(dst, &tmp, o as f32);
} else {
let mode = g.blend;
let k = o as f32;
dst.data.par_iter_mut().zip(tmp.data.par_iter()).for_each(|(d, s)| {
let s = [s[0] * k, s[1] * k, s[2] * k, s[3] * k];
if s[3] > 0.0 {
*d = blend_pixel(mode, *d, s);
}
});
}
}
/// The product of a group's clip-path coverages.
fn clip_coverage(g: &Group, m: Affine, w: u32, h: u32) -> Vec<f32> {
let mut out = vec![1.0f32; w as usize * h as usize];
for (path, rule) in &g.clip {
let rule = match rule {
FillRule::NonZero => effectcraft_path::FillRule::NonZero,
FillRule::EvenOdd => effectcraft_path::FillRule::EvenOdd,
};
let cov = effectcraft_path::fill_coverage(std::slice::from_ref(path), &mat3(m), w, h, rule);
out.par_iter_mut().zip(cov.data.par_iter()).for_each(|(o, c)| *o *= c);
}
out
}
/// 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>) {
if !g.knockout {
for c in &g.children {
draw_node(c, m, 1.0, dst);
}
return;
}
for c in &g.children {
let mut ci = Image::new(dst.width, dst.height);
draw_node(c, m, 1.0, &mut ci);
// 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 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]));
if f <= 0.0 {
return;
}
let b = backdrop.map(|b| b.data[i]).unwrap_or(zero);
for k in 0..4 {
d[k] = d[k] * (1.0 - f) + cp[k] + b[k] * (f - cp[3]);
}
});
}
}
/// A node with its constant opacities set to 1 (its shape, for knockout groups).
fn opaque(n: &Node) -> Node {
match n {
Node::Group(g) => {
let mut g = g.clone();
g.opacity = 1.0;
g.children = g.children.iter().map(opaque).collect();
Node::Group(g)
}
Node::Shape(s) => {
let mut s = s.clone();
s.opacity = 1.0;
s.fill_opacity = 1.0;
if let Some(st) = &mut s.stroke {
st.opacity = 1.0;
}
Node::Shape(s)
}
Node::Image(i) => {
let mut i = i.clone();
i.opacity = 1.0;
Node::Image(i)
}
}
}
/// `src` composited onto `dst` with a blend mode (both premultiplied RGBA):
/// `co = cs·(1 − αb) + cb·(1 − αs) + αs·αb·B(cb, cs)` (ISO 32000-1 §11.3.6).
pub fn blend_pixel(mode: BlendMode, dst: [f32; 4], src: [f32; 4]) -> [f32; 4] {
let (ab, as_) = (dst[3], src[3]);
let un = |c: [f32; 4]| if c[3] > 0.0 { [c[0] / c[3], c[1] / c[3], c[2] / c[3]] } else { [0.0; 3] };
let (cb, cs) = (un(dst), un(src));
let b = blend_color(mode, cb, cs);
let mut out = [0.0; 4];
for i in 0..3 {
out[i] = src[i] * (1.0 - ab) + dst[i] * (1.0 - as_) + as_ * ab * b[i];
}
out[3] = as_ + ab * (1.0 - as_);
out
}
fn blend_color(mode: BlendMode, cb: [f32; 3], cs: [f32; 3]) -> [f32; 3] {
use BlendMode::*;
let sep = |f: fn(f32, f32) -> f32| [f(cb[0], cs[0]), f(cb[1], cs[1]), f(cb[2], cs[2])];
fn hard(b: f32, s: f32) -> f32 {
if s <= 0.5 {
b * 2.0 * s
} else {
let t = 2.0 * s - 1.0;
b + t - b * t
}
}
match mode {
Normal => cs,
Multiply => sep(|b, s| b * s),
Screen => sep(|b, s| b + s - b * s),
Overlay => sep(|b, s| hard(s, b)),
Darken => sep(f32::min),
Lighten => sep(f32::max),
ColorDodge => sep(|b, s| {
if b <= 0.0 {
0.0
} else if s >= 1.0 {
1.0
} else {
(b / (1.0 - s)).min(1.0)
}
}),
ColorBurn => sep(|b, s| {
if b >= 1.0 {
1.0
} else if s <= 0.0 {
0.0
} else {
1.0 - ((1.0 - b) / s).min(1.0)
}
}),
HardLight => sep(hard),
SoftLight => sep(|b, s| {
if s <= 0.5 {
b - (1.0 - 2.0 * s) * b * (1.0 - b)
} else {
let d = if b <= 0.25 { ((16.0 * b - 12.0) * b + 4.0) * b } else { b.sqrt() };
b + (2.0 * s - 1.0) * (d - b)
}
}),
Difference => sep(|b, s| (b - s).abs()),
Exclusion => sep(|b, s| b + s - 2.0 * b * s),
Hue => set_lum(set_sat(cs, sat(cb)), lum(cb)),
Saturation => set_lum(set_sat(cb, sat(cs)), lum(cb)),
Color => set_lum(cs, lum(cb)),
Luminosity => set_lum(cb, lum(cs)),
}
}
fn lum(c: [f32; 3]) -> f32 {
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
fn clip_color(c: [f32; 3]) -> [f32; 3] {
let l = lum(c);
let n = c[0].min(c[1]).min(c[2]);
let x = c[0].max(c[1]).max(c[2]);
let mut c = c;
if n < 0.0 && l - n > 0.0 {
c = c.map(|v| l + (v - l) * l / (l - n));
}
if x > 1.0 && x - l > 0.0 {
c = c.map(|v| l + (v - l) * (1.0 - l) / (x - l));
}
c
}
fn set_lum(c: [f32; 3], l: f32) -> [f32; 3] {
let d = l - lum(c);
clip_color(c.map(|v| v + d))
}
fn sat(c: [f32; 3]) -> f32 {
c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
}
fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
let (mx, mn) = (c[0].max(c[1]).max(c[2]), c[0].min(c[1]).min(c[2]));
if mx - mn <= 0.0 {
return [0.0; 3];
}
c.map(|v| (v - mn) * s / (mx - mn))
}
fn draw_node(n: &Node, m: Affine, opacity: f64, dst: &mut Image) {
match n {
Node::Group(g) => draw_group(g, m, opacity, dst),
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);
draw_shape(s, m, 1.0, &mut tmp);
over(dst, &tmp, (opacity * s.opacity) as f32);
} else {
draw_shape(s, m, opacity * s.opacity, dst);
}
}
Node::Image(im) => draw_image(im, m, opacity * im.opacity, dst),
}
}
/// Draw a raster image: bilinear samples inside the image's (anti-aliased) quadrilateral.
fn draw_image(im: &crate::Image, m: Affine, opacity: f64, dst: &mut Image) {
let (iw, ih) = (im.width as usize, im.height as usize);
if iw == 0 || ih == 0 || im.rgba.len() < iw * ih * 4 || opacity <= 0.0 {
return;
}
let full = m * im.transform;
let inv = full.inverse();
if !inv.as_coeffs().iter().all(|v| v.is_finite()) {
return;
}
let quad = kurbo::Rect::new(0.0, 0.0, iw as f64, ih as f64).to_path(0.01);
let cov = effectcraft_path::fill_coverage(std::slice::from_ref(&quad), &mat3(full), dst.width, dst.height, effectcraft_path::FillRule::NonZero);
let w = dst.width as usize;
if w == 0 {
return;
}
let px = |x: usize, y: usize| -> [f32; 4] {
let i = (y * iw + x) * 4;
let a = im.rgba[i + 3] as f32 / 255.0;
[im.rgba[i] as f32 / 255.0 * a, im.rgba[i + 1] as f32 / 255.0 * a, im.rgba[i + 2] as f32 / 255.0 * a, a]
};
// Pixel footprint in image pixels: box-filter heavy minification instead of aliasing.
let c = inv.as_coeffs();
let fx = (c[0] * c[0] + c[1] * c[1]).sqrt();
let fy = (c[2] * c[2] + c[3] * c[3]).sqrt();
let taps = (fx.max(fy).ceil() as usize).clamp(1, 8);
let k = opacity as f32;
dst.data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
for (x, d) in row.iter_mut().enumerate() {
let cv = cov.data[y * w + x];
if cv <= 0.0 {
continue;
}
let mut acc = [0.0f32; 4];
for ty in 0..taps {
for tx in 0..taps {
let sx = x as f64 + (tx as f64 + 0.5) / taps as f64;
let sy = y as f64 + (ty as f64 + 0.5) / taps as f64;
let p = inv * Point::new(sx, sy);
let (u, v) = ((p.x - 0.5).clamp(0.0, iw as f64 - 1.0), (p.y - 0.5).clamp(0.0, ih as f64 - 1.0));
let (x0, y0) = (u.floor() as usize, v.floor() as usize);
let (x1, y1) = ((x0 + 1).min(iw - 1), (y0 + 1).min(ih - 1));
let (a, b) = ((u - x0 as f64) as f32, (v - y0 as f64) as f32);
let (p00, p10, p01, p11) = (px(x0, y0), px(x1, y0), px(x0, y1), px(x1, y1));
for i in 0..4 {
acc[i] += (p00[i] * (1.0 - a) + p10[i] * a) * (1.0 - b) + (p01[i] * (1.0 - a) + p11[i] * a) * b;
}
}
}
let n = (taps * taps) as f32;
let s = acc.map(|v| v / n * cv * k);
if s[3] <= 0.0 {
continue;
}
for i in 0..3 {
d[i] = s[i] + d[i] * (1.0 - s[3]);
}
d[3] = s[3] + d[3] * (1.0 - s[3]);
}
});
}
/// `dst = src·k over dst` (premultiplied).
fn over(dst: &mut Image, src: &Image, k: f32) {
dst.data.par_iter_mut().zip(src.data.par_iter()).for_each(|(d, s)| {
let a = s[3] * k;
if a > 0.0 {
for c in 0..3 {
d[c] = s[c] * k + d[c] * (1.0 - a);
}
d[3] = a + d[3] * (1.0 - a);
}
});
}
fn draw_shape(s: &Shape, m: Affine, opacity: f64, dst: &mut Image) {
let m = m * s.transform;
let path = s.geom.to_path();
let bbox = path.bounding_box();
let (w, h) = (dst.width, dst.height);
if let Some(fill) = &s.fill {
let rule = match s.fill_rule {
FillRule::NonZero => effectcraft_path::FillRule::NonZero,
FillRule::EvenOdd => effectcraft_path::FillRule::EvenOdd,
};
let cov = effectcraft_path::fill_coverage(std::slice::from_ref(&path), &mat3(m), w, h, rule);
paint(dst, &cov, fill, (opacity * s.fill_opacity) as f32, m, bbox);
}
if let Some(st) = &s.stroke {
let style = effectcraft_path::StrokeStyle {
width: st.width,
cap: match st.cap {
crate::Cap::Butt => effectcraft_path::Cap::Butt,
crate::Cap::Round => effectcraft_path::Cap::Round,
crate::Cap::Square => effectcraft_path::Cap::Square,
},
join: match st.join {
crate::Join::Miter => effectcraft_path::Join::Miter,
crate::Join::Round => effectcraft_path::Join::Round,
crate::Join::Bevel => effectcraft_path::Join::Bevel,
},
miter: st.miter,
dash: st.dash.clone(),
taper: None,
wave: None,
};
let cov = effectcraft_path::stroke_coverage(std::slice::from_ref(&path), &style, &mat3(m), w, h);
paint(dst, &cov, &st.paint, (opacity * st.opacity) as f32, m, bbox);
}
}
/// Composite paint through coverage.
fn paint(dst: &mut Image, cov: &Mask, p: &Paint, opacity: f32, m: Affine, bbox: kurbo::Rect) {
let w = dst.width as usize;
if w == 0 {
return;
}
let shader = match p {
Paint::Color(c) => Shader::Solid([c[0] as f32, c[1] as f32, c[2] as f32, 1.0]),
Paint::Gradient(g) => {
let unit = if g.bbox_units { Affine::new([bbox.width(), 0.0, 0.0, bbox.height(), bbox.x0, bbox.y0]) } else { Affine::IDENTITY };
let to_grad = (m * unit * g.transform).inverse();
if !to_grad.as_coeffs().iter().all(|v| v.is_finite()) {
return;
}
Shader::Gradient(g, to_grad)
}
};
dst.data.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
for (x, d) in row.iter_mut().enumerate() {
let c = cov.data[y * w + x];
if c <= 0.0 {
continue;
}
let col = shader.at(x as f64 + 0.5, y as f64 + 0.5);
let a = col[3] * c * opacity;
if a <= 0.0 {
continue;
}
for k in 0..3 {
d[k] = col[k] * a + d[k] * (1.0 - a);
}
d[3] = a + d[3] * (1.0 - a);
}
});
}
enum Shader<'a> {
Solid([f32; 4]),
Gradient(&'a Gradient, Affine),
}
impl Shader<'_> {
/// Straight RGBA at a pixel centre.
fn at(&self, x: f64, y: f64) -> [f32; 4] {
match self {
Shader::Solid(c) => *c,
Shader::Gradient(g, inv) => {
let p = *inv * Point::new(x, y);
let t = match g.kind {
GradientKind::Linear { x1, y1, x2, y2 } => {
let (dx, dy) = (x2 - x1, y2 - y1);
let l2 = dx * dx + dy * dy;
if l2 <= 0.0 { 1.0 } else { ((p.x - x1) * dx + (p.y - y1) * dy) / l2 }
}
GradientKind::Radial { cx, cy, r, fx, fy } => radial_t(p, cx, cy, r, fx, fy),
};
eval_stops(&g.stops, spread(t, g.spread))
}
}
}
}
/// Radial gradient parameter with a focal point: the circle through `p` from the focus.
pub(crate) fn radial_t(p: Point, cx: f64, cy: f64, r: f64, fx: f64, fy: f64) -> f64 {
if r <= 0.0 {
return 1.0;
}
let (dx, dy) = (p.x - fx, p.y - fy);
let (ex, ey) = (fx - cx, fy - cy);
if ex * ex + ey * ey < 1e-12 {
return (dx * dx + dy * dy).sqrt() / r;
}
// Solve |e + u·d|² = r² for u > 0; t = 1/u.
let a = dx * dx + dy * dy;
if a < 1e-18 {
return 0.0;
}
let b = ex * dx + ey * dy;
let c = ex * ex + ey * ey - r * r;
let disc = b * b - a * c;
if disc < 0.0 {
return 1.0;
}
let u = (-b + disc.sqrt()) / a;
if u <= 0.0 { 1.0 } else { 1.0 / u }
}
fn spread(t: f64, s: Spread) -> f64 {
match s {
Spread::Pad => t.clamp(0.0, 1.0),
Spread::Repeat => t.rem_euclid(1.0),
Spread::Reflect => {
let r = t.rem_euclid(2.0);
if r > 1.0 { 2.0 - r } else { r }
}
}
}
pub(crate) fn eval_stops(stops: &[(f64, [f64; 4])], t: f64) -> [f32; 4] {
let Some(first) = stops.first() else { return [0.0; 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))
}