unity_asset_decode/sprite/
processor.rs

1//! Sprite processing implementation
2//!
3//! This module provides high-level sprite processing functionality including
4//! image extraction and sprite atlas handling.
5
6use super::parser::SpriteParser;
7use super::types::*;
8use crate::error::{BinaryError, Result};
9use crate::object::UnityObject;
10use crate::texture::Texture2D;
11use crate::unity_version::UnityVersion;
12use image::{RgbaImage, imageops};
13
14/// Sprite processor
15///
16/// This struct provides high-level methods for processing Unity Sprite objects,
17/// including parsing, image extraction, and atlas handling.
18pub struct SpriteProcessor {
19    parser: SpriteParser,
20    config: SpriteConfig,
21}
22
23impl SpriteProcessor {
24    /// Create a new Sprite processor
25    pub fn new(version: UnityVersion) -> Self {
26        Self {
27            parser: SpriteParser::new(version),
28            config: SpriteConfig::default(),
29        }
30    }
31
32    /// Create a Sprite processor with custom configuration
33    pub fn with_config(version: UnityVersion, config: SpriteConfig) -> Self {
34        Self {
35            parser: SpriteParser::new(version),
36            config,
37        }
38    }
39
40    /// Parse Sprite from Unity object
41    pub fn parse_sprite(&self, object: &UnityObject) -> Result<SpriteResult> {
42        self.parser.parse_from_unity_object(object)
43    }
44
45    /// Process sprite with image extraction
46    pub fn process_sprite_with_texture(
47        &self,
48        sprite_object: &UnityObject,
49        texture: &Texture2D,
50    ) -> Result<SpriteResult> {
51        let mut result = self.parse_sprite(sprite_object)?;
52
53        if self.config.extract_images {
54            match self.extract_sprite_image(&result.sprite, texture) {
55                Ok(image_data) => {
56                    result = result.with_image(image_data);
57                }
58                Err(e) => {
59                    result.add_warning(format!("Failed to extract sprite image: {}", e));
60                }
61            }
62        }
63
64        Ok(result)
65    }
66
67    /// Extract sprite image from texture
68    pub fn extract_sprite_image(&self, sprite: &Sprite, texture: &Texture2D) -> Result<Vec<u8>> {
69        // Get texture image data using converter
70        let converter = crate::texture::Texture2DConverter::new(self.parser.version().clone());
71        let texture_image = converter.decode_to_image(texture)?;
72
73        // Calculate sprite bounds
74        let sprite_rect = sprite.get_rect();
75        let texture_width = texture_image.width();
76        let texture_height = texture_image.height();
77
78        // Validate sprite bounds
79        if sprite_rect.x < 0.0
80            || sprite_rect.y < 0.0
81            || sprite_rect.x + sprite_rect.width > texture_width as f32
82            || sprite_rect.y + sprite_rect.height > texture_height as f32
83        {
84            return Err(BinaryError::invalid_data(
85                "Sprite rect is outside texture bounds",
86            ));
87        }
88
89        // Check size limits
90        if let Some((max_width, max_height)) = self.config.max_sprite_size
91            && (sprite_rect.width > max_width as f32 || sprite_rect.height > max_height as f32)
92        {
93            return Err(BinaryError::invalid_data(
94                "Sprite size exceeds maximum allowed size",
95            ));
96        }
97
98        // Extract sprite region
99        let x = sprite_rect.x as u32;
100        let y = sprite_rect.y as u32;
101        let width = sprite_rect.width as u32;
102        let height = sprite_rect.height as u32;
103
104        // Unity uses bottom-left origin, but image crate uses top-left
105        // So we need to flip the Y coordinate
106        let flipped_y = texture_height - y - height;
107
108        let sprite_image =
109            imageops::crop_imm(&texture_image, x, flipped_y, width, height).to_image();
110
111        // Apply transformations if enabled
112        let final_image = if self.config.apply_transformations {
113            self.apply_sprite_transformations(sprite_image, sprite)?
114        } else {
115            sprite_image
116        };
117
118        // Convert to PNG bytes
119        let mut png_data = Vec::new();
120        {
121            use image::ImageEncoder;
122            use image::codecs::png::PngEncoder;
123
124            let encoder = PngEncoder::new(&mut png_data);
125            encoder
126                .write_image(
127                    final_image.as_raw(),
128                    final_image.width(),
129                    final_image.height(),
130                    image::ExtendedColorType::Rgba8,
131                )
132                .map_err(|e| BinaryError::generic(format!("Failed to encode PNG: {}", e)))?;
133        }
134
135        Ok(png_data)
136    }
137
138    /// Apply sprite transformations (pivot, offset, etc.)
139    fn apply_sprite_transformations(&self, image: RgbaImage, sprite: &Sprite) -> Result<RgbaImage> {
140        // Apply offset if needed
141        if sprite.offset_x != 0.0 || sprite.offset_y != 0.0 {
142            // For now, we don't apply offset transformations to the image itself
143            // This would require creating a larger canvas and positioning the sprite
144            // which is more complex and depends on the use case
145        }
146
147        // Apply pivot transformations if needed
148        if sprite.pivot_x != 0.5 || sprite.pivot_y != 0.5 {
149            // Similar to offset, pivot transformations are typically handled
150            // by the rendering system rather than modifying the image data
151        }
152
153        Ok(image)
154    }
155
156    /// Process sprite atlas
157    pub fn process_sprite_atlas(&self, atlas_sprites: &[&UnityObject]) -> Result<SpriteAtlas> {
158        if !self.config.process_atlas {
159            return Err(BinaryError::unsupported("Atlas processing is disabled"));
160        }
161
162        let mut atlas = SpriteAtlas {
163            name: "SpriteAtlas".to_string(),
164            ..Default::default()
165        };
166
167        for sprite_obj in atlas_sprites {
168            let sprite_result = self.parse_sprite(sprite_obj)?;
169            let sprite = sprite_result.sprite;
170
171            let sprite_info = SpriteInfo {
172                name: sprite.name.clone(),
173                rect: sprite.get_rect(),
174                offset: sprite.get_offset(),
175                pivot: sprite.get_pivot(),
176                border: sprite.get_border(),
177                pixels_to_units: sprite.pixels_to_units,
178                is_polygon: sprite.is_polygon,
179                texture_path_id: sprite.render_data.texture_path_id,
180                is_atlas_sprite: sprite.is_atlas_sprite(),
181            };
182
183            atlas.sprites.push(sprite_info);
184
185            if sprite.is_atlas_sprite() {
186                atlas.packed_sprites.push(sprite.name);
187            }
188        }
189
190        Ok(atlas)
191    }
192
193    /// Get supported sprite features for this Unity version
194    pub fn get_supported_features(&self) -> Vec<&'static str> {
195        let version = self.parser.version();
196        let mut features = vec!["basic_sprite", "rect", "pivot"];
197
198        if version.major >= 5 {
199            features.push("border");
200            features.push("pixels_to_units");
201        }
202
203        if version.major >= 2017 {
204            features.push("polygon_sprites");
205            features.push("sprite_atlas");
206        }
207
208        if version.major >= 2018 {
209            features.push("sprite_mesh");
210            features.push("sprite_physics");
211        }
212
213        features
214    }
215
216    /// Check if a feature is supported
217    pub fn is_feature_supported(&self, feature: &str) -> bool {
218        self.get_supported_features().contains(&feature)
219    }
220
221    /// Get the current configuration
222    pub fn config(&self) -> &SpriteConfig {
223        &self.config
224    }
225
226    /// Set the configuration
227    pub fn set_config(&mut self, config: SpriteConfig) {
228        self.config = config;
229    }
230
231    /// Get the Unity version
232    pub fn version(&self) -> &UnityVersion {
233        self.parser.version()
234    }
235
236    /// Set the Unity version
237    pub fn set_version(&mut self, version: UnityVersion) {
238        self.parser.set_version(version);
239    }
240
241    /// Validate sprite data
242    pub fn validate_sprite(&self, sprite: &Sprite) -> Result<()> {
243        // Check basic validity
244        if sprite.rect_width <= 0.0 || sprite.rect_height <= 0.0 {
245            return Err(BinaryError::invalid_data("Sprite has invalid dimensions"));
246        }
247
248        if sprite.pixels_to_units <= 0.0 {
249            return Err(BinaryError::invalid_data(
250                "Sprite has invalid pixels_to_units",
251            ));
252        }
253
254        // Check pivot bounds
255        if sprite.pivot_x < 0.0
256            || sprite.pivot_x > 1.0
257            || sprite.pivot_y < 0.0
258            || sprite.pivot_y > 1.0
259        {
260            return Err(BinaryError::invalid_data("Sprite pivot is out of bounds"));
261        }
262
263        // Check size limits if configured
264        if let Some((max_width, max_height)) = self.config.max_sprite_size
265            && (sprite.rect_width > max_width as f32 || sprite.rect_height > max_height as f32)
266        {
267            return Err(BinaryError::invalid_data(
268                "Sprite size exceeds maximum allowed size",
269            ));
270        }
271
272        Ok(())
273    }
274
275    /// Get sprite statistics
276    pub fn get_sprite_stats(&self, sprites: &[&Sprite]) -> SpriteStats {
277        let mut stats = SpriteStats {
278            total_sprites: sprites.len(),
279            ..Default::default()
280        };
281
282        for sprite in sprites {
283            stats.total_area += sprite.get_area();
284
285            if sprite.has_border() {
286                stats.nine_slice_count += 1;
287            }
288
289            if sprite.is_polygon {
290                stats.polygon_count += 1;
291            }
292
293            if sprite.is_atlas_sprite() {
294                stats.atlas_sprite_count += 1;
295            }
296
297            // Track size distribution
298            let area = sprite.get_area();
299            if area < 1024.0 {
300                stats.small_sprites += 1;
301            } else if area < 16384.0 {
302                stats.medium_sprites += 1;
303            } else {
304                stats.large_sprites += 1;
305            }
306        }
307
308        if !sprites.is_empty() {
309            stats.average_area = stats.total_area / sprites.len() as f32;
310        }
311
312        stats
313    }
314}
315
316impl Default for SpriteProcessor {
317    fn default() -> Self {
318        Self::new(UnityVersion::default())
319    }
320}
321
322/// Sprite processing statistics
323#[derive(Debug, Clone, Default)]
324pub struct SpriteStats {
325    pub total_sprites: usize,
326    pub total_area: f32,
327    pub average_area: f32,
328    pub nine_slice_count: usize,
329    pub polygon_count: usize,
330    pub atlas_sprite_count: usize,
331    pub small_sprites: usize,  // < 32x32
332    pub medium_sprites: usize, // 32x32 to 128x128
333    pub large_sprites: usize,  // > 128x128
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_processor_creation() {
342        let version = UnityVersion::default();
343        let processor = SpriteProcessor::new(version);
344        assert_eq!(processor.version(), &UnityVersion::default());
345    }
346
347    #[test]
348    fn test_supported_features() {
349        let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
350        let processor = SpriteProcessor::new(version);
351
352        let features = processor.get_supported_features();
353        assert!(features.contains(&"basic_sprite"));
354        assert!(features.contains(&"polygon_sprites"));
355        assert!(features.contains(&"sprite_atlas"));
356        assert!(processor.is_feature_supported("sprite_mesh"));
357    }
358
359    #[test]
360    fn test_sprite_validation() {
361        let processor = SpriteProcessor::default();
362        let mut sprite = Sprite::default();
363
364        // Invalid sprite (zero dimensions)
365        assert!(processor.validate_sprite(&sprite).is_err());
366
367        // Valid sprite
368        sprite.rect_width = 100.0;
369        sprite.rect_height = 100.0;
370        assert!(processor.validate_sprite(&sprite).is_ok());
371
372        // Invalid pivot
373        sprite.pivot_x = 2.0;
374        assert!(processor.validate_sprite(&sprite).is_err());
375    }
376}