Files
vectorcraft/crates/pdf/src/import_shading.rs
T

291 lines
14 KiB
Rust

//! Shadings: axial and radial ones become gradients (their stops sampled from the shading
//! function in its own colour model; an end that doesn't extend clips the painted area), patch
//! and triangle meshes become gradient meshes, a gradient drawn through a luminosity mask that is
//! the same gradient in grey (how stop opacity is written) gets its opacity stops back, and a shape
//! clipping a gradient that fills it becomes one path with the gradient.
use std::sync::Arc;
use craft_pdf::interpret::pattern::ShadingPattern;
use craft_pdf::interpret::shading::{ShadingFunction, ShadingType};
use kurbo::{BezPath, Circle, Point, Rect, Shape, Vec2};
use vectorcraft_color::{Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
use vectorcraft_doc::live::{GradientMesh, lerp_color};
use vectorcraft_doc::{AppearanceItem, Node, NodeKind, OpacityMask};
use vectorcraft_geom::PathData;
use crate::import::{closed_subpaths, round3};
use crate::import_color::{Colors, Native};
use crate::import_mask::{contains, is_rectangle, luminance};
/// The most mesh patches (or triangles) imported per shading: past it, the shading is a flat
/// colour.
const MAX_PATCHES: usize = 4096;
/// An axial or radial shading as a gradient paint, with whether it extends past its start and
/// its end.
pub(crate) fn shading_gradient(sp: &ShadingPattern, colors: &mut Colors<'_>) -> Option<(GradientPaint, [bool; 2])> {
let ShadingType::RadialAxial { coords, domain, function, axial, extend } = sp.shading.shading_type.as_ref() else {
return None;
};
let m = sp.matrix;
let cs = &sp.shading.color_space;
let at = |t: f32| function.eval(&smallvec::smallvec![domain[0] + (domain[1] - domain[0]) * t]);
let space = colors.shading_space(cs, at(0.0)?.len());
// RGB to decide which samples are stops; the stops keep the shading's model.
let sample = |t: f32| -> Option<Color> {
let [r, g, b, _] = cs.to_rgba(&at(t)?, 1.0, false).components();
Some(Color::rgb(round3(r), round3(g), round3(b)))
};
// Sample densely, then drop samples that linear interpolation reproduces.
const N: usize = 64;
let mut pts: Vec<(f32, [f32; 3])> = Vec::with_capacity(N + 1);
for i in 0..=N {
let t = i as f32 / N as f32;
pts.push((t, sample(t)?.to_rgb()));
}
let mut keep = vec![0usize];
for w in 1..N {
let (Some(a), Some(p), Some(b)) = (keep.last().and_then(|&k| pts.get(k)), pts.get(w), pts.get(w + 1)) else { continue };
let u = (p.0 - a.0) / (b.0 - a.0).max(1e-6);
let off = (0..3).map(|k| (a.1[k] + (b.1[k] - a.1[k]) * u - p.1[k]).abs()).fold(0.0f32, f32::max);
if off > 1.5 / 255.0 {
keep.push(w);
}
}
keep.push(N);
let ts: Vec<f32> = keep.iter().filter_map(|&k| pts.get(k)).map(|p| p.0).collect();
let mut stop_at = |t: f32, offset: f32| -> GradientStop {
let rgb = sample(t).unwrap_or(Color::BLACK);
match at(t).and_then(|v| colors.native(space, &v)) {
Some(Native { color, link: Some((name, tint)) }) => GradientStop { swatch: Some(name), tint, ..GradientStop::new(offset, color) },
Some(Native { color, link: None }) => GradientStop::new(offset, color),
None => GradientStop::new(offset, rgb),
}
};
let c = |x: f32, y: f32| m * Point::new(x as f64, y as f64);
let (kind, geom, stops) = if *axial {
let geom = GradientGeom { start: c(coords[0], coords[1]), end: c(coords[2], coords[3]), aspect: 1.0, focal: None };
(GradientKind::Linear, geom, ts.iter().map(|&t| stop_at(t, t)).collect::<Vec<_>>())
} else {
let (r0, r1) = (coords[2].max(0.0), coords[5].max(1e-6));
// The end circle, mapped (as an ellipse) into the page; a start circle off its centre
// gives the focal point.
let p = |x: f32, y: f32| Point::new(x as f64, y as f64);
let centre = p(coords[3], coords[4]);
let mut geom = GradientGeom { start: centre, end: centre + Vec2::new(r1 as f64, 0.0), aspect: 1.0, focal: None };
geom.set_focal(Some(p(coords[0], coords[1])));
geom.transform(m, GradientKind::Radial);
// Offsets are relative to the outer radius; an inner radius shifts them outwards.
let stops = ts.iter().map(|&t| stop_at(t, (r0 + (r1 - r0) * t) / r1)).collect();
(GradientKind::Radial, geom, stops)
};
let mut g = GradientPaint::new(Gradient { kind, stops });
g.geom = Some(geom);
Some((g, *extend))
}
/// Where an axial or radial shading that doesn't extend at both ends paints, as a clip path in
/// document space (even-odd) for art filling `bounds`; `None` when it extends at both.
pub(crate) fn extend_clip(sp: &ShadingPattern, bounds: Rect) -> Option<BezPath> {
let ShadingType::RadialAxial { coords, axial, extend, .. } = sp.shading.shading_type.as_ref() else {
return None;
};
if extend[0] && extend[1] {
return None;
}
let m = sp.matrix;
let inv = m.inverse();
let local = inv.transform_rect_bbox(bounds);
if !local.is_finite() || m.determinant().abs() < 1e-12 {
return None;
}
let f = |i: usize| coords.get(i).copied().unwrap_or(0.0) as f64;
let mut out = BezPath::new();
if *axial {
let (p0, p1) = (Point::new(f(0), f(1)), Point::new(f(2), f(3)));
let len = (p1 - p0).hypot();
if len < 1e-9 {
return None;
}
let u = (p1 - p0) / len;
let n = Vec2::new(-u.y, u.x);
let corners = [local.origin(), Point::new(local.x1, local.y0), Point::new(local.x1, local.y1), Point::new(local.x0, local.y1)];
let along = corners.map(|c| (c - p0).dot(u));
let across = corners.map(|c| (c - p0).dot(n));
let min = |v: [f64; 4]| v.iter().copied().fold(f64::INFINITY, f64::min);
let max = |v: [f64; 4]| v.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let lo = if extend[0] { min(along) } else { min(along).max(0.0) };
let hi = if extend[1] { max(along) } else { max(along).min(len) };
let (b0, b1) = (min(across), max(across));
if hi > lo {
let q = |a: f64, b: f64| m * (p0 + u * a + n * b);
out.move_to(q(lo, b0));
out.line_to(q(hi, b0));
out.line_to(q(hi, b1));
out.line_to(q(lo, b1));
out.close_path();
}
return Some(out);
}
let start = Circle::new((f(0), f(1)), f(2).max(0.0));
let end = Circle::new((f(3), f(4)), f(5).max(0.0));
// Past the end: the end circle bounds the paint; before the start: the start circle is a hole.
let outer = if extend[1] { local.inflate(1.0, 1.0).to_path(0.1) } else { end.to_path(0.1) };
out.extend(m * outer);
if !extend[0] && start.radius > 1e-9 {
out.extend(m * start.to_path(0.1));
}
Some(out)
}
/// The colour of shading components `v` (the input of the shading's function, when it has one).
fn mesh_color(v: &[f32], function: Option<&ShadingFunction>, sp: &ShadingPattern, colors: &mut Colors<'_>) -> Color {
let comps = match function {
Some(f) => f.eval(&v.iter().copied().collect()).map(|c| c.to_vec()).unwrap_or_default(),
None => v.to_vec(),
};
let space = colors.shading_space(&sp.shading.color_space, comps.len());
colors.native(space, &comps).map(|n| n.color).unwrap_or_else(|| {
let [r, g, b, _] = sp.shading.color_space.to_rgba(&comps, 1.0, false).components();
Color::rgb(round3(r), round3(g), round3(b))
})
}
/// A patch mesh or triangle mesh shading as gradient meshes in document space (one per patch;
/// a triangle is a patch with two corners together). `None`: not a mesh, or too big.
pub(crate) fn mesh_shading(sp: &ShadingPattern, colors: &mut Colors<'_>) -> Option<Vec<GradientMesh>> {
let m = sp.matrix;
let mut out = vec![];
match sp.shading.shading_type.as_ref() {
ShadingType::CoonsPatchMesh { patches, function } => {
if patches.len() > MAX_PATCHES {
return None;
}
for p in patches {
let colors = [0, 1, 2, 3].map(|i| mesh_color(p.colors.get(i).map_or(&[][..], |c| c.as_slice()), function.as_ref(), sp, colors));
out.extend(GradientMesh::coons(&p.control_points.map(|q| m * q), colors));
}
}
// A tensor patch's boundary is a Coons patch's (its four inner points are dropped).
ShadingType::TensorProductPatchMesh { patches, function } => {
if patches.len() > MAX_PATCHES {
return None;
}
for p in patches {
let colors = [0, 1, 2, 3].map(|i| mesh_color(p.colors.get(i).map_or(&[][..], |c| c.as_slice()), function.as_ref(), sp, colors));
out.extend(p.control_points.map(|q| m * q).first_chunk::<12>().and_then(|cp| GradientMesh::coons(cp, colors)));
}
}
ShadingType::TriangleMesh { triangles, function } => {
if triangles.len() > MAX_PATCHES {
return None;
}
for t in triangles {
let v = [&t.p0, &t.p1, &t.p2].map(|v| (m * v.point, mesh_color(v.colors.as_slice(), function.as_ref(), sp, colors)));
out.extend(GradientMesh::triangle(v));
}
}
_ => return None,
}
Some(out)
}
/// The only painted leaf of mask art (through groups of one object and clip groups of one).
fn only_leaf(n: &Node) -> Option<&Node> {
match &n.kind {
NodeKind::Group { children, clip } => {
let rest = if *clip { children.get(1..)? } else { children.as_slice() };
match rest {
[one] if n.opacity >= 0.999 && n.extras().mask.is_none() => only_leaf(one),
_ => None,
}
}
NodeKind::Path { .. } => Some(n),
_ => None,
}
}
/// The gradient a path paints with its only fill or stroke.
fn only_gradient(n: &Node) -> Option<&GradientPaint> {
match n.appearance.items.as_slice() {
[item] => match item.paint() {
Paint::Gradient(g) => Some(g),
_ => None,
},
_ => None,
}
}
fn same_geom(a: &GradientGeom, b: &GradientGeom) -> bool {
let near = |p: Point, q: Point| p.distance(q) < 0.5;
near(a.start, b.start) && near(a.end, b.end) && near(a.focal_point(), b.focal_point()) && (a.aspect - b.aspect).abs() < 0.01
}
/// `content` (a path painting one gradient) drawn through `mask` (clipped, not inverted, the same
/// gradient in shades of grey: how gradient stop opacity is written) as one gradient with the
/// mask's luminance as stop opacity.
pub(crate) fn fold_stop_opacity(content: &Node, mask: &OpacityMask) -> Option<Node> {
if !mask.clip || mask.invert || mask.disabled || content.is_container() {
return None;
}
let g = only_gradient(content)?;
let leaf = only_leaf(&mask.art)?;
let mg = only_gradient(leaf)?;
let opaque = |n: &Node| n.opacity >= 0.999 && n.appearance.items.iter().all(|i| i.opacity() >= 0.999);
if mg.gradient.kind != g.gradient.kind || !opaque(leaf) || !same_geom(g.geom.as_ref()?, mg.geom.as_ref()?) {
return None;
}
let mut offsets: Vec<f32> = g.gradient.stops.iter().chain(&mg.gradient.stops).map(|s| s.offset).collect();
offsets.sort_by(f32::total_cmp);
offsets.dedup_by(|a, b| (*a - *b).abs() < 1e-4);
let stops = offsets
.iter()
.map(|&t| {
let (mc, mo) = mg.gradient.sample(t);
let alpha = round3(luminance(&mc) * mo);
let mut s = match g.gradient.stops.iter().find(|s| (s.offset - t).abs() < 1e-4) {
Some(s) => s.clone(),
None => {
let (c, o) = g.gradient.sample_with(t, lerp_color);
GradientStop { opacity: o, ..GradientStop::new(t, c) }
}
};
s.opacity = round3(s.opacity * alpha);
s
})
.collect();
let mut out = content.clone();
let mut paint = (*g).clone();
paint.gradient.stops = stops;
if let Some(item) = out.appearance.items.first_mut() {
*item.paint_mut() = Paint::Gradient(Box::new(paint));
}
Some(out)
}
/// `node` (filling `bounds`) clipped to `clip`: a clip group.
pub(crate) fn clipped(clip: Node, node: Node, id: vectorcraft_doc::NodeId) -> Node {
Node::new(id, NodeKind::Group { children: vec![Arc::new(clip), Arc::new(node)], clip: true })
}
/// A shape filled with a gradient, as files write it (the shape as a clip, then `sh` filling the
/// clip's bounds), as one path: the clip's outline with `content`'s look. `None` unless `content`
/// is one rectangle covering the clip, filled with a gradient placed on the page and nothing else.
pub(crate) fn gradient_shape(clip: &Node, content: &[Arc<Node>]) -> Option<Node> {
let ([only], NodeKind::Path { path: outline, rule, .. }) = (content, &clip.kind) else { return None };
let NodeKind::Path { path: area, clipping: false, guide: false, .. } = &only.kind else { return None };
let [AppearanceItem::Fill(f)] = only.appearance.items.as_slice() else { return None };
let Paint::Gradient(g) = &f.paint else { return None };
let (area, outline) = (area.to_bezpath(), outline.to_bezpath());
let covers = is_rectangle(&area) && contains(area.bounding_box().inflate(0.01, 0.01), outline.bounding_box());
if g.geom.is_none() || !covers {
return None;
}
let mut shape = (**only).clone();
// A clip closes its subpaths, as a fill does.
let path = PathData::from_bezpath(&closed_subpaths(&outline));
shape.kind = NodeKind::Path { path, rule: *rule, live: None, clipping: false, guide: false };
Some(shape)
}