use sha2::{Digest, Sha256};
pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
let mut h = Sha256::new();
h.update(bytes);
h.finalize().iter().map(|b| format!("{b:02x}")).collect()
}
pub(crate) fn image_digest(data: &[u8]) -> String {
match pil_bytes(data) {
Some(px) => sha256_hex(&px),
None => sha256_hex(data),
}
}
fn pil_bytes(data: &[u8]) -> Option<Vec<u8>> {
if is_png(data) {
pil_png_bytes(data)
} else {
decode_jpeg(data).map(|j| j.data)
}
}
fn is_png(data: &[u8]) -> bool {
data.starts_with(b"\x89PNG\r\n\x1a\n")
}
fn decode_jpeg(data: &[u8]) -> Option<crate::jpeg::Image> {
if !data.starts_with(&[0xFF, 0xD8]) {
return None;
}
crate::jpeg::decode(data, true, 1)
.ok()
.filter(|j| matches!(j.channels, 1 | 3))
}
pub(crate) fn asset_png(data: &[u8]) -> Option<Vec<u8>> {
if is_png(data) {
return Some(data.to_vec());
}
let j = decode_jpeg(data)?;
let mut buf = Vec::new();
{
let mut enc = png::Encoder::new(&mut buf, j.width as u32, j.height as u32);
enc.set_color(if j.channels == 1 {
png::ColorType::Grayscale
} else {
png::ColorType::Rgb
});
enc.set_depth(png::BitDepth::Eight);
let mut w = enc.write_header().ok()?;
w.write_image_data(&j.data).ok()?;
}
Some(buf)
}
fn pil_png_bytes(data: &[u8]) -> Option<Vec<u8>> {
use png::{BitDepth, ColorType};
let mut dec = png::Decoder::new(std::io::Cursor::new(data));
dec.set_transformations(png::Transformations::IDENTITY);
let mut reader = dec.read_info().ok()?;
let mut buf = vec![0; reader.output_buffer_size()?];
let info = reader.next_frame(&mut buf).ok()?;
let (w, h) = (info.width as usize, info.height as usize);
let line = info.line_size;
let rows = buf.chunks_exact(line).take(h);
let depth = info.bit_depth as u8;
let out = match (info.color_type, info.bit_depth) {
(ColorType::Grayscale, BitDepth::One) => rows.flat_map(|r| r.iter().copied()).collect(),
(ColorType::Grayscale, BitDepth::Two | BitDepth::Four) => {
let scale = if depth == 2 { 0x55 } else { 0x11 };
rows.flat_map(|r| unpack(r, depth, w).map(move |v| v * scale))
.collect()
}
(ColorType::Indexed, BitDepth::One | BitDepth::Two | BitDepth::Four) => {
rows.flat_map(|r| unpack(r, depth, w)).collect()
}
(ColorType::Grayscale, BitDepth::Sixteen) => rows
.flat_map(|r| r[..2 * w].chunks_exact(2).flat_map(|s| [s[1], s[0]]))
.collect(),
(ColorType::GrayscaleAlpha, BitDepth::Sixteen) => rows
.flat_map(|r| {
r[..4 * w]
.chunks_exact(4)
.flat_map(|s| [s[0], s[0], s[0], s[2]])
})
.collect(),
(ColorType::Rgb | ColorType::Rgba, BitDepth::Sixteen) => {
let n = info.color_type.samples() * w * 2;
rows.flat_map(|r| r[..n].iter().step_by(2).copied())
.collect()
}
(_, BitDepth::Eight) => {
let n = info.color_type.samples() * w;
rows.flat_map(|r| r[..n].iter().copied()).collect()
}
_ => return None,
};
Some(out)
}
fn unpack(row: &[u8], depth: u8, w: usize) -> impl Iterator<Item = u8> + '_ {
let per = 8 / depth as usize;
let mask = (1u16 << depth) as u8 - 1;
(0..w).map(move |i| {
let shift = 8 - depth as usize * (i % per + 1);
(row[i / per] >> shift) & mask
})
}
pub(crate) fn docling_data_uri(img: &crate::PictureImage) -> (String, String) {
match asset_png(&img.data) {
Some(png) if !is_png(&img.data) => (
"image/png".to_string(),
format!("data:image/png;base64,{}", crate::base64::encode(&png)),
),
_ => (img.mimetype.clone(), img.data_uri()),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn png(w: u32, h: u32, depth: u8, color: u8, rows: &[&[u8]], plte: Option<&[u8]>) -> Vec<u8> {
let mut enc_buf = Vec::new();
{
let mut enc = png::Encoder::new(&mut enc_buf, w, h);
enc.set_depth(png::BitDepth::from_u8(depth).unwrap());
enc.set_color(png::ColorType::from_u8(color).unwrap());
if let Some(p) = plte {
enc.set_palette(p.to_vec());
}
let mut wr = enc.write_header().unwrap();
let data: Vec<u8> = rows.iter().flat_map(|r| r.iter().copied()).collect();
wr.write_image_data(&data).unwrap();
}
enc_buf
}
#[test]
fn rgb8_is_the_raw_samples() {
let f = png(2, 1, 8, 2, &[&[1, 2, 3, 4, 5, 6]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![1, 2, 3, 4, 5, 6]);
assert_ne!(image_digest(&f), sha256_hex(&f));
}
#[test]
fn sub_byte_gray_is_scaled_to_l() {
let f = png(3, 1, 2, 0, &[&[0b00_01_10_11]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![0x00, 0x55, 0xaa]);
let f = png(3, 1, 4, 0, &[&[0x0f, 0x10]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![0x00, 0xff, 0x11]);
}
#[test]
fn one_bit_gray_stays_packed_and_palette_unpacks_to_indices() {
let f = png(10, 1, 1, 0, &[&[0b1010_0000, 0b1100_0000]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![0b1010_0000, 0b1100_0000]);
let f = png(3, 1, 2, 3, &[&[0b11_01_00_00]], Some(&[0; 12]));
assert_eq!(pil_png_bytes(&f).unwrap(), vec![3, 1, 0]);
}
#[test]
fn sixteen_bit_modes_follow_pillow() {
let f = png(1, 1, 16, 0, &[&[0x12, 0x34]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![0x34, 0x12]);
let f = png(1, 1, 16, 2, &[&[1, 2, 3, 4, 5, 6]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![1, 3, 5]);
let f = png(1, 1, 16, 4, &[&[7, 8, 9, 10]], None);
assert_eq!(pil_png_bytes(&f).unwrap(), vec![7, 7, 7, 9]);
}
#[test]
fn jpeg_digest_is_pillows_libjpeg_decode() {
let dir =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../docling-pdf/tests/data/jpeg");
for (name, want) in [
(
"rgb_420",
"50690f45a9c80a48b5e2e0e38008cd3ac26b8d92af24f2acddbdabcb6ec0c4c1",
),
(
"gray_progressive",
"dc231f8f1ef7b807f59587511af8a8d1b830b4e0cff8a185aa16188195641f06",
),
(
"rgb_444_progressive",
"e78a1d2a7181f6781e71b695d918f431e8735ecea30fc14983206f5cbd214a1e",
),
] {
let Ok(jpg) = std::fs::read(dir.join(format!("{name}.jpg"))) else {
return; };
assert_eq!(image_digest(&jpg), want, "{name}");
let png = asset_png(&jpg).unwrap();
assert_eq!(
pil_png_bytes(&png).map(|b| sha256_hex(&b)).as_deref(),
Some(want)
);
}
}
#[test]
fn non_png_keeps_the_encoded_bytes_digest() {
assert_eq!(image_digest(b"\xff\xd8\xff"), sha256_hex(b"\xff\xd8\xff"));
}
#[test]
fn matches_docling_for_the_issue_527_fixture_image() {
const HEX: &str = "89504e470d0a1a0a0000000d49484452000000080000000808020000004b6d29dc\
0000000f49444154789c6368c001188696040082f360019cee0f2400000000\
49454e44ae426082";
let f: Vec<u8> = (0..HEX.len() / 2)
.map(|i| u8::from_str_radix(&HEX[2 * i..2 * i + 2], 16).unwrap())
.collect();
assert_eq!(
image_digest(&f),
"fa7b78cc215df21d7ce54d8c3c6637c326dab95c10fbc12263101365973f4268"
);
}
}