1use crate::binary::{Error, Reader, Result, Writer};
4use crate::records::{hex, unhex};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct ImageRecord {
10 pub header: String,
12 pub data: String,
14 pub trailing: String,
16}
17#[derive(Debug, Clone, PartialEq)]
19pub struct DecodedImage {
20 pub width: u32,
22 pub height: u32,
24 pub rgba: Vec<u8>,
26}
27struct Layout {
28 width: usize,
29 height: usize,
30 bits: usize,
31 offset: usize,
32 stride: usize,
33 top: bool,
34 right: bool,
35 palette: usize,
36}
37fn error(message: &str) -> Error {
38 Error {
39 offset: 0,
40 message: message.into(),
41 }
42}
43pub fn parse_bmp(bytes: &[u8]) -> Result<ImageRecord> {
45 parse_image("bmp", bytes)
46}
47pub fn write_bmp(image: &ImageRecord) -> Result<Vec<u8>> {
49 write_image("bmp", image)
50}
51pub fn decode_bmp(image: &ImageRecord) -> Result<DecodedImage> {
53 decode_image("bmp", image)
54}
55pub fn create_bmp(width: u32, height: u32, rgba: &[u8]) -> Result<ImageRecord> {
57 create_image("bmp", width, height, rgba)
58}
59pub fn parse_tga(bytes: &[u8]) -> Result<ImageRecord> {
61 parse_image("tga", bytes)
62}
63pub fn write_tga(image: &ImageRecord) -> Result<Vec<u8>> {
65 write_image("tga", image)
66}
67pub fn decode_tga(image: &ImageRecord) -> Result<DecodedImage> {
69 decode_image("tga", image)
70}
71pub fn create_tga(width: u32, height: u32, rgba: &[u8]) -> Result<ImageRecord> {
73 create_image("tga", width, height, rgba)
74}
75
76fn layout(format: &str, bytes: &[u8]) -> Result<Layout> {
77 let mut reader = Reader::new(bytes);
78 reader.raw(if format == "bmp" { 54 } else { 18 })?;
79 let word = |at| u16::from_le_bytes([bytes[at], bytes[at + 1]]);
80 let dword = |at| u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]);
81 let (width, height, bits, offset, stride, top, right, palette) = if format == "bmp" {
82 let dib = dword(14) as usize;
83 let offset = dword(10) as usize;
84 let width = dword(18) as i32;
85 let signed_height = dword(22) as i32;
86 let height = signed_height.unsigned_abs() as usize;
87 let bits = word(28) as usize;
88 if word(0) != 0x4d42
89 || dib < 40
90 || offset < 14 + dib
91 || word(26) != 1
92 || dword(30) != 0
93 || !matches!(bits, 8 | 24 | 32)
94 || width <= 0
95 {
96 return Err(error("Only BI_RGB 8/24/32-bit BMP is supported"));
97 }
98 let palette = 14 + dib;
99 if bits == 8 {
100 let colors = if dword(46) == 0 {
101 256
102 } else {
103 dword(46) as usize
104 };
105 if colors > 256 || palette + colors * 4 > offset {
106 return Err(error("Invalid BMP palette"));
107 }
108 }
109 let width = width as usize;
110 (
111 width,
112 height,
113 bits,
114 offset,
115 (width * bits).div_ceil(32) * 4,
116 signed_height < 0,
117 false,
118 palette,
119 )
120 } else if format == "tga" {
121 let (width, height, bits) = (word(12) as usize, word(14) as usize, bytes[16] as usize);
122 if bytes[1] != 0 || bytes[2] != 2 || !matches!(bits, 24 | 32) || bytes[17] & 0xc0 != 0 {
123 return Err(error("Only uncompressed true-color TGA is supported"));
124 }
125 (
126 width,
127 height,
128 bits,
129 18 + bytes[0] as usize,
130 width * (bits / 8),
131 bytes[17] & 32 != 0,
132 bytes[17] & 16 != 0,
133 0,
134 )
135 } else {
136 return Err(error("Unsupported image format"));
137 };
138 if width == 0
139 || height == 0
140 || width > 16384
141 || height > 16384
142 || width * height > 16_777_216
143 || offset > bytes.len()
144 || stride * height > bytes.len() - offset
145 {
146 return Err(error("Truncated image or pixel budget exceeded"));
147 }
148 Ok(Layout {
149 width,
150 height,
151 bits,
152 offset,
153 stride,
154 top,
155 right,
156 palette,
157 })
158}
159pub(crate) fn parse_image(format: &str, bytes: &[u8]) -> Result<ImageRecord> {
161 let info = layout(format, bytes)?;
162 let end = info.offset + info.stride * info.height;
163 Ok(ImageRecord {
164 header: hex(&bytes[..info.offset]),
165 data: hex(&bytes[info.offset..end]),
166 trailing: hex(&bytes[end..]),
167 })
168}
169pub(crate) fn write_image(format: &str, image: &ImageRecord) -> Result<Vec<u8>> {
171 let mut writer = Writer::default();
172 writer.raw(&unhex(&image.header)?)?;
173 writer.raw(&unhex(&image.data)?)?;
174 writer.raw(&unhex(&image.trailing)?)?;
175 let bytes = writer.finish();
176 let parsed = parse_image(format, &bytes)?;
177 if parsed.header != image.header.to_lowercase() || parsed.data != image.data.to_lowercase() {
178 return Err(error("Image header and payload size mismatch"));
179 }
180 Ok(bytes)
181}
182pub(crate) fn decode_image(format: &str, image: &ImageRecord) -> Result<DecodedImage> {
184 let bytes = write_image(format, image)?;
185 let info = layout(format, &bytes)?;
186 let mut rgba = vec![0; info.width * info.height * 4];
187 for y in 0..info.height {
188 for x in 0..info.width {
189 let row = if info.top { y } else { info.height - 1 - y };
190 let column = if info.right { info.width - 1 - x } else { x };
191 let mut at = info.offset + row * info.stride + column * (info.bits / 8);
192 if info.bits == 8 {
193 at = info.palette + bytes[at] as usize * 4;
194 if at + 4 > info.offset {
195 return Err(error("BMP palette index out of range"));
196 }
197 }
198 rgba[(y * info.width + x) * 4..(y * info.width + x) * 4 + 4].copy_from_slice(&[
199 bytes[at + 2],
200 bytes[at + 1],
201 bytes[at],
202 if info.bits == 32 { bytes[at + 3] } else { 255 },
203 ]);
204 }
205 }
206 Ok(DecodedImage {
207 width: info.width as u32,
208 height: info.height as u32,
209 rgba,
210 })
211}
212pub(crate) fn create_image(
214 format: &str,
215 width: u32,
216 height: u32,
217 rgba: &[u8],
218) -> Result<ImageRecord> {
219 if width == 0
220 || height == 0
221 || width > 16384
222 || height > 16384
223 || width as usize * height as usize > 16_777_216
224 || rgba.len() != width as usize * height as usize * 4
225 {
226 return Err(error("RGBA dimensions mismatch"));
227 }
228 let mut header = if format == "bmp" {
229 vec![0; 54]
230 } else if format == "tga" {
231 vec![0; 18]
232 } else {
233 return Err(error("Unsupported image format"));
234 };
235 if format == "bmp" {
236 header[0..2].copy_from_slice(b"BM");
237 header[2..6].copy_from_slice(&(54 + rgba.len() as u32).to_le_bytes());
238 header[10..14].copy_from_slice(&54_u32.to_le_bytes());
239 header[14..18].copy_from_slice(&40_u32.to_le_bytes());
240 header[18..22].copy_from_slice(&width.to_le_bytes());
241 header[22..26].copy_from_slice(&(-(height as i32)).to_le_bytes());
242 header[26..28].copy_from_slice(&1_u16.to_le_bytes());
243 header[28..30].copy_from_slice(&32_u16.to_le_bytes());
244 header[34..38].copy_from_slice(&(rgba.len() as u32).to_le_bytes());
245 } else {
246 header[2] = 2;
247 header[12..14].copy_from_slice(&(width as u16).to_le_bytes());
248 header[14..16].copy_from_slice(&(height as u16).to_le_bytes());
249 header[16] = 32;
250 header[17] = 40;
251 }
252 let mut pixels = rgba.to_vec();
253 for pixel in pixels.as_chunks_mut::<4>().0 {
254 pixel.swap(0, 2);
255 }
256 Ok(ImageRecord {
257 header: hex(&header),
258 data: hex(&pixels),
259 trailing: String::new(),
260 })
261}
262#[cfg(test)]
263mod tests {
264 use super::*;
265 #[test]
266 fn pixel_roundtrip() -> Result<()> {
267 for format in ["bmp", "tga"] {
268 let pixels = [255, 0, 0, 255, 0, 0, 255, 128];
269 let image = create_image(format, 2, 1, &pixels)?;
270 assert_eq!(decode_image(format, &image)?.rgba, pixels);
271 let bytes = write_image(format, &image)?;
272 assert_eq!(write_image(format, &parse_image(format, &bytes)?)?, bytes);
273 }
274 Ok(())
275 }
276}