Skip to main content

oxihuman_export/
psd_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Photoshop PSD stub export.
6
7/// PSD layer blend mode.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum PsdBlendMode {
10    Normal,
11    Multiply,
12    Screen,
13    Overlay,
14}
15
16impl PsdBlendMode {
17    /// Four-character code for this blend mode.
18    pub fn fourcc(&self) -> &'static str {
19        match self {
20            Self::Normal => "norm",
21            Self::Multiply => "mul ",
22            Self::Screen => "scrn",
23            Self::Overlay => "over",
24        }
25    }
26}
27
28/// A PSD layer.
29#[derive(Debug, Clone)]
30pub struct PsdLayer {
31    pub name: String,
32    pub width: u32,
33    pub height: u32,
34    pub opacity: u8,
35    pub blend_mode: PsdBlendMode,
36    pub visible: bool,
37    pub pixels: Vec<[u8; 4]>,
38}
39
40impl PsdLayer {
41    /// Create a new PSD layer filled with solid color.
42    pub fn new_solid(name: &str, width: u32, height: u32, color: [u8; 4]) -> Self {
43        let pixels = vec![color; (width * height) as usize];
44        Self {
45            name: name.to_string(),
46            width,
47            height,
48            opacity: 255,
49            blend_mode: PsdBlendMode::Normal,
50            visible: true,
51            pixels,
52        }
53    }
54
55    /// Pixel count.
56    pub fn pixel_count(&self) -> usize {
57        self.pixels.len()
58    }
59}
60
61/// PSD document stub.
62#[derive(Debug, Clone)]
63pub struct PsdExport {
64    pub width: u32,
65    pub height: u32,
66    pub layers: Vec<PsdLayer>,
67}
68
69impl PsdExport {
70    /// Create a new PSD document.
71    pub fn new(width: u32, height: u32) -> Self {
72        Self {
73            width,
74            height,
75            layers: Vec::new(),
76        }
77    }
78
79    /// Add a layer.
80    pub fn add_layer(&mut self, layer: PsdLayer) {
81        self.layers.push(layer);
82    }
83
84    /// Return layer count.
85    pub fn layer_count(&self) -> usize {
86        self.layers.len()
87    }
88
89    /// Count visible layers.
90    pub fn visible_layer_count(&self) -> usize {
91        self.layers.iter().filter(|l| l.visible).count()
92    }
93}
94
95/// Validate PSD document.
96pub fn validate_psd(doc: &PsdExport) -> bool {
97    doc.width > 0 && doc.height > 0
98}
99
100/// Estimate PSD file size (stub).
101pub fn estimate_psd_bytes(doc: &PsdExport) -> usize {
102    let layer_data: usize = doc.layers.iter().map(|l| l.pixel_count() * 4 + 256).sum();
103    26 + layer_data + (doc.width * doc.height) as usize * 4
104}
105
106/// Serialize PSD metadata to JSON (stub).
107pub fn psd_metadata_json(doc: &PsdExport) -> String {
108    format!(
109        "{{\"width\":{},\"height\":{},\"layers\":{}}}",
110        doc.width,
111        doc.height,
112        doc.layer_count()
113    )
114}
115
116/// Find layer by name.
117pub fn find_psd_layer<'a>(doc: &'a PsdExport, name: &str) -> Option<&'a PsdLayer> {
118    doc.layers.iter().find(|l| l.name == name)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn sample_doc() -> PsdExport {
126        let mut doc = PsdExport::new(128, 128);
127        doc.add_layer(PsdLayer::new_solid(
128            "Background",
129            128,
130            128,
131            [200, 200, 200, 255],
132        ));
133        let mut fg = PsdLayer::new_solid("Foreground", 128, 128, [100, 0, 0, 200]);
134        fg.blend_mode = PsdBlendMode::Multiply;
135        doc.add_layer(fg);
136        doc
137    }
138
139    #[test]
140    fn test_layer_count() {
141        /* document has correct layer count */
142        assert_eq!(sample_doc().layer_count(), 2);
143    }
144
145    #[test]
146    fn test_visible_layer_count() {
147        /* all layers visible by default */
148        assert_eq!(sample_doc().visible_layer_count(), 2);
149    }
150
151    #[test]
152    fn test_validate_valid() {
153        /* valid document passes */
154        assert!(validate_psd(&sample_doc()));
155    }
156
157    #[test]
158    fn test_blend_mode_fourcc() {
159        /* blend mode fourcc codes are distinct */
160        assert_ne!(
161            PsdBlendMode::Normal.fourcc(),
162            PsdBlendMode::Multiply.fourcc()
163        );
164    }
165
166    #[test]
167    fn test_estimate_bytes_positive() {
168        /* estimated size is positive */
169        assert!(estimate_psd_bytes(&sample_doc()) > 0);
170    }
171
172    #[test]
173    fn test_metadata_json() {
174        /* metadata JSON contains layer count */
175        let json = psd_metadata_json(&sample_doc());
176        assert!(json.contains("layers"));
177    }
178
179    #[test]
180    fn test_find_psd_layer() {
181        /* find_psd_layer locates layer by name */
182        let doc = sample_doc();
183        assert!(find_psd_layer(&doc, "Background").is_some());
184        assert!(find_psd_layer(&doc, "None").is_none());
185    }
186
187    #[test]
188    fn test_pixel_count() {
189        /* pixel count matches dimensions */
190        let l = PsdLayer::new_solid("L", 16, 16, [0; 4]);
191        assert_eq!(l.pixel_count(), 256);
192    }
193}