386 lines
16 KiB
Rust
386 lines
16 KiB
Rust
//! Device-independent bitmaps (DIBs), the pixels inside image records: written bottom-up as 24-bit
|
||
//! (opaque, composited over white) or 32-bit premultiplied BGRA (AlphaBlend's source); read from 1,
|
||
//! 4, 8, 16, 24 and 32-bit uncompressed and bit-field bitmaps and from embedded JPEG and PNG.
|
||
|
||
use std::io::Cursor;
|
||
|
||
use crate::bytes::Reader;
|
||
|
||
/// Most pixels along a side of an image the writers store (larger ones are scaled down).
|
||
const MAX_SIDE: u32 = 8192;
|
||
/// Most pixels an image the writers store holds.
|
||
const MAX_PIXELS: u64 = 1 << 24;
|
||
/// Most pixels along a side, and in all, of a bitmap the reader decodes.
|
||
const MAX_READ_SIDE: u32 = 1 << 15;
|
||
const MAX_READ_PIXELS: u64 = 1 << 26;
|
||
|
||
const BI_RGB: u32 = 0;
|
||
const BI_BITFIELDS: u32 = 3;
|
||
const BI_JPEG: u32 = 4;
|
||
const BI_PNG: u32 = 5;
|
||
|
||
/// Pixels with straight (not premultiplied) alpha, row by row from the top.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub(crate) struct Rgba {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub pixels: Vec<u8>,
|
||
}
|
||
|
||
impl Rgba {
|
||
/// `img`, scaled down to what a metafile stores when it is larger.
|
||
pub fn fitted(img: image::RgbaImage) -> Self {
|
||
let (w, h) = img.dimensions();
|
||
let side = f64::from(w.max(h)) / f64::from(MAX_SIDE);
|
||
let area = (u64::from(w) * u64::from(h)) as f64 / MAX_PIXELS as f64;
|
||
let k = side.max(area.sqrt());
|
||
let img = if k > 1.0 {
|
||
let nw = ((f64::from(w) / k).floor() as u32).max(1);
|
||
let nh = ((f64::from(h) / k).floor() as u32).max(1);
|
||
image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Triangle)
|
||
} else {
|
||
img
|
||
};
|
||
let (width, height) = img.dimensions();
|
||
Self { width, height, pixels: img.into_raw() }
|
||
}
|
||
|
||
/// Every pixel fully opaque?
|
||
pub fn opaque(&self) -> bool {
|
||
self.pixels.as_chunks::<4>().0.iter().all(|p| p[3] == 255)
|
||
}
|
||
|
||
/// As PNG bytes.
|
||
pub fn to_png(&self) -> Result<Vec<u8>, String> {
|
||
let img = image::RgbaImage::from_raw(self.width, self.height, self.pixels.clone()).ok_or("the image's pixels don't match its size")?;
|
||
let mut png = vec![];
|
||
img.write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).map_err(|e| e.to_string())?;
|
||
Ok(png)
|
||
}
|
||
}
|
||
|
||
/// A BITMAPINFOHEADER of a bottom-up, uncompressed `width` × `height` bitmap of `bits` per pixel.
|
||
fn header(width: u32, height: u32, bits: u16, size: usize) -> Vec<u8> {
|
||
let mut h = crate::bytes::Out::default();
|
||
h.u32(40);
|
||
h.i32(i32::try_from(width).unwrap_or(i32::MAX));
|
||
h.i32(i32::try_from(height).unwrap_or(i32::MAX));
|
||
h.u16(1);
|
||
h.u16(bits);
|
||
h.u32(BI_RGB);
|
||
h.u32(u32::try_from(size).unwrap_or(0));
|
||
// 2835 pixels a metre: 72 ppi (the size on the page comes from the record).
|
||
h.i32(2835);
|
||
h.i32(2835);
|
||
h.u32(0);
|
||
h.u32(0);
|
||
h.0
|
||
}
|
||
|
||
/// The bytes of a row of `width` pixels of `bits` each, padded to 4 bytes.
|
||
fn stride(width: u32, bits: u32) -> Option<usize> {
|
||
let row_bits = u64::from(width).checked_mul(u64::from(bits))?;
|
||
usize::try_from(row_bits.div_ceil(32) * 4).ok()
|
||
}
|
||
|
||
/// `img` at `opacity`, composited over white, as a 24-bit DIB → (header, bits).
|
||
pub(crate) fn rgb24(img: &Rgba, opacity: f32) -> (Vec<u8>, Vec<u8>) {
|
||
let row = stride(img.width, 24).unwrap_or(0);
|
||
let mut bits = vec![0u8; row * img.height as usize];
|
||
let over = |c: u8, a: u32| ((u32::from(c) * a + 255 * (255 - a) + 127) / 255) as u8;
|
||
for (y, src) in img.pixels.chunks_exact((img.width as usize * 4).max(1)).enumerate() {
|
||
// Bottom-up: the first stored row is the image's last.
|
||
let Some(at) = (img.height as usize).checked_sub(y + 1).map(|r| r * row) else { continue };
|
||
let Some(dst) = bits.get_mut(at..at + img.width as usize * 3) else { continue };
|
||
for (d, p) in dst.as_chunks_mut::<3>().0.iter_mut().zip(src.as_chunks::<4>().0) {
|
||
let a = (f32::from(p[3]) * opacity.clamp(0.0, 1.0)).round() as u32;
|
||
d.copy_from_slice(&[over(p[2], a), over(p[1], a), over(p[0], a)]);
|
||
}
|
||
}
|
||
(header(img.width, img.height, 24, bits.len()), bits)
|
||
}
|
||
|
||
/// `img` at `opacity` as a 32-bit DIB of premultiplied BGRA → (header, bits).
|
||
pub(crate) fn bgra32(img: &Rgba, opacity: f32) -> (Vec<u8>, Vec<u8>) {
|
||
let row = img.width as usize * 4;
|
||
let mut bits = vec![0u8; row * img.height as usize];
|
||
for (y, src) in img.pixels.chunks_exact(row.max(1)).enumerate() {
|
||
let Some(at) = (img.height as usize).checked_sub(y + 1).map(|r| r * row) else { continue };
|
||
let Some(dst) = bits.get_mut(at..at + row) else { continue };
|
||
for (d, p) in dst.as_chunks_mut::<4>().0.iter_mut().zip(src.as_chunks::<4>().0) {
|
||
let a = (f32::from(p[3]) * opacity.clamp(0.0, 1.0)).round() as u32;
|
||
let pre = |c: u8| ((u32::from(c) * a + 127) / 255) as u8;
|
||
d.copy_from_slice(&[pre(p[2]), pre(p[1]), pre(p[0]), a as u8]);
|
||
}
|
||
}
|
||
(header(img.width, img.height, 32, bits.len()), bits)
|
||
}
|
||
|
||
/// How the fourth byte of 32-bit pixels reads.
|
||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||
pub(crate) enum Alpha {
|
||
/// Unused (StretchDIBits and BitBlt ignore it): every pixel opaque.
|
||
Ignore,
|
||
/// Premultiplied alpha (AlphaBlend with per-pixel alpha).
|
||
Premultiplied,
|
||
}
|
||
|
||
/// A colour channel of `v` under `mask`, scaled to 0–255.
|
||
fn channel(v: u32, mask: u32) -> u8 {
|
||
if mask == 0 {
|
||
return 0;
|
||
}
|
||
let shift = mask.trailing_zeros();
|
||
let max = u64::from(mask >> shift);
|
||
let x = u64::from((v & mask) >> shift);
|
||
((x * 255 + max / 2) / max.max(1)).min(255) as u8
|
||
}
|
||
|
||
/// Read a DIB from its BITMAPINFO (`bmi`: the header and colour table) and pixel `bits`.
|
||
pub(crate) fn decode(bmi: &[u8], bits: &[u8], alpha: Alpha) -> Result<Rgba, String> {
|
||
let bad = || "a bitmap is damaged".to_string();
|
||
let mut r = Reader::new(bmi);
|
||
let size = r.u32().ok_or_else(bad)?;
|
||
let (width, height, bpp, compression, colors, palette_entry, masks_at) = if size == 12 {
|
||
// BITMAPCOREHEADER: 16-bit sizes, RGB triples.
|
||
let w = i32::from(r.u16().ok_or_else(bad)?);
|
||
let h = i32::from(r.u16().ok_or_else(bad)?);
|
||
r.skip(2).ok_or_else(bad)?;
|
||
let bpp = r.u16().ok_or_else(bad)?;
|
||
(w, h, bpp, BI_RGB, 0, 3, 0)
|
||
} else if size >= 40 {
|
||
let w = r.i32().ok_or_else(bad)?;
|
||
let h = r.i32().ok_or_else(bad)?;
|
||
r.skip(2).ok_or_else(bad)?;
|
||
let bpp = r.u16().ok_or_else(bad)?;
|
||
let c = r.u32().ok_or_else(bad)?;
|
||
r.skip(12).ok_or_else(bad)?;
|
||
let used = r.u32().ok_or_else(bad)?;
|
||
(w, h, bpp, c, used, 4, 40)
|
||
} else {
|
||
return Err(bad());
|
||
};
|
||
if compression == BI_JPEG || compression == BI_PNG {
|
||
// The size comes from the image header, so an over-limit bitmap is refused before any pixels are decoded.
|
||
let (w, h) = image::ImageReader::new(Cursor::new(bits)).with_guessed_format().map_err(|_| bad())?.into_dimensions().map_err(|_| bad())?;
|
||
if w > MAX_READ_SIDE || h > MAX_READ_SIDE || u64::from(w) * u64::from(h) > MAX_READ_PIXELS {
|
||
return Err("a bitmap is too large to read".into());
|
||
}
|
||
let img = image::load_from_memory(bits).map_err(|_| bad())?.to_rgba8();
|
||
return Ok(Rgba { width: w, height: h, pixels: img.into_raw() });
|
||
}
|
||
if compression != BI_RGB && compression != BI_BITFIELDS {
|
||
return Err("compressed (RLE) bitmaps can't be read yet".into());
|
||
}
|
||
let top_down = height < 0;
|
||
let (w, h) = (width.unsigned_abs(), height.unsigned_abs());
|
||
if w == 0 || h == 0 {
|
||
return Err("a bitmap is empty".into());
|
||
}
|
||
if w > MAX_READ_SIDE || h > MAX_READ_SIDE || u64::from(w) * u64::from(h) > MAX_READ_PIXELS {
|
||
return Err("a bitmap is too large to read".into());
|
||
}
|
||
let header_len = size as usize;
|
||
// Bit-field masks follow a plain 40-byte header, or sit inside the larger ones.
|
||
let fields = compression == BI_BITFIELDS;
|
||
let masks: Option<[u32; 4]> = fields.then(|| {
|
||
let mut m = Reader::new(bmi.get(masks_at..).unwrap_or_default());
|
||
let rgb = [m.u32().unwrap_or(0), m.u32().unwrap_or(0), m.u32().unwrap_or(0)];
|
||
// An alpha mask comes with BITMAPV3INFOHEADER and later.
|
||
let a = if header_len >= 56 { m.u32().unwrap_or(0) } else { 0 };
|
||
[rgb[0], rgb[1], rgb[2], a]
|
||
});
|
||
let palette_at = header_len + if fields && header_len == 40 { 12 } else { 0 };
|
||
let bpp_u = u32::from(bpp);
|
||
let palette: Vec<[u8; 3]> = if bpp_u <= 8 {
|
||
let n = if colors == 0 { 1usize << bpp_u } else { (colors as usize).min(256) };
|
||
bmi.get(palette_at..)
|
||
.unwrap_or_default()
|
||
.chunks_exact(palette_entry)
|
||
.take(n)
|
||
.map(|e| [e.get(2).copied().unwrap_or(0), e.get(1).copied().unwrap_or(0), e.first().copied().unwrap_or(0)])
|
||
.collect()
|
||
} else {
|
||
vec![]
|
||
};
|
||
if !matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32) {
|
||
return Err(format!("{bpp}-bit bitmaps can't be read"));
|
||
}
|
||
let row = stride(w, bpp_u).ok_or_else(bad)?;
|
||
if bits.len() < row.checked_mul(h as usize).ok_or_else(bad)? {
|
||
return Err(bad());
|
||
}
|
||
let (mr, mg, mb, ma) = match (masks, bpp) {
|
||
(Some([r, g, b, a]), _) => (r, g, b, a),
|
||
(None, 16) => (0x7c00, 0x03e0, 0x001f, 0),
|
||
_ => (0x00ff_0000, 0x0000_ff00, 0x0000_00ff, 0xff00_0000),
|
||
};
|
||
let mut pixels = Vec::with_capacity(w as usize * h as usize * 4);
|
||
for y in 0..h as usize {
|
||
let src_row = if top_down { y } else { h as usize - 1 - y };
|
||
let line = bits.get(src_row * row..src_row * row + row).ok_or_else(bad)?;
|
||
for x in 0..w as usize {
|
||
let px: [u8; 4] = match bpp {
|
||
1 | 4 | 8 => {
|
||
let bit = x * bpp_u as usize;
|
||
let byte = line.get(bit / 8).copied().unwrap_or(0);
|
||
let idx = (byte >> (8 - bpp_u as usize - bit % 8)) & ((1u16 << bpp_u) - 1) as u8;
|
||
let [r, g, b] = palette.get(idx as usize).copied().unwrap_or([0, 0, 0]);
|
||
[r, g, b, 255]
|
||
}
|
||
16 => {
|
||
let v = u32::from(u16::from_le_bytes([line.get(x * 2).copied().unwrap_or(0), line.get(x * 2 + 1).copied().unwrap_or(0)]));
|
||
[channel(v, mr), channel(v, mg), channel(v, mb), 255]
|
||
}
|
||
24 => {
|
||
let p = line.get(x * 3..x * 3 + 3).unwrap_or(&[0, 0, 0]);
|
||
[p[2], p[1], p[0], 255]
|
||
}
|
||
_ => {
|
||
let p = line.get(x * 4..x * 4 + 4).unwrap_or(&[0, 0, 0, 0]);
|
||
let v = u32::from_le_bytes([p[0], p[1], p[2], p[3]]);
|
||
let a = if alpha == Alpha::Premultiplied && ma != 0 { channel(v, ma) } else { 255 };
|
||
let un =
|
||
|c: u8| if alpha == Alpha::Premultiplied && a < 255 { (u32::from(c) * 255 / u32::from(a.max(1))).min(255) as u8 } else { c };
|
||
[un(channel(v, mr)), un(channel(v, mg)), un(channel(v, mb)), if alpha == Alpha::Premultiplied { a } else { 255 }]
|
||
}
|
||
};
|
||
pixels.extend_from_slice(&px);
|
||
}
|
||
}
|
||
Ok(Rgba { width: w, height: h, pixels })
|
||
}
|
||
|
||
/// A packed DIB (a BITMAPINFO and its bits, one after the other, as WMF records hold them) split
|
||
/// into the two.
|
||
pub(crate) fn split(packed: &[u8]) -> Option<(&[u8], &[u8])> {
|
||
let mut r = Reader::new(packed);
|
||
let size = r.u32()? as usize;
|
||
let (bpp, compression, used, entry) = if size == 12 {
|
||
r.skip(6)?;
|
||
(r.u16()?, BI_RGB, 0, 3)
|
||
} else {
|
||
r.skip(10)?;
|
||
let bpp = r.u16()?;
|
||
let c = r.u32()?;
|
||
r.skip(12)?;
|
||
(bpp, c, r.u32()? as usize, 4)
|
||
};
|
||
let masks = if compression == BI_BITFIELDS && size == 40 { 12 } else { 0 };
|
||
let colors = if used > 0 {
|
||
used.min(256)
|
||
} else if bpp <= 8 {
|
||
1usize << bpp
|
||
} else {
|
||
0
|
||
};
|
||
let len = size.checked_add(masks)?.checked_add(colors * entry)?;
|
||
Some((packed.get(..len)?, packed.get(len..)?))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn sample() -> Rgba {
|
||
// 3 × 2: red, green, blue / half-transparent black, transparent, white.
|
||
let pixels = vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 128, 0, 0, 0, 0, 255, 255, 255, 255];
|
||
Rgba { width: 3, height: 2, pixels }
|
||
}
|
||
|
||
/// CRC-32 of a PNG chunk.
|
||
fn crc32(data: &[u8]) -> u32 {
|
||
let mut crc = !0u32;
|
||
for &b in data {
|
||
crc ^= u32::from(b);
|
||
for _ in 0..8 {
|
||
crc = if crc & 1 == 1 { (crc >> 1) ^ 0xEDB8_8320 } else { crc >> 1 };
|
||
}
|
||
}
|
||
!crc
|
||
}
|
||
|
||
#[test]
|
||
fn oversized_png_is_refused_from_its_header() {
|
||
let mut png = vec![];
|
||
image::GrayImage::new(1, 1).write_to(&mut Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||
// Claim a width over the limit in the IHDR chunk (and fix its CRC); the pixel data no longer
|
||
// matches, so decoding it would fail with a different error.
|
||
png[16..20].copy_from_slice(&(MAX_READ_SIDE + 1).to_be_bytes());
|
||
let crc = crc32(&png[12..29]);
|
||
png[29..33].copy_from_slice(&crc.to_be_bytes());
|
||
let mut bmi = vec![0u8; 40];
|
||
bmi[..4].copy_from_slice(&40u32.to_le_bytes());
|
||
bmi[16..20].copy_from_slice(&BI_PNG.to_le_bytes());
|
||
let err = decode(&bmi, &png, Alpha::Ignore).unwrap_err();
|
||
assert_eq!(err, "a bitmap is too large to read");
|
||
}
|
||
|
||
#[test]
|
||
fn rgb24_is_bottom_up_bgr_over_white() {
|
||
let (bmi, bits) = rgb24(&sample(), 1.0);
|
||
assert_eq!(bmi.len(), 40);
|
||
assert_eq!(&bmi[14..16], &[24, 0], "24 bits a pixel");
|
||
// Rows of 9 bytes padded to 12, the last row first: half-transparent black over white is
|
||
// mid grey, transparent is white.
|
||
assert_eq!(bits, vec![127, 127, 127, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0]);
|
||
}
|
||
|
||
#[test]
|
||
fn bgra32_is_premultiplied() {
|
||
let (bmi, bits) = bgra32(&sample(), 1.0);
|
||
assert_eq!(&bmi[14..16], &[32, 0]);
|
||
assert_eq!(&bits[..12], &[0, 0, 0, 128, 0, 0, 0, 0, 255, 255, 255, 255]);
|
||
assert_eq!(&bits[12..16], &[0, 0, 255, 255], "red, as BGRA");
|
||
// At half opacity.
|
||
let (_, half) = bgra32(&sample(), 0.5);
|
||
assert_eq!(&half[12..16], &[0, 0, 128, 128]);
|
||
}
|
||
|
||
#[test]
|
||
fn rgb24_round_trips_over_white() {
|
||
let (bmi, bits) = rgb24(&sample(), 1.0);
|
||
assert_eq!(bmi.len(), 40);
|
||
// Rows of 9 bytes padded to 12.
|
||
assert_eq!(bits.len(), 24);
|
||
let back = decode(&bmi, &bits, Alpha::Ignore).unwrap();
|
||
assert_eq!((back.width, back.height), (3, 2));
|
||
assert_eq!(&back.pixels[..12], &[255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]);
|
||
// Half-transparent black over white is mid grey; transparent is white.
|
||
assert_eq!(&back.pixels[12..16], &[127, 127, 127, 255]);
|
||
assert_eq!(&back.pixels[16..20], &[255, 255, 255, 255]);
|
||
}
|
||
|
||
#[test]
|
||
fn bgra32_round_trips_with_alpha() {
|
||
let (bmi, bits) = bgra32(&sample(), 1.0);
|
||
let back = decode(&bmi, &bits, Alpha::Premultiplied).unwrap();
|
||
assert_eq!(&back.pixels[..12], &sample().pixels[..12]);
|
||
assert_eq!(back.pixels[15], 128);
|
||
assert_eq!(back.pixels[19], 0);
|
||
// Read as StretchDIBits does, alpha is ignored.
|
||
assert!(decode(&bmi, &bits, Alpha::Ignore).unwrap().opaque());
|
||
}
|
||
|
||
#[test]
|
||
fn palette_bitmaps_and_damage() {
|
||
// A 1-bit 2 × 1 bitmap: black then white.
|
||
let mut bmi = header(2, 1, 1, 4);
|
||
bmi.extend_from_slice(&[0, 0, 0, 0, 255, 255, 255, 0]);
|
||
let back = decode(&bmi, &[0b0100_0000, 0, 0, 0], Alpha::Ignore).unwrap();
|
||
assert_eq!(back.pixels, vec![0, 0, 0, 255, 255, 255, 255, 255]);
|
||
// Too few bits, a silly size, an unknown depth: errors, no panic.
|
||
assert!(decode(&bmi, &[0], Alpha::Ignore).is_err());
|
||
assert!(decode(&header(1 << 20, 1 << 20, 24, 0), &[], Alpha::Ignore).is_err());
|
||
assert!(decode(&header(1, 1, 7, 0), &[0; 4], Alpha::Ignore).is_err());
|
||
assert!(decode(&[1, 2], &[], Alpha::Ignore).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn large_images_are_scaled_to_fit() {
|
||
let img = image::RgbaImage::new(MAX_SIDE * 2, 4);
|
||
let f = Rgba::fitted(img);
|
||
assert_eq!((f.width, f.height), (MAX_SIDE, 2));
|
||
}
|
||
}
|