Skip to main content

rpfm_lib/files/image/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Image file handling with DDS conversion support.
12//!
13//! This module provides the [`Image`] type for working with image files in Total War PackFiles.
14//! It stores raw image data and provides automatic conversion of DDS textures to PNG format
15//! for easier viewing and editing.
16//!
17//! # Supported Formats
18//!
19//! The following image formats are recognized:
20//! - **JPEG** (`.jpg`, `.jpeg`) - Standard photo compression
21//! - **PNG** (`.png`) - Lossless compressed images with alpha
22//! - **TGA** (`.tga`) - Targa images (common in game assets)
23//! - **DDS** (`.dds`) - DirectDraw Surface textures (Total War's primary format)
24//! - **GIF** (`.gif`) - Animated or simple images
25//!
26//! # DDS Conversion
27//!
28//! DDS files are automatically converted to PNG format when decoded to enable easier
29//! viewing in standard image viewers and editors. The original DDS data is preserved
30//! for re-encoding.
31//!
32//! The conversion process handles various DDS formats:
33//! - Standard DDS formats supported by the `image` crate
34//! - BC3_UNORM compressed textures via re-compression
35//! - RGBA_U8 color formats
36//!
37//! # Example
38//!
39//! ```ignore
40//! use rpfm_lib::files::{Decodeable, image::Image, DecodeableExtraData};
41//! use std::io::Cursor;
42//!
43//! // Read a DDS texture
44//! let dds_data = std::fs::read("texture.dds").unwrap();
45//! let mut reader = Cursor::new(dds_data);
46//! let mut extra = DecodeableExtraData::default();
47//! extra.set_is_dds(true);
48//! let image = Image::decode(&mut reader, &Some(extra)).unwrap();
49//!
50//! // Access converted PNG data for viewing
51//! if let Some(png_data) = image.converted_data() {
52//!     // Display in image viewer
53//! }
54//! ```
55
56use dds::{ColorFormat, Decoder, ImageViewMut};
57use getset::*;
58use image::{ImageFormat, ImageReader, RgbaImage};
59use serde_derive::{Serialize, Deserialize};
60
61use std::io::Cursor;
62
63use crate::binary::{ReadBytes, WriteBytes};
64use crate::error::{Result, RLibError};
65use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable};
66
67/// Extensions used by Images.
68pub const EXTENSIONS: [&str; 6] = [
69    ".jpg",
70    ".jpeg",
71    ".tga",
72    ".dds",
73    ".png",
74    ".gif"
75];
76
77#[cfg(test)] mod image_test;
78
79//---------------------------------------------------------------------------//
80//                              Enum & Structs
81//---------------------------------------------------------------------------//
82
83/// In-memory representation of an image file.
84///
85/// Stores the raw image data in its original format, plus optionally converted PNG data
86/// for DDS textures. The original data is preserved to allow lossless re-encoding.
87///
88/// # Fields
89///
90/// * `data` - Raw binary data in the original image format
91/// * `converted_data` - For DDS files, PNG-converted data for viewing (optional)
92///
93/// # Getters
94///
95/// Fields have public getters via the `getset` crate:
96/// - `data()` - Get reference to original image data
97/// - `converted_data()` - Get reference to converted PNG data (DDS only)
98///
99/// # DDS Handling
100///
101/// When a DDS file is decoded:
102/// 1. `data` contains the original DDS bytes
103/// 2. `converted_data` contains PNG-converted bytes for display
104/// 3. Encoding writes back the original DDS data from `data`
105///
106/// For non-DDS formats:
107/// 1. `data` contains the image bytes
108/// 2. `converted_data` is `None`
109///
110/// # Example
111///
112/// ```ignore
113/// use rpfm_lib::files::{Decodeable, Encodeable, image::Image};
114/// use std::io::Cursor;
115///
116/// # let png_bytes = vec![137, 80, 78, 71]; // PNG header
117/// let mut reader = Cursor::new(png_bytes);
118/// let image = Image::decode(&mut reader, &None).unwrap();
119///
120/// // Access original data
121/// let original = image.data();
122/// ```
123#[derive(Default, PartialEq, Eq, Clone, Debug, Getters, Serialize, Deserialize)]
124#[getset(get = "pub")]
125pub struct Image {
126    /// Original raw image data in native format.
127    data: Vec<u8>,
128
129    /// PNG-converted data for DDS textures (for viewing/editing).
130    converted_data: Option<Vec<u8>>,
131}
132
133//---------------------------------------------------------------------------//
134//                           Implementation of Image
135//---------------------------------------------------------------------------//
136
137impl Decodeable for Image {
138
139    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
140        let len = data.len()?;
141        let data = data.read_slice(len as usize, false)?;
142        let mut converted_data = None;
143
144        if let Some(extra_data) = extra_data {
145
146            // For dds files, we try to convert them to png instead.
147            if extra_data.is_dds {
148
149                match ImageReader::new(Cursor::new(&data))
150                    .with_guessed_format()?
151                    .decode() {
152                    Ok(image) => {
153                        let mut cdata = vec![];
154                        image.write_to(&mut Cursor::new(&mut cdata), ImageFormat::Png)?;
155                        converted_data = Some(cdata);
156                    }
157
158                    // If it fails, use the dds crate to decode the raw pixel data and convert directly to PNG.
159                    Err(_) => {
160
161                        // Decode the data from the file into a single dds texture.
162                        let mut decoder = Decoder::new(Cursor::new(&data))?;
163                        let size = decoder.main_size();
164                        let mut dds_data = vec![0_u8; size.pixels() as usize * 4];
165
166                        // This can return None if the format is wrong.
167                        if let Some(view) = ImageViewMut::new(&mut dds_data, size, ColorFormat::RGBA_U8) {
168                            decoder.read_surface(view)?;
169
170                            // Convert the raw RGBA buffer directly to PNG.
171                            if let Some(rgba_image) = RgbaImage::from_raw(size.width, size.height, dds_data) {
172                                let mut cdata = vec![];
173                                rgba_image.write_to(&mut Cursor::new(&mut cdata), ImageFormat::Png)?;
174                                converted_data = Some(cdata);
175                            } else {
176                                return Err(RLibError::DecodingDDSColourFormatUnsupported)
177                            }
178                        } else {
179                            return Err(RLibError::DecodingDDSColourFormatUnsupported)
180                        }
181                    },
182                }
183            }
184        }
185
186        Ok(Self {
187            data,
188            converted_data,
189        })
190    }
191}
192
193impl Encodeable for Image {
194
195    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, _extra_data: &Option<EncodeableExtraData>) -> Result<()> {
196        buffer.write_all(&self.data).map_err(From::from)
197    }
198}