Skip to main content

wow_blp/types/
header.rs

1pub use super::locator::MipmapLocator;
2pub use super::version::BlpVersion;
3use std::fmt;
4
5/// The content field determines how the image data is stored. CONTENT_JPEG
6/// uses non-standard JPEG (JFIF) file compression of BGRA colour component
7/// values rather than the usual Y′CbCr color component values.
8/// CONTENT_DIRECT refers to a variety of storage formats which can be
9/// directly read as pixel values.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum BlpContentTag {
12    /// JPEG compressed image data
13    Jpeg,
14    /// Direct pixel data (palettized or uncompressed)
15    Direct,
16}
17
18/// Error type for unknown content tag values
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct UnknownContent(u32);
21
22impl fmt::Display for UnknownContent {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "Unknown content field value: {}", self.0)
25    }
26}
27
28impl TryFrom<u32> for BlpContentTag {
29    type Error = UnknownContent;
30
31    fn try_from(val: u32) -> Result<BlpContentTag, Self::Error> {
32        match val {
33            0 => Ok(BlpContentTag::Jpeg),
34            1 => Ok(BlpContentTag::Direct),
35            _ => Err(UnknownContent(val)),
36        }
37    }
38}
39
40impl From<BlpContentTag> for u32 {
41    fn from(val: BlpContentTag) -> u32 {
42        match val {
43            BlpContentTag::Jpeg => 0,
44            BlpContentTag::Direct => 1,
45        }
46    }
47}
48
49/// BLP file header structure
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub struct BlpHeader {
52    /// BLP format version
53    pub version: BlpVersion,
54    /// Content encoding type
55    pub content: BlpContentTag,
56    /// Version-specific flags
57    pub flags: BlpFlags,
58    /// Image width in pixels
59    pub width: u32,
60    /// Image height in pixels
61    pub height: u32,
62    /// Mipmap location information
63    pub mipmap_locator: MipmapLocator,
64}
65
66impl BlpHeader {
67    /// Calculate needed count of mipmaps for the defined size
68    pub fn mipmaps_count(&self) -> usize {
69        if self.has_mipmaps() {
70            let width_n = (self.width as f32).log2() as usize;
71            let height_n = (self.height as f32).log2() as usize;
72            width_n.max(height_n)
73        } else {
74            0
75        }
76    }
77
78    /// Returns 'true' if the header defines that the image has mipmaps
79    pub fn has_mipmaps(&self) -> bool {
80        self.flags.has_mipmaps()
81    }
82
83    /// Return expected size of mipmap for the given mipmap level.
84    /// 0 level means original image.
85    pub fn mipmap_size(&self, i: usize) -> (u32, u32) {
86        if i == 0 {
87            (self.width, self.height)
88        } else {
89            ((self.width >> i).max(1), (self.height >> i).max(1))
90        }
91    }
92
93    /// Return expected count of pixels in mipmap at the level i.
94    /// 0 level means original image.
95    pub fn mipmap_pixels(&self, i: usize) -> u32 {
96        let (w, h) = self.mipmap_size(i);
97        w * h
98    }
99
100    /// Return alpha bits count in encoding
101    pub fn alpha_bits(&self) -> u32 {
102        self.flags.alpha_bits()
103    }
104
105    /// Get the alpha type for BLP2 format, if available
106    pub fn alpha_type(&self) -> Option<AlphaType> {
107        self.flags.alpha_type()
108    }
109
110    /// Validate the BLP header for compatibility with a specific WoW version
111    pub fn validate_for_wow_version(&self, wow_version: WowVersion) -> Result<(), String> {
112        if let Some(alpha_type) = self.alpha_type()
113            && !alpha_type.is_supported_in_version(wow_version)
114        {
115            return Err(format!(
116                "Alpha type {alpha_type:?} not supported in WoW version {wow_version:?}"
117            ));
118        }
119        Ok(())
120    }
121
122    /// Return offsets and sizes of internal mipmaps. For external returns [None]
123    pub fn internal_mipmaps(&self) -> Option<([u32; 16], [u32; 16])> {
124        match self.mipmap_locator {
125            MipmapLocator::Internal { offsets, sizes } => Some((offsets, sizes)),
126            MipmapLocator::External => None,
127        }
128    }
129
130    /// Get size of header in bytes. Doesn't count jpeg header or color map.
131    pub fn size(version: BlpVersion) -> usize {
132        4 // magic
133        + 4 // content
134        + 4 // flags or alpha_bits
135        + 4 // width 
136        + 4 // height
137        + if version < BlpVersion::Blp2 {8} else {0} // extra and has_mipmaps
138        + if version > BlpVersion::Blp0 {16*4*2} else {0} // mipmap locator
139    }
140}
141
142impl Default for BlpHeader {
143    fn default() -> Self {
144        BlpHeader {
145            version: BlpVersion::Blp1,
146            content: BlpContentTag::Jpeg,
147            flags: Default::default(),
148            width: 1,
149            height: 1,
150            mipmap_locator: Default::default(),
151        }
152    }
153}
154
155/// Alpha channel encoding type for BLP2 format
156/// Based on empirical analysis of WoW versions 1.12.1 through 5.4.8
157#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
158pub enum AlphaType {
159    /// No alpha channel
160    None = 0,
161    /// 1-bit alpha (binary transparency)
162    OneBit = 1,
163    /// Enhanced alpha blending (introduced in TBC 2.4.3)
164    Enhanced = 7,
165    /// 8-bit alpha channel
166    EightBit = 8,
167}
168
169impl std::fmt::Display for AlphaType {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self {
172            AlphaType::None => write!(f, "None (0)"),
173            AlphaType::OneBit => write!(f, "1-bit (1)"),
174            AlphaType::Enhanced => write!(f, "Enhanced (7)"),
175            AlphaType::EightBit => write!(f, "8-bit (8)"),
176        }
177    }
178}
179
180impl AlphaType {
181    /// Returns true if this alpha type was available in the specified WoW version
182    pub fn is_supported_in_version(self, wow_version: WowVersion) -> bool {
183        match self {
184            AlphaType::None | AlphaType::OneBit | AlphaType::EightBit => true,
185            AlphaType::Enhanced => wow_version >= WowVersion::TBC,
186        }
187    }
188
189    /// Get the effective alpha bits for this alpha type
190    pub fn alpha_bits(self) -> u8 {
191        match self {
192            AlphaType::None => 0,
193            AlphaType::OneBit => 1,
194            AlphaType::Enhanced => 8, // Enhanced uses 8-bit precision
195            AlphaType::EightBit => 8,
196        }
197    }
198}
199
200/// Error type for unknown alpha type values
201#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
202pub struct UnknownAlphaType(u8);
203
204impl fmt::Display for UnknownAlphaType {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "Unknown alpha_type field value: {}", self.0)
207    }
208}
209
210impl TryFrom<u8> for AlphaType {
211    type Error = UnknownAlphaType;
212
213    fn try_from(val: u8) -> Result<AlphaType, Self::Error> {
214        match val {
215            0 => Ok(AlphaType::None),
216            1 => Ok(AlphaType::OneBit),
217            7 => Ok(AlphaType::Enhanced),
218            8 => Ok(AlphaType::EightBit),
219            _ => Err(UnknownAlphaType(val)),
220        }
221    }
222}
223
224impl From<AlphaType> for u8 {
225    fn from(val: AlphaType) -> u8 {
226        val as u8
227    }
228}
229
230/// WoW version for format compatibility checking
231#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
232pub enum WowVersion {
233    /// World of Warcraft 1.12.1 (Vanilla)
234    Vanilla,
235    /// The Burning Crusade 2.4.3
236    TBC,
237    /// Wrath of the Lich King 3.3.5a
238    WotLK,
239    /// Cataclysm 4.3.4
240    Cataclysm,
241    /// Mists of Pandaria 5.4.8
242    MoP,
243}
244
245/// Compression type for BLP2 format
246#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
247pub enum Compression {
248    /// JPEG compression (rarely used in BLP2)
249    Jpeg, // adhoc compression, never met in BLP2
250    /// Palettized 256-color format
251    Raw1,
252    /// Uncompressed RGBA format
253    Raw3,
254    /// DXT compression (S3TC)
255    Dxtc,
256}
257
258/// Error type for unknown compression values
259#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
260pub struct UnknownCompression(u8);
261
262impl fmt::Display for UnknownCompression {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        write!(f, "Unknown compression field value: {}", self.0)
265    }
266}
267
268impl TryFrom<u8> for Compression {
269    type Error = UnknownCompression;
270
271    fn try_from(val: u8) -> Result<Compression, Self::Error> {
272        match val {
273            0 => Ok(Compression::Jpeg),
274            1 => Ok(Compression::Raw1),
275            2 => Ok(Compression::Dxtc),
276            3 => Ok(Compression::Raw3),
277            _ => Err(UnknownCompression(val)),
278        }
279    }
280}
281
282impl From<Compression> for u8 {
283    fn from(val: Compression) -> u8 {
284        match val {
285            Compression::Jpeg => 0,
286            Compression::Raw1 => 1,
287            Compression::Dxtc => 2,
288            Compression::Raw3 => 3,
289        }
290    }
291}
292
293/// Part of header that depends on the version
294#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
295pub enum BlpFlags {
296    /// For version >= 2
297    Blp2 {
298        /// Compression method used
299        compression: Compression,
300        /// Alpha channel bit depth (0, 1, 4, or 8)
301        alpha_bits: u8,
302        /// Alpha encoding type (typed enum for better validation)
303        alpha_type: AlphaType,
304        /// Whether the image has mipmaps
305        has_mipmaps: u8,
306    },
307    /// For version < 2
308    Old {
309        /// Alpha channel bit depth
310        alpha_bits: u32,
311        /// Extra field (usually 4 or 5)
312        extra: u32, // no purpose, default is 5
313        /// Whether the image has mipmaps (0 or 1)
314        has_mipmaps: u32, // boolean
315    },
316}
317
318impl Default for BlpFlags {
319    fn default() -> Self {
320        BlpFlags::Old {
321            alpha_bits: 8,
322            extra: 8,
323            has_mipmaps: 1,
324        }
325    }
326}
327
328impl BlpFlags {
329    /// Returns 'true' if the header defines that the image has mipmaps
330    pub fn has_mipmaps(&self) -> bool {
331        match self {
332            BlpFlags::Blp2 { has_mipmaps, .. } => *has_mipmaps != 0,
333            BlpFlags::Old { has_mipmaps, .. } => *has_mipmaps != 0,
334        }
335    }
336
337    /// Get count of bits alpha channel is encoded in content
338    pub fn alpha_bits(&self) -> u32 {
339        match self {
340            BlpFlags::Blp2 { compression, .. } if *compression == Compression::Raw3 => 4,
341            BlpFlags::Blp2 { alpha_bits, .. } => *alpha_bits as u32,
342            BlpFlags::Old { alpha_bits, .. } => *alpha_bits,
343        }
344    }
345
346    /// Get the alpha type for BLP2 format, returns None for older formats
347    pub fn alpha_type(&self) -> Option<AlphaType> {
348        match self {
349            BlpFlags::Blp2 { alpha_type, .. } => Some(*alpha_type),
350            BlpFlags::Old { .. } => None,
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_alpha_type_conversion() {
361        assert_eq!(AlphaType::None as u8, 0);
362        assert_eq!(AlphaType::OneBit as u8, 1);
363        assert_eq!(AlphaType::Enhanced as u8, 7);
364        assert_eq!(AlphaType::EightBit as u8, 8);
365
366        // Test TryFrom conversions
367        assert_eq!(AlphaType::try_from(0).unwrap(), AlphaType::None);
368        assert_eq!(AlphaType::try_from(1).unwrap(), AlphaType::OneBit);
369        assert_eq!(AlphaType::try_from(7).unwrap(), AlphaType::Enhanced);
370        assert_eq!(AlphaType::try_from(8).unwrap(), AlphaType::EightBit);
371
372        // Test invalid conversion
373        assert!(AlphaType::try_from(9).is_err());
374    }
375
376    #[test]
377    fn test_alpha_type_wow_version_support() {
378        // Vanilla supports all except Enhanced
379        assert!(AlphaType::None.is_supported_in_version(WowVersion::Vanilla));
380        assert!(AlphaType::OneBit.is_supported_in_version(WowVersion::Vanilla));
381        assert!(!AlphaType::Enhanced.is_supported_in_version(WowVersion::Vanilla));
382        assert!(AlphaType::EightBit.is_supported_in_version(WowVersion::Vanilla));
383
384        // TBC+ supports all
385        assert!(AlphaType::Enhanced.is_supported_in_version(WowVersion::TBC));
386        assert!(AlphaType::Enhanced.is_supported_in_version(WowVersion::Cataclysm));
387    }
388
389    #[test]
390    fn test_mipmap_count() {
391        let header = BlpHeader {
392            width: 512,
393            height: 256,
394            version: BlpVersion::Blp0,
395            ..Default::default()
396        };
397        assert_eq!(header.mipmaps_count(), 9);
398
399        let header = BlpHeader {
400            width: 512,
401            height: 256,
402            version: BlpVersion::Blp1,
403            ..Default::default()
404        };
405        assert_eq!(header.mipmaps_count(), 9);
406
407        let header = BlpHeader {
408            width: 1,
409            height: 4,
410            ..Default::default()
411        };
412        assert_eq!(header.mipmaps_count(), 2);
413
414        let header = BlpHeader {
415            width: 4,
416            height: 7,
417            ..Default::default()
418        };
419        assert_eq!(header.mipmaps_count(), 2);
420
421        let header = BlpHeader {
422            width: 768,
423            height: 128,
424            ..Default::default()
425        };
426        assert_eq!(header.mipmaps_count(), 9);
427    }
428}