Merge the fixes to #5461 into this PR

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz Martens
2026-10-31 00:20:10 +01:00
co-authored by Claude Opus 5.5
9 changed files with 310 additions and 15 deletions
Generated
+1
View File
@@ -4395,6 +4395,7 @@ dependencies = [
"pdfcraft-create",
"pdfcraft-edit",
"pdfcraft-export",
"pdfcraft-filters",
"pdfcraft-fonts",
"pdfcraft-forms",
"pdfcraft-geom",
+3 -1
View File
@@ -91,5 +91,7 @@ recognisable in a diff and byte-stable when re-serialized.
- Structural validation against the Arlington model (`core.arlington-validation`), which will live
in the reserved `arlington` crate.
- Image codecs. `pdfcraft-filters` carries the non-image filters and CCITT fax and JBIG2 decoding
(`Document::decoded_image`, a JBIG2 image's `/JBIG2Globals` within `MAX_JBIG2_GLOBALS`, 32 MiB);
(`Document::decoded_image`, a JBIG2 image's `/JBIG2Globals` within `MAX_JBIG2_GLOBALS`, 32 MiB,
their filters undone once a document for the images that share them, up to 64 MiB kept, while
their segments are decoded for every image);
DCT and JPX are still decoded through the bootstrap renderer (`core.image-filters`, M2.6).
+3
View File
@@ -49,6 +49,9 @@ pub struct Document {
repair_log: Arc<Vec<String>>,
cache: Arc<Mutex<HashMap<u32, Arc<Object>>>>,
objstms: Arc<Mutex<HashMap<u32, Arc<ObjStm>>>>,
/// What `/JBIG2Globals` streams of the file decode to, by object number: the images of a scan
/// share them, and each would otherwise decode them again (see `Document::jbig2_globals`).
jbig2_globals: Arc<Mutex<HashMap<u32, Arc<Vec<u8>>>>>,
stream_limit: usize,
overlay: BTreeMap<u32, Slot>,
next_num: u32,
+45 -12
View File
@@ -5,6 +5,10 @@ use super::{
Arc, CosError, Cow, Dict, Document, Lexer, MAX_DECODED, MAX_JBIG2_GLOBALS, ObjRef, Object, Stream, is_whitespace, parse_indirect_shared,
};
/// The most decoded `/JBIG2Globals` a document keeps for its images to share (see
/// `Document::jbig2_globals`): two streams at [`MAX_JBIG2_GLOBALS`].
const JBIG2_GLOBALS_HELD: usize = 2 * MAX_JBIG2_GLOBALS;
impl Document {
pub fn bytes(&self) -> &Arc<Vec<u8>> {
&self.data
@@ -301,13 +305,13 @@ impl Document {
};
// `/Width` and `/Height` are read as given directly, as every caller reads them first: one
// given by reference has the image refused there, before it gets here.
let height = s.dict.int(b"Height").and_then(|h| u32::try_from(h).ok()).unwrap_or(0);
let height = s.dict.int(b"Height").and_then(|h| u32::try_from(h).ok());
if let Some(((pdfcraft_filters::Filter::Jbig2, _), before)) = chain.split_last() {
let Some(columns) = s.dict.int(b"Width").and_then(|w| u32::try_from(w).ok()) else {
return Err(CosError::Filter("a JBIG2 image's /Width isn't a size".into()));
let (Some(columns), Some(height)) = (s.dict.int(b"Width").and_then(|w| u32::try_from(w).ok()), height) else {
return Err(CosError::Filter("a JBIG2 image's /Width or /Height isn't a size".into()));
};
let globals = self.jbig2_globals(s)?;
return pdfcraft_filters::decode_jbig2(&undo(before)?, globals.as_deref(), columns, height, max).map_err(filter);
return pdfcraft_filters::decode_jbig2(&undo(before)?, globals.as_deref().map(Vec::as_slice), columns, height, max).map_err(filter);
}
let Some(p) = self.fax_params(s) else { return s.decoded_within(max) };
// As many rows as the image has: a /Rows below /Height would cut the picture short, and
@@ -317,24 +321,53 @@ impl Document {
return Err(CosError::Filter("a fax image's /Columns aren't its /Width".into()));
}
let stride = usize::try_from(p.columns).unwrap_or(usize::MAX).div_ceil(8).max(1);
let rows = u32::try_from(max / stride).unwrap_or(u32::MAX).min(height);
let rows = u32::try_from(max / stride).unwrap_or(u32::MAX).min(height.unwrap_or(0));
let data = undo(chain.split_last().map_or(&[][..], |(_, before)| before))?;
pdfcraft_filters::decode_ccitt(&data, &p, rows).map_err(filter)
}
/// What the `/JBIG2Globals` stream of JBIG2 image `s` (in its last filter's `/DecodeParms`)
/// decodes to, within [`MAX_JBIG2_GLOBALS`]; `None` without one. Globals that can't be read
/// are an error, rather than the image decoded without them.
fn jbig2_globals(&self, s: &Stream) -> Result<Option<Vec<u8>>, CosError> {
let globals = self.last_filter_parms(s).get(b"JBIG2Globals").map(|g| self.resolve(g));
let Some(Object::Stream(g)) = globals.as_deref() else { return Ok(None) };
self.with_direct_parms(g).decoded_within(MAX_JBIG2_GLOBALS).map(Some).map_err(|e| {
/// decodes to, within [`MAX_JBIG2_GLOBALS`]; `None` without one (a reference to no object is
/// one to null, ISO 32000-2 §7.3.10, which is an absent entry, §7.3.7). The image is then read
/// without them, as the renderer draws it, so an image that needed them mostly fails to decode
/// and can decode to other pixels. A stream that is there and can't be read is an error, rather
/// than the image decoded without it.
///
/// The images of a scan share one globals stream, so a stream of the file, not edited since
/// it was read (the same in every clone) and with its `/DecodeParms` direct (by reference they
/// are another object, which can change while this one doesn't), has its filters undone once a
/// document and is kept, up to [`JBIG2_GLOBALS_HELD`] in all; others are read each time. That
/// saves the filters only: the segments it holds are decoded again for every image.
fn jbig2_globals(&self, s: &Stream) -> Result<Option<Arc<Vec<u8>>>, CosError> {
let Some(entry) = self.last_filter_parms(s).get(b"JBIG2Globals").cloned() else { return Ok(None) };
let held = match entry {
Object::Ref(r) if !self.overlay.contains_key(&r.num) => Some(r.num),
_ => None,
};
if let Some(hit) = held.and_then(|num| self.jbig2_globals.lock().ok()?.get(&num).cloned()) {
return Ok(Some(hit));
}
let Object::Stream(g) = &*self.resolve(&entry) else { return Ok(None) };
let readable = self.with_direct_parms(g);
let held = held.filter(|_| matches!(readable, Cow::Borrowed(_)));
let mut data = readable.decoded_within(MAX_JBIG2_GLOBALS).map_err(|e| {
let detail = match e {
CosError::Filter(detail) => detail,
e => e.to_string(),
};
CosError::Filter(format!("the JBIG2 globals can't be read: {detail}"))
})
})?;
// What is kept counts by its length, so that's all it may hold on to.
data.shrink_to_fit();
let data = Arc::new(data);
// A poisoned lock only means nothing is kept.
if let (Some(num), Ok(mut kept)) = (held, self.jbig2_globals.lock()) {
let total = kept.values().map(|g| g.len()).fold(data.len(), usize::saturating_add);
if total <= JBIG2_GLOBALS_HELD {
kept.insert(num, data.clone());
}
}
Ok(Some(data))
}
pub fn try_get(&self, num: u32) -> Result<Arc<Object>, CosError> {
+2
View File
@@ -19,6 +19,7 @@ impl Document {
repair_log: Arc::new(Vec::new()),
cache: Arc::default(),
objstms: Arc::default(),
jbig2_globals: Arc::default(),
stream_limit: crate::object::MAX_DECODED,
overlay: BTreeMap::new(),
next_num: 1,
@@ -102,6 +103,7 @@ impl Document {
repair_log: Arc::new(Vec::new()),
cache: Arc::default(),
objstms: Arc::default(),
jbig2_globals: Arc::default(),
stream_limit: stream_limit.min(crate::object::MAX_DECODED),
overlay: BTreeMap::new(),
next_num: 1,
+153
View File
@@ -173,6 +173,159 @@ fn fax_images_decode_with_their_parameters() {
assert!(behind(&[Object::Int(5), fax], &[null, own], coded.clone()).is_err(), "a /Filter entry that isn't a name");
}
/// Images that share a `/JBIG2Globals` stream of the file decode it once a document: it is kept
/// for the next, in every clone. A globals stream edited since is read afresh (the kept one is the
/// file's), and objects added in memory aren't kept.
#[test]
fn shared_jbig2_globals_are_read_once() {
let bits = [0b1111_1000, 0, 0b1111_1000, 0, 0b1111_1000, 0];
let coded = pdfcraft_filters::encode_jbig2_generic(&bits, 10, 3).unwrap();
let mut doc = Document::new_empty();
let mut d = Dict::new();
d.set(b"Filter".to_vec(), Object::name("FlateDecode"));
let skipped = [0, 0, 0, 0, 0x3E, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1];
let globals = doc.add(Object::Stream(Stream::from_raw(d, pdfcraft_filters::encode_flate(&skipped))));
let image = |doc: &mut Document| {
let mut d = Dict::new();
d.set(b"Width".to_vec(), Object::Int(10));
d.set(b"Height".to_vec(), Object::Int(3));
d.set(b"Filter".to_vec(), Object::name("JBIG2Decode"));
let mut parms = Dict::new();
parms.set(b"JBIG2Globals".to_vec(), Object::Ref(globals));
d.set(b"DecodeParms".to_vec(), Object::Dict(parms));
doc.add(Object::Stream(Stream::from_raw(d, coded.clone())))
};
let (a, b) = (image(&mut doc), image(&mut doc));
let stream = |doc: &Document, r: ObjRef| match &*doc.get(r) {
Object::Stream(s) => s.clone(),
_ => panic!("not a stream"),
};
assert_eq!(doc.decoded_image(&stream(&doc, a)).unwrap(), bits);
assert!(doc.jbig2_globals.lock().unwrap().is_empty(), "an object added in memory isn't kept");
// The same objects read from a file, the catalog holding on to the images (a full save may
// number them anew).
let root = doc.root().unwrap();
doc.update_dict(root, |c| c.set(b"Images".to_vec(), Object::Array(vec![Object::Ref(a), Object::Ref(b)]))).unwrap();
let file = crate::write_full(&doc, &crate::SaveOptions::default()).unwrap();
let mut doc = Document::open(Arc::new(file)).unwrap();
let images = doc.get(doc.root().unwrap()).as_dict().unwrap().get(b"Images").unwrap().as_array().unwrap().clone();
let (a, b) = (images[0].as_ref().unwrap(), images[1].as_ref().unwrap());
let parms = stream(&doc, a).dict.get(b"DecodeParms").unwrap().as_dict().unwrap().clone();
let globals = parms.reference(b"JBIG2Globals").unwrap();
let clone = doc.clone();
assert_eq!(doc.decoded_image(&stream(&doc, a)).unwrap(), bits);
assert_eq!(doc.jbig2_globals.lock().unwrap().len(), 1, "kept");
let kept = doc.jbig2_globals.lock().unwrap()[&globals.num].clone();
assert_eq!(clone.decoded_image(&stream(&clone, b)).unwrap(), bits, "the clone's other image");
let shared = clone.jbig2_globals.lock().unwrap()[&globals.num].clone();
assert!(Arc::ptr_eq(&kept, &shared), "the clone uses the one kept, not a second decode");
doc.set(globals, Object::Stream(Stream::from_raw(Dict::new(), b"not JBIG2".to_vec())));
assert!(doc.decoded_image(&stream(&doc, b)).is_err(), "the edited globals are read afresh");
assert_eq!(clone.decoded_image(&stream(&clone, b)).unwrap(), bits, "the clone keeps the file's");
}
/// A JBIG2 image's size is read as given directly, as every caller reads it first, and an image
/// with neither is an error, not an image of no rows.
#[test]
fn a_jbig2_image_needs_its_size_given_directly() {
let coded = pdfcraft_filters::encode_jbig2_generic(&[0xFF, 0xFF], 8, 2).unwrap();
let mut doc = Document::new_empty();
let size = doc.add(Object::Int(2));
let image = |width: Object, height: Object| {
let mut d = Dict::new();
d.set(b"Width".to_vec(), width);
d.set(b"Height".to_vec(), height);
d.set(b"Filter".to_vec(), Object::name("JBIG2Decode"));
doc.decoded_image(&Stream::from_raw(d, coded.clone()))
};
assert!(image(Object::Int(8), Object::Int(2)).is_ok());
assert!(image(Object::Ref(size), Object::Int(2)).is_err(), "/Width by reference");
assert!(image(Object::Int(8), Object::Ref(size)).is_err(), "/Height by reference");
}
/// A document that kept globals of 64 MiB keeps no more, and the images past that still read:
/// three streams of 22 MiB, shared by three images, are two kept and one decoded each time.
#[test]
fn kept_jbig2_globals_are_bounded_in_total() {
let bits = [0b1111_1000, 0, 0b1111_1000, 0, 0b1111_1000, 0];
let coded = pdfcraft_filters::encode_jbig2_generic(&bits, 10, 3).unwrap();
let length = 22 * 1024 * 1024;
let mut skipped = vec![0, 0, 0, 0, 0x3E, 0, 0];
skipped.extend_from_slice(&u32::try_from(4 + length).unwrap().to_be_bytes());
skipped.extend_from_slice(&[0, 0, 0, 1]);
skipped.resize(skipped.len() + length, 0);
let mut doc = Document::new_empty();
let mut images = Vec::new();
for _ in 0..3 {
let mut d = Dict::new();
d.set(b"Filter".to_vec(), Object::name("FlateDecode"));
let globals = doc.add(Object::Stream(Stream::from_raw(d, pdfcraft_filters::encode_flate(&skipped))));
let mut d = Dict::new();
d.set(b"Width".to_vec(), Object::Int(10));
d.set(b"Height".to_vec(), Object::Int(3));
d.set(b"Filter".to_vec(), Object::name("JBIG2Decode"));
let mut parms = Dict::new();
parms.set(b"JBIG2Globals".to_vec(), Object::Ref(globals));
d.set(b"DecodeParms".to_vec(), Object::Dict(parms));
images.push(Object::Ref(doc.add(Object::Stream(Stream::from_raw(d, coded.clone())))));
}
let root = doc.root().unwrap();
doc.update_dict(root, |c| c.set(b"Images".to_vec(), Object::Array(images))).unwrap();
let doc = Document::open(Arc::new(crate::write_full(&doc, &crate::SaveOptions::default()).unwrap())).unwrap();
let images = doc.get(doc.root().unwrap()).as_dict().unwrap().get(b"Images").unwrap().as_array().unwrap().clone();
for image in &images {
let Object::Stream(s) = &*doc.get(image.as_ref().unwrap()) else { panic!("not a stream") };
assert_eq!(doc.decoded_image(s).unwrap(), bits, "every image reads");
}
let kept = doc.jbig2_globals.lock().unwrap();
assert_eq!(kept.len(), 2, "two fit in 64 MiB, a third doesn't");
assert!(kept.values().all(|g| g.capacity() == g.len()), "what is kept holds no more than it counts");
}
/// Globals whose `/DecodeParms` are given by reference aren't kept: that's another object, so
/// editing it changes what they decode to while the stream itself, the key kept by, stays the same.
#[test]
fn jbig2_globals_with_parameters_by_reference_are_read_afresh() {
let bits = [0b1111_1000, 0, 0b1111_1000, 0, 0b1111_1000, 0];
let coded = pdfcraft_filters::encode_jbig2_generic(&bits, 10, 3).unwrap();
let skipped = [0, 0, 0, 0, 0x3E, 0, 0, 0, 0, 0, 4, 0, 0, 0, 1];
let params = pdfcraft_filters::Params { predictor: 12, colors: 1, bits_per_component: 8, columns: 15, early_change: 1 };
let mut doc = Document::new_empty();
let mut parms = Dict::new();
parms.set(b"Predictor".to_vec(), Object::Int(12));
parms.set(b"Columns".to_vec(), Object::Int(15));
let parms = doc.add(Object::Dict(parms));
let mut d = Dict::new();
d.set(b"Filter".to_vec(), Object::name("FlateDecode"));
d.set(b"DecodeParms".to_vec(), Object::Ref(parms));
let predicted = pdfcraft_filters::encode(&pdfcraft_filters::Filter::Flate, &params, &skipped).unwrap();
let globals = doc.add(Object::Stream(Stream::from_raw(d, predicted)));
let mut d = Dict::new();
d.set(b"Width".to_vec(), Object::Int(10));
d.set(b"Height".to_vec(), Object::Int(3));
d.set(b"Filter".to_vec(), Object::name("JBIG2Decode"));
let mut image_parms = Dict::new();
image_parms.set(b"JBIG2Globals".to_vec(), Object::Ref(globals));
d.set(b"DecodeParms".to_vec(), Object::Dict(image_parms));
let image = doc.add(Object::Stream(Stream::from_raw(d, coded)));
let root = doc.root().unwrap();
doc.update_dict(root, |c| c.set(b"Image".to_vec(), Object::Ref(image))).unwrap();
let mut doc = Document::open(Arc::new(crate::write_full(&doc, &crate::SaveOptions::default()).unwrap())).unwrap();
let image = doc.get(doc.root().unwrap()).as_dict().unwrap().reference(b"Image").unwrap();
let stream = |doc: &Document| match &*doc.get(image) {
Object::Stream(s) => s.clone(),
_ => panic!("not a stream"),
};
assert_eq!(doc.decoded_image(&stream(&doc)).unwrap(), bits);
assert!(doc.jbig2_globals.lock().unwrap().is_empty(), "not kept");
let globals = stream(&doc).dict.get(b"DecodeParms").unwrap().as_dict().unwrap().reference(b"JBIG2Globals").unwrap();
let Object::Stream(g) = &*doc.get(globals) else { panic!("not a stream") };
let parms = g.dict.reference(b"DecodeParms").unwrap();
doc.set(parms, Object::Dict(Dict::new()));
assert!(doc.decoded_image(&stream(&doc)).is_err(), "without the predictor they read otherwise");
}
/// A JBIG2 image reads through its decoder, behind other filters too, with the `/JBIG2Globals` of
/// its own `/DecodeParms` (a dictionary or its place in an array, /Crypt counting, by reference
/// too): globals that can't be read are an error, and ones at another filter's place aren't its
+2
View File
@@ -58,3 +58,5 @@ getrandom = { version = "0.4", features = ["wasm_js"] }
[dev-dependencies]
pdfcraft-testkit = { workspace = true }
# JBIG2 fixtures (`encode_jbig2_generic`).
pdfcraft-filters = { workspace = true }
+99
View File
@@ -8079,6 +8079,105 @@ fn copy_image_finds_a_picture_inside_form_artwork_under_text() {
assert!(d.page_image_copy_file(0, 1).is_err());
}
/// Copy Image reads a JBIG2 image's `/JBIG2Globals`: here they hold the page information, and the
/// image's own stream only an extension segment decoders skip, so it has a picture only through
/// them. Its default pixel is black.
#[test]
fn copy_image_reads_the_globals_of_a_jbig2_picture() {
let mut globals = vec![0, 0, 0, 0, 48, 0, 1, 0, 0, 0, 19];
globals.extend_from_slice(&16u32.to_be_bytes());
globals.extend_from_slice(&2u32.to_be_bytes());
globals.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0]); // resolution, flags, striping
let image = [0, 0, 0, 1, 62, 0, 1, 0, 0, 0, 0];
let stream = |dict: &str, data: &[u8]| {
let mut o = format!("<< {dict} /Length {} >>\nstream\n", data.len()).into_bytes();
o.extend_from_slice(data);
o.extend_from_slice(b"\nendstream");
o
};
let objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >>".to_vec(),
stream("", b"q 160 0 0 20 20 100 cm /Im1 Do Q"),
stream(
"/Type /XObject /Subtype /Image /Width 16 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 /Filter /JBIG2Decode /DecodeParms << /JBIG2Globals 6 0 R >>",
&image,
),
stream("", &globals),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
let mut s = Session::new();
let id = s.open("scan.pdf", None, Arc::new(out), None).unwrap();
let d = s.get(id).unwrap();
let (w, h, rgba) = d.page_image_rgba(0, 0).unwrap();
assert_eq!((w, h), (16, 2));
assert!(rgba.chunks(4).all(|p| p == [0, 0, 0, 255]), "the page's default pixel, from the globals");
}
/// Copy Image takes a JBIG2 picture as its pixels and as a PNG file, behind another filter
/// (ASCIIHexDecode) too: Export's decoding, through `Document::decoded_image`.
#[test]
fn copy_image_copies_a_jbig2_picture_behind_another_filter() {
// 16 × 2: the left half of the top row black, the rest white.
let bits = [0x00, 0xFF, 0xFF, 0xFF];
let coded = pdfcraft_filters::encode_jbig2_generic(&bits, 16, 2).unwrap();
let hex: Vec<u8> = coded.iter().flat_map(|b| format!("{b:02x}").into_bytes()).chain(*b">").collect();
let stream = |dict: &str, data: &[u8]| {
let mut o = format!("<< {dict} /Length {} >>\nstream\n", data.len()).into_bytes();
o.extend_from_slice(data);
o.extend_from_slice(b"\nendstream");
o
};
let objs: Vec<Vec<u8>> = vec![
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
b"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".to_vec(),
b"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >>".to_vec(),
stream("", b"q 160 0 0 20 20 100 cm /Im1 Do Q"),
stream(
"/Type /XObject /Subtype /Image /Width 16 /Height 2 /ColorSpace /DeviceGray /BitsPerComponent 1 /Filter [/ASCIIHexDecode /JBIG2Decode]",
&hex,
),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n", i + 1).as_bytes());
out.extend_from_slice(o);
out.extend_from_slice(b"\nendobj\n");
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offsets {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
let mut s = Session::new();
let id = s.open("scan.pdf", None, Arc::new(out), None).unwrap();
let d = s.get(id).unwrap();
assert_eq!(d.page_image_at(0, [30.0, 115.0]), Some(0));
assert_eq!(d.page_image_copy_file(0, 0).unwrap().0, "png");
let (w, h, rgba) = d.page_image_rgba(0, 0).unwrap();
assert_eq!((w, h), (16, 2));
let grey: Vec<u8> = rgba.chunks(4).map(|px| px[0]).collect();
let expected: Vec<u8> = (0..32).map(|i| if i < 8 { 0 } else { 255 }).collect();
assert_eq!(grey, expected, "the picture, black as 0");
}
/// `fixture(2)` with the catalog pointing at a page tree object the file does not have (#1656).
fn fixture_without_page_tree() -> Vec<u8> {
let bytes = fixture(2);
+2 -2
View File
@@ -130,7 +130,7 @@ milestone = "M1"
status = "partial"
commands = []
tools = []
evidence = ["crates/filters/src/ccitt.rs::decoding_gives_the_rows_back", "crates/filters/src/ccitt.rs::group_3_rows_on_bytes_read_by_the_codes_in_the_data", "crates/cos/src/document/tests.rs::fax_images_decode_with_their_parameters", "crates/filters/src/jbig2/decode.rs::both_codings_decode_to_their_samples", "crates/filters/src/jbig2/decode.rs::what_it_cannot_decode_is_an_error", "crates/cos/src/document/tests.rs::jbig2_images_decode_with_their_globals"]
evidence = ["crates/filters/src/ccitt.rs::decoding_gives_the_rows_back", "crates/filters/src/ccitt.rs::group_3_rows_on_bytes_read_by_the_codes_in_the_data", "crates/cos/src/document/tests.rs::fax_images_decode_with_their_parameters", "crates/filters/src/jbig2/decode.rs::both_codings_decode_to_their_samples", "crates/filters/src/jbig2/decode.rs::what_it_cannot_decode_is_an_error", "crates/cos/src/document/tests.rs::jbig2_images_decode_with_their_globals", "crates/cos/src/document/tests.rs::shared_jbig2_globals_are_read_once"]
notes = "decoded through the hayro bootstrap for display; own codecs are M1.1/M1.2. CCITT fax decoding is in filters (decode_ccitt, through hayro-ccitt), which Document::decoded_image uses for Export ▸ Images, the optimizer and redaction; so is JBIG2 decoding (decode_jbig2, through hayro-jbig2, the page bounded before it is decoded), which it uses with the image's /JBIG2Globals (within 32 MiB) for Export ▸ Images, Copy Image and redaction. Byte-aligned Group 3 is read by its end-of-line codes when the data starts with one and on bytes otherwise; still limited: data with codes but none before the first row (not as T.4 sends them) is read on bytes, as the renderer draws it, and at /K 0 may come out as other rows, which the optimizer then writes back. The bootstrap renderer draws byte-aligned Group 3 whose fill ends each end-of-line code on a byte as other rows (black box, at every /K), which decode_ccitt reads right; redaction removes Group 3 on bytes whole"
[[feature]]
@@ -1598,7 +1598,7 @@ milestone = "M3"
status = "partial"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/viewer.rs::a_clicked_picture_is_selected_and_copied_in_its_original_format", "crates/ui-egui/tests/it/viewer.rs::right_click_on_a_picture_copies_it_at_full_size", "crates/ui-egui/tests/it/viewer.rs::a_picture_inside_form_artwork_under_text_is_selected_and_copied", "crates/engine/src/tests.rs::copy_image_finds_a_picture_inside_form_artwork_under_text"]
evidence = ["crates/ui-egui/tests/it/viewer.rs::a_clicked_picture_is_selected_and_copied_in_its_original_format", "crates/ui-egui/tests/it/viewer.rs::right_click_on_a_picture_copies_it_at_full_size", "crates/ui-egui/tests/it/viewer.rs::a_picture_inside_form_artwork_under_text_is_selected_and_copied", "crates/engine/src/tests.rs::copy_image_finds_a_picture_inside_form_artwork_under_text", "crates/engine/src/tests.rs::copy_image_copies_a_jbig2_picture_behind_another_filter", "crates/engine/src/tests.rs::copy_image_reads_the_globals_of_a_jbig2_picture"]
notes = "Select tool: a click selects a raster image (also inside Form artwork or under text) and ⌘C copies it as a file in its original format (JPEG as stored, else PNG); right-click ▸ Copy Image copies its pixels at full resolution. Missing: inline images (BI/ID/EI), JPEG 2000 images, multi-format clipboard (file and bitmap at once), the web build"
[[feature]]