diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index f701463eae..8c7db432fe 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -2346,7 +2346,8 @@ fn a_lookup_table_is_read_within_the_exports_allowance() { /// A JPEG's colour space is read once for an export, its lookup table paid for once: whether it is /// CMYK (a negative as is) and what its samples are both come from that reading. An indexed JPEG -/// isn't valid, and is written as it is, but its soft mask makes it go on past the first. +/// isn't valid, and is written as it is, but its soft mask makes it go on past the first. With the +/// mask, a table the allowance can't pay for leaves the image out, with the reason. #[test] fn a_jpegs_lookup_table_is_paid_for_once() { use pdfcraft_cos::{Dict, Document, ImageBudget, Object, PdfString, Stream}; @@ -2354,9 +2355,12 @@ fn a_jpegs_lookup_table_is_paid_for_once() { let lookup = Stream::flate(Dict::new(), &table); let mut jpeg = Vec::new(); image::GrayImage::from_pixel(2, 1, image::Luma([1])).write_to(&mut std::io::Cursor::new(&mut jpeg), image::ImageFormat::Jpeg).unwrap(); - let spent = |as_stream: bool| { + // `given`: None leaves the colour space out, Some(true) gives the table as a stream. + let exported = |given: Option, work: u64| { let mut doc = Document::new_empty(); - let table = if as_stream { Object::Ref(doc.add(Object::Stream(lookup.clone()))) } else { Object::String(PdfString::literal(table.to_vec())) }; + let table = given.map(|as_stream| { + if as_stream { Object::Ref(doc.add(Object::Stream(lookup.clone()))) } else { Object::String(PdfString::literal(table.to_vec())) } + }); let mut mask = Dict::new(); for (k, v) in [("Subtype", Object::name("Image")), ("Width", Object::Int(2)), ("Height", Object::Int(1)), ("BitsPerComponent", Object::Int(8))] @@ -2371,17 +2375,33 @@ fn a_jpegs_lookup_table_is_paid_for_once() { { dict.set(k.as_bytes().to_vec(), v); } - dict.set(b"ColorSpace".to_vec(), Object::Array(vec![Object::name("Indexed"), Object::name("DeviceRGB"), Object::Int(1), table])); + if let Some(table) = table { + dict.set(b"ColorSpace".to_vec(), Object::Array(vec![Object::name("Indexed"), Object::name("DeviceRGB"), Object::Int(1), table])); + } dict.set(b"Filter".to_vec(), Object::name("DCTDecode")); dict.set(b"SMask".to_vec(), Object::Ref(mask)); let image = doc.add(Object::Stream(Stream::from_raw(dict, jpeg.clone()))); - let mut budget = ImageBudget::with_work(u64::MAX); + let mut budget = ImageBudget::with_work(work); let file = crate::extract::image_file_within(&doc, image, &mut budget); - assert_eq!(file, Ok(("jpg", jpeg.clone())), "written as it is"); - u64::MAX - budget.work_left() + (file, work - budget.work_left(), budget.refused()) }; + let (direct, direct_cost, _) = exported(Some(false), u64::MAX); + let (streamed, streamed_cost, _) = exported(Some(true), u64::MAX); + assert_eq!(direct, Ok(("jpg", jpeg.clone())), "written as it is"); + assert_eq!(streamed, direct); let read = (lookup.raw.len() + table.len()) as u64 + pdfcraft_filters::FLATE_READ_WORK * lookup.raw.len() as u64; - assert_eq!(spent(true), spent(false) + read, "one reading of the table"); + assert_eq!(streamed_cost, direct_cost + read, "one reading of the table"); + + // Without a colour space nothing is read after the soft mask, so this is what the mask costs. + let (none, mask, _) = exported(None, u64::MAX); + assert_eq!(none.unwrap_err(), "the image has no colour space"); + // Read first, the table would be refused a step short of the mask too, and the mask after it. + assert!(read >= mask, "the table costs at least what the mask does"); + assert_eq!(exported(Some(true), mask - 1).2, 1, "the mask is read first, and refused alone"); + let (refused, _, count) = exported(Some(true), mask + read - 1); + let refused = refused.unwrap_err(); + assert!(refused.contains("more image decoding than"), "{refused}"); + assert_eq!(count, 1, "refused at the table, and nothing after it asks to be paid for"); } /// A JPEG is written as it is, at no cost, unless something means decoding it, here a mask: that