use rustyfi_backend::ImageResource;
use super::base64;
pub(super) fn data_uri(image: &ImageResource) -> String {
match &image.jpeg_dct {
Some(jpeg) => {
let mut out = String::from("data:image/jpeg;base64,");
out.push_str(&base64::encode(&jpeg.bytes));
out
}
None => {
let mut out = String::from("data:image/bmp;base64,");
out.push_str(&base64::encode(&encode_bmp(image)));
out
}
}
}
fn encode_bmp(image: &ImageResource) -> Vec<u8> {
let w = image.px_w as usize;
let h = image.px_h as usize;
let row_bytes = w * 3;
let padded_row = (row_bytes + 3) & !3;
let pixel_data_len = padded_row * h;
let pixel_data_offset: u32 = 14 + 40;
let file_size = pixel_data_offset as usize + pixel_data_len;
let mut buf = Vec::with_capacity(file_size);
buf.extend_from_slice(b"BM");
buf.extend_from_slice(&(file_size as u32).to_le_bytes());
buf.extend_from_slice(&[0, 0, 0, 0]); buf.extend_from_slice(&pixel_data_offset.to_le_bytes());
buf.extend_from_slice(&40u32.to_le_bytes()); buf.extend_from_slice(&(w as i32).to_le_bytes()); buf.extend_from_slice(&(h as i32).to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&24u16.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(pixel_data_len as u32).to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u32.to_le_bytes());
for src_row in (0..h).rev() {
let start = src_row * row_bytes;
for col in 0..w {
let px = start + col * 3;
let (r, g, b) = (
image.samples[px],
image.samples[px + 1],
image.samples[px + 2],
);
buf.push(b);
buf.push(g);
buf.push(r);
}
buf.resize(buf.len() + (padded_row - row_bytes), 0);
}
debug_assert_eq!(buf.len(), file_size);
buf
}
#[cfg(test)]
mod tests {
use super::*;
fn tiny_2x2() -> ImageResource {
ImageResource {
samples: vec![
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, ],
px_w: 2,
px_h: 2,
jpeg_dct: None,
pdf: None,
}
}
#[test]
fn encodes_a_valid_bmp_header() {
let bmp = encode_bmp(&tiny_2x2());
assert_eq!(&bmp[0..2], b"BM", "missing BMP magic");
let file_size = u32::from_le_bytes(bmp[2..6].try_into().unwrap());
assert_eq!(file_size as usize, bmp.len());
let data_offset = u32::from_le_bytes(bmp[10..14].try_into().unwrap());
assert_eq!(data_offset, 54);
let width = i32::from_le_bytes(bmp[18..22].try_into().unwrap());
let height = i32::from_le_bytes(bmp[22..26].try_into().unwrap());
assert_eq!(width, 2);
assert_eq!(height, 2, "expected a positive (bottom-up) height");
let bpp = u16::from_le_bytes(bmp[28..30].try_into().unwrap());
assert_eq!(bpp, 24);
}
#[test]
fn bottom_up_row_order_puts_the_source_bottom_row_first() {
let bmp = encode_bmp(&tiny_2x2());
let px0 = &bmp[54..57];
assert_eq!(
px0,
&[255, 0, 0],
"first BMP row's first pixel should be blue (BGR)"
);
let px1 = &bmp[57..60];
assert_eq!(
px1,
&[255, 255, 255],
"first BMP row's second pixel should be white (BGR)"
);
}
#[test]
fn data_uri_has_the_expected_scheme_and_mime() {
let uri = data_uri(&tiny_2x2());
assert!(
uri.starts_with("data:image/bmp;base64,"),
"unexpected URI: {uri}"
);
}
#[test]
fn a_baseline_jpeg_passes_through_verbatim() {
let jpeg_bytes = vec![0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3, 0xFF, 0xD9];
let res = ImageResource {
jpeg_dct: Some(rustyfi_backend::JpegDct {
bytes: jpeg_bytes.clone(),
components: 3,
}),
..tiny_2x2()
};
let uri = data_uri(&res);
assert!(uri.starts_with("data:image/jpeg;base64,"), "{uri}");
assert_eq!(
uri,
format!("data:image/jpeg;base64,{}", super::base64::encode(&jpeg_bytes)),
"the original JPEG stream must be embedded unchanged"
);
}
}