1use std::fs;
2use std::io::{Error as IoError, ErrorKind};
3use std::path::{Path, PathBuf};
4
5use webp_rust::encoder::{
6 encode_lossless_image_to_webp_with_options, encode_lossy_image_to_webp_with_options,
7};
8use webp_rust::{ImageBuffer, LosslessEncodingOptions, LossyEncodingOptions};
9
10type Error = Box<dyn std::error::Error>;
11
12const FILE_HEADER_SIZE: usize = 14;
13const MIN_INFO_HEADER_SIZE: usize = 40;
14
15fn invalid_data(message: &'static str) -> Error {
16 Box::new(IoError::new(ErrorKind::InvalidData, message))
17}
18
19fn invalid_input(message: &'static str) -> Error {
20 Box::new(IoError::new(ErrorKind::InvalidInput, message))
21}
22
23fn invalid_input_owned(message: String) -> Error {
24 Box::new(IoError::new(ErrorKind::InvalidInput, message))
25}
26
27fn read_u16_le(data: &[u8], offset: usize) -> Result<u16, Error> {
28 let bytes = data
29 .get(offset..offset + 2)
30 .ok_or_else(|| invalid_data("BMP header is truncated"))?;
31 Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
32}
33
34fn read_u32_le(data: &[u8], offset: usize) -> Result<u32, Error> {
35 let bytes = data
36 .get(offset..offset + 4)
37 .ok_or_else(|| invalid_data("BMP header is truncated"))?;
38 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
39}
40
41fn read_i32_le(data: &[u8], offset: usize) -> Result<i32, Error> {
42 let bytes = data
43 .get(offset..offset + 4)
44 .ok_or_else(|| invalid_data("BMP header is truncated"))?;
45 Ok(i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
46}
47
48fn row_stride(width: usize, bytes_per_pixel: usize) -> Result<usize, Error> {
49 let raw = width
50 .checked_mul(bytes_per_pixel)
51 .ok_or_else(|| invalid_data("BMP row size overflow"))?;
52 Ok((raw + 3) & !3)
53}
54
55fn decode_bmp_to_rgba(data: &[u8]) -> Result<ImageBuffer, Error> {
56 if data.len() < FILE_HEADER_SIZE + MIN_INFO_HEADER_SIZE {
57 return Err(invalid_data("BMP file is too small"));
58 }
59 if &data[0..2] != b"BM" {
60 return Err(invalid_data("expected a BMP file"));
61 }
62
63 let pixel_offset = read_u32_le(data, 10)? as usize;
64 let dib_header_size = read_u32_le(data, 14)? as usize;
65 if dib_header_size < MIN_INFO_HEADER_SIZE {
66 return Err(invalid_data("unsupported BMP DIB header"));
67 }
68 if data.len() < FILE_HEADER_SIZE + dib_header_size {
69 return Err(invalid_data("BMP DIB header is truncated"));
70 }
71
72 let width_i32 = read_i32_le(data, 18)?;
73 let height_i32 = read_i32_le(data, 22)?;
74 let planes = read_u16_le(data, 26)?;
75 let bits_per_pixel = read_u16_le(data, 28)?;
76 let compression = read_u32_le(data, 30)?;
77
78 if planes != 1 {
79 return Err(invalid_data("unsupported BMP plane count"));
80 }
81 if compression != 0 {
82 return Err(invalid_data("only uncompressed BMP is supported"));
83 }
84 if width_i32 <= 0 {
85 return Err(invalid_data("BMP width must be positive"));
86 }
87 if height_i32 == 0 {
88 return Err(invalid_data("BMP height must be non-zero"));
89 }
90
91 let bytes_per_pixel = match bits_per_pixel {
92 24 => 3usize,
93 32 => 4usize,
94 _ => return Err(invalid_data("only 24bpp and 32bpp BMP are supported")),
95 };
96
97 let width = width_i32 as usize;
98 let top_down = height_i32 < 0;
99 let height = height_i32
100 .checked_abs()
101 .ok_or_else(|| invalid_data("BMP height is out of range"))? as usize;
102
103 let stride = row_stride(width, bytes_per_pixel)?;
104 let pixel_bytes = stride
105 .checked_mul(height)
106 .ok_or_else(|| invalid_data("BMP pixel storage overflow"))?;
107 let pixel_end = pixel_offset
108 .checked_add(pixel_bytes)
109 .ok_or_else(|| invalid_data("BMP pixel offset overflow"))?;
110 if pixel_offset > data.len() || pixel_end > data.len() {
111 return Err(invalid_data("BMP pixel data is truncated"));
112 }
113
114 let rgba_len = width
115 .checked_mul(height)
116 .and_then(|pixels| pixels.checked_mul(4))
117 .ok_or_else(|| invalid_data("BMP output size overflow"))?;
118 let mut rgba = vec![0u8; rgba_len];
119
120 for y in 0..height {
121 let src_y = if top_down { y } else { height - 1 - y };
122 let src_row = pixel_offset + src_y * stride;
123 let dst_row = y * width * 4;
124 for x in 0..width {
125 let src = src_row + x * bytes_per_pixel;
126 let dst = dst_row + x * 4;
127 rgba[dst] = data[src + 2];
128 rgba[dst + 1] = data[src + 1];
129 rgba[dst + 2] = data[src];
130 rgba[dst + 3] = if bytes_per_pixel == 4 {
131 data[src + 3]
132 } else {
133 0xff
134 };
135 }
136 }
137
138 Ok(ImageBuffer {
139 width,
140 height,
141 rgba,
142 })
143}
144
145fn default_output_path(input: &Path) -> PathBuf {
146 let mut output = input.to_path_buf();
147 output.set_extension("webp");
148 output
149}
150
151fn usage() -> &'static str {
152 "usage: cargo run --example bmp2webp -- [-z 0..9] [--lossy --quality 0..100 [--lossy-opt-level 0..9]] [--opt-level 0..9] <input.bmp> [output.webp]"
153}
154
155fn parse_u8_level(value: &str, what: &str) -> Result<u8, Error> {
156 value
157 .parse::<u8>()
158 .map_err(|_| invalid_input_owned(format!("invalid {what}: {value}")))
159}
160
161fn parse_optimization_level(value: &str) -> Result<u8, Error> {
162 let level = parse_u8_level(value, "optimization level")?;
163 if level > 9 {
164 return Err(invalid_input("optimization level must be in 0..=9"));
165 }
166 Ok(level)
167}
168
169fn parse_lossy_optimization_level(value: &str) -> Result<u8, Error> {
170 let level = parse_u8_level(value, "lossy optimization level")?;
171 if level > 9 {
172 return Err(invalid_input("lossy optimization level must be in 0..=9"));
173 }
174 Ok(level)
175}
176
177fn parse_quality(value: &str) -> Result<u8, Error> {
178 let quality = value
179 .parse::<u8>()
180 .map_err(|_| invalid_input_owned(format!("invalid quality: {value}")))?;
181 if quality > 100 {
182 return Err(invalid_input("quality must be in 0..=100"));
183 }
184 Ok(quality)
185}
186
187fn main() -> Result<(), Error> {
188 let mut args = std::env::args_os().skip(1);
189 let mut input = None;
190 let mut output = None;
191 let mut options = LosslessEncodingOptions::default();
192 let mut lossy = false;
193 let mut lossy_options = LossyEncodingOptions::default();
194 let mut shared_optimization_level = None;
195 let mut lossless_optimization_explicit = false;
196 let mut lossy_optimization_explicit = false;
197
198 while let Some(arg) = args.next() {
199 match arg.to_string_lossy().as_ref() {
200 "--lossy" => lossy = true,
201 "--opt" | "--opt-level" | "-O" => {
202 let value = args.next().ok_or_else(|| invalid_input(usage()))?;
203 options.optimization_level = parse_optimization_level(&value.to_string_lossy())?;
204 lossless_optimization_explicit = true;
205 }
206 "--quality" | "-q" => {
207 let value = args.next().ok_or_else(|| invalid_input(usage()))?;
208 lossy_options.quality = parse_quality(&value.to_string_lossy())?;
209 }
210 "-z" => {
211 let value = args.next().ok_or_else(|| invalid_input(usage()))?;
212 shared_optimization_level = Some(parse_u8_level(
213 &value.to_string_lossy(),
214 "optimization level",
215 )?);
216 }
217 "--lossy-opt-level" => {
218 let value = args.next().ok_or_else(|| invalid_input(usage()))?;
219 lossy_options.optimization_level =
220 parse_lossy_optimization_level(&value.to_string_lossy())?;
221 lossy_optimization_explicit = true;
222 }
223 _ => {
224 if input.is_none() {
225 input = Some(PathBuf::from(arg));
226 } else if output.is_none() {
227 output = Some(PathBuf::from(arg));
228 } else {
229 return Err(invalid_input(usage()));
230 }
231 }
232 }
233 }
234
235 if let Some(level) = shared_optimization_level {
236 if lossy {
237 if !lossy_optimization_explicit {
238 lossy_options.optimization_level = if level <= 9 {
239 level
240 } else {
241 return Err(invalid_input("lossy optimization level must be in 0..=9"));
242 };
243 }
244 } else if !lossless_optimization_explicit {
245 options.optimization_level = if level <= 9 {
246 level
247 } else {
248 return Err(invalid_input("optimization level must be in 0..=9"));
249 };
250 }
251 }
252
253 let input = input.ok_or_else(|| invalid_input(usage()))?;
254 let output = output.unwrap_or_else(|| default_output_path(&input));
255
256 let data = fs::read(&input)?;
257 let image = decode_bmp_to_rgba(&data)?;
258 let webp = if lossy {
259 encode_lossy_image_to_webp_with_options(&image, &lossy_options)?
260 } else {
261 encode_lossless_image_to_webp_with_options(&image, &options)?
262 };
263
264 if let Some(parent) = output.parent() {
265 if !parent.as_os_str().is_empty() {
266 fs::create_dir_all(parent)?;
267 }
268 }
269 fs::write(&output, webp)?;
270
271 println!("{}", output.display());
272 Ok(())
273}