use super::parser::SpriteParser;
use super::types::*;
use crate::error::{BinaryError, Result};
use crate::object::UnityObject;
use crate::texture::Texture2D;
use crate::unity_version::UnityVersion;
use image::{RgbaImage, imageops};
pub struct SpriteProcessor {
parser: SpriteParser,
config: SpriteConfig,
}
impl SpriteProcessor {
pub fn new(version: UnityVersion) -> Self {
Self {
parser: SpriteParser::new(version),
config: SpriteConfig::default(),
}
}
pub fn with_config(version: UnityVersion, config: SpriteConfig) -> Self {
Self {
parser: SpriteParser::new(version),
config,
}
}
pub fn parse_sprite(&self, object: &UnityObject) -> Result<SpriteResult> {
self.parser.parse_from_unity_object(object)
}
pub fn process_sprite_with_texture(
&self,
sprite_object: &UnityObject,
texture: &Texture2D,
) -> Result<SpriteResult> {
let mut result = self.parse_sprite(sprite_object)?;
if self.config.extract_images {
match self.extract_sprite_image(&result.sprite, texture) {
Ok(image_data) => {
result = result.with_image(image_data);
}
Err(e) => {
result.add_warning(format!("Failed to extract sprite image: {}", e));
}
}
}
Ok(result)
}
pub fn extract_sprite_image(&self, sprite: &Sprite, texture: &Texture2D) -> Result<Vec<u8>> {
let converter = crate::texture::Texture2DConverter::new(self.parser.version().clone());
let texture_image = converter.decode_to_image(texture)?;
let sprite_rect = sprite.get_rect();
let texture_width = texture_image.width();
let texture_height = texture_image.height();
if sprite_rect.x < 0.0
|| sprite_rect.y < 0.0
|| sprite_rect.x + sprite_rect.width > texture_width as f32
|| sprite_rect.y + sprite_rect.height > texture_height as f32
{
return Err(BinaryError::invalid_data(
"Sprite rect is outside texture bounds",
));
}
if let Some((max_width, max_height)) = self.config.max_sprite_size
&& (sprite_rect.width > max_width as f32 || sprite_rect.height > max_height as f32)
{
return Err(BinaryError::invalid_data(
"Sprite size exceeds maximum allowed size",
));
}
let x = sprite_rect.x as u32;
let y = sprite_rect.y as u32;
let width = sprite_rect.width as u32;
let height = sprite_rect.height as u32;
let flipped_y = texture_height - y - height;
let sprite_image =
imageops::crop_imm(&texture_image, x, flipped_y, width, height).to_image();
let final_image = if self.config.apply_transformations {
self.apply_sprite_transformations(sprite_image, sprite)?
} else {
sprite_image
};
let mut png_data = Vec::new();
{
use image::ImageEncoder;
use image::codecs::png::PngEncoder;
let encoder = PngEncoder::new(&mut png_data);
encoder
.write_image(
final_image.as_raw(),
final_image.width(),
final_image.height(),
image::ExtendedColorType::Rgba8,
)
.map_err(|e| BinaryError::generic(format!("Failed to encode PNG: {}", e)))?;
}
Ok(png_data)
}
fn apply_sprite_transformations(&self, image: RgbaImage, sprite: &Sprite) -> Result<RgbaImage> {
if sprite.offset_x != 0.0 || sprite.offset_y != 0.0 {
}
if sprite.pivot_x != 0.5 || sprite.pivot_y != 0.5 {
}
Ok(image)
}
pub fn process_sprite_atlas(&self, atlas_sprites: &[&UnityObject]) -> Result<SpriteAtlas> {
if !self.config.process_atlas {
return Err(BinaryError::unsupported("Atlas processing is disabled"));
}
let mut atlas = SpriteAtlas {
name: "SpriteAtlas".to_string(),
..Default::default()
};
for sprite_obj in atlas_sprites {
let sprite_result = self.parse_sprite(sprite_obj)?;
let sprite = sprite_result.sprite;
let sprite_info = SpriteInfo {
name: sprite.name.clone(),
rect: sprite.get_rect(),
offset: sprite.get_offset(),
pivot: sprite.get_pivot(),
border: sprite.get_border(),
pixels_to_units: sprite.pixels_to_units,
is_polygon: sprite.is_polygon,
texture_path_id: sprite.render_data.texture_path_id,
is_atlas_sprite: sprite.is_atlas_sprite(),
};
atlas.sprites.push(sprite_info);
if sprite.is_atlas_sprite() {
atlas.packed_sprites.push(sprite.name);
}
}
Ok(atlas)
}
pub fn get_supported_features(&self) -> Vec<&'static str> {
let version = self.parser.version();
let mut features = vec!["basic_sprite", "rect", "pivot"];
if version.major >= 5 {
features.push("border");
features.push("pixels_to_units");
}
if version.major >= 2017 {
features.push("polygon_sprites");
features.push("sprite_atlas");
}
if version.major >= 2018 {
features.push("sprite_mesh");
features.push("sprite_physics");
}
features
}
pub fn is_feature_supported(&self, feature: &str) -> bool {
self.get_supported_features().contains(&feature)
}
pub fn config(&self) -> &SpriteConfig {
&self.config
}
pub fn set_config(&mut self, config: SpriteConfig) {
self.config = config;
}
pub fn version(&self) -> &UnityVersion {
self.parser.version()
}
pub fn set_version(&mut self, version: UnityVersion) {
self.parser.set_version(version);
}
pub fn validate_sprite(&self, sprite: &Sprite) -> Result<()> {
if sprite.rect_width <= 0.0 || sprite.rect_height <= 0.0 {
return Err(BinaryError::invalid_data("Sprite has invalid dimensions"));
}
if sprite.pixels_to_units <= 0.0 {
return Err(BinaryError::invalid_data(
"Sprite has invalid pixels_to_units",
));
}
if sprite.pivot_x < 0.0
|| sprite.pivot_x > 1.0
|| sprite.pivot_y < 0.0
|| sprite.pivot_y > 1.0
{
return Err(BinaryError::invalid_data("Sprite pivot is out of bounds"));
}
if let Some((max_width, max_height)) = self.config.max_sprite_size
&& (sprite.rect_width > max_width as f32 || sprite.rect_height > max_height as f32)
{
return Err(BinaryError::invalid_data(
"Sprite size exceeds maximum allowed size",
));
}
Ok(())
}
pub fn get_sprite_stats(&self, sprites: &[&Sprite]) -> SpriteStats {
let mut stats = SpriteStats {
total_sprites: sprites.len(),
..Default::default()
};
for sprite in sprites {
stats.total_area += sprite.get_area();
if sprite.has_border() {
stats.nine_slice_count += 1;
}
if sprite.is_polygon {
stats.polygon_count += 1;
}
if sprite.is_atlas_sprite() {
stats.atlas_sprite_count += 1;
}
let area = sprite.get_area();
if area < 1024.0 {
stats.small_sprites += 1;
} else if area < 16384.0 {
stats.medium_sprites += 1;
} else {
stats.large_sprites += 1;
}
}
if !sprites.is_empty() {
stats.average_area = stats.total_area / sprites.len() as f32;
}
stats
}
}
impl Default for SpriteProcessor {
fn default() -> Self {
Self::new(UnityVersion::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct SpriteStats {
pub total_sprites: usize,
pub total_area: f32,
pub average_area: f32,
pub nine_slice_count: usize,
pub polygon_count: usize,
pub atlas_sprite_count: usize,
pub small_sprites: usize, pub medium_sprites: usize, pub large_sprites: usize, }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_processor_creation() {
let version = UnityVersion::default();
let processor = SpriteProcessor::new(version);
assert_eq!(processor.version(), &UnityVersion::default());
}
#[test]
fn test_supported_features() {
let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
let processor = SpriteProcessor::new(version);
let features = processor.get_supported_features();
assert!(features.contains(&"basic_sprite"));
assert!(features.contains(&"polygon_sprites"));
assert!(features.contains(&"sprite_atlas"));
assert!(processor.is_feature_supported("sprite_mesh"));
}
#[test]
fn test_sprite_validation() {
let processor = SpriteProcessor::default();
let mut sprite = Sprite::default();
assert!(processor.validate_sprite(&sprite).is_err());
sprite.rect_width = 100.0;
sprite.rect_height = 100.0;
assert!(processor.validate_sprite(&sprite).is_ok());
sprite.pivot_x = 2.0;
assert!(processor.validate_sprite(&sprite).is_err());
}
}