Skip to main content

blp/core/from/
image.rs

1use crate::core::image::{ImageBlp, MAX_MIPS};
2use crate::core::mipmap::Mipmap;
3use crate::core::types::SourceKind;
4use crate::error::error::BlpError;
5use image;
6use psd::Psd;
7
8const MAX_POW2: u32 = 8192; // adjust upper bound if needed
9
10/// Checks if buffer is a PSD file by signature
11fn is_psd_file(buf: &[u8]) -> bool {
12    // PSD files start with "8BPS" signature
13    buf.len() >= 4 && &buf[0..4] == b"8BPS"
14}
15
16/// Gets PSD file dimensions without full decoding
17fn get_psd_dimensions(buf: &[u8]) -> Result<(u32, u32), BlpError> {
18    let psd = Psd::from_bytes(buf).map_err(|e| BlpError::new("error-psd-parse").with_arg("error", e.to_string()))?;
19
20    let width = psd.width();
21    let height = psd.height();
22
23    if width == 0 || height == 0 {
24        return Err(BlpError::new("error-psd-invalid-dimensions")
25            .with_arg("width", width)
26            .with_arg("height", height));
27    }
28
29    Ok((width, height))
30}
31
32fn pow2_list_up_to(max_v: u32) -> Vec<u32> {
33    let mut v = 1u32;
34    let mut out = Vec::new();
35    while v <= max_v {
36        out.push(v);
37        if v == u32::MAX / 2 {
38            break;
39        }
40        v <<= 1;
41    }
42    out
43}
44
45/// Choose target frame (W*, H*) — powers of two.
46/// Criteria (lexicographically):
47///   1) minimum scale s = max(W*/w0, H*/h0) (no distortion, "minimal stretch")
48///   2) minimum difference in aspect ratio |(W*/H*) - (w0/h0)|
49///   3) minimum area W* * H*
50/// Returns (W*, H*).
51fn pick_pow2_cover(w0: u32, h0: u32) -> (u32, u32) {
52    debug_assert!(w0 > 0 && h0 > 0);
53    let ws = pow2_list_up_to(MAX_POW2);
54    let hs = pow2_list_up_to(MAX_POW2);
55
56    let w0f = w0 as f64;
57    let h0f = h0 as f64;
58    let ar0 = w0f / h0f;
59
60    let mut best = None::<(f64, f64, u64, u32, u32)>; // (s, ar_diff, area, W, H)
61
62    for &ww in &ws {
63        // if very small powers of two — skip obviously smaller than source:
64        // BUT we allow "sub-frames" smaller than source (this will increase s), so don't filter.
65        for &hh in &hs {
66            let s = (ww as f64 / w0f).max(hh as f64 / h0f); // cover scale
67            if s < 1.0 {
68                // Must cover the frame — if s<1, image won't cover the frame.
69                continue;
70            }
71            let ar = ww as f64 / hh as f64;
72            let ar_diff = (ar - ar0).abs();
73            let area = (ww as u64) * (hh as u64);
74
75            let cand = (s, ar_diff, area, ww, hh);
76            match best {
77                None => best = Some(cand),
78                Some(cur) => {
79                    // comparison: s, then ar_diff, then area
80                    if cand.0 < cur.0 || (cand.0 == cur.0 && (cand.1 < cur.1 || (cand.1 == cur.1 && cand.2 < cur.2))) {
81                        best = Some(cand);
82                    }
83                }
84            }
85        }
86    }
87
88    if let Some((_s, _ard, _area, ww, hh)) = best { (ww, hh) } else { (w0, h0) }
89}
90
91/// Create mipmaps for the given base dimensions.
92/// first_image: Some(image) for the first mipmap if available, None otherwise.
93fn create_mipmaps(base_w: u32, base_h: u32, first_image: Option<image::RgbaImage>) -> Vec<Mipmap> {
94    // How many levels to 1×1 inclusive:
95    // floor(log2(max)) + 1  ==  32 - leading_zeros(max)  (for u32)
96    let levels = (32 - base_w.max(base_h).leading_zeros()) as usize;
97
98    let mut mipmaps = Vec::with_capacity(MAX_MIPS);
99    let (mut w, mut h) = (base_w, base_h);
100
101    for i in 0..MAX_MIPS {
102        if i < levels {
103            mipmaps.push(Mipmap { width: w, height: h, image: if i == 0 { first_image.clone() } else { None }, offset: 0, length: 0 });
104            // halve, but not below 1
105            w = (w / 2).max(1);
106            h = (h / 2).max(1);
107        } else {
108            // tail — missing levels
109            mipmaps.push(Mipmap::default());
110        }
111    }
112
113    mipmaps
114}
115
116impl ImageBlp {
117    /// Lightweight path for "arbitrary image": layout only without RGBA.
118    /// Supports both regular image formats (via image library),
119    /// and Adobe Photoshop (PSD) files with automatic signature detection.
120    ///
121    /// 1) Read source dimensions (without full decoding)
122    /// 2) Choose target frame (W*,H*) — powers of two by "minimum upscale" and "minimum crop" rule
123    /// 3) Form mipmap chain (only width/height), image=None
124    ///    Tail after 1×1 filled with 0×0 (not 1×1).
125    pub fn from_buf_image(buf: &[u8]) -> Result<Self, BlpError> {
126        // Get image dimensions without full decoding
127        let (w0, h0) = if is_psd_file(buf) {
128            get_psd_dimensions(buf)?
129        } else {
130            let reader = image::ImageReader::new(std::io::Cursor::new(buf))
131                .with_guessed_format()
132                .map_err(|_| BlpError::new("error-image-load"))?;
133            let dimensions = reader
134                .into_dimensions()
135                .map_err(|_| BlpError::new("error-image-load"))?;
136            dimensions
137        };
138
139        if w0 == 0 || h0 == 0 {
140            return Err(BlpError::new("error-image-empty")
141                .with_arg("width", w0)
142                .with_arg("height", h0));
143        }
144
145        let (base_w, base_h) = pick_pow2_cover(w0, h0);
146
147        let mipmaps = create_mipmaps(base_w, base_h, None);
148
149        Ok(ImageBlp { width: base_w, height: base_h, mipmaps, source: SourceKind::Image, ..Default::default() })
150    }
151
152    /// Create BLP from raw RGBA buffer.
153    /// Buffer must be in RGBA format (4 bytes per pixel).
154    /// Width and height must match the buffer size.
155    pub fn from_rgba_impl(rgba_buf: &[u8], width: u32, height: u32) -> Result<Self, BlpError> {
156        if width == 0 || height == 0 {
157            return Err(BlpError::new("error-image-empty")
158                .with_arg("width", width)
159                .with_arg("height", height));
160        }
161
162        let expected_size = (width * height * 4) as usize;
163        if rgba_buf.len() != expected_size {
164            return Err(BlpError::new("error-rgba-buffer-size")
165                .with_arg("expected", expected_size)
166                .with_arg("actual", rgba_buf.len()));
167        }
168
169        // Create RgbaImage from buffer
170        let rgba_image = image::RgbaImage::from_raw(width, height, rgba_buf.to_vec()).ok_or_else(|| BlpError::new("error-rgba-image-creation"))?;
171
172        let (base_w, base_h) = pick_pow2_cover(width, height);
173
174        let mipmaps = create_mipmaps(base_w, base_h, Some(rgba_image));
175
176        Ok(ImageBlp { width: base_w, height: base_h, mipmaps, source: SourceKind::Image, ..Default::default() })
177    }
178}