Skip to main content

webp2bmp/
webp2bmp.rs

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}