glacier_texture/
convert.rs

1use crate::convert::TextureConversionError::DirectXTexError;
2use crate::enums::RenderFormat;
3use crate::texture_map::{MipLevel, TextureMap};
4use directxtex::{
5    HResultError, Image, ScratchImage, TexMetadata, CP_FLAGS, DDS_FLAGS, DXGI_FORMAT,
6    TEX_FILTER_FLAGS, TEX_THRESHOLD_DEFAULT, TGA_FLAGS,
7};
8use png::ColorType;
9use std::io;
10use std::io::{BufWriter, Cursor, Write};
11use thiserror::Error;
12
13#[cfg(feature = "image")]
14use crate::image::TextureMapDecoder;
15#[cfg(feature = "image")]
16use image::{DynamicImage, ImageResult};
17
18
19#[derive(Error, Debug)]
20pub enum TextureConversionError {
21    #[error("Io error {0}")]
22    IoError(#[from] io::Error),
23
24    #[error("DirectxTex error {0}")]
25    DirectXTexError(#[from] HResultError),
26
27    #[error("Invalid texture: {0}")]
28    InvalidTexture(String),
29
30    #[error("Tried to read mip level {0}, which is out of bounds [0..{0}]")]
31    MipOutOfBounds(usize, usize),
32}
33
34/// Converts a `TextureMap` into a DDS (DirectDraw Surface) image file.
35pub fn create_dds(tex: &TextureMap) -> Result<Vec<u8>, TextureConversionError> {
36    let mut mips = (0..tex.num_mip_levels())
37        .filter_map(|i| -> Option<MipLevel> {
38            if let Ok(mip) = tex.mipmap(i) {
39                if mip.height > 0 && mip.width > 0 {
40                    Some(mip)
41                } else {
42                    None
43                }
44            } else {
45                None
46            }
47        })
48        .collect::<Vec<_>>();
49
50    let first_mip = mips.first().ok_or(TextureConversionError::InvalidTexture(
51        "There are no textures in the data".to_string(),
52    ))?;
53
54    let meta_data = TexMetadata {
55        width: first_mip.width,
56        height: first_mip.height,
57        depth: 0,
58        array_size: 1,
59        mip_levels: mips.len(),
60        misc_flags: 0,
61        misc_flags2: 0,
62        format: tex.format().into(),
63        dimension: tex.dimensions().into(),
64    };
65
66    let images_result: Result<Vec<Image>, TextureConversionError> = mips
67        .iter_mut()
68        .map(|mip| -> Result<Image, TextureConversionError> {
69            let pitch = DXGI_FORMAT::from(tex.format())
70                .compute_pitch(mip.width, mip.height, CP_FLAGS::CP_FLAGS_NONE)
71                .map_err(DirectXTexError)?;
72
73            Ok(Image {
74                width: mip.width,
75                height: mip.height,
76                format: tex.format().into(),
77                row_pitch: pitch.row,
78                slice_pitch: pitch.slice,
79                pixels: mip.data.as_mut_ptr(),
80            })
81        })
82        .collect();
83
84    let images = images_result?;
85
86    let blob = directxtex::save_dds(
87        images.as_slice(),
88        &meta_data,
89        DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT,
90    )
91    .map_err(DirectXTexError)?;
92    Ok(Vec::from(blob.buffer()))
93}
94
95
96/// Converts a `TextureMap` into a TGA (Targa) image file.
97/// # Warning
98/// The TGA format does **not** support 16-bit per channel formats such as `R16G16B16A16`.
99/// If the input texture uses this format, the function may fail or produce incorrect output.
100pub fn create_tga(tex: &TextureMap) -> Result<Vec<u8>, TextureConversionError> {
101    let dds = create_dds(tex)?;
102    let mut scratch_image = ScratchImage::load_dds(
103        dds.as_slice(),
104        DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT,
105        None,
106        None,
107    )
108    .map_err(DirectXTexError)?;
109    scratch_image = decompress_dds(tex, scratch_image)?;
110    let blob = scratch_image
111        .image(0, 0, 0)
112        .unwrap()
113        .save_tga(TGA_FLAGS::TGA_FLAGS_NONE, None)
114        .map_err(DirectXTexError)?;
115    Ok(Vec::from(blob.buffer()))
116}
117
118/// Converts a `TextureMap` into a PNG image file.
119pub fn create_png(tex: &TextureMap) -> Result<Vec<u8>, TextureConversionError> {
120    let dds = create_dds(tex)?;
121    let mut scratch_image = ScratchImage::load_dds(
122        dds.as_slice(),
123        DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT,
124        None,
125        None,
126    ).map_err(DirectXTexError)?;
127
128    let buf = Vec::new();
129    let cursor = Cursor::new(buf);
130    let mut w = BufWriter::new(cursor);
131
132    scratch_image = decompress_dds(tex, scratch_image)?;
133
134    let render_format: RenderFormat = scratch_image.metadata().format.try_into().unwrap();
135
136    let color_type = match render_format {
137        RenderFormat::A8 => Some(ColorType::Grayscale),
138        RenderFormat::R16G16B16A16 => Some(ColorType::Rgba),
139        RenderFormat::R8G8B8A8 => Some(ColorType::Rgb),
140        RenderFormat::R8G8 => Some(ColorType::Grayscale),
141        _ => None,
142    };
143
144    let bit_depth = match render_format {
145        RenderFormat::R16G16B16A16 => png::BitDepth::Sixteen,
146        _ => png::BitDepth::Eight,
147    };
148
149    let mut encoder = png::Encoder::new(
150        &mut w,
151        scratch_image.metadata().width as u32,
152        scratch_image.metadata().height as u32,
153    );
154    encoder.set_color(color_type.unwrap());
155    encoder.set_depth(bit_depth);
156    let mut writer = encoder.write_header().unwrap();
157
158    let blob = scratch_image
159        .image(0, 0, 0)
160        .unwrap()
161        .save_dds(DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT)?;
162
163    writer.write_image_data(blob.buffer()).unwrap(); // Save
164
165    writer.finish().unwrap();
166    w.flush()?;
167
168    let cursor = w.into_inner().unwrap();
169    Ok(cursor.into_inner())
170}
171
172#[cfg(feature = "image")]
173pub fn create_dynamic_image(tex: &TextureMap) -> ImageResult<DynamicImage> {
174    DynamicImage::from_decoder(TextureMapDecoder::from_texture_map(tex.clone()))
175}
176
177pub(crate) fn decompress_dds(
178    tex: &TextureMap,
179    scratch_image: ScratchImage,
180) -> Result<ScratchImage, TextureConversionError> {
181    let mut scratch_image = scratch_image;
182    if tex.format().is_compressed() {
183        scratch_image = directxtex::decompress(
184            scratch_image.images(),
185            scratch_image.metadata(),
186            match tex.format().num_channels() {
187                1 => DXGI_FORMAT::DXGI_FORMAT_A8_UNORM,
188                2 => DXGI_FORMAT::DXGI_FORMAT_R8G8_UNORM,
189                4 => DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
190                _ => DXGI_FORMAT::DXGI_FORMAT_UNKNOWN,
191            },
192        )
193        .map_err(DirectXTexError)?
194    }
195
196    if tex.format() == RenderFormat::R16G16B16A16 {
197        scratch_image = directxtex::convert(
198            scratch_image.images(),
199            scratch_image.metadata(),
200            DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
201            TEX_FILTER_FLAGS::TEX_FILTER_DEFAULT | TEX_FILTER_FLAGS::TEX_FILTER_FORCE_NON_WIC,
202            TEX_THRESHOLD_DEFAULT,
203        )
204        .map_err(DirectXTexError)?;
205    }
206
207    //generate missing blue channel
208    if tex.format().num_channels() == 2 {
209        scratch_image = directxtex::convert(
210            scratch_image.images(),
211            scratch_image.metadata(),
212            DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
213            TEX_FILTER_FLAGS::TEX_FILTER_DEFAULT,
214            TEX_THRESHOLD_DEFAULT,
215        )
216        .map_err(DirectXTexError)?;
217
218        for pixel in scratch_image.pixels_mut().chunks_mut(4) {
219            if pixel.len() != 4 {
220                continue;
221            }
222            let x = pixel[0] as f64 / 255.0;
223            let y = pixel[1] as f64 / 255.0;
224            pixel[2] = (f64::sqrt(1.0 - (x * x - y * y)) * 255.0) as u8;
225        }
226    }
227    Ok(scratch_image)
228}
229
230pub fn create_mip_dds(
231    tex: &TextureMap,
232    mip_level: usize,
233    decompress: bool,
234) -> Result<Vec<u8>, TextureConversionError> {
235    if let Ok(mut mip) = tex.mipmap(mip_level) {
236        let meta_data = TexMetadata {
237            width: mip.width,
238            height: mip.height,
239            depth: 0,
240            array_size: 1,
241            mip_levels: 1,
242            misc_flags: 0,
243            misc_flags2: 0,
244            format: tex.format().into(),
245            dimension: tex.dimensions().into(),
246        };
247        let pitch = DXGI_FORMAT::from(tex.format())
248            .compute_pitch(mip.width, mip.height, CP_FLAGS::CP_FLAGS_NONE)
249            .map_err(DirectXTexError)?;
250
251        let image = Image {
252            width: mip.width,
253            height: mip.height,
254            format: tex.format().into(),
255            row_pitch: pitch.row,
256            slice_pitch: pitch.slice,
257            pixels: mip.data.as_mut_ptr(),
258        };
259
260        let mut blob =
261            directxtex::save_dds(&[image], &meta_data, DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT)
262                .map_err(DirectXTexError)?;
263        if decompress {
264            let dds = ScratchImage::load_dds(blob.buffer(), DDS_FLAGS::DDS_FLAGS_NONE, None, None)
265                .map_err(DirectXTexError)?;
266            let new_dds = decompress_dds(tex, dds)?;
267            blob = directxtex::save_dds(
268                new_dds.images(),
269                new_dds.metadata(),
270                DDS_FLAGS::DDS_FLAGS_NONE,
271            )
272            .map_err(DirectXTexError)?;
273        }
274        Ok(Vec::from(blob.buffer()))
275    } else {
276        Err(TextureConversionError::MipOutOfBounds(
277            mip_level,
278            tex.num_mip_levels(),
279        ))
280    }
281}