Skip to main content

csp_format/
effect.rs

1//! Layer effects (`Layer.LayerEffectInfo`): border (フチ), tone, watercolour
2//! edge, line extraction and their options, and the border drawing.
3//!
4//! The blob is big endian: the total size (`u32`), a version (`u32`, 2),
5//! then named sections. A name is a `u32` count of UTF-16BE units and the
6//! units. `EffectEdge` and `EffectTone`, the oldest two, have fixed sizes;
7//! every later section starts with its size (`u32`, counting itself).
8//! `LayerEffectAttached` = 1 switches the effects on; the blob keeps the
9//! settings of switched-off effects too.
10//!
11//! The border was measured against CSP's own export of a file made for it
12//! (`docs/validation/V6-made-files.md`): within 2/255 for widths 0.5–20,
13//! any colour, hard and soft edges, with and without
14//! `LayerEffectEdgeNewRender`.
15
16use crate::binary::{Reader, Writer};
17use crate::error::{Error, Result, bail};
18use crate::image::RgbaImage;
19
20/// Sizes of the sections that have no size field.
21const FIXED_SECTIONS: [(&str, usize); 2] = [("EffectEdge", 24), ("EffectTone", 76)];
22
23/// A parsed `LayerEffectInfo` blob; sections in file order.
24#[derive(Debug, Clone, PartialEq)]
25pub struct LayerEffects {
26    /// Format version (2).
27    pub version: u32,
28    /// (name, bytes) of each section, in stored order; the bytes follow the name.
29    pub sections: Vec<(String, Vec<u8>)>,
30}
31
32/// The border effect (フチ).
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct Border {
35    /// The border is on.
36    pub enabled: bool,
37    /// Width in canvas pixels.
38    pub width: f64,
39    /// Colour as 8-bit RGB.
40    pub color: [u8; 3],
41}
42
43impl LayerEffects {
44    /// Parses a `LayerEffectInfo` blob.
45    pub fn parse(data: &[u8]) -> Result<Self> {
46        let mut reader = Reader::new(data);
47        let total = reader.be_u32()? as usize;
48        if total != data.len() {
49            bail!("LayerEffectInfo says {total} bytes, has {}", data.len());
50        }
51        let version = reader.be_u32()?;
52        let mut sections = Vec::new();
53        while !reader.is_empty() {
54            let name = reader.utf16_name()?;
55            let size = match FIXED_SECTIONS.iter().find(|(fixed, _)| *fixed == name) {
56                Some((_, size)) => *size,
57                None => {
58                    let size = Reader::at(data, reader.position()).be_u32()? as usize;
59                    if size < 4 {
60                        bail!("Effect section {name} has size {size}");
61                    }
62                    size
63                }
64            };
65            let start = reader.position();
66            reader.skip(size)?;
67            // Newer CSP versions append fields after a section's stated size
68            // (EffectLine: 116 stated, 136 stored); they belong to the
69            // section, up to the next name.
70            while !reader.is_empty() && !is_section_name(data, reader.position()) {
71                reader.skip(1)?;
72            }
73            sections.push((name, data[start..reader.position()].to_vec()));
74        }
75        Ok(LayerEffects { version, sections })
76    }
77
78    /// The blob; unchanged sections give the same bytes.
79    pub fn to_bytes(&self) -> Vec<u8> {
80        let mut body = Writer::new();
81        body.be_u32(self.version);
82        for (name, payload) in &self.sections {
83            let units: Vec<u16> = name.encode_utf16().collect();
84            body.be_u32(units.len() as u32);
85            for unit in units {
86                body.be_u16(unit);
87            }
88            body.bytes(payload);
89        }
90        let mut out = Writer::new();
91        out.be_u32(body.len() as u32 + 4).bytes(&body.data);
92        out.data
93    }
94
95    /// The bytes of a section.
96    pub fn section(&self, name: &str) -> Option<&[u8]> {
97        self.sections.iter().find(|(n, _)| n == name).map(|(_, payload)| payload.as_slice())
98    }
99
100    /// The bytes of a section, mutably.
101    pub fn section_mut(&mut self, name: &str) -> Option<&mut Vec<u8>> {
102        self.sections.iter_mut().find(|(n, _)| n == name).map(|(_, payload)| payload)
103    }
104
105    /// The border settings (`EffectEdge`): on/off (`u32`), width (`f64`),
106    /// colour as three `u32` whose top byte is the 8-bit value.
107    pub fn border(&self) -> Option<Border> {
108        let mut reader = Reader::new(self.section("EffectEdge")?);
109        let enabled = reader.be_u32().ok()? != 0;
110        let width = reader.be_f64().ok()?;
111        let mut color = [0u8; 3];
112        for c in &mut color {
113            *c = (reader.be_u32().ok()? >> 24) as u8;
114        }
115        Some(Border { enabled, width, color })
116    }
117
118    /// Writes the border settings into `EffectEdge` (keeping its other bytes). Fails when the blob has no such section.
119    pub fn set_border(&mut self, border: Border) -> Result<()> {
120        let payload = self.section_mut("EffectEdge").ok_or_else(|| Error::format("No EffectEdge section"))?;
121        let mut writer = Writer::new();
122        writer.be_u32(border.enabled as u32).be_f64(border.width);
123        for c in border.color {
124            writer.be_u32(u32::from_be_bytes([c; 4]));
125        }
126        payload.splice(0..writer.len(), writer.data);
127        Ok(())
128    }
129
130    /// 描画の透明度を反映 (`EffectApplyOpacity`): the border follows the
131    /// layer's alpha. Defaults to on when the section is missing.
132    pub fn apply_opacity(&self) -> bool {
133        self.section("EffectApplyOpacity").and_then(|s| Reader::at(s, 4).be_u32().ok()).is_none_or(|v| v != 0)
134    }
135
136    /// Names of the switched-on effects among the ones with an on/off flag.
137    pub fn enabled(&self) -> Vec<&str> {
138        let mut names = Vec::new();
139        if self.border().is_some_and(|border| border.enabled) {
140            names.push("EffectEdge");
141        }
142        let flag = |name: &str, at: usize| self.section(name).and_then(|s| Reader::at(s, at).be_u32().ok());
143        for (name, at) in [("EffectTone", 0), ("EffectWaterEdge", 4), ("EffectLine", 4), ("EffectTonePosterize", 4)] {
144            if flag(name, at).is_some_and(|v| v != 0) {
145                names.push(name);
146            }
147        }
148        names
149    }
150}
151
152/// Whether a section name ("Effect…") starts at `position`.
153fn is_section_name(data: &[u8], position: usize) -> bool {
154    let mut reader = Reader::at(data, position);
155    let Ok(count) = reader.be_u32() else { return false };
156    (6..=64).contains(&count) && reader.bytes(12).is_ok_and(|units| units.chunks(2).map(|u| u[1]).eq(*b"Effect"))
157}
158
159/// Draw a border around `image` (straight alpha, 8 bit). The result is
160/// larger by the border's reach on every side; the second value is that
161/// margin, to move the image's origin by.
162///
163/// The border's alpha at a pixel is the largest `a(q) · k(|p − q|)` over
164/// the layer's pixels q, with `k(d) = clamp(width + 1.5 − d, 0, 1)` (pixel
165/// centres; `a` binarised when the border ignores the layer's alpha).
166/// The layer is drawn over its border, and the border only fills the
167/// alpha up to that value: the result's alpha is `max(a, border)`.
168pub fn draw_border(image: &RgbaImage, border: &Border, apply_opacity: bool) -> (RgbaImage, u32) {
169    let margin = (border.width + 1.5).ceil().max(0.0) as u32;
170    let (w, h) = (image.width + 2 * margin, image.height + 2 * margin);
171    let mut alpha = vec![0f32; (w * h) as usize];
172    for y in 0..image.height {
173        for x in 0..image.width {
174            let a = image.pixel(x, y)[3];
175            let a = if apply_opacity { a as f32 / 255.0 } else { (a > 0) as u8 as f32 };
176            alpha[((y + margin) * w + x + margin) as usize] = a;
177        }
178    }
179    let dilated = dilate(&alpha, w as usize, h as usize, border.width);
180    let mut out = RgbaImage::new(w, h);
181    let color = border.color.map(|c| c as f32);
182    for y in 0..h {
183        for x in 0..w {
184            let i = (y * w + x) as usize;
185            let inside = x >= margin && y >= margin && x < image.width + margin && y < image.height + margin;
186            let p = if inside { image.pixel(x - margin, y - margin) } else { [0; 4] };
187            let a = p[3] as f32 / 255.0;
188            let total = dilated[i].max(a);
189            if total <= 0.0 {
190                continue;
191            }
192            let edge = total - a;
193            let rgb = [0, 1, 2].map(|c| ((p[c] as f32 * a + color[c] * edge) / total).round().clamp(0.0, 255.0) as u8);
194            out.set_pixel(x, y, [rgb[0], rgb[1], rgb[2], (total * 255.0).round() as u8]);
195        }
196    }
197    (out, margin)
198}
199
200/// `max_q a(q) · clamp(width + 1.5 − |p − q|, 0, 1)` for every pixel p.
201/// Per row offset dy: a running maximum over the chord where the weight is
202/// 1 (van Herk / Gil-Werman, constant time per pixel), plus the few
203/// pixels at the chord's ends where the weight falls from 1 to 0.
204fn dilate(alpha: &[f32], w: usize, h: usize, width: f64) -> Vec<f32> {
205    let mut out = vec![0f32; w * h];
206    let (full, zero) = (width + 0.5, width + 1.5);
207    let reach = zero.ceil() as i64;
208    // Rows that have any alpha.
209    let used: Vec<bool> = (0..h).map(|y| alpha[y * w..(y + 1) * w].iter().any(|&a| a > 0.0)).collect();
210    let mut running = vec![0f32; w];
211    for dy in -reach..=reach {
212        let dyf = dy as f64;
213        let chord = if full > dyf.abs() { Some((full * full - dyf * dyf).sqrt().floor() as usize) } else { None };
214        let mut ramp = Vec::new();
215        for dx in -reach..=reach {
216            let d = (dx as f64).hypot(dyf);
217            let k = (zero - d).clamp(0.0, 1.0);
218            let in_chord = chord.is_some_and(|c| dx.unsigned_abs() as usize <= c);
219            if k > 0.0 && !in_chord {
220                ramp.push((dx, k as f32));
221            }
222        }
223        for y in 0..h as i64 {
224            let source = y + dy;
225            if source < 0 || source >= h as i64 || !used[source as usize] {
226                continue;
227            }
228            let row = &alpha[source as usize * w..(source as usize + 1) * w];
229            let target = &mut out[y as usize * w..(y as usize + 1) * w];
230            if let Some(half) = chord {
231                running_max(row, half, &mut running);
232                for (t, r) in target.iter_mut().zip(&running) {
233                    *t = t.max(*r);
234                }
235            }
236            for &(dx, k) in &ramp {
237                for x in 0..w as i64 {
238                    let sx = x + dx;
239                    if sx >= 0 && sx < w as i64 {
240                        let v = row[sx as usize] * k;
241                        if v > target[x as usize] {
242                            target[x as usize] = v;
243                        }
244                    }
245                }
246            }
247        }
248    }
249    out
250}
251
252/// `out[x] = max(row[x − half ..= x + half])` (clipped to the row).
253fn running_max(row: &[f32], half: usize, out: &mut [f32]) {
254    let n = row.len();
255    let size = 2 * half + 1;
256    // Prefix maxima within blocks (left to right) and suffix maxima (right to left).
257    let mut prefix = vec![0f32; n];
258    let mut suffix = vec![0f32; n];
259    for i in 0..n {
260        prefix[i] = if i % size == 0 { row[i] } else { prefix[i - 1].max(row[i]) };
261    }
262    for i in (0..n).rev() {
263        suffix[i] = if i % size == size - 1 || i == n - 1 { row[i] } else { suffix[i + 1].max(row[i]) };
264    }
265    for (x, o) in out.iter_mut().enumerate() {
266        let lo = x.saturating_sub(half);
267        let hi = (x + half).min(n - 1);
268        // [lo, hi] spans at most two blocks: suffix of the first, prefix of the second.
269        *o = if lo / size == hi / size {
270            row[lo..=hi].iter().copied().fold(0.0, f32::max)
271        } else {
272            suffix[lo].max(prefix[hi])
273        };
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn sample() -> Vec<u8> {
282        let mut effects = LayerEffects { version: 2, sections: Vec::new() };
283        let mut edge = Writer::new();
284        edge.be_u32(1).be_f64(5.0).be_u32(0xffff_ffff).be_u32(0).be_u32(0x8080_8080);
285        effects.sections.push(("EffectEdge".into(), edge.data));
286        effects.sections.push(("EffectTone".into(), vec![0; 76]));
287        effects.sections.push(("EffectApplyOpacity".into(), vec![0, 0, 0, 8, 0, 0, 0, 1]));
288        // A section with fields after its stated size (as EffectLine in CSP 3+).
289        effects.sections.push(("EffectLine".into(), vec![0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1]));
290        effects.to_bytes()
291    }
292
293    #[test]
294    fn parses_and_round_trips() {
295        let data = sample();
296        let effects = LayerEffects::parse(&data).unwrap();
297        assert_eq!(effects.to_bytes(), data);
298        assert_eq!(effects.border(), Some(Border { enabled: true, width: 5.0, color: [255, 0, 128] }));
299        assert!(effects.apply_opacity());
300        assert_eq!(effects.enabled(), ["EffectEdge"]);
301        let mut changed = effects.clone();
302        changed.set_border(Border { enabled: false, width: 2.5, color: [1, 2, 3] }).unwrap();
303        assert_eq!(LayerEffects::parse(&changed.to_bytes()).unwrap().border().unwrap().width, 2.5);
304        assert!(LayerEffects::parse(&data[..data.len() - 1]).is_err());
305    }
306
307    #[test]
308    fn border_matches_the_measured_profile() {
309        // One opaque pixel; width 3: full up to 3.5 px, half at 4 px, none from 4.5 px.
310        let mut image = RgbaImage::new(1, 1);
311        image.set_pixel(0, 0, [10, 20, 30, 255]);
312        let border = Border { enabled: true, width: 3.0, color: [0, 0, 0] };
313        let (out, margin) = draw_border(&image, &border, true);
314        assert_eq!(margin, 5);
315        let alpha = |dx: u32, dy: u32| out.pixel(margin + dx, margin + dy)[3];
316        assert_eq!(out.pixel(margin, margin), [10, 20, 30, 255]);
317        assert_eq!(alpha(3, 0), 255);
318        assert_eq!(alpha(2, 2), 255); // 2.83
319        assert_eq!(alpha(4, 0), 128); // 4.0 → 0.5
320        assert_eq!(alpha(3, 3), 66); // 4.24 → 0.26
321        assert_eq!(alpha(5, 0), 0);
322        assert_eq!(out.pixel(margin + 4, margin), [0, 0, 0, 128]);
323    }
324
325    #[test]
326    fn running_max_matches_brute_force() {
327        let row: Vec<f32> = (0..37).map(|i| ((i * 7919) % 31) as f32).collect();
328        for half in 0..8 {
329            let mut out = vec![0.0; row.len()];
330            running_max(&row, half, &mut out);
331            for (x, value) in out.iter().enumerate() {
332                let lo = x.saturating_sub(half);
333                let hi = (x + half).min(row.len() - 1);
334                assert_eq!(*value, row[lo..=hi].iter().copied().fold(0.0, f32::max), "half {half} x {x}");
335            }
336        }
337    }
338}