Skip to main content

ag_psd/
ase.rs

1/*
2File: crates/ag-psd/src/ase.rs
3
4Purpose:
5чтение/запись палитр Adobe Swatch Exchange (.ase).
6
7Source compatibility:
8- порт upstream-файла `test/ag-psd/src/ase.ts`.
9- upstream предоставляет ТОЛЬКО `readAse` (read-only). `write_ase` добавлен здесь
10  как симметричная функция (нужна для round-trip теста); upstream-аналога нет.
11
12Main responsibilities:
13- декодировать/кодировать блоки ASEF (цвета, группы) с типами RGB/CMYK/Gray/LAB.
14*/
15
16use crate::reader::{
17    read_float32, read_signature, read_uint16, read_uint32, read_unicode_string_with_length,
18    PsdReader, ReadError, ReadResult,
19};
20use crate::writer::{
21    create_writer, get_writer_buffer, write_float32, write_signature, write_uint16, write_uint32,
22    PsdWriter,
23};
24
25/// TS `AseColorType`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AseColorType {
28    /// "global"
29    Global,
30    /// "spot"
31    Spot,
32    /// "normal"
33    Normal,
34}
35
36impl AseColorType {
37    /// `colorTypes[index]` — индекс из файла -> тип.
38    fn from_index(index: u16) -> Option<AseColorType> {
39        match index {
40            0 => Some(AseColorType::Global),
41            1 => Some(AseColorType::Spot),
42            2 => Some(AseColorType::Normal),
43            _ => None,
44        }
45    }
46
47    fn to_index(self) -> u16 {
48        match self {
49            AseColorType::Global => 0,
50            AseColorType::Spot => 1,
51            AseColorType::Normal => 2,
52        }
53    }
54}
55
56/// TS `AseColor.color` union (RGB / CMYK / Gray / LAB), несёт `type`.
57#[derive(Debug, Clone, PartialEq)]
58pub enum AseColorValue {
59    /// 'RGB '
60    Rgb {
61        r: f32,
62        g: f32,
63        b: f32,
64        type_: AseColorType,
65    },
66    /// 'CMYK'
67    Cmyk {
68        c: f32,
69        m: f32,
70        y: f32,
71        k: f32,
72        type_: AseColorType,
73    },
74    /// 'Gray'
75    Gray { k: f32, type_: AseColorType },
76    /// 'LAB '
77    Lab {
78        l: f32,
79        a: f32,
80        b: f32,
81        type_: AseColorType,
82    },
83}
84
85/// TS `AseColor`.
86#[derive(Debug, Clone, PartialEq)]
87pub struct AseColor {
88    pub name: String,
89    pub color: AseColorValue,
90}
91
92/// TS `AseGroup`.
93#[derive(Debug, Clone, PartialEq)]
94pub struct AseGroup {
95    pub name: String,
96    pub colors: Vec<AseColor>,
97}
98
99/// TS `Ase.colors` union элемент (`AseGroup | AseColor`).
100#[derive(Debug, Clone, PartialEq)]
101pub enum AseEntry {
102    Color(AseColor),
103    Group(AseGroup),
104}
105
106/// TS `Ase`.
107#[derive(Debug, Clone, Default, PartialEq)]
108pub struct Ase {
109    pub colors: Vec<AseEntry>,
110}
111
112/// Порт `readAse(buffer)`.
113pub fn read_ase(buffer: &[u8]) -> ReadResult<Ase> {
114    let reader = &mut PsdReader::new(buffer, None, None);
115
116    let signature = read_signature(reader)?; // ASEF
117    if signature != "ASEF" {
118        return Err(ReadError::StrictViolation("Invalid signature".to_string()));
119    }
120    let version_major = read_uint16(reader)?; // 1
121    let version_minor = read_uint16(reader)?; // 0
122    if version_major != 1 || version_minor != 0 {
123        return Err(ReadError::StrictViolation("Invalid version".to_string()));
124    }
125    let blocks_count = read_uint32(reader)?;
126
127    let mut ase = Ase { colors: Vec::new() };
128    // `group` в upstream указывает либо в корень `ase`, либо в последнюю
129    // открытую группу. Здесь — индекс открытой группы в `ase.colors` (None = корень).
130    let mut current_group: Option<usize> = None;
131
132    for _ in 0..blocks_count {
133        let type_ = read_uint16(reader)?;
134        let length = read_uint32(reader)? as usize;
135        let end = reader.offset + length;
136
137        match type_ {
138            0x0001 => {
139                // color
140                let name_length = read_uint16(reader)? as usize;
141                let name = read_unicode_string_with_length(reader, name_length)?;
142                let color_mode = read_signature(reader)?;
143                let color = match color_mode.as_str() {
144                    "RGB " => AseColorValue::Rgb {
145                        r: read_float32(reader)?,
146                        g: read_float32(reader)?,
147                        b: read_float32(reader)?,
148                        type_: read_color_type(reader)?,
149                    },
150                    "CMYK" => AseColorValue::Cmyk {
151                        c: read_float32(reader)?,
152                        m: read_float32(reader)?,
153                        y: read_float32(reader)?,
154                        k: read_float32(reader)?,
155                        type_: read_color_type(reader)?,
156                    },
157                    "Gray" => AseColorValue::Gray {
158                        k: read_float32(reader)?,
159                        type_: read_color_type(reader)?,
160                    },
161                    "LAB " => AseColorValue::Lab {
162                        l: read_float32(reader)?,
163                        a: read_float32(reader)?,
164                        b: read_float32(reader)?,
165                        type_: read_color_type(reader)?,
166                    },
167                    _ => {
168                        return Err(ReadError::StrictViolation("Invalid color mode".to_string()))
169                    }
170                };
171                let entry = AseColor { name, color };
172                match current_group {
173                    Some(gi) => {
174                        if let AseEntry::Group(g) = &mut ase.colors[gi] {
175                            g.colors.push(entry);
176                        }
177                    }
178                    None => ase.colors.push(AseEntry::Color(entry)),
179                }
180            }
181            0xC001 => {
182                // group start
183                let name_length = read_uint16(reader)? as usize;
184                let name = read_unicode_string_with_length(reader, name_length)?;
185                ase.colors.push(AseEntry::Group(AseGroup {
186                    name,
187                    colors: Vec::new(),
188                }));
189                current_group = Some(ase.colors.len() - 1);
190            }
191            0xC002 => {
192                // group end
193                current_group = None;
194            }
195            _ => return Err(ReadError::StrictViolation("Invalid block type".to_string())),
196        }
197
198        reader.offset = end;
199    }
200
201    Ok(ase)
202}
203
204fn read_color_type(reader: &mut PsdReader) -> ReadResult<AseColorType> {
205    let index = read_uint16(reader)?;
206    AseColorType::from_index(index)
207        .ok_or_else(|| ReadError::StrictViolation(format!("Invalid color type: {}", index)))
208}
209
210/// Запись палитры в формат ASEF (симметрия `read_ase`; upstream-аналога нет).
211///
212/// Каждый блок пишется как `uint16 type` + `uint32 length` + payload длины
213/// `length`; имена кодируются как `uint16 codeUnitCount` (включая завершающий 0)
214/// и затем UTF-16 BE code units (как `readUnicodeStringWithLength`).
215pub fn write_ase(ase: &Ase) -> Vec<u8> {
216    let mut writer = create_writer(4096);
217
218    write_signature(&mut writer, "ASEF");
219    write_uint16(&mut writer, 1); // version major
220    write_uint16(&mut writer, 0); // version minor
221
222    // Считаем блоки: каждая группа = group-start + N цветов + group-end.
223    let mut blocks_count: u32 = 0;
224    for entry in &ase.colors {
225        match entry {
226            AseEntry::Color(_) => blocks_count += 1,
227            AseEntry::Group(g) => blocks_count += 2 + g.colors.len() as u32,
228        }
229    }
230    write_uint32(&mut writer, blocks_count);
231
232    for entry in &ase.colors {
233        match entry {
234            AseEntry::Color(c) => write_color_block(&mut writer, c),
235            AseEntry::Group(g) => {
236                write_block(&mut writer, 0xC001, |w| write_name(w, &g.name));
237                for c in &g.colors {
238                    write_color_block(&mut writer, c);
239                }
240                write_block(&mut writer, 0xC002, |_| {});
241            }
242        }
243    }
244
245    get_writer_buffer(&writer)
246}
247
248fn write_name(writer: &mut PsdWriter, name: &str) {
249    // длина в code units включает завершающий ноль (readUnicodeStringWithLength
250    // отбрасывает хвостовой \0 на последней позиции).
251    let units: Vec<u16> = name.encode_utf16().collect();
252    write_uint16(writer, (units.len() + 1) as u16);
253    for u in &units {
254        write_uint16(writer, *u);
255    }
256    write_uint16(writer, 0); // trailing null
257}
258
259fn write_color_block(writer: &mut PsdWriter, c: &AseColor) {
260    write_block(writer, 0x0001, |w| {
261        write_name(w, &c.name);
262        match &c.color {
263            AseColorValue::Rgb { r, g, b, type_ } => {
264                write_signature(w, "RGB ");
265                write_float32(w, *r);
266                write_float32(w, *g);
267                write_float32(w, *b);
268                write_uint16(w, type_.to_index());
269            }
270            AseColorValue::Cmyk { c, m, y, k, type_ } => {
271                write_signature(w, "CMYK");
272                write_float32(w, *c);
273                write_float32(w, *m);
274                write_float32(w, *y);
275                write_float32(w, *k);
276                write_uint16(w, type_.to_index());
277            }
278            AseColorValue::Gray { k, type_ } => {
279                write_signature(w, "Gray");
280                write_float32(w, *k);
281                write_uint16(w, type_.to_index());
282            }
283            AseColorValue::Lab { l, a, b, type_ } => {
284                write_signature(w, "LAB ");
285                write_float32(w, *l);
286                write_float32(w, *a);
287                write_float32(w, *b);
288                write_uint16(w, type_.to_index());
289            }
290        }
291    });
292}
293
294/// Пишет блок `uint16 type` + `uint32 length` + payload, бэкпатчит длину.
295fn write_block<F: FnOnce(&mut PsdWriter)>(writer: &mut PsdWriter, type_: u16, func: F) {
296    write_uint16(writer, type_);
297    let length_offset = writer.offset;
298    write_uint32(writer, 0); // placeholder
299    let start = writer.offset;
300    func(writer);
301    let length = (writer.offset - start) as u32;
302    writer.buffer[length_offset..length_offset + 4].copy_from_slice(&length.to_be_bytes());
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    fn sample() -> Ase {
310        Ase {
311            colors: vec![
312                AseEntry::Color(AseColor {
313                    name: "Red".to_string(),
314                    color: AseColorValue::Rgb {
315                        r: 1.0,
316                        g: 0.0,
317                        b: 0.0,
318                        type_: AseColorType::Global,
319                    },
320                }),
321                AseEntry::Group(AseGroup {
322                    name: "Grays".to_string(),
323                    colors: vec![
324                        AseColor {
325                            name: "Mid".to_string(),
326                            color: AseColorValue::Gray {
327                                k: 0.5,
328                                type_: AseColorType::Normal,
329                            },
330                        },
331                        AseColor {
332                            name: "Cyanish".to_string(),
333                            color: AseColorValue::Cmyk {
334                                c: 1.0,
335                                m: 0.0,
336                                y: 0.0,
337                                k: 0.0,
338                                type_: AseColorType::Spot,
339                            },
340                        },
341                    ],
342                }),
343                AseEntry::Color(AseColor {
344                    name: "Lab".to_string(),
345                    color: AseColorValue::Lab {
346                        l: 50.0,
347                        a: 10.0,
348                        b: -20.0,
349                        type_: AseColorType::Normal,
350                    },
351                }),
352            ],
353        }
354    }
355
356    #[test]
357    fn ase_round_trip() {
358        let ase = sample();
359        let bytes = write_ase(&ase);
360        let decoded = read_ase(&bytes).expect("read_ase");
361        assert_eq!(ase, decoded);
362    }
363
364    #[test]
365    fn ase_rejects_bad_signature() {
366        let bytes = b"XXXX\x00\x01\x00\x00\x00\x00\x00\x00";
367        assert!(read_ase(bytes).is_err());
368    }
369
370    fn fixture(sub: &str) -> std::path::PathBuf {
371        let mut p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
372        p.pop();
373        p.pop();
374        p.push("test/ag-psd/test/ase-read");
375        p.push(sub);
376        p.push("src.ase");
377        p
378    }
379
380    #[test]
381    fn ase_decodes_photoshop_fixture() {
382        let path = fixture("from-photoshop");
383        if !path.exists() {
384            eprintln!("ase fixture missing, skipping");
385            return;
386        }
387        let data = std::fs::read(&path).unwrap();
388        let ase = read_ase(&data).expect("decode ase fixture");
389        assert!(!ase.colors.is_empty());
390        // first entry is an RGB color "#FFCCCC" global
391        match &ase.colors[0] {
392            AseEntry::Color(c) => {
393                assert_eq!(c.name, "#FFCCCC");
394                match &c.color {
395                    AseColorValue::Rgb { r, g, b, type_ } => {
396                        assert_eq!(*r, 1.0);
397                        assert!((*g - 0.79998779).abs() < 1e-4);
398                        assert!((*b - 0.79998779).abs() < 1e-4);
399                        assert_eq!(*type_, AseColorType::Global);
400                    }
401                    other => panic!("expected rgb, got {:?}", other),
402                }
403            }
404            other => panic!("expected color entry, got {:?}", other),
405        }
406    }
407
408    #[test]
409    fn ase_fixture_round_trip() {
410        let path = fixture("piratetrousle-dusk");
411        if !path.exists() {
412            eprintln!("ase fixture missing, skipping");
413            return;
414        }
415        let data = std::fs::read(&path).unwrap();
416        let ase = read_ase(&data).expect("decode");
417        // re-encode then decode again must match the decoded structure
418        let bytes = write_ase(&ase);
419        let again = read_ase(&bytes).expect("re-decode");
420        assert_eq!(ase, again);
421    }
422
423    #[test]
424    fn ase_smoke_header() {
425        let bytes = write_ase(&Ase { colors: vec![] });
426        // ASEF + version(1,0) + blocksCount(0)
427        assert_eq!(&bytes[0..4], b"ASEF");
428        let decoded = read_ase(&bytes).unwrap();
429        assert!(decoded.colors.is_empty());
430    }
431}