use pdfrum_object::{Array, ByteSpan, Dict, Name, Object, Stream};
use super::EmbeddedImage;
use crate::doc::EditDoc;
use crate::error::Error;
use crate::names;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sof {
width: u32,
height: u32,
components: u8,
bits: u8,
}
const VALID_COMPONENTS: [u8; 3] = [1, 3, 4];
const VALID_BITS: [u8; 5] = [1, 2, 4, 8, 16];
const JP2_SIGNATURE: [u8; 12] = [
0x00, 0x00, 0x00, 0x0C, b'j', b'P', b' ', b' ', 0x0D, 0x0A, 0x87, 0x0A,
];
const J2K_SOC_SIZ: [u8; 4] = [0xFF, 0x4F, 0xFF, 0x51];
pub(super) fn embed(doc: &mut EditDoc<'_>, bytes: &[u8]) -> Result<EmbeddedImage, Error> {
if is_jpeg2000(bytes) {
return embed_jpx(doc, bytes);
}
let sof = parse_sof(bytes).ok_or(Error::UnrecognisedImageData)?;
if !VALID_COMPONENTS.contains(&sof.components) || !VALID_BITS.contains(&sof.bits) {
return Err(Error::UnrecognisedImageData);
}
let mut dict = image_dict(sof.width, sof.height);
let space = match sof.components {
1 => names::DEVICE_GRAY.clone(),
3 => names::DEVICE_RGB.clone(),
_ => {
dict.push(names::DECODE.clone(), Object::Array(cmyk_decode()));
names::DEVICE_CMYK.clone()
}
};
dict.push(names::COLOR_SPACE.clone(), Object::Name(space));
dict.push(
names::BITS_PER_COMPONENT.clone(),
Object::Int(i64::from(sof.bits)),
);
dict.push(
names::FILTER.clone(),
Object::Name(names::DCT_DECODE.clone()),
);
if !has_colour_transform(bytes, sof.components) {
let parms = Dict::from_pairs([(names::COLOR_TRANSFORM.clone(), Object::Int(0))]);
dict.push(names::DECODE_PARMS.clone(), Object::Dict(parms));
}
Ok(EmbeddedImage {
image: doc.add(Object::Stream(Box::new(Stream::new(
dict,
ByteSpan::from(bytes.to_vec()),
)))),
width: sof.width,
height: sof.height,
})
}
fn embed_jpx(doc: &mut EditDoc<'_>, bytes: &[u8]) -> Result<EmbeddedImage, Error> {
let (width, height) = jpx_size(bytes).ok_or(Error::UnrecognisedImageData)?;
let mut dict = image_dict(width, height);
dict.push(
names::FILTER.clone(),
Object::Name(names::JPX_DECODE.clone()),
);
Ok(EmbeddedImage {
image: doc.add(Object::Stream(Box::new(Stream::new(
dict,
ByteSpan::from(bytes.to_vec()),
)))),
width,
height,
})
}
pub(super) fn image_dict(width: u32, height: u32) -> Dict {
Dict::from_pairs([
(names::TYPE.clone(), Object::Name(names::XOBJECT.clone())),
(names::SUBTYPE.clone(), Object::Name(names::IMAGE.clone())),
(names::WIDTH.clone(), Object::Int(i64::from(width))),
(names::HEIGHT.clone(), Object::Int(i64::from(height))),
])
}
fn cmyk_decode() -> Array {
Array::of(
[1, 0, 1, 0, 1, 0, 1, 0]
.into_iter()
.map(Object::Int)
.collect::<Vec<_>>(),
)
}
fn is_jpeg2000(bytes: &[u8]) -> bool {
bytes.starts_with(&JP2_SIGNATURE) || bytes.starts_with(&J2K_SOC_SIZ)
}
fn jpx_size(bytes: &[u8]) -> Option<(u32, u32)> {
let soc = bytes.windows(4).position(|w| w == J2K_SOC_SIZ)?;
let grid = bytes.get(soc.checked_add(8)?..)?.first_chunk::<16>()?;
let at = |i: usize| -> u32 {
let mut b = [0u8; 4];
b.copy_from_slice(grid.get(i..i + 4).unwrap_or(&[0; 4]));
u32::from_be_bytes(b)
};
let width = at(0).checked_sub(at(8))?;
let height = at(4).checked_sub(at(12))?;
(width > 0 && height > 0).then_some((width, height))
}
fn parse_sof(bytes: &[u8]) -> Option<Sof> {
if !bytes.starts_with(&[0xFF, 0xD8]) {
return None;
}
let mut i = 2usize;
loop {
let marker = next_marker(bytes, &mut i)?;
if is_sof(marker) {
let head = bytes.get(i..)?.first_chunk::<8>()?;
let bits = *head.get(2)?;
let height = u32::from(u16::from_be_bytes([*head.get(3)?, *head.get(4)?]));
let width = u32::from(u16::from_be_bytes([*head.get(5)?, *head.get(6)?]));
let components = *head.get(7)?;
return (width > 0 && height > 0).then_some(Sof {
width,
height,
components,
bits,
});
}
if marker == 0xDA {
return None;
}
let len = usize::from(u16::from_be_bytes(*bytes.get(i..)?.first_chunk::<2>()?));
i = i.checked_add(len.max(2))?;
}
}
fn next_marker(bytes: &[u8], i: &mut usize) -> Option<u8> {
loop {
while bytes.get(*i) == Some(&0xFF) {
*i = i.checked_add(1)?;
}
let marker = *bytes.get(*i)?;
*i = i.checked_add(1)?;
if !matches!(marker, 0xD0..=0xD9 | 0x01 | 0x00) {
return Some(marker);
}
if marker == 0xD9 {
return None;
}
while bytes.get(*i).is_some_and(|b| *b != 0xFF) {
*i = i.checked_add(1)?;
}
}
}
fn is_sof(marker: u8) -> bool {
matches!(marker, 0xC0..=0xCF) && !matches!(marker, 0xC4 | 0xC8 | 0xCC)
}
fn has_colour_transform(bytes: &[u8], components: u8) -> bool {
match adobe_transform(bytes) {
Some(transform) => transform == 1 || transform == 2,
None => components == 3,
}
}
fn adobe_transform(bytes: &[u8]) -> Option<u8> {
let mut i = 2usize;
loop {
let marker = next_marker(bytes, &mut i)?;
if marker == 0xDA || is_sof(marker) {
return None;
}
let len = usize::from(u16::from_be_bytes(*bytes.get(i..)?.first_chunk::<2>()?));
if marker == 0xEE {
let payload = bytes.get(i.checked_add(2)?..i.checked_add(len)?)?;
if payload.starts_with(b"Adobe") {
return payload.last().copied();
}
}
i = i.checked_add(len.max(2))?;
}
}
pub(super) fn device_space(components: u8) -> Option<Name> {
match components {
1 => Some(names::DEVICE_GRAY.clone()),
3 => Some(names::DEVICE_RGB.clone()),
4 => Some(names::DEVICE_CMYK.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{Sof, adobe_transform, has_colour_transform, jpx_size, parse_sof};
const MONA_LISA: &[u8] = include_bytes!("../../../pdfrum/tests/fixtures/mona_lisa.jpg");
const GRAY_JP2: &[u8] = include_bytes!("../../../pdfrum/tests/fixtures/gray.jp2");
#[test]
fn the_sof_of_a_real_jpeg_reads_back() {
assert_eq!(
parse_sof(MONA_LISA),
Some(Sof {
width: 120,
height: 120,
components: 3,
bits: 8,
})
);
}
#[test]
fn a_jfif_rgb_jpeg_is_colour_transformed() {
assert_eq!(adobe_transform(MONA_LISA), None);
assert!(has_colour_transform(MONA_LISA, 3));
}
#[test]
fn a_jp2_file_yields_its_grid_size() {
assert_eq!(jpx_size(GRAY_JP2), Some((4, 4)));
}
#[test]
fn junk_is_not_a_jpeg() {
assert_eq!(parse_sof(b"not a jpeg at all"), None);
assert_eq!(parse_sof(&[0xFF, 0xD8]), None);
assert_eq!(parse_sof(&[0xFF, 0xD8, 0xFF, 0xC0, 0x00]), None);
}
#[test]
fn a_zero_dimension_frame_is_refused() {
let mut jpeg = vec![0xFF, 0xD8, 0xFF, 0xC0, 0x00, 0x11, 0x08];
jpeg.extend_from_slice(&0u16.to_be_bytes());
jpeg.extend_from_slice(&8u16.to_be_bytes());
jpeg.push(3);
assert_eq!(parse_sof(&jpeg), None);
}
}