mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
M2.6: test that a JPEG refused at its lookup table is left out
CodeRabbit's follow-up on #5825: the indexed JPEG test ran only with an unlimited allowance, so nothing checked that a refused lookup-table read leaves a JPEG out (`a_lookup_table_is_read_within_the_exports_allowance` checks a Flate image). The test now measures what the soft mask costs alone (no colour space, so nothing is read after it) and allows a step less than the mask and the table: the export is refused with the reason, and only the table is refused, nothing after it. A step short of the mask, the mask alone is refused, so the mask is read first and the case above is refused at the table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
0c9cd267f5
commit
a2b2bc5e6d
1 file changed
+28
-8
@@ -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<bool>, 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
|
||||
|
||||
Reference in new issue
Block a user