Skip to main content

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::{BufWriter, Cursor, Write};
10use std::ptr::NonNull;
11use std::{io, slice};
12use thiserror::Error;
13
14#[cfg(feature = "image")]
15use crate::image::TextureMapDecoder;
16#[cfg(feature = "image")]
17use image::{DynamicImage, ImageResult};
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/// Converts a `TextureMap` into a TGA (Targa) image file.
96/// # Warning
97/// The TGA format does **not** support 16-bit per channel formats such as `R16G16B16A16`.
98/// If the input texture uses this format, the function may fail or produce incorrect output.
99pub fn create_tga(tex: &TextureMap) -> Result<Vec<u8>, TextureConversionError> {
100    let dds = create_dds(tex)?;
101    let mut scratch_image = ScratchImage::load_dds(
102        dds.as_slice(),
103        DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT,
104        None,
105        None,
106    )
107    .map_err(DirectXTexError)?;
108    scratch_image = decompress_dds(tex, scratch_image)?;
109    ensure_8bit_colors(&mut 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    )
127    .map_err(DirectXTexError)?;
128
129    let buf = Vec::new();
130    let cursor = Cursor::new(buf);
131    let mut w = BufWriter::new(cursor);
132
133    scratch_image = decompress_dds(tex, scratch_image)?;
134
135    let render_format: RenderFormat = scratch_image.metadata().format.try_into().unwrap();
136
137    let color_type = match render_format {
138        RenderFormat::A8 => Some(ColorType::Grayscale),
139        RenderFormat::R16G16B16A16 => Some(ColorType::Rgba),
140        RenderFormat::R8G8B8A8 => Some(ColorType::Rgb),
141        RenderFormat::R8G8 => Some(ColorType::Grayscale),
142        _ => None,
143    };
144
145    let bit_depth = match render_format {
146        RenderFormat::R16G16B16A16 => png::BitDepth::Sixteen,
147        _ => png::BitDepth::Eight,
148    };
149
150    let mut encoder = png::Encoder::new(
151        &mut w,
152        scratch_image.metadata().width as u32,
153        scratch_image.metadata().height as u32,
154    );
155    encoder.set_color(color_type.unwrap());
156    encoder.set_depth(bit_depth);
157    let mut writer = encoder.write_header().unwrap();
158
159    let blob = scratch_image
160        .image(0, 0, 0)
161        .unwrap()
162        .save_dds(DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT)?;
163
164    writer.write_image_data(blob.buffer()).unwrap(); // Save
165
166    writer.finish().unwrap();
167    w.flush()?;
168
169    let cursor = w.into_inner().unwrap();
170    Ok(cursor.into_inner())
171}
172
173#[cfg(feature = "image")]
174pub fn create_dynamic_image(tex: &TextureMap) -> ImageResult<DynamicImage> {
175    DynamicImage::from_decoder(TextureMapDecoder::from_texture_map(tex.clone()))
176}
177
178pub(crate) fn decompress_dds(
179    tex: &TextureMap,
180    scratch_image: ScratchImage,
181) -> Result<ScratchImage, TextureConversionError> {
182    let mut scratch_image = scratch_image;
183    if tex.format().is_compressed() {
184        scratch_image = directxtex::decompress(
185            scratch_image.images(),
186            scratch_image.metadata(),
187            tex.format().decompressed_format().into(),
188        )
189        .map_err(DirectXTexError)?
190    }
191
192    //generate missing blue channel
193    if tex.format().num_channels() == 2 {
194        scratch_image = directxtex::convert(
195            scratch_image.images(),
196            scratch_image.metadata(),
197            DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
198            TEX_FILTER_FLAGS::TEX_FILTER_DEFAULT,
199            TEX_THRESHOLD_DEFAULT,
200        )
201        .map_err(DirectXTexError)?;
202
203        for pixel in scratch_image.pixels_mut().chunks_mut(4) {
204            if pixel.len() != 4 {
205                continue;
206            }
207            let x = pixel[0] as f64 / 255.0;
208            let y = pixel[1] as f64 / 255.0;
209            pixel[2] = (f64::sqrt(1.0 - (x * x - y * y)) * 255.0) as u8;
210        }
211    }
212    Ok(scratch_image)
213}
214
215pub(crate) fn ensure_8bit_colors(
216    scratch_image: &mut ScratchImage,
217) -> Result<(), TextureConversionError> {
218    if scratch_image.metadata().format == DXGI_FORMAT::DXGI_FORMAT_R16G16B16A16_FLOAT {
219        *scratch_image = directxtex::convert(
220            scratch_image.images(),
221            scratch_image.metadata(),
222            DXGI_FORMAT::DXGI_FORMAT_R8G8B8A8_UNORM,
223            TEX_FILTER_FLAGS::TEX_FILTER_DEFAULT | TEX_FILTER_FLAGS::TEX_FILTER_FORCE_NON_WIC,
224            TEX_THRESHOLD_DEFAULT,
225        )
226        .map_err(DirectXTexError)?;
227    }
228    Ok(())
229}
230
231pub(crate) fn image_pixels(image: &Image) -> Option<Vec<u8>> {
232    let pixels = NonNull::new(image.pixels)?;
233    let scanlines = image.format.compute_scanlines(image.height);
234    let buffer_size = image.row_pitch.checked_mul(scanlines)?;
235    let raw_slice = unsafe { slice::from_raw_parts(pixels.as_ptr(), buffer_size) };
236    let raw_buffer = raw_slice.to_vec();
237    Some(raw_buffer)
238}
239
240pub fn create_mip_dds(
241    tex: &TextureMap,
242    mip_level: usize,
243    decompress: bool,
244) -> Result<Vec<u8>, TextureConversionError> {
245    if let Ok(mut mip) = tex.mipmap(mip_level) {
246        let meta_data = TexMetadata {
247            width: mip.width,
248            height: mip.height,
249            depth: 0,
250            array_size: 1,
251            mip_levels: 1,
252            misc_flags: 0,
253            misc_flags2: 0,
254            format: tex.format().into(),
255            dimension: tex.dimensions().into(),
256        };
257        let pitch = DXGI_FORMAT::from(tex.format())
258            .compute_pitch(mip.width, mip.height, CP_FLAGS::CP_FLAGS_NONE)
259            .map_err(DirectXTexError)?;
260
261        let image = Image {
262            width: mip.width,
263            height: mip.height,
264            format: tex.format().into(),
265            row_pitch: pitch.row,
266            slice_pitch: pitch.slice,
267            pixels: mip.data.as_mut_ptr(),
268        };
269
270        let mut blob =
271            directxtex::save_dds(&[image], &meta_data, DDS_FLAGS::DDS_FLAGS_FORCE_DX10_EXT)
272                .map_err(DirectXTexError)?;
273        if decompress {
274            let dds = ScratchImage::load_dds(blob.buffer(), DDS_FLAGS::DDS_FLAGS_NONE, None, None)
275                .map_err(DirectXTexError)?;
276            let new_dds = decompress_dds(tex, dds)?;
277            blob = directxtex::save_dds(
278                new_dds.images(),
279                new_dds.metadata(),
280                DDS_FLAGS::DDS_FLAGS_NONE,
281            )
282            .map_err(DirectXTexError)?;
283        }
284        Ok(Vec::from(blob.buffer()))
285    } else {
286        Err(TextureConversionError::MipOutOfBounds(
287            mip_level,
288            tex.num_mip_levels(),
289        ))
290    }
291}