use pdfrum_object::{Array, ByteSpan, Dict, Name, Object, PdfString, Stream};
use super::EmbeddedImage;
use super::jpeg::image_dict;
use crate::doc::EditDoc;
use crate::error::Error;
use crate::names;
const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1A, b'\n'];
#[derive(Debug, Clone, PartialEq, Eq)]
struct Png {
width: u32,
height: u32,
depth: u8,
colour: Colour,
palette: Vec<u8>,
transparency: Transparency,
interlaced: Interlace,
data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Colour {
Grey,
Rgb,
Palette,
GreyAlpha,
RgbAlpha,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Transparency {
None,
Keyed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Interlace {
None,
Adam7,
}
pub(super) fn embed(doc: &mut EditDoc<'_>, bytes: &[u8]) -> Result<EmbeddedImage, Error> {
let png = parse(bytes).ok_or(Error::UnrecognisedImageData)?;
let components: u8 = match (png.colour, png.transparency, png.interlaced, png.depth) {
(_, _, Interlace::Adam7, _)
| (Colour::GreyAlpha | Colour::RgbAlpha, ..)
| (_, Transparency::Keyed, ..)
| (_, _, _, 16) => return Err(Error::PngNeedsDecoding),
(Colour::Grey | Colour::Palette, ..) => 1,
(Colour::Rgb, ..) => 3,
};
let mut dict = image_dict(png.width, png.height);
let space = match png.colour {
Colour::Palette => {
let entries = png.palette.len() / 3;
if entries == 0 {
return Err(Error::UnrecognisedImageData);
}
Object::Array(Array::of([
Object::Name(Name::from("Indexed")),
Object::Name(names::DEVICE_RGB.clone()),
Object::Int(i64::try_from(entries - 1).unwrap_or(0)),
Object::Str(PdfString::hex(
png.palette.get(..entries * 3).unwrap_or_default(),
)),
]))
}
Colour::Grey => Object::Name(names::DEVICE_GRAY.clone()),
Colour::Rgb | Colour::GreyAlpha | Colour::RgbAlpha => {
Object::Name(names::DEVICE_RGB.clone())
}
};
dict.push(names::COLOR_SPACE.clone(), space);
dict.push(
names::BITS_PER_COMPONENT.clone(),
Object::Int(i64::from(png.depth)),
);
dict.push(
names::FILTER.clone(),
Object::Name(names::FLATE_DECODE.clone()),
);
dict.push(
names::DECODE_PARMS.clone(),
Object::Dict(Dict::from_pairs([
(Name::from("Predictor"), Object::Int(15)),
(Name::from("Colors"), Object::Int(i64::from(components))),
(
names::BITS_PER_COMPONENT.clone(),
Object::Int(i64::from(png.depth)),
),
(names::COLUMNS.clone(), Object::Int(i64::from(png.width))),
])),
);
let (width, height) = (png.width, png.height);
let image = doc.add(Object::Stream(Box::new(Stream::new(
dict,
ByteSpan::from(png.data),
))));
Ok(EmbeddedImage {
image,
width,
height,
})
}
fn parse(bytes: &[u8]) -> Option<Png> {
let mut rest = bytes.strip_prefix(&SIGNATURE)?;
let mut png: Option<Png> = None;
while rest.len() >= 12 {
let length = usize::try_from(u32::from_be_bytes(rest.get(..4)?.try_into().ok()?)).ok()?;
let kind = rest.get(4..8)?;
let body = rest.get(8..8usize.checked_add(length)?)?;
rest = rest.get(12usize.checked_add(length)?..)?;
match kind {
b"IHDR" => png = Some(header(body)?),
b"PLTE" => png.as_mut()?.palette = body.to_vec(),
b"tRNS" => png.as_mut()?.transparency = Transparency::Keyed,
b"IDAT" => png.as_mut()?.data.extend_from_slice(body),
b"IEND" => break,
_ => {}
}
}
png.filter(|png| !png.data.is_empty())
}
fn header(body: &[u8]) -> Option<Png> {
let word = |at: usize| -> Option<u32> {
Some(u32::from_be_bytes(body.get(at..at + 4)?.try_into().ok()?))
};
let (width, height) = (word(0)?, word(4)?);
let depth = *body.get(8)?;
let colour = match body.get(9)? {
0 => Colour::Grey,
2 => Colour::Rgb,
3 => Colour::Palette,
4 => Colour::GreyAlpha,
6 => Colour::RgbAlpha,
_ => return None,
};
let interlaced = match body.get(12)? {
0 => Interlace::None,
1 => Interlace::Adam7,
_ => return None,
};
(width > 0 && height > 0 && [1, 2, 4, 8, 16].contains(&depth)).then_some(Png {
width,
height,
depth,
colour,
palette: Vec::new(),
transparency: Transparency::None,
interlaced,
data: Vec::new(),
})
}
#[cfg(test)]
mod tests {
use super::{Colour, parse};
fn one_pixel(colour_type: u8) -> Vec<u8> {
let chunk = |kind: &[u8], body: &[u8]| {
let mut out = u32::try_from(body.len())
.unwrap_or(0)
.to_be_bytes()
.to_vec();
out.extend_from_slice(kind);
out.extend_from_slice(body);
out.extend_from_slice(&[0, 0, 0, 0]);
out
};
let mut bytes = super::SIGNATURE.to_vec();
bytes.extend(chunk(
b"IHDR",
&[0, 0, 0, 1, 0, 0, 0, 1, 8, colour_type, 0, 0, 0],
));
bytes.extend(chunk(
b"IDAT",
&[0x78, 0x01, 0x01, 0x04, 0x00, 0xFB, 0xFF, 0, 255, 0, 0],
));
bytes.extend(chunk(b"IEND", &[]));
bytes
}
#[test]
fn the_header_and_data_are_read() {
let png = parse(&one_pixel(2)).expect("a PNG");
assert_eq!(
(png.width, png.height, png.depth, png.colour),
(1, 1, 8, Colour::Rgb)
);
assert_eq!(png.data.len(), 11);
}
#[test]
fn junk_is_not_a_png() {
assert!(parse(b"GIF89a").is_none());
assert!(parse(&super::SIGNATURE).is_none());
}
}