Skip to main content

ag_psd/
csh.rs

1/*
2File: crates/ag-psd/src/csh.rs
3
4Purpose:
5чтение custom shapes Photoshop (.csh).
6
7Source compatibility:
8- порт upstream-файла `test/ag-psd/src/csh.ts`.
9- upstream предоставляет ТОЛЬКО `readCsh` (read-only). `write_csh` добавлен здесь
10  как симметричная функция (нужна для round-trip теста); upstream-аналога нет.
11- `readVectorMask` / `readBezierKnot` / `booleanOperations` из `additionalInfo.ts`
12  ещё не портированы в крейте — портированы локально здесь как `read_vector_mask`
13  (DEPENDENCY GAP: должны переехать в `additional_info` при его портировании).
14
15Main responsibilities:
16- декодировать/кодировать секции 'cush' с векторными контурами фигур.
17*/
18
19use crate::psd::{BezierKnot, BezierPath, BooleanOperation, FillRule, LayerVectorMask};
20use crate::reader::{
21    check_signature, read_fixed_point_path32, read_int16, read_pascal_string, read_uint16,
22    read_uint32, read_unicode_string, skip_bytes, PsdReader, ReadError, ReadResult,
23};
24use crate::writer::{
25    create_writer, get_writer_buffer, write_fixed_point_path32, write_int16, write_pascal_string,
26    write_uint16, write_uint32, write_unicode_string, write_zeros, PsdWriter,
27};
28
29/// Элемент `Csh.shapes`: `LayerVectorMask & { name, id, width, height }`.
30#[derive(Debug, Clone, Default)]
31pub struct CshShape {
32    pub name: String,
33    pub id: String,
34    pub width: u32,
35    pub height: u32,
36    pub mask: LayerVectorMask,
37}
38
39/// TS `Csh`.
40#[derive(Debug, Clone, Default)]
41pub struct Csh {
42    pub shapes: Vec<CshShape>,
43}
44
45/// `booleanOperations: BooleanOperation[]` из additionalInfo.ts.
46fn boolean_operation(index: i16) -> Option<BooleanOperation> {
47    match index {
48        0 => Some(BooleanOperation::Exclude),
49        1 => Some(BooleanOperation::Combine),
50        2 => Some(BooleanOperation::Subtract),
51        3 => Some(BooleanOperation::Intersect),
52        _ => None,
53    }
54}
55
56/// Порт `readBezierKnot(reader, width, height)`.
57fn read_bezier_knot(reader: &mut PsdReader, width: f64, height: f64) -> ReadResult<Vec<f64>> {
58    let y0 = read_fixed_point_path32(reader)? * height;
59    let x0 = read_fixed_point_path32(reader)? * width;
60    let y1 = read_fixed_point_path32(reader)? * height;
61    let x1 = read_fixed_point_path32(reader)? * width;
62    let y2 = read_fixed_point_path32(reader)? * height;
63    let x2 = read_fixed_point_path32(reader)? * width;
64    Ok(vec![x0, y0, x1, y1, x2, y2])
65}
66
67/// Порт `readVectorMask(reader, vectorMask, width, height, size)`.
68///
69/// DEPENDENCY GAP: должна жить в `additional_info` (ещё не портирован).
70fn read_vector_mask(
71    reader: &mut PsdReader,
72    vector_mask: &mut LayerVectorMask,
73    width: f64,
74    height: f64,
75    size: usize,
76) -> ReadResult<()> {
77    let end = reader.offset + size;
78    let mut current: Option<usize> = None; // индекс активного path в vector_mask.paths
79
80    while end.saturating_sub(reader.offset) >= 26 {
81        let selector = read_uint16(reader)?;
82
83        match selector {
84            0 | 3 => {
85                // Closed (0) / Open (3) subpath length record
86                let _count = read_uint16(reader)?;
87                let bool_op = read_int16(reader)?;
88                let flags = read_uint16(reader)?; // bit 1 always 1 ?
89                skip_bytes(reader, 18);
90                let mut path = BezierPath {
91                    open: selector == 3,
92                    operation: None,
93                    knots: Vec::new(),
94                    fill_rule: if flags == 2 {
95                        FillRule::NonZero
96                    } else {
97                        FillRule::EvenOdd
98                    },
99                };
100                if bool_op != -1 {
101                    path.operation = boolean_operation(bool_op);
102                }
103                vector_mask.paths.push(path);
104                current = Some(vector_mask.paths.len() - 1);
105            }
106            1 | 2 | 4 | 5 => {
107                // Bezier knot, linked (1/4) or unlinked (2/5)
108                let points = read_bezier_knot(reader, width, height)?;
109                let idx = current
110                    .ok_or_else(|| ReadError::StrictViolation("Invalid vmsk section".to_string()))?;
111                vector_mask.paths[idx].knots.push(BezierKnot {
112                    linked: selector == 1 || selector == 4,
113                    points,
114                });
115            }
116            6 => {
117                // Path fill rule record
118                skip_bytes(reader, 24);
119            }
120            7 => {
121                // Clipboard record
122                let top = read_fixed_point_path32(reader)?;
123                let left = read_fixed_point_path32(reader)?;
124                let bottom = read_fixed_point_path32(reader)?;
125                let right = read_fixed_point_path32(reader)?;
126                let resolution = read_fixed_point_path32(reader)?;
127                skip_bytes(reader, 4);
128                vector_mask.clipboard = Some(crate::psd::VectorMaskClipboard {
129                    top,
130                    left,
131                    bottom,
132                    right,
133                    resolution,
134                });
135            }
136            8 => {
137                // Initial fill rule record
138                vector_mask.fill_starts_with_all_pixels = Some(read_uint16(reader)? != 0);
139                skip_bytes(reader, 22);
140            }
141            _ => return Err(ReadError::StrictViolation("Invalid vmsk section".to_string())),
142        }
143    }
144
145    Ok(())
146}
147
148/// Порт `readCsh(buffer)`.
149pub fn read_csh(buffer: &[u8]) -> ReadResult<Csh> {
150    let reader = &mut PsdReader::new(buffer, None, None);
151    let mut csh = Csh { shapes: Vec::new() };
152
153    check_signature(reader, "cush", None)?;
154    if read_uint32(reader)? != 2 {
155        return Err(ReadError::StrictViolation("Invalid version".to_string()));
156    }
157    let count = read_uint32(reader)?;
158
159    for _ in 0..count {
160        let name = read_unicode_string(reader)?;
161        while reader.offset % 4 != 0 {
162            reader.offset += 1; // pad to 4byte bounds
163        }
164        if read_uint32(reader)? != 1 {
165            return Err(ReadError::StrictViolation("Invalid shape version".to_string()));
166        }
167        let size = read_uint32(reader)? as usize;
168        let end = reader.offset + size;
169        let id = read_pascal_string(reader, 1)?;
170        // this might not be correct ???
171        let y1 = read_uint32(reader)?;
172        let x1 = read_uint32(reader)?;
173        let y2 = read_uint32(reader)?;
174        let x2 = read_uint32(reader)?;
175        let width = x2 - x1;
176        let height = y2 - y1;
177        let mut mask = LayerVectorMask::default();
178        read_vector_mask(
179            reader,
180            &mut mask,
181            width as f64,
182            height as f64,
183            end - reader.offset,
184        )?;
185        csh.shapes.push(CshShape {
186            name,
187            id,
188            width,
189            height,
190            mask,
191        });
192
193        reader.offset = end;
194    }
195
196    Ok(csh)
197}
198
199// ===========================================================================
200// Writer (симметрия read_csh; upstream-аналога нет)
201// ===========================================================================
202
203fn boolean_operation_index(op: BooleanOperation) -> i16 {
204    match op {
205        BooleanOperation::Exclude => 0,
206        BooleanOperation::Combine => 1,
207        BooleanOperation::Subtract => 2,
208        BooleanOperation::Intersect => 3,
209    }
210}
211
212fn write_bezier_knot(writer: &mut PsdWriter, points: &[f64], width: f64, height: f64) {
213    // обратный порядок read_bezier_knot: x делится на width, y — на height.
214    let safe = |v: f64, d: f64| if d != 0.0 { v / d } else { 0.0 };
215    write_fixed_point_path32(writer, safe(points[1], height)); // y0
216    write_fixed_point_path32(writer, safe(points[0], width)); // x0
217    write_fixed_point_path32(writer, safe(points[3], height)); // y1
218    write_fixed_point_path32(writer, safe(points[2], width)); // x1
219    write_fixed_point_path32(writer, safe(points[5], height)); // y2
220    write_fixed_point_path32(writer, safe(points[4], width)); // x2
221}
222
223fn write_vector_mask(writer: &mut PsdWriter, mask: &LayerVectorMask, width: f64, height: f64) {
224    // initial fill rule record (selector 8) если задано
225    if let Some(fill) = mask.fill_starts_with_all_pixels {
226        write_uint16(writer, 8);
227        write_uint16(writer, if fill { 1 } else { 0 });
228        write_zeros(writer, 22);
229    }
230
231    if let Some(clip) = &mask.clipboard {
232        write_uint16(writer, 7);
233        write_fixed_point_path32(writer, clip.top);
234        write_fixed_point_path32(writer, clip.left);
235        write_fixed_point_path32(writer, clip.bottom);
236        write_fixed_point_path32(writer, clip.right);
237        write_fixed_point_path32(writer, clip.resolution);
238        write_zeros(writer, 4);
239    }
240
241    for path in &mask.paths {
242        // subpath length record (selector 0 closed / 3 open)
243        write_uint16(writer, if path.open { 3 } else { 0 });
244        write_uint16(writer, path.knots.len() as u16); // count
245        write_int16(
246            writer,
247            path.operation.map(boolean_operation_index).unwrap_or(-1),
248        );
249        write_uint16(
250            writer,
251            match path.fill_rule {
252                FillRule::NonZero => 2,
253                FillRule::EvenOdd => 0,
254            },
255        );
256        write_zeros(writer, 18);
257
258        for knot in &path.knots {
259            // 1 closed-linked / 2 closed-unlinked / 4 open-linked / 5 open-unlinked
260            let selector = match (path.open, knot.linked) {
261                (false, true) => 1,
262                (false, false) => 2,
263                (true, true) => 4,
264                (true, false) => 5,
265            };
266            write_uint16(writer, selector);
267            write_bezier_knot(writer, &knot.points, width, height);
268        }
269    }
270}
271
272/// Запись custom shapes (симметрия `read_csh`; upstream-аналога нет).
273pub fn write_csh(csh: &Csh) -> Vec<u8> {
274    let mut writer = create_writer(4096);
275
276    for b in b"cush" {
277        crate::writer::write_uint8(&mut writer, *b);
278    }
279    write_uint32(&mut writer, 2); // version
280    write_uint32(&mut writer, csh.shapes.len() as u32);
281
282    for shape in &csh.shapes {
283        write_unicode_string(&mut writer, &shape.name);
284        while writer.offset % 4 != 0 {
285            crate::writer::write_uint8(&mut writer, 0);
286        }
287        write_uint32(&mut writer, 1); // shape version
288
289        // size placeholder, бэкпатчим после записи тела
290        let size_offset = writer.offset;
291        write_uint32(&mut writer, 0);
292        let body_start = writer.offset;
293
294        write_pascal_string(&mut writer, &shape.id, 1);
295        // bounds y1,x1,y2,x2 (предполагаем origin 0,0)
296        write_uint32(&mut writer, 0); // y1
297        write_uint32(&mut writer, 0); // x1
298        write_uint32(&mut writer, shape.height); // y2
299        write_uint32(&mut writer, shape.width); // x2
300
301        write_vector_mask(
302            &mut writer,
303            &shape.mask,
304            shape.width as f64,
305            shape.height as f64,
306        );
307
308        let size = (writer.offset - body_start) as u32;
309        writer.buffer[size_offset..size_offset + 4].copy_from_slice(&size.to_be_bytes());
310    }
311
312    get_writer_buffer(&writer)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    fn approx_eq(a: f64, b: f64) -> bool {
320        (a - b).abs() < 1e-3
321    }
322
323    fn sample() -> Csh {
324        let mut mask = LayerVectorMask::default();
325        mask.paths.push(BezierPath {
326            open: false,
327            operation: Some(BooleanOperation::Combine),
328            knots: vec![
329                BezierKnot {
330                    linked: true,
331                    points: vec![10.0, 20.0, 11.0, 21.0, 12.0, 22.0],
332                },
333                BezierKnot {
334                    linked: false,
335                    points: vec![30.0, 40.0, 31.0, 41.0, 32.0, 42.0],
336                },
337            ],
338            fill_rule: FillRule::NonZero,
339        });
340
341        Csh {
342            shapes: vec![CshShape {
343                name: "Square".to_string(),
344                id: "abc123".to_string(),
345                width: 100,
346                height: 80,
347                mask,
348            }],
349        }
350    }
351
352    #[test]
353    fn csh_round_trip() {
354        let csh = sample();
355        let bytes = write_csh(&csh);
356        let decoded = read_csh(&bytes).expect("read_csh");
357
358        assert_eq!(decoded.shapes.len(), 1);
359        let s = &decoded.shapes[0];
360        assert_eq!(s.name, "Square");
361        assert_eq!(s.id, "abc123");
362        assert_eq!(s.width, 100);
363        assert_eq!(s.height, 80);
364        assert_eq!(s.mask.paths.len(), 1);
365
366        let p = &s.mask.paths[0];
367        assert!(!p.open);
368        assert_eq!(p.operation, Some(BooleanOperation::Combine));
369        assert_eq!(p.fill_rule, FillRule::NonZero);
370        assert_eq!(p.knots.len(), 2);
371        assert!(p.knots[0].linked);
372        assert!(!p.knots[1].linked);
373
374        let orig = &csh.shapes[0].mask.paths[0].knots[0].points;
375        let got = &p.knots[0].points;
376        for i in 0..6 {
377            assert!(approx_eq(orig[i], got[i]), "knot point {} mismatch", i);
378        }
379    }
380
381    #[test]
382    fn csh_decodes_fixture() {
383        let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
384        path.pop();
385        path.pop();
386        path.push("test/ag-psd/test/csh-read/animals/src.csh");
387        if !path.exists() {
388            eprintln!("csh fixture missing, skipping");
389            return;
390        }
391        let data = std::fs::read(&path).unwrap();
392        let csh = read_csh(&data).expect("decode csh fixture");
393        assert!(!csh.shapes.is_empty());
394        let first = &csh.shapes[0];
395        assert_eq!(first.name, "Bone");
396        assert_eq!(first.id, "26a9b56b-d040-11d5-a39c-fd27718ef272");
397        assert_eq!(first.width, 194);
398        assert_eq!(first.height, 90);
399        assert!(!first.mask.paths.is_empty());
400        let p = &first.mask.paths[0];
401        assert!(!p.open);
402        assert_eq!(p.operation, Some(BooleanOperation::Combine));
403        assert!(!p.knots.is_empty());
404        // first knot first point ~57.249 (x0)
405        assert!((p.knots[0].points[0] - 57.249).abs() < 0.1);
406    }
407
408    #[test]
409    fn csh_rejects_bad_signature() {
410        let bytes = b"XXXX\x00\x00\x00\x02\x00\x00\x00\x00";
411        assert!(read_csh(bytes).is_err());
412    }
413}