1use std::fs;
2use std::path::{Path, PathBuf};
3
4use webp_rust::decode;
5use webp_rust::decoder::{decode_animation_webp, get_features};
6
7type Error = Box<dyn std::error::Error>;
8type DecoderError = webp_rust::decoder::DecoderError;
9
10const FILE_HEADER_SIZE: usize = 14;
11const INFO_HEADER_SIZE: usize = 40;
12const BMP_HEADER_SIZE: usize = FILE_HEADER_SIZE + INFO_HEADER_SIZE;
13const BITS_PER_PIXEL: usize = 24;
14const PIXELS_PER_METER: u32 = 3_780;
15
16fn row_stride(width: usize) -> Result<usize, DecoderError> {
17 let raw = width
18 .checked_mul(3)
19 .ok_or(DecoderError::InvalidParam("BMP row size overflow"))?;
20 Ok((raw + 3) & !3)
21}
22
23fn encode_bmp24_from_rgba(
24 width: usize,
25 height: usize,
26 rgba: &[u8],
27) -> Result<Vec<u8>, DecoderError> {
28 if width == 0 || height == 0 {
29 return Err(DecoderError::InvalidParam(
30 "BMP dimensions must be non-zero",
31 ));
32 }
33
34 let expected_len = width
35 .checked_mul(height)
36 .and_then(|pixels| pixels.checked_mul(4))
37 .ok_or(DecoderError::InvalidParam("RGBA buffer size overflow"))?;
38 if rgba.len() != expected_len {
39 return Err(DecoderError::InvalidParam(
40 "RGBA buffer length does not match dimensions",
41 ));
42 }
43
44 let stride = row_stride(width)?;
45 let pixel_bytes = stride
46 .checked_mul(height)
47 .ok_or(DecoderError::InvalidParam("BMP pixel storage overflow"))?;
48 let file_size = BMP_HEADER_SIZE
49 .checked_add(pixel_bytes)
50 .ok_or(DecoderError::InvalidParam("BMP file size overflow"))?;
51
52 let mut bmp = vec![0u8; file_size];
53
54 bmp[0..2].copy_from_slice(b"BM");
55 bmp[2..6].copy_from_slice(&(file_size as u32).to_le_bytes());
56 bmp[10..14].copy_from_slice(&(BMP_HEADER_SIZE as u32).to_le_bytes());
57
58 bmp[14..18].copy_from_slice(&(INFO_HEADER_SIZE as u32).to_le_bytes());
59 bmp[18..22].copy_from_slice(&(width as i32).to_le_bytes());
60 bmp[22..26].copy_from_slice(&(height as i32).to_le_bytes());
61 bmp[26..28].copy_from_slice(&(1u16).to_le_bytes());
62 bmp[28..30].copy_from_slice(&(BITS_PER_PIXEL as u16).to_le_bytes());
63 bmp[34..38].copy_from_slice(&(pixel_bytes as u32).to_le_bytes());
64 bmp[38..42].copy_from_slice(&PIXELS_PER_METER.to_le_bytes());
65 bmp[42..46].copy_from_slice(&PIXELS_PER_METER.to_le_bytes());
66
67 let mut dest_offset = BMP_HEADER_SIZE;
68 let mut row = vec![0u8; stride];
69 for y in (0..height).rev() {
70 row.fill(0);
71 let src_row = y * width * 4;
72 for x in 0..width {
73 let src = src_row + x * 4;
74 let dst = x * 3;
75 row[dst] = rgba[src + 2];
76 row[dst + 1] = rgba[src + 1];
77 row[dst + 2] = rgba[src];
78 }
79 bmp[dest_offset..dest_offset + stride].copy_from_slice(&row);
80 dest_offset += stride;
81 }
82
83 Ok(bmp)
84}
85
86fn default_output_path(input: &Path) -> PathBuf {
87 let mut output = input.to_path_buf();
88 output.set_extension("bmp");
89 output
90}
91
92fn default_animation_output_prefix(input: &Path) -> PathBuf {
93 let mut output = input.to_path_buf();
94 output.set_extension("");
95 output
96}
97
98fn animation_frame_path(prefix: &Path, index: usize) -> PathBuf {
99 let parent = prefix.parent().unwrap_or_else(|| Path::new(""));
100 let stem = prefix
101 .file_name()
102 .and_then(|name| name.to_str())
103 .filter(|name| !name.is_empty())
104 .unwrap_or("frame");
105 parent.join(format!("{stem}_{index:04}.bmp"))
106}
107
108fn main() -> Result<(), Error> {
109 let mut args = std::env::args_os().skip(1);
110 let input = args
111 .next()
112 .map(PathBuf::from)
113 .unwrap_or_else(|| PathBuf::from("_testdata/sample.webp"));
114 let output = args.next().map(PathBuf::from).unwrap_or_else(|| {
115 if input == PathBuf::from("_testdata/sample.webp") {
116 PathBuf::from("target/sample.bmp")
117 } else if input == PathBuf::from("_testdata/sample_animation.webp") {
118 PathBuf::from("target/sample_animation")
119 } else {
120 default_output_path(&input)
121 }
122 });
123
124 let data = fs::read(&input)?;
125 let features = get_features(&data)?;
126
127 if features.has_animation {
128 let mut prefix = if output.is_dir() {
129 output.join(
130 input
131 .file_stem()
132 .and_then(|stem| stem.to_str())
133 .unwrap_or("frame"),
134 )
135 } else {
136 output
137 };
138 if prefix.extension().is_some() {
139 prefix.set_extension("");
140 } else if prefix.as_os_str().is_empty() {
141 prefix = default_animation_output_prefix(&input);
142 }
143
144 let animation = decode_animation_webp(&data)?;
145 let mut written_paths = Vec::with_capacity(animation.frames.len());
146 for (index, frame) in animation.frames.into_iter().enumerate() {
147 let path = animation_frame_path(&prefix, index);
148 if let Some(parent) = path.parent() {
149 if !parent.as_os_str().is_empty() {
150 fs::create_dir_all(parent)?;
151 }
152 }
153 let bmp = encode_bmp24_from_rgba(animation.width, animation.height, &frame.rgba)?;
154 fs::write(&path, bmp)?;
155 written_paths.push(path);
156 }
157 for path in written_paths {
158 println!("{}", path.display());
159 }
160 return Ok(());
161 }
162
163 let image = decode(&data)?;
164 let bmp = encode_bmp24_from_rgba(image.width, image.height, &image.rgba)?;
165
166 if let Some(parent) = output.parent() {
167 if !parent.as_os_str().is_empty() {
168 fs::create_dir_all(parent)?;
169 }
170 }
171 fs::write(&output, bmp)?;
172
173 println!("{}", output.display());
174 Ok(())
175}