use pdfboss_core::geom::{Matrix, Point, Rect};
#[cfg(test)]
use pdfboss_core::{block_on, Document, Immediate};
use pdfboss_core::{AsyncObjectSource, Dict, Object};
use crate::color::ColorSpace;
use crate::raster::Mask;
use crate::Pixmap;
const MAX_PIXELS: usize = 1 << 26;
const MAX_DIM: usize = 1 << 16;
pub(crate) struct DrawParams<'a> {
pub ctm: Matrix,
pub alpha: f32,
pub fill_rgb: [u8; 3],
pub clip: Option<&'a Mask>,
}
struct Rgba<'a> {
width: usize,
height: usize,
pixels: Pixels<'a>,
truncated: bool,
}
enum Pixels<'a> {
Quads(Vec<u8>),
Packed {
data: &'a [u8],
bpc: usize,
row_bytes: usize,
lut: Vec<[u8; 4]>,
},
}
impl Rgba<'_> {
fn at(&self, i: usize, j: usize) -> [u8; 4] {
match &self.pixels {
Pixels::Quads(data) => {
let off = (j * self.width + i) * 4;
data.get(off..off + 4)
.and_then(|s| <[u8; 4]>::try_from(s).ok())
.unwrap_or([0; 4])
}
Pixels::Packed {
data,
bpc,
row_bytes,
lut,
} => {
let bit = i * bpc;
let byte = data.get(j * row_bytes + bit / 8).copied().unwrap_or(0);
let shift = 8 - bpc - bit % 8;
let mask = ((1u16 << bpc) - 1) as u8;
lut.get(usize::from((byte >> shift) & mask))
.copied()
.unwrap_or([0; 4])
}
}
}
}
pub(crate) enum Drawn {
Whole,
Truncated,
Nothing,
}
pub(crate) struct ImageMeta {
dct: bool,
width: Option<f64>,
height: Option<f64>,
pub(crate) stencil: bool,
decode: Option<Vec<f32>>,
cs: Option<ColorSpace>,
bpc: Option<f64>,
}
impl ImageMeta {
#[cfg(test)]
pub(crate) fn read(doc: &Document, dict: &Dict, cs_obj: Option<&Object>) -> ImageMeta {
block_on(Self::read_with(&Immediate(doc), dict, cs_obj))
}
pub(crate) async fn read_with<S: AsyncObjectSource>(
src: &S,
dict: &Dict,
cs_obj: Option<&Object>,
) -> ImageMeta {
let cs = match cs_obj {
Some(obj) => Some(ColorSpace::parse_with(src, obj).await),
None => None,
};
ImageMeta {
dct: is_dct(src, dict).await,
width: num_of(src, dict, "Width").await,
height: num_of(src, dict, "Height").await,
stencil: bool_of(src, dict, "ImageMask").await.unwrap_or(false),
decode: floats_of(src, dict, "Decode").await,
cs,
bpc: num_of(src, dict, "BitsPerComponent").await,
}
}
}
pub(crate) fn draw(pix: &mut Pixmap, meta: &ImageMeta, data: &[u8], p: &DrawParams) -> Drawn {
match decode_rgba(meta, data, p.fill_rgb) {
Some(img) => {
let truncated = img.truncated;
draw_rgba(pix, &img, p);
if truncated {
Drawn::Truncated
} else {
Drawn::Whole
}
}
None => Drawn::Nothing,
}
}
async fn num_of<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Option<f64> {
src.resolve(dict.get(key)?).await.ok()?.as_f64()
}
async fn bool_of<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Option<bool> {
src.resolve(dict.get(key)?).await.ok()?.as_bool()
}
async fn floats_of<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Option<Vec<f32>> {
let arr = match src.resolve(dict.get(key)?).await {
Ok(Object::Array(a)) => a,
_ => return None,
};
let mut out = Vec::with_capacity(arr.len());
for item in &arr {
let v = src.resolve(item).await.ok()?.as_f64()? as f32;
if !v.is_finite() {
return None;
}
out.push(v);
}
Some(out)
}
async fn trailing_filter<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Option<String> {
let filter = dict.get("Filter")?;
let name = match src.resolve(filter).await {
Ok(Object::Name(n)) => n,
Ok(Object::Array(items)) => {
let last = items.last()?;
match src.resolve(last).await {
Ok(Object::Name(n)) => n,
_ => return None,
}
}
_ => return None,
};
Some(name.0)
}
async fn is_dct<S: AsyncObjectSource>(src: &S, dict: &Dict) -> bool {
matches!(
trailing_filter(src, dict).await.as_deref(),
Some("DCTDecode" | "DCT")
)
}
fn sample_bits(data: &[u8], bit: usize, bpc: usize) -> u32 {
if bpc == 8 {
return u32::from(data.get(bit / 8).copied().unwrap_or(0));
}
let mut v = 0u32;
for i in 0..bpc {
let b = bit + i;
let byte = data.get(b / 8).copied().unwrap_or(0);
v = (v << 1) | u32::from((byte >> (7 - b % 8)) & 1);
}
v
}
fn short_of_samples(data: &[u8], row_bits: usize, height: usize) -> bool {
data.len() < (row_bits / 8).saturating_mul(height)
}
fn decode_rgba<'a>(meta: &ImageMeta, data: &'a [u8], fill_rgb: [u8; 3]) -> Option<Rgba<'a>> {
if meta.dct {
return decode_jpeg(data);
}
let width = meta.width? as usize;
let height = meta.height? as usize;
if width == 0 || height == 0 || width > MAX_DIM || height > MAX_DIM {
return None;
}
width.checked_mul(height).filter(|&n| n <= MAX_PIXELS)?;
let decode = meta.decode.as_deref();
if meta.stencil {
return Some(decode_stencil(width, height, data, decode, fill_rgb));
}
let cs = meta.cs.as_ref().unwrap_or(&ColorSpace::DeviceGray);
let bpc = match meta.bpc.map(|v| v as i64) {
Some(v @ (1 | 2 | 4 | 8 | 16)) => v as usize,
_ => 8,
};
Some(decode_samples(width, height, data, cs, bpc, decode))
}
fn decode_stencil(
width: usize,
height: usize,
data: &[u8],
decode: Option<&[f32]>,
fill_rgb: [u8; 3],
) -> Rgba<'static> {
let invert = matches!(decode, Some([d0, d1, ..]) if d0 > d1);
let stride_bits = width.div_ceil(8) * 8;
let mut out = vec![0u8; width * height * 4];
for y in 0..height {
for x in 0..width {
let raw = sample_bits(data, y * stride_bits + x, 1);
if (raw == 0) != invert {
let off = (y * width + x) * 4;
out[off..off + 3].copy_from_slice(&fill_rgb);
out[off + 3] = 255;
}
}
}
Rgba {
width,
height,
pixels: Pixels::Quads(out),
truncated: short_of_samples(data, stride_bits, height),
}
}
fn decode_samples<'a>(
width: usize,
height: usize,
data: &'a [u8],
cs: &ColorSpace,
bpc: usize,
decode: Option<&[f32]>,
) -> Rgba<'a> {
let ncomp = cs.components().clamp(1, 8);
let max = ((1u32 << bpc) - 1) as f32;
let default_hi = if matches!(cs, ColorSpace::Indexed { .. }) {
max
} else {
1.0
};
let ranges: Vec<(f32, f32)> = (0..ncomp)
.map(|c| match decode {
Some(d) if d.len() >= 2 * (c + 1) => (d[2 * c], d[2 * c + 1]),
_ => (0.0, default_hi),
})
.collect();
let stride_bits = (ncomp * bpc * width).div_ceil(8) * 8;
let truncated = short_of_samples(data, stride_bits, height);
if ncomp == 1 && bpc <= 8 {
return Rgba {
width,
height,
pixels: Pixels::Packed {
data,
bpc,
row_bytes: (width * bpc).div_ceil(8),
lut: sample_lut(cs, bpc, ranges[0], max),
},
truncated,
};
}
let mut out = vec![0u8; width * height * 4];
let mut comps = [0.0f32; 8];
for y in 0..height {
for x in 0..width {
let bit0 = y * stride_bits + x * ncomp * bpc;
for (c, comp) in comps.iter_mut().enumerate().take(ncomp) {
let raw = sample_bits(data, bit0 + c * bpc, bpc) as f32;
let (d0, d1) = ranges[c];
*comp = d0 + raw * (d1 - d0) / max;
}
let rgb = cs.to_rgb(&comps[..ncomp]);
let off = (y * width + x) * 4;
for (i, v) in rgb.iter().enumerate() {
out[off + i] = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
out[off + 3] = 255;
}
}
Rgba {
width,
height,
pixels: Pixels::Quads(out),
truncated,
}
}
fn sample_lut(cs: &ColorSpace, bpc: usize, range: (f32, f32), max: f32) -> Vec<[u8; 4]> {
let (d0, d1) = range;
(0..1usize << bpc)
.map(|raw| {
let rgb = cs.to_rgb(&[d0 + raw as f32 * (d1 - d0) / max]);
let mut px = [0u8, 0, 0, 255];
for (slot, v) in px.iter_mut().zip(rgb) {
*slot = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
px
})
.collect()
}
fn decode_jpeg<'a>(data: &[u8]) -> Option<Rgba<'a>> {
let mut dec = jpeg_decoder::Decoder::new(data);
dec.read_info().ok()?;
let info = dec.info()?;
let (w, h) = (info.width as usize, info.height as usize);
if w == 0 || h == 0 || w > MAX_DIM || h > MAX_DIM {
return None;
}
w.checked_mul(h).filter(|&n| n <= MAX_PIXELS)?;
dec.set_max_decoding_buffer_size(MAX_PIXELS * 4);
let pixels = dec.decode().ok()?;
let mut out = vec![255u8; w * h * 4];
match info.pixel_format {
jpeg_decoder::PixelFormat::L8 => {
for (i, &g) in pixels.iter().enumerate().take(w * h) {
out[i * 4..i * 4 + 3].copy_from_slice(&[g, g, g]);
}
}
jpeg_decoder::PixelFormat::L16 => {
for (i, pair) in pixels.chunks_exact(2).enumerate().take(w * h) {
let g = pair[0]; out[i * 4..i * 4 + 3].copy_from_slice(&[g, g, g]);
}
}
jpeg_decoder::PixelFormat::RGB24 => {
for (i, rgb) in pixels.chunks_exact(3).enumerate().take(w * h) {
out[i * 4..i * 4 + 3].copy_from_slice(rgb);
}
}
jpeg_decoder::PixelFormat::CMYK32 => {
for (i, cmyk) in pixels.chunks_exact(4).enumerate().take(w * h) {
let rgb = inverted_cmyk_to_rgb([cmyk[0], cmyk[1], cmyk[2], cmyk[3]]);
out[i * 4..i * 4 + 3].copy_from_slice(&rgb);
}
}
}
Some(Rgba {
width: w,
height: h,
pixels: Pixels::Quads(out),
truncated: false,
})
}
fn inverted_cmyk_to_rgb(px: [u8; 4]) -> [u8; 3] {
let ink = |v: u8| 1.0 - f32::from(v) / 255.0;
let rgb = ColorSpace::DeviceCMYK.to_rgb(&[ink(px[0]), ink(px[1]), ink(px[2]), ink(px[3])]);
[
(rgb[0] * 255.0 + 0.5) as u8,
(rgb[1] * 255.0 + 0.5) as u8,
(rgb[2] * 255.0 + 0.5) as u8,
]
}
fn composite_over(dst: &mut [u8], rgb: [u8; 3], a: f32) {
let da = f32::from(dst[3]) / 255.0;
let oa = a + da * (1.0 - a);
if oa <= 0.0 {
dst.copy_from_slice(&[0, 0, 0, 0]);
return;
}
for i in 0..3 {
let s = f32::from(rgb[i]);
let d = f32::from(dst[i]);
dst[i] = ((s * a + d * da * (1.0 - a)) / oa + 0.5) as u8;
}
dst[3] = (oa * 255.0 + 0.5) as u8;
}
fn draw_rgba(pix: &mut Pixmap, img: &Rgba<'_>, p: &DrawParams) {
let Some(inv) = p.ctm.invert() else {
return;
};
let alpha = if p.alpha.is_finite() {
p.alpha.clamp(0.0, 1.0)
} else {
1.0
};
if alpha <= 0.0 {
return;
}
let bbox = Rect::new(0.0, 0.0, 1.0, 1.0).transform(p.ctm);
let x0 = bbox.x0.floor().max(0.0) as u32;
let y0 = bbox.y0.floor().max(0.0) as u32;
let x1 = (bbox.x1.ceil().max(0.0) as u32).min(pix.width);
let y1 = (bbox.y1.ceil().max(0.0) as u32).min(pix.height);
for py in y0..y1 {
for px in x0..x1 {
let u = inv.apply(Point::new(px as f32 + 0.5, py as f32 + 0.5));
if !(0.0..1.0).contains(&u.x) || !(0.0..1.0).contains(&u.y) {
continue;
}
let i = ((u.x * img.width as f32) as usize).min(img.width - 1);
let j = (((1.0 - u.y) * img.height as f32) as usize).min(img.height - 1);
let s = img.at(i, j);
let mut a = f32::from(s[3]) / 255.0 * alpha;
if let Some(mask) = p.clip {
a *= f32::from(mask.coverage(px, py)) / 255.0;
}
if a <= 0.0 {
continue;
}
let off = ((py * pix.width + px) * 4) as usize;
let dst = &mut pix.data[off..off + 4];
if a >= 1.0 {
dst.copy_from_slice(&[s[0], s[1], s[2], 255]);
} else {
composite_over(dst, [s[0], s[1], s[2]], a);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pdfboss_core::parser::{NoResolve, Parser};
use pdfboss_testkit::PdfBuilder;
fn test_doc() -> Document {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
Document::load(b.build(1)).unwrap()
}
fn dict(src: &[u8]) -> Dict {
match Parser::new(src).parse_object(&NoResolve).unwrap() {
Object::Dict(d) => d,
other => panic!("expected dict, got {other:?}"),
}
}
fn obj(src: &[u8]) -> Object {
Parser::new(src).parse_object(&NoResolve).unwrap()
}
fn rgba_at(img: &Rgba<'_>, x: usize, y: usize) -> [u8; 4] {
img.at(x, y)
}
fn decode_rgba<'a>(
doc: &Document,
dict: &Dict,
data: &'a [u8],
cs_obj: Option<&Object>,
fill_rgb: [u8; 3],
) -> Option<Rgba<'a>> {
super::decode_rgba(&ImageMeta::read(doc, dict, cs_obj), data, fill_rgb)
}
#[test]
fn sample_bits_all_depths() {
let data = [0b1011_0110, 0b0101_0011];
assert_eq!(sample_bits(&data, 0, 1), 1);
assert_eq!(sample_bits(&data, 1, 1), 0);
assert_eq!(sample_bits(&data, 0, 2), 0b10);
assert_eq!(sample_bits(&data, 2, 2), 0b11);
assert_eq!(sample_bits(&data, 0, 4), 0b1011);
assert_eq!(sample_bits(&data, 4, 4), 0b0110);
assert_eq!(sample_bits(&data, 0, 8), 0b1011_0110);
assert_eq!(sample_bits(&data, 8, 8), 0b0101_0011);
assert_eq!(sample_bits(&data, 0, 16), 0b1011_0110_0101_0011);
assert_eq!(sample_bits(&data, 16, 8), 0);
}
#[test]
fn gray_bpc_variants_decode() {
let doc = test_doc();
let d = dict(b"<< /Width 2 /Height 2 /BitsPerComponent 8 >>");
let img = decode_rgba(&doc, &d, &[0, 85, 170, 255], None, [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [0, 0, 0, 255]);
assert_eq!(rgba_at(&img, 1, 0), [85, 85, 85, 255]);
assert_eq!(rgba_at(&img, 1, 1), [255, 255, 255, 255]);
let d = dict(b"<< /Width 2 /Height 1 /BitsPerComponent 1 >>");
let img = decode_rgba(&doc, &d, &[0b1000_0000], None, [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [255, 255, 255, 255]);
assert_eq!(rgba_at(&img, 1, 0), [0, 0, 0, 255]);
let d = dict(b"<< /Width 2 /Height 1 /BitsPerComponent 4 >>");
let img = decode_rgba(&doc, &d, &[0xF0], None, [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [255, 255, 255, 255]);
assert_eq!(rgba_at(&img, 1, 0), [0, 0, 0, 255]);
let d = dict(b"<< /Width 1 /Height 1 /BitsPerComponent 16 >>");
let img = decode_rgba(&doc, &d, &[0x80, 0x00], None, [0; 3]).unwrap();
let [r, ..] = rgba_at(&img, 0, 0);
assert!((127..=129).contains(&r), "16-bit mid gray {r}");
}
#[test]
fn the_lookup_table_paints_what_per_pixel_conversion_would() {
let cs = ColorSpace::DeviceGray;
let width = 13;
let height = 3;
for bpc in [1usize, 2, 4, 8] {
let row_bytes = (width * bpc).div_ceil(8);
let full: Vec<u8> = (0..row_bytes * height)
.map(|i| (i as u8).wrapping_mul(37).wrapping_add(11))
.collect();
for data in [&full[..], &full[..row_bytes + 1]] {
for range in [(0.0f32, 1.0f32), (1.0, 0.0), (0.25, 0.75)] {
let max = ((1u32 << bpc) - 1) as f32;
let decode = [range.0, range.1];
let got = decode_samples(width, height, data, &cs, bpc, Some(&decode));
let stride_bits = row_bytes * 8;
for y in 0..height {
for x in 0..width {
let raw = sample_bits(data, y * stride_bits + x * bpc, bpc) as f32;
let (d0, d1) = range;
let rgb = cs.to_rgb(&[d0 + raw * (d1 - d0) / max]);
let mut want = [0u8, 0, 0, 255];
for (slot, v) in want.iter_mut().zip(rgb) {
*slot = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
}
assert_eq!(
got.at(x, y),
want,
"bpc {bpc} range {range:?} at ({x},{y}) \
with {} bytes",
data.len()
);
}
}
}
}
}
}
#[test]
fn rows_are_byte_aligned() {
let doc = test_doc();
let d = dict(b"<< /Width 3 /Height 2 /BitsPerComponent 1 >>");
let img = decode_rgba(&doc, &d, &[0b1010_0000, 0b0100_0000], None, [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0)[0], 255);
assert_eq!(rgba_at(&img, 1, 0)[0], 0);
assert_eq!(rgba_at(&img, 2, 0)[0], 255);
assert_eq!(rgba_at(&img, 0, 1)[0], 0);
assert_eq!(rgba_at(&img, 1, 1)[0], 255);
assert_eq!(rgba_at(&img, 2, 1)[0], 0);
}
#[test]
fn decode_array_inverts_gray() {
let doc = test_doc();
let d = dict(b"<< /Width 2 /Height 1 /BitsPerComponent 8 /Decode [1 0] >>");
let img = decode_rgba(&doc, &d, &[0, 255], None, [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [255, 255, 255, 255], "0 inverts to 1");
assert_eq!(rgba_at(&img, 1, 0), [0, 0, 0, 255], "255 inverts to 0");
}
#[test]
fn rgb_and_cmyk_samples_decode() {
let doc = test_doc();
let d = dict(b"<< /Width 2 /Height 1 /BitsPerComponent 8 >>");
let cs = obj(b"/DeviceRGB");
let img = decode_rgba(&doc, &d, &[255, 0, 0, 0, 0, 255], Some(&cs), [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [255, 0, 0, 255]);
assert_eq!(rgba_at(&img, 1, 0), [0, 0, 255, 255]);
let d = dict(b"<< /Width 1 /Height 1 /BitsPerComponent 8 >>");
let cs = obj(b"/DeviceCMYK");
let img = decode_rgba(&doc, &d, &[255, 0, 0, 0], Some(&cs), [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [0, 255, 255, 255], "pure cyan");
}
#[test]
fn indexed_lookup_via_palette() {
let doc = test_doc();
let cs = obj(b"[/Indexed /DeviceRGB 3 <FF0000 00FF00 0000FF 000000>]");
let d = dict(b"<< /Width 4 /Height 1 /BitsPerComponent 2 >>");
let img = decode_rgba(&doc, &d, &[0b00_01_10_11], Some(&cs), [0; 3]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [255, 0, 0, 255]);
assert_eq!(rgba_at(&img, 1, 0), [0, 255, 0, 255]);
assert_eq!(rgba_at(&img, 2, 0), [0, 0, 255, 255]);
assert_eq!(rgba_at(&img, 3, 0), [0, 0, 0, 255]);
}
#[test]
fn stencil_and_inverted_stencil() {
let doc = test_doc();
let d = dict(b"<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 >>");
let img = decode_rgba(&doc, &d, &[0x40, 0x80], None, [10, 20, 30]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [10, 20, 30, 255], "0 paints");
assert_eq!(rgba_at(&img, 1, 0), [0, 0, 0, 0], "1 transparent");
assert_eq!(rgba_at(&img, 0, 1), [0, 0, 0, 0]);
assert_eq!(rgba_at(&img, 1, 1), [10, 20, 30, 255]);
let d = dict(b"<< /Width 2 /Height 2 /ImageMask true /BitsPerComponent 1 /Decode [1 0] >>");
let img = decode_rgba(&doc, &d, &[0x40, 0x80], None, [10, 20, 30]).unwrap();
assert_eq!(rgba_at(&img, 0, 0), [0, 0, 0, 0], "inverted: 0 transparent");
assert_eq!(rgba_at(&img, 1, 0), [10, 20, 30, 255], "inverted: 1 paints");
}
fn tiny_jpeg() -> Vec<u8> {
let mut j = vec![0xFF, 0xD8]; j.extend_from_slice(&[0xFF, 0xDB, 0x00, 0x43, 0x00]); j.extend_from_slice(&[1u8; 64]);
j.extend_from_slice(&[
0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
]);
j.extend_from_slice(&[0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01]);
j.extend_from_slice(&[0u8; 15]);
j.push(0x00);
j.extend_from_slice(&[0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01]);
j.extend_from_slice(&[0u8; 15]);
j.push(0x00);
j.extend_from_slice(&[
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x3F,
]);
j.extend_from_slice(&[0xFF, 0xD9]); j
}
#[test]
fn dct_image_decodes_via_jpeg() {
let doc = test_doc();
let d = dict(
b"<< /Width 1 /Height 1 /BitsPerComponent 8 /Filter /DCTDecode \
/ColorSpace /DeviceGray >>",
);
let jpeg = tiny_jpeg();
let img = decode_rgba(&doc, &d, &jpeg, None, [0; 3]).expect("jpeg decodes");
assert_eq!((img.width, img.height), (1, 1));
let [r, g, b, a] = rgba_at(&img, 0, 0);
assert_eq!((r, g), (r, r), "gray");
assert!((120..=136).contains(&r), "mid gray, got {r}");
assert_eq!((g, b, a), (r, r, 255));
assert!(decode_rgba(&doc, &d, &[1, 2, 3], None, [0; 3]).is_none());
}
#[test]
fn jpeg_with_huge_sof_dimensions_is_rejected_before_decoding() {
let doc = test_doc();
let d = dict(
b"<< /Width 1 /Height 1 /BitsPerComponent 8 /Filter /DCTDecode \
/ColorSpace /DeviceGray >>",
);
let mut j = tiny_jpeg();
let sof = j.windows(2).position(|w| w == [0xFF, 0xC0]).expect("SOF0");
j[sof + 5..sof + 9].copy_from_slice(&[0xFF; 4]);
let start = std::time::Instant::now();
assert!(decode_rgba(&doc, &d, &j, None, [0; 3]).is_none());
assert!(
start.elapsed() < std::time::Duration::from_secs(2),
"header-only rejection must not attempt a decode-sized allocation"
);
let mut j = tiny_jpeg();
let sof = j.windows(2).position(|w| w == [0xFF, 0xC0]).expect("SOF0");
j[sof + 5..sof + 9].copy_from_slice(&[0, 0, 0, 0]);
assert!(decode_rgba(&doc, &d, &j, None, [0; 3]).is_none());
}
#[test]
fn inverted_cmyk_conversion() {
assert_eq!(inverted_cmyk_to_rgb([255, 255, 255, 255]), [255, 255, 255]);
assert_eq!(inverted_cmyk_to_rgb([255, 255, 255, 0]), [0, 0, 0]);
assert_eq!(inverted_cmyk_to_rgb([0, 255, 255, 255]), [0, 255, 255]);
}
#[test]
fn bad_dimensions_are_rejected() {
let doc = test_doc();
for src in [
b"<< /Width 0 /Height 2 >>".as_slice(),
b"<< /Height 2 >>".as_slice(),
b"<< /Width 100000 /Height 100000 >>".as_slice(),
] {
assert!(
decode_rgba(&doc, &dict(src), &[], None, [0; 3]).is_none(),
"{}",
String::from_utf8_lossy(src)
);
}
}
fn quad_image() -> Rgba<'static> {
Rgba {
width: 2,
height: 2,
pixels: Pixels::Quads(vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
]),
truncated: false,
}
}
fn pix_at(pix: &Pixmap, x: u32, y: u32) -> [u8; 4] {
let off = ((y * pix.width + x) * 4) as usize;
pix.data[off..off + 4].try_into().unwrap()
}
#[test]
fn draw_maps_row_zero_to_the_v1_edge() {
let mut pix = Pixmap::new(8, 8);
let p = DrawParams {
ctm: Matrix::scale(8.0, 8.0),
alpha: 1.0,
fill_rgb: [0; 3],
clip: None,
};
draw_rgba(&mut pix, &quad_image(), &p);
assert_eq!(pix_at(&pix, 1, 1), [0, 0, 255, 255], "row 1 left on top");
assert_eq!(pix_at(&pix, 6, 1), [255, 255, 255, 255], "row 1 right");
assert_eq!(pix_at(&pix, 1, 6), [255, 0, 0, 255], "row 0 left below");
assert_eq!(pix_at(&pix, 6, 6), [0, 255, 0, 255], "row 0 right");
}
#[test]
fn draw_respects_offset_alpha_and_clip() {
let mut pix = Pixmap::new(8, 8);
pix.fill([255, 255, 255, 255]);
let ctm = Matrix::scale(4.0, 4.0).concat(Matrix::translate(4.0, 0.0));
let mut clip = Mask::new(8, 8);
clip.data.iter_mut().for_each(|c| *c = 255);
for y in 0..8 {
clip.data[y * 8 + 7] = 0;
}
let p = DrawParams {
ctm,
alpha: 0.5,
fill_rgb: [0; 3],
clip: Some(&clip),
};
draw_rgba(&mut pix, &quad_image(), &p);
assert_eq!(pix_at(&pix, 1, 1), [255, 255, 255, 255], "outside image");
let [r, g, b, _] = pix_at(&pix, 5, 1);
assert_eq!(b, 255, "blue keeps its own channel");
assert!((127..=129).contains(&r), "50% blend r {r}");
assert!((127..=129).contains(&g), "50% blend g {g}");
assert_eq!(pix_at(&pix, 7, 1), [255, 255, 255, 255], "clipped column");
}
#[test]
fn an_opaque_source_composites_to_a_plain_copy() {
for &under in &[
[0, 0, 0, 0],
[255, 255, 255, 255],
[17, 200, 3, 128],
[9, 9, 9, 1],
] {
for &rgb in &[[0, 0, 0], [255, 255, 255], [12, 34, 56]] {
let mut dst = under;
composite_over(&mut dst, rgb, 1.0);
assert_eq!(
dst,
[rgb[0], rgb[1], rgb[2], 255],
"opaque {rgb:?} over {under:?}"
);
}
}
}
#[test]
fn degenerate_ctm_draws_nothing() {
let mut pix = Pixmap::new(4, 4);
let p = DrawParams {
ctm: Matrix::scale(0.0, 0.0),
alpha: 1.0,
fill_rgb: [0; 3],
clip: None,
};
draw_rgba(&mut pix, &quad_image(), &p);
assert!(pix.data.iter().all(|&b| b == 0), "pixmap untouched");
}
}