unity_asset_binary/texture/helpers/
swizzler.rs

1//! Texture swizzling utilities
2//!
3//! This module provides functionality for texture data manipulation and channel swizzling.
4//! Inspired by UnityPy's TextureSwizzler.
5
6use crate::error::{BinaryError, Result};
7use image::RgbaImage;
8
9/// Texture swizzling utility
10///
11/// This struct provides methods for manipulating texture data,
12/// including channel swizzling and data transformation.
13pub struct TextureSwizzler;
14
15impl TextureSwizzler {
16    /// Swap red and blue channels (RGBA -> BGRA or vice versa)
17    pub fn swap_rb_channels(image: &mut RgbaImage) {
18        for pixel in image.pixels_mut() {
19            let temp = pixel[0]; // Store R
20            pixel[0] = pixel[2]; // R = B
21            pixel[2] = temp; // B = R
22        }
23    }
24
25    /// Flip image vertically (Unity textures are often upside down)
26    pub fn flip_vertical(image: &RgbaImage) -> RgbaImage {
27        let (width, height) = image.dimensions();
28        let mut flipped = RgbaImage::new(width, height);
29
30        for y in 0..height {
31            for x in 0..width {
32                let src_pixel = image.get_pixel(x, y);
33                flipped.put_pixel(x, height - 1 - y, *src_pixel);
34            }
35        }
36
37        flipped
38    }
39
40    /// Flip image horizontally
41    pub fn flip_horizontal(image: &RgbaImage) -> RgbaImage {
42        let (width, height) = image.dimensions();
43        let mut flipped = RgbaImage::new(width, height);
44
45        for y in 0..height {
46            for x in 0..width {
47                let src_pixel = image.get_pixel(x, y);
48                flipped.put_pixel(width - 1 - x, y, *src_pixel);
49            }
50        }
51
52        flipped
53    }
54
55    /// Apply gamma correction
56    pub fn apply_gamma(image: &mut RgbaImage, gamma: f32) {
57        let inv_gamma = 1.0 / gamma;
58
59        for pixel in image.pixels_mut() {
60            // Apply gamma to RGB channels, leave alpha unchanged
61            for i in 0..3 {
62                let normalized = pixel[i] as f32 / 255.0;
63                let corrected = normalized.powf(inv_gamma);
64                pixel[i] = (corrected * 255.0).clamp(0.0, 255.0) as u8;
65            }
66        }
67    }
68
69    /// Convert to grayscale using luminance formula
70    pub fn to_grayscale(image: &RgbaImage) -> RgbaImage {
71        let (width, height) = image.dimensions();
72        let mut gray = RgbaImage::new(width, height);
73
74        for (x, y, pixel) in image.enumerate_pixels() {
75            // Use standard luminance formula
76            let luminance =
77                (0.299 * pixel[0] as f32 + 0.587 * pixel[1] as f32 + 0.114 * pixel[2] as f32) as u8;
78
79            gray.put_pixel(
80                x,
81                y,
82                image::Rgba([luminance, luminance, luminance, pixel[3]]),
83            );
84        }
85
86        gray
87    }
88
89    /// Premultiply alpha
90    pub fn premultiply_alpha(image: &mut RgbaImage) {
91        for pixel in image.pixels_mut() {
92            let alpha = pixel[3] as f32 / 255.0;
93
94            // Premultiply RGB channels by alpha
95            for i in 0..3 {
96                pixel[i] = (pixel[i] as f32 * alpha) as u8;
97            }
98        }
99    }
100
101    /// Unpremultiply alpha
102    pub fn unpremultiply_alpha(image: &mut RgbaImage) {
103        for pixel in image.pixels_mut() {
104            let alpha = pixel[3] as f32 / 255.0;
105
106            if alpha > 0.0 {
107                // Unpremultiply RGB channels by alpha
108                for i in 0..3 {
109                    pixel[i] = ((pixel[i] as f32 / alpha).clamp(0.0, 255.0)) as u8;
110                }
111            }
112        }
113    }
114
115    /// Apply channel mask (set specific channels to 0 or 255)
116    pub fn apply_channel_mask(image: &mut RgbaImage, mask: [Option<u8>; 4]) {
117        for pixel in image.pixels_mut() {
118            for (i, &mask_value) in mask.iter().enumerate() {
119                if let Some(value) = mask_value {
120                    pixel[i] = value;
121                }
122            }
123        }
124    }
125
126    /// Extract single channel as grayscale image
127    pub fn extract_channel(image: &RgbaImage, channel: usize) -> Result<RgbaImage> {
128        if channel >= 4 {
129            return Err(BinaryError::invalid_data("Channel index must be 0-3"));
130        }
131
132        let (width, height) = image.dimensions();
133        let mut result = RgbaImage::new(width, height);
134
135        for (x, y, pixel) in image.enumerate_pixels() {
136            let value = pixel[channel];
137            result.put_pixel(x, y, image::Rgba([value, value, value, 255]));
138        }
139
140        Ok(result)
141    }
142
143    /// Combine channels from different images
144    pub fn combine_channels(
145        r_image: Option<&RgbaImage>,
146        g_image: Option<&RgbaImage>,
147        b_image: Option<&RgbaImage>,
148        a_image: Option<&RgbaImage>,
149    ) -> Result<RgbaImage> {
150        // Get dimensions from the first available image
151        let (width, height) = [r_image, g_image, b_image, a_image]
152            .iter()
153            .find_map(|img| img.map(|i| i.dimensions()))
154            .ok_or_else(|| BinaryError::invalid_data("At least one image must be provided"))?;
155
156        let mut result = RgbaImage::new(width, height);
157
158        for (x, y, pixel) in result.enumerate_pixels_mut() {
159            pixel[0] = r_image.map_or(0, |img| img.get_pixel(x, y)[0]);
160            pixel[1] = g_image.map_or(0, |img| img.get_pixel(x, y)[0]);
161            pixel[2] = b_image.map_or(0, |img| img.get_pixel(x, y)[0]);
162            pixel[3] = a_image.map_or(255, |img| img.get_pixel(x, y)[0]);
163        }
164
165        Ok(result)
166    }
167
168    /// Resize image using nearest neighbor (for pixel art)
169    pub fn resize_nearest(image: &RgbaImage, new_width: u32, new_height: u32) -> RgbaImage {
170        let (old_width, old_height) = image.dimensions();
171        let mut result = RgbaImage::new(new_width, new_height);
172
173        for (x, y, pixel) in result.enumerate_pixels_mut() {
174            let src_x = (x * old_width / new_width).min(old_width - 1);
175            let src_y = (y * old_height / new_height).min(old_height - 1);
176            *pixel = *image.get_pixel(src_x, src_y);
177        }
178
179        result
180    }
181
182    /// Apply Unity-specific texture corrections
183    pub fn apply_unity_corrections(image: &mut RgbaImage, flip_y: bool, swap_rb: bool) {
184        if swap_rb {
185            Self::swap_rb_channels(image);
186        }
187
188        if flip_y {
189            *image = Self::flip_vertical(image);
190        }
191    }
192}