//! Placed-layer (smart object) geometry: the non-affine corner quad (a projective map of the //! content rectangle) and the placed-layer warp (`warp` in `SoLd`: the named warp styles with //! bend and horizontal / vertical distortion, and custom envelope warps whose quilt mesh is a //! grid of bicubic Bézier patches), and a renderer that bakes both into document-space pixels. //! //! The custom mesh is evaluated exactly (bicubic Bézier patches over `meshPoints`, split by //! `quiltSliceX` / `quiltSliceY`). Photoshop does not publish the curves of its named styles; //! the styles here are clean-room geometric definitions of what each name describes (Arc, //! Arch, Bulge, Shell, Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze, Twist), matched by //! behaviour, not numerically. use crate::descriptor::{DValue, Descriptor}; use crate::{Pixels, SmartObject}; /// A placed layer's warp. #[derive(Clone, Debug, Default, PartialEq)] pub struct Warp { /// `warpNone`, `warpArc`, `warpArcLower`, `warpArcUpper`, `warpArch`, `warpBulge`, /// `warpShellLower`, `warpShellUpper`, `warpFlag`, `warpWave`, `warpFish`, `warpRise`, /// `warpFisheye`, `warpInflate`, `warpSqueeze`, `warpTwist`, `warpCustom`. pub style: String, /// Bend, −100..100 (%). pub bend: f64, /// Horizontal and vertical distortion, −100..100 (%). pub horizontal: f64, pub vertical: f64, /// The style is applied rotated a quarter turn (`warpRotate` = vertical). pub rotate_vertical: bool, /// The warped area in content pixels: left, top, right, bottom. pub bounds: [f64; 4], /// Custom envelope: patch columns / rows and the (3·cols+1)×(3·rows+1) control points /// (content pixels, row-major). pub cols: usize, pub rows: usize, pub mesh: Vec<[f64; 2]>, } fn floats(v: Option<&DValue>) -> Vec { match v { Some(DValue::UnitFloats(_, v)) => v.clone(), Some(DValue::List(l)) => l .iter() .filter_map(|x| match x { DValue::Double(v) | DValue::UnitFloat(_, v) => Some(*v), DValue::Integer(i) => Some(*i as f64), _ => None, }) .collect(), _ => vec![], } } impl Warp { /// Read a `warp` descriptor; `None` for no warp. pub fn read(d: &Descriptor) -> Option { let style = d.enum_value("warpStyle").unwrap_or("warpNone").to_string(); let b = d.obj("bounds"); let bounds = b .map(|b| [b.num("Left").unwrap_or(0.0), b.num("Top ").unwrap_or(0.0), b.num("Rght").unwrap_or(0.0), b.num("Btom").unwrap_or(0.0)]) .unwrap_or([0.0; 4]); let mut w = Warp { bend: d.num("warpValue").unwrap_or(0.0), horizontal: d.num("warpPerspective").unwrap_or(0.0), vertical: d.num("warpPerspectiveOther").unwrap_or(0.0), rotate_vertical: d.enum_value("warpRotate") == Some("Vrtc"), bounds, cols: 1, rows: 1, mesh: vec![], style, }; if let Some(env) = d.obj("customEnvelopeWarp") { let cols = floats(env.get("quiltSliceX")).len().saturating_sub(1).max(1); let rows = floats(env.get("quiltSliceY")).len().saturating_sub(1).max(1); if let Some(DValue::ObjectArray(_, pts)) = env.get("meshPoints") { let xs = floats(pts.get("Hrzn")); let ys = floats(pts.get("Vrtc")); let n = (3 * cols + 1) * (3 * rows + 1); if xs.len() == n && ys.len() == n { w.cols = cols; w.rows = rows; w.mesh = xs.into_iter().zip(ys).map(|(x, y)| [x, y]).collect(); } } } let identity = w.style == "warpNone" || (w.style != "warpCustom" && w.bend == 0.0 && w.horizontal == 0.0 && w.vertical == 0.0); let custom_ok = w.style != "warpCustom" || !w.mesh.is_empty(); (!identity && custom_ok).then_some(w) } /// The warp of a content point (content pixels → content pixels). pub fn apply(&self, p: [f64; 2], w: f64, h: f64) -> [f64; 2] { let [l, t, r, b] = if self.bounds[2] > self.bounds[0] && self.bounds[3] > self.bounds[1] { self.bounds } else { [0.0, 0.0, w, h] }; let (bw, bh) = (r - l, b - t); let (u, v) = ((p[0] - l) / bw, (p[1] - t) / bh); if self.style == "warpCustom" { return self.mesh_point(u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)); } // Styles work in a centred frame (pixels), optionally rotated a quarter turn. let (x, y, fw, fh) = if self.rotate_vertical { (bh * (v - 0.5), -bw * (u - 0.5), bh, bw) } else { (bw * (u - 0.5), bh * (v - 0.5), bw, bh) }; let [mut x2, mut y2] = style_map(&self.style, self.bend / 100.0, x, y, fw, fh); // Distortions: a perspective-like taper across the width (horizontal) or height. let (hd, vd) = if self.rotate_vertical { (self.vertical / 100.0, self.horizontal / 100.0) } else { (self.horizontal / 100.0, self.vertical / 100.0) }; let s = (x2 / (fw / 2.0)).clamp(-1.5, 1.5); let tt = (y2 / (fh / 2.0)).clamp(-1.5, 1.5); y2 *= 1.0 + hd * s; x2 *= 1.0 + vd * tt; let (ox, oy) = if self.rotate_vertical { (-y2, x2) } else { (x2, y2) }; [l + bw / 2.0 + ox, t + bh / 2.0 + oy] } /// A point of the custom quilt mesh at (u, v) ∈ [0, 1]². fn mesh_point(&self, u: f64, v: f64) -> [f64; 2] { let (cols, rows) = (self.cols, self.rows); let pu = (u * cols as f64).min(cols as f64 - 1e-9); let pv = (v * rows as f64).min(rows as f64 - 1e-9); let (ci, cj) = (pu.floor() as usize, pv.floor() as usize); let (fu, fv) = (pu - ci as f64, pv - cj as f64); let stride = 3 * cols + 1; let bern = |t: f64| { let s = 1.0 - t; [s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t] }; let (bu, bv) = (bern(fu), bern(fv)); let mut out = [0.0; 2]; for j in 0..4 { for i in 0..4 { let q = self.mesh[(3 * cj + j) * stride + 3 * ci + i]; let k = bu[i] * bv[j]; out[0] += q[0] * k; out[1] += q[1] * k; } } out } } /// A named style in a centred frame `fw`×`fh` (y down), bend `b` ∈ [−1, 1]. fn style_map(style: &str, b: f64, x: f64, y: f64, fw: f64, fh: f64) -> [f64; 2] { use std::f64::consts::PI; let (hw, hh) = (fw / 2.0, fh / 2.0); let s = x / hw; // −1..1 across let t = y / hh; // −1..1 down // Concentric arcs: the midline bends through an angle of b·π, rows keep their distance. let arc = |x: f64, y: f64| -> [f64; 2] { let theta = b * PI; if theta.abs() < 1e-9 { return [x, y]; } let r = fw / theta; let phi = x / hw * theta / 2.0; let rr = r - y; // Arc centre below the shape for upward bends; keep the midline's centre fixed. [rr * phi.sin(), r - rr * phi.cos()] }; let bump = 1.0 - s * s; match style { "warpArc" => arc(x, y), "warpArcLower" => { let a = arc(x, y); let k = (t + 1.0) / 2.0; [x + (a[0] - x) * k, y + (a[1] - y) * k] } "warpArcUpper" => { let a = arc(x, y); let k = (1.0 - t) / 2.0; [x + (a[0] - x) * k, y + (a[1] - y) * k] } "warpArch" => [x, y - b * hh * bump], "warpBulge" => [x, y + t * b * hh * bump], "warpShellLower" => [x * (1.0 - b * 0.25 * (1.0 - t) / 2.0), y + (t + 1.0) / 2.0 * b * hh * bump], "warpShellUpper" => [x * (1.0 - b * 0.25 * (t + 1.0) / 2.0), y - (1.0 - t) / 2.0 * b * hh * bump], "warpFlag" => [x, y - b * hh * 0.5 * (PI * s).sin()], "warpWave" => [x + b * hw * 0.1 * (PI * t).sin(), y - b * hh * 0.5 * (PI * s).sin() * (1.0 - 0.5 * t)], "warpFish" => [x, y + t * b * hh * (PI * (s + 1.0) / 2.0).sin() * (1.0 - 0.5 * (s + 1.0) / 2.0)], "warpRise" => [x, y - b * hh * (PI * s / 2.0).sin()], "warpFisheye" | "warpInflate" => { let k = if style == "warpFisheye" { (1.0 - (s * s + t * t) / 2.0).max(0.0) } else { 1.0 }; [x * (1.0 + b * 0.5 * k * (1.0 - t * t)), y * (1.0 + b * 0.5 * k * bump)] } "warpSqueeze" => [x * (1.0 - b * 0.5 * (1.0 - t * t)), y * (1.0 + b * 0.5 * bump)], "warpTwist" => { let r = ((s * s + t * t) / 2.0).sqrt().min(1.0); let a = b * PI / 2.0 * (1.0 - r); let (sn, cs) = a.sin_cos(); [x * cs - y * sn, x * sn + y * cs] } _ => [x, y], } } /// The projective map taking the unit square's corners (0,0), (1,0), (1,1), (0,1) to `q` /// (Heckbert's square-to-quad), as a 3×3 matrix applied to (u, v, 1). fn square_to_quad(q: [[f64; 2]; 4]) -> [[f64; 3]; 3] { let [p0, p1, p2, p3] = q; let sx = p0[0] - p1[0] + p2[0] - p3[0]; let sy = p0[1] - p1[1] + p2[1] - p3[1]; if sx.abs() < 1e-12 && sy.abs() < 1e-12 { return [[p1[0] - p0[0], p2[0] - p1[0], p0[0]], [p1[1] - p0[1], p2[1] - p1[1], p0[1]], [0.0, 0.0, 1.0]]; } let (dx1, dx2) = (p1[0] - p2[0], p3[0] - p2[0]); let (dy1, dy2) = (p1[1] - p2[1], p3[1] - p2[1]); let den = dx1 * dy2 - dx2 * dy1; let g = (sx * dy2 - dx2 * sy) / den; let h = (dx1 * sy - sx * dy1) / den; [[p1[0] - p0[0] + g * p1[0], p3[0] - p0[0] + h * p3[0], p0[0]], [p1[1] - p0[1] + g * p1[1], p3[1] - p0[1] + h * p3[1], p0[1]], [g, h, 1.0]] } impl SmartObject { /// Whether the corner quad is a parallelogram (an affine placement). pub fn is_affine(&self) -> bool { let q = self.quad; let ex = q[0][0] + q[2][0] - q[1][0] - q[3][0]; let ey = q[0][1] + q[2][1] - q[1][1] - q[3][1]; ex.abs() < 1e-3 && ey.abs() < 1e-3 } /// Whether placing needs baking (a perspective quad or a warp). pub fn needs_bake(&self) -> bool { !self.is_affine() || self.warp.is_some() } /// A content point (pixels of a `w`×`h` content) in document pixels: the warp, then the quad. pub fn map(&self, p: [f64; 2], w: f64, h: f64) -> [f64; 2] { let p = match &self.warp { Some(wp) => wp.apply(p, w, h), None => p, }; let m = square_to_quad(self.quad); let (u, v) = (p[0] / w.max(1e-9), p[1] / h.max(1e-9)); let z = m[2][0] * u + m[2][1] * v + m[2][2]; let z = if z.abs() < 1e-12 { 1e-12 } else { z }; [(m[0][0] * u + m[0][1] * v + m[0][2]) / z, (m[1][0] * u + m[1][1] * v + m[1][2]) / z] } /// The document-space bounds (x0, y0, x1, y1) of the placed (warped) content. pub fn placed_bounds(&self, w: f64, h: f64) -> [f64; 4] { let n = 32; let mut b = [f64::MAX, f64::MAX, f64::MIN, f64::MIN]; for j in 0..=n { for i in 0..=n { let p = self.map([i as f64 / n as f64 * w, j as f64 / n as f64 * h], w, h); b = [b[0].min(p[0]), b[1].min(p[1]), b[2].max(p[0]), b[3].max(p[1])]; } } [b[0].floor(), b[1].floor(), b[2].ceil(), b[3].ceil()] } /// The baked size for a `w`×`h` content at `k` pixels per document pixel. pub fn placed_size(&self, w: f64, h: f64, k: f64) -> (u32, u32) { let bb = self.placed_bounds(w, h); (((bb[2] - bb[0]) * k).ceil().clamp(1.0, 16384.0) as u32, ((bb[3] - bb[1]) * k).ceil().clamp(1.0, 16384.0) as u32) } /// Bake the content (straight RGBA) as placed: warped and mapped by the quad, rendered at /// `k` pixels per document pixel (into `size` pixels when given; see [`Self::placed_size`]). /// Returns the pixels and their document-space origin. pub fn render_placed(&self, content: &Pixels, k: f64, size: Option<(u32, u32)>) -> Option<(Pixels, [f64; 2])> { let (cw, ch) = (content.width as f64, content.height as f64); if content.width == 0 || content.height == 0 { return None; } let bb = self.placed_bounds(cw, ch); let (ow, oh) = match size { Some((w, h)) => (w.max(1), h.max(1)), None => self.placed_size(cw, ch, k), }; let (ow, oh) = (ow as usize, oh as usize); // A forward grid, filled triangle by triangle with inverse barycentric sampling. let n = 48usize; let mut dst = Vec::with_capacity((n + 1) * (n + 1)); let mut src = Vec::with_capacity((n + 1) * (n + 1)); for j in 0..=n { for i in 0..=n { let s = [i as f64 / n as f64 * cw, j as f64 / n as f64 * ch]; let d = self.map(s, cw, ch); dst.push([(d[0] - bb[0]) * k, (d[1] - bb[1]) * k]); src.push(s); } } let mut out = vec![[0.0f32; 4]; ow * oh]; let sample = |x: f64, y: f64| -> [f32; 4] { let (fx, fy) = ((x - 0.5).clamp(0.0, cw - 1.0), (y - 0.5).clamp(0.0, ch - 1.0)); let (x0, y0) = (fx.floor() as usize, fy.floor() as usize); let (x1, y1) = ((x0 + 1).min(content.width as usize - 1), (y0 + 1).min(content.height as usize - 1)); let (a, b) = ((fx - x0 as f64) as f32, (fy - y0 as f64) as f32); let at = |x: usize, y: usize| { let p = content.data[y * content.width as usize + x]; [p[0] * p[3], p[1] * p[3], p[2] * p[3], p[3]] }; let (p00, p10, p01, p11) = (at(x0, y0), at(x1, y0), at(x0, y1), at(x1, y1)); let mut o = [0.0; 4]; for c in 0..4 { o[c] = (p00[c] * (1.0 - a) + p10[c] * a) * (1.0 - b) + (p01[c] * (1.0 - a) + p11[c] * a) * b; } o }; let idx = |i: usize, j: usize| j * (n + 1) + i; for j in 0..n { for i in 0..n { for tri in [[idx(i, j), idx(i + 1, j), idx(i + 1, j + 1)], [idx(i, j), idx(i + 1, j + 1), idx(i, j + 1)]] { let [a, b, c] = tri.map(|t| dst[t]); let den = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]); if den.abs() < 1e-12 { continue; } let x0 = a[0].min(b[0]).min(c[0]).floor().max(0.0) as usize; let x1 = (a[0].max(b[0]).max(c[0]).ceil() as usize).min(ow); let y0 = a[1].min(b[1]).min(c[1]).floor().max(0.0) as usize; let y1 = (a[1].max(b[1]).max(c[1]).ceil() as usize).min(oh); for y in y0..y1 { for x in x0..x1 { let (px, py) = (x as f64 + 0.5, y as f64 + 0.5); let l1 = ((b[1] - c[1]) * (px - c[0]) + (c[0] - b[0]) * (py - c[1])) / den; let l2 = ((c[1] - a[1]) * (px - c[0]) + (a[0] - c[0]) * (py - c[1])) / den; let l3 = 1.0 - l1 - l2; if l1 < -1e-9 || l2 < -1e-9 || l3 < -1e-9 { continue; } let [sa, sb, sc] = tri.map(|t| src[t]); let sx = sa[0] * l1 + sb[0] * l2 + sc[0] * l3; let sy = sa[1] * l1 + sb[1] * l2 + sc[1] * l3; out[y * ow + x] = sample(sx, sy); } } } } } // Back to straight alpha. for p in &mut out { if p[3] > 0.0 { for c in 0..3 { p[c] /= p[3]; } } } Some((Pixels { width: ow as u32, height: oh as u32, data: out }, [bb[0], bb[1]])) } } #[cfg(test)] mod tests { use super::*; fn so(quad: [[f64; 2]; 4], warp: Option) -> SmartObject { SmartObject { uuid: "x".into(), quad, size: Some([20.0, 10.0]), warp } } #[test] fn perspective_quad_maps_corners_and_is_projective() { let q = [[10.0, 10.0], [50.0, 0.0], [50.0, 40.0], [10.0, 30.0]]; let s = so(q, None); assert!(!s.is_affine() && s.needs_bake()); for (c, want) in [([0.0, 0.0], q[0]), ([20.0, 0.0], q[1]), ([20.0, 10.0], q[2]), ([0.0, 10.0], q[3])] { let p = s.map(c, 20.0, 10.0); assert!((p[0] - want[0]).abs() < 1e-9 && (p[1] - want[1]).abs() < 1e-9, "{c:?} → {p:?}"); } // Projective, not bilinear: the content centre is where the diagonals cross. let c = s.map([10.0, 5.0], 20.0, 10.0); assert!((c[0] - 30.0).abs() > 0.5, "{c:?}"); let affine = so([[0.0, 0.0], [20.0, 0.0], [20.0, 10.0], [0.0, 10.0]], None); assert!(affine.is_affine() && !affine.needs_bake()); } #[test] fn custom_mesh_and_styles_warp() { // An identity 4×4 mesh shifted 3 px right in its interior row: interior points move. let mut mesh = vec![]; for j in 0..4 { for i in 0..4 { mesh.push([i as f64 * 20.0 / 3.0 + if j == 1 || j == 2 { 3.0 } else { 0.0 }, j as f64 * 10.0 / 3.0]); } } let w = Warp { style: "warpCustom".into(), bounds: [0.0, 0.0, 20.0, 10.0], cols: 1, rows: 1, mesh, ..Default::default() }; let p = w.apply([10.0, 5.0], 20.0, 10.0); assert!((p[0] - 12.25).abs() < 1e-9 && (p[1] - 5.0).abs() < 1e-9, "{p:?}"); assert_eq!(w.apply([0.0, 0.0], 20.0, 10.0), [0.0, 0.0]); // Arc: the top centre rises, the bottom centre too (concentric arcs); Bulge: top up, // bottom down; Flag keeps the centre. let style = |s: &str| Warp { style: s.into(), bend: 50.0, bounds: [0.0, 0.0, 200.0, 100.0], ..Default::default() }; let top = style("warpArc").apply([100.0, 0.0], 200.0, 100.0); let corner = style("warpArc").apply([0.0, 0.0], 200.0, 100.0); assert!(top[1] < corner[1], "arc {top:?} {corner:?}"); let b = style("warpBulge"); assert!(b.apply([100.0, 0.0], 200.0, 100.0)[1] < 0.0 && b.apply([100.0, 100.0], 200.0, 100.0)[1] > 100.0); assert!((style("warpFlag").apply([100.0, 50.0], 200.0, 100.0)[1] - 50.0).abs() < 1e-9); // Vertical rotation swaps the roles of the axes. let mut v = style("warpBulge"); v.rotate_vertical = true; let l = v.apply([0.0, 50.0], 200.0, 100.0); assert!(l[0] < 0.0 && (l[1] - 50.0).abs() < 1e-9, "{l:?}"); // Horizontal distortion: the right side grows taller. let d = Warp { style: "warpArc".into(), horizontal: 50.0, bounds: [0.0, 0.0, 200.0, 100.0], ..Default::default() }; assert!(d.apply([200.0, 0.0], 200.0, 100.0)[1] < d.apply([0.0, 0.0], 200.0, 100.0)[1]); } #[test] fn render_placed_bakes_a_perspective_quad() { // 2×1 content: red | green, pinned to a trapezoid. let content = Pixels { width: 2, height: 1, data: vec![[1.0, 0.0, 0.0, 1.0], [0.0, 1.0, 0.0, 1.0]] }; let s = SmartObject { uuid: "x".into(), quad: [[0.0, 0.0], [40.0, 5.0], [40.0, 15.0], [0.0, 20.0]], size: None, warp: None }; let (px, origin) = s.render_placed(&content, 1.0, None).unwrap(); assert_eq!(origin, [0.0, 0.0]); assert_eq!((px.width, px.height), (40, 20)); let at = |x: usize, y: usize| px.data[y * 40 + x]; assert!(at(5, 10)[0] > 0.9 && at(5, 10)[3] > 0.99, "{:?}", at(5, 10)); assert!(at(35, 10)[1] > 0.9, "{:?}", at(35, 10)); assert_eq!(at(38, 1)[3], 0.0, "outside the trapezoid"); } }