use miniz_oxide::deflate::compress_to_vec_zlib;
use qualia_transport::image::GrayImage;
const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
const COMPRESSION_LEVEL: u8 = 6;
pub(crate) fn encode(image: &GrayImage) -> Vec<u8> {
let width = image.width();
let height = image.height();
let mut raw = Vec::with_capacity((width as usize + 1) * height as usize);
for row in 0..height {
raw.push(0);
for column in 0..width {
raw.push(image.pixel(column, row).unwrap_or(u8::MAX));
}
}
let mut out = Vec::with_capacity(raw.len() / 4 + 128);
out.extend_from_slice(&SIGNATURE);
let mut ihdr = Vec::with_capacity(13);
ihdr.extend_from_slice(&width.to_be_bytes());
ihdr.extend_from_slice(&height.to_be_bytes());
ihdr.push(8);
ihdr.push(0);
ihdr.push(0);
ihdr.push(0);
ihdr.push(0);
chunk(&mut out, b"IHDR", &ihdr);
chunk(
&mut out,
b"IDAT",
&compress_to_vec_zlib(&raw, COMPRESSION_LEVEL),
);
chunk(&mut out, b"IEND", &[]);
out
}
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
let length = u32::try_from(data.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&length.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(data);
let mut crc = Crc::new();
crc.update(kind);
crc.update(data);
out.extend_from_slice(&crc.finish().to_be_bytes());
}
struct Crc {
value: u32,
}
impl Crc {
const POLYNOMIAL: u32 = 0xEDB8_8320;
const fn new() -> Self {
Self { value: 0xFFFF_FFFF }
}
fn update(&mut self, bytes: &[u8]) {
for byte in bytes {
self.value ^= u32::from(*byte);
for _ in 0..8 {
let mask = (self.value & 1).wrapping_neg();
self.value = (self.value >> 1) ^ (Self::POLYNOMIAL & mask);
}
}
}
const fn finish(self) -> u32 {
!self.value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn image(width: u32, height: u32, dark: impl Fn(u32, u32) -> bool) -> GrayImage {
let mut pixels = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(if dark(x, y) { 0 } else { u8::MAX });
}
}
GrayImage::from_luma8(width, height, pixels).expect("the size is consistent")
}
#[test]
fn the_container_carries_the_image_and_its_checksums() {
let picture = image(7, 3, |x, y| (x + y) % 2 == 0);
let png = encode(&picture);
assert_eq!(
&png[..8],
&SIGNATURE,
"the file must start with the signature"
);
let mut offset = 8;
let mut kinds: Vec<[u8; 4]> = Vec::new();
let mut idat: Vec<u8> = Vec::new();
let mut header: Vec<u8> = Vec::new();
loop {
let length = u32::from_be_bytes(png[offset..offset + 4].try_into().expect("four bytes"))
as usize;
let kind: [u8; 4] = png[offset + 4..offset + 8].try_into().expect("four bytes");
let data = &png[offset + 8..offset + 8 + length];
let stored = u32::from_be_bytes(
png[offset + 8 + length..offset + 12 + length]
.try_into()
.expect("four bytes"),
);
let mut crc = Crc::new();
crc.update(&kind);
crc.update(data);
assert_eq!(
crc.finish(),
stored,
"the CRC of {} does not match its contents",
String::from_utf8_lossy(&kind)
);
kinds.push(kind);
match &kind {
b"IHDR" => header = data.to_vec(),
b"IDAT" => idat.extend_from_slice(data),
b"IEND" => break,
other => panic!("unexpected chunk {}", String::from_utf8_lossy(other)),
}
offset += 12 + length;
}
assert_eq!(kinds, vec![*b"IHDR", *b"IDAT", *b"IEND"]);
assert_eq!(offset + 12, png.len(), "nothing may follow IEND");
assert_eq!(
u32::from_be_bytes(header[0..4].try_into().expect("four bytes")),
7
);
assert_eq!(
u32::from_be_bytes(header[4..8].try_into().expect("four bytes")),
3
);
assert_eq!(
&header[8..13],
&[8, 0, 0, 0, 0],
"8 bits per sample, greyscale, deflate, adaptive filtering, no interlace"
);
let raw = miniz_oxide::inflate::decompress_to_vec_zlib(&idat).expect("the stream is zlib");
assert_eq!(raw.len(), (7 + 1) * 3);
assert_eq!(raw[0], 0, "every row uses filter type 0");
assert_eq!(raw[1], 0, "a dark module is written as 0");
assert_eq!(raw[2], 255, "a light module is written as 255");
}
#[test]
fn a_single_pixel_encodes() {
let png = encode(&image(1, 1, |_, _| false));
assert!(png.starts_with(&SIGNATURE));
assert!(png.ends_with(&[0xAE, 0x42, 0x60, 0x82]), "IEND's own CRC");
}
#[test]
fn a_code_written_as_a_png_is_read_back() {
let text = "otpauth://totp/GitHub:alice?secret=JBSWY3DPEHPK3PXP&issuer=GitHub";
let symbol =
qualia_transport::qr::encode_text(text, qualia_transport::qr::EcLevel::Low, 40)
.expect("the URI fits");
let scale = 6;
let quiet = 4;
let side = (symbol.size() + 2 * quiet) * scale;
let drawn = symbol.render(scale, quiet).expect("the geometry is legal");
let canvas = image(side.into(), side.into(), |x, y| {
drawn.pixel(x, y) == Some(0)
});
let read = crate::import::qr_text_from_png(&encode(&canvas))
.expect("the reader must accept what the writer produced");
assert_eq!(read, text);
}
}