fix(render): a JBIG2 scan whose data starts with a JBIG2 file header draws instead of a white page (hayro-jbig2 patch 17) (#6279)

This commit is contained in:
mtlcl410
2026-11-09 08:54:03 +08:00
committed by GitHub
parent c9b5ee185b
commit 58c30af312
5 changed files with 174 additions and 8 deletions
+119
View File
@@ -110,6 +110,125 @@ mod tests {
data
}
/// The JBIG2 file header's ID string (ISO 14492 D.4.1).
const FILE_HEADER_ID: [u8; 8] = [0x97, 0x4A, 0x42, 0x32, 0x0D, 0x0A, 0x1A, 0x0A];
/// `embedded`, segments as [`encode_jbig2_generic`] writes them (11-byte headers: no references,
/// a one-byte page association), behind a file header with `flags` and the page count `pages`,
/// in the organization `flags` declares (D.4.2, bit 0): 1 sequential, each segment's header
/// before its data (D.1); 0 random access, every header first, the last an end of file segment
/// numbered 2, which tells where they end, then every segment's data in the same order (D.2).
fn with_file_header(embedded: &[u8], flags: u8, pages: Option<u32>) -> Vec<u8> {
let mut data = FILE_HEADER_ID.to_vec();
data.push(flags);
data.extend(pages.map(u32::to_be_bytes).into_iter().flatten());
if flags & 0x01 != 0 {
data.extend_from_slice(embedded);
return data;
}
let (mut headers, mut bodies, mut rest) = (Vec::new(), Vec::new(), embedded);
while !rest.is_empty() {
let (header, after) = rest.split_at(11);
assert_eq!(header[5], 0, "no references");
let (body, after) = after.split_at(u32::from_be_bytes(header[7..11].try_into().unwrap()) as usize);
headers.extend_from_slice(header);
bodies.extend_from_slice(body);
rest = after;
}
headers.extend_from_slice(&[0, 0, 0, 2, 51, 0, 1, 0, 0, 0, 0]);
data.extend(headers);
data.extend(bodies);
data
}
/// A stream that starts with a JBIG2 file header (ISO 14492 D.4), which some producers write
/// into `JBIG2Decode` data although ISO 32000-1 7.4.7 has the embedded organization there,
/// decodes past it as Acrobat and PDFBox draw it (hayro-jbig2 patch 17), in either organization
/// the header declares, with the page count and without it. "Bit 0: File organisation type. If
/// this bit is 0, the file uses the random-access organisation. If this bit is 1, the file uses
/// the sequential organisation." (D.4.2) A header with its reserved flag bits set is an error,
/// not a panic.
#[test]
fn a_stream_with_a_jbig2_file_header_decodes_past_it() {
let (w, h) = (203u32, 41u32);
let bits = noise(w, h);
let max = bits.len();
let embedded = encode_jbig2_generic(&bits, w, h).unwrap();
let decode = |flags: u8, pages: Option<u32>| decode_jbig2(&with_file_header(&embedded, flags, pages), None, w, h, max);
// Sequential organization (bit 0 is 1), one page.
assert_eq!(decode(0x01, Some(1)).unwrap(), bits, "sequential, page count given");
// Sequential, number of pages unknown (bit 1 is 1): no count follows (D.4.3).
assert_eq!(decode(0x03, None).unwrap(), bits, "sequential, page count unknown");
// Random-access organization (bit 0 is 0): the segment headers, then their data.
assert_ne!(with_file_header(&embedded, 0x00, Some(1))[13..], embedded[..], "laid out otherwise");
assert_eq!(decode(0x00, Some(1)).unwrap(), bits, "random access, page count given");
assert_eq!(decode(0x02, None).unwrap(), bits, "random access, page count unknown");
assert!(decode(0x11, Some(1)).is_err(), "reserved bits");
}
/// A file header cut short, or the segments after it, is an error and not a panic: the ID string
/// alone or in part, a header whose flags promise a page count with fewer than its four bytes,
/// a header with no segments after it, and segments cut anywhere in either organization. No
/// prefix of a whole stream decodes to its samples.
#[test]
fn a_truncated_jbig2_file_header_is_an_error() {
let (w, h) = (24u32, 8u32);
let bits = noise(w, h);
let max = bits.len();
let embedded = encode_jbig2_generic(&bits, w, h).unwrap();
let decode = |data: &[u8]| decode_jbig2(data, None, w, h, max);
assert!(decode(&FILE_HEADER_ID[..5]).is_err(), "part of the ID string");
assert!(decode(&FILE_HEADER_ID).is_err(), "the ID string, no flags");
for count in 0..4 {
let mut data = FILE_HEADER_ID.to_vec();
data.push(0x01);
data.extend_from_slice(&1u32.to_be_bytes()[..count]);
assert!(decode(&data).is_err(), "{count} bytes of the page count");
}
for (flags, pages) in [(0x01, Some(1)), (0x03, None), (0x00, Some(1)), (0x02, None)] {
let whole = with_file_header(&embedded, flags, pages);
let header = 9 + if pages.is_some() { 4 } else { 0 };
assert!(decode(&whole[..header]).is_err(), "flags {flags:#04x}: the header alone");
assert!(decode(&whole[..whole.len() - 1]).is_err(), "flags {flags:#04x}: the last byte cut");
for end in 0..whole.len() {
assert_ne!(decode(&whole[..end]).ok(), Some(bits.clone()), "flags {flags:#04x}: {end} bytes");
}
assert_eq!(decode(&whole).unwrap(), bits, "flags {flags:#04x}: whole");
}
}
/// The segments after a file header count toward hayro-jbig2's cap on the segments of a stream
/// together with its `/JBIG2Globals`, in either organization (patches 8 and 17): globals of
/// extension segments, which decode to nothing, fill the cap to the image's segments, and one
/// more is refused.
#[test]
fn segments_after_a_jbig2_file_header_count_with_the_globals() {
/// hayro-jbig2's `MAX_SEGMENTS`, the most segments of a stream with its globals.
const MAX_SEGMENTS: u32 = 1 << 20;
let (w, h) = (24u32, 8u32);
let bits = noise(w, h);
let max = bits.len();
let embedded = encode_jbig2_generic(&bits, w, h).unwrap();
// Numbered past the image's segments (0 to 2), of no page, with no data.
let globals = |count: u32| -> Vec<u8> {
(16..16 + count)
.flat_map(|number| {
let mut segment = number.to_be_bytes().to_vec();
segment.extend_from_slice(&[62, 0, 0, 0, 0, 0, 0]);
segment
})
.collect()
};
// The image's page information and region, and in random access its end of file segment.
for (flags, organization, image) in [(0x01, "sequential", 2), (0x00, "random access", 3)] {
let data = with_file_header(&embedded, flags, Some(1));
let (at_cap, past_cap) = (globals(MAX_SEGMENTS - image), globals(MAX_SEGMENTS - image + 1));
assert_eq!(decode_jbig2(&data, Some(&at_cap), w, h, max).unwrap(), bits, "{organization}: at the cap");
let refused = decode_jbig2(&data, Some(&past_cap), w, h, max).unwrap_err();
assert!(refused.to_string().contains("too many segments"), "{organization}: {refused}");
}
}
/// Either coding decodes to its samples, black as 0, the bits that pad each row 0: a generic
/// region of noise, and symbols for letters repeated across the page. With no rows asked for,
/// nothing is decoded.
+17
View File
@@ -4073,6 +4073,23 @@ fn jbig2_image_draws_with(stream: &[u8], space: &str, entries: &str, objects: &[
}
}
/// A `JBIG2Decode` image whose data starts with a JBIG2 file header (ISO 14492 D.4) draws, in
/// either organization the header declares (hayro-jbig2 patch 17): sequential (flags bit 0 is 1,
/// D.1) and random access (bit 0 is 0, D.2: the segment headers, ending with an end of file
/// segment, then their data). A header with reserved flag bits set leaves the image out.
#[test]
fn a_jbig2_image_with_a_file_header_draws() {
const ID: [u8; 8] = [0x97, 0x4A, 0x42, 0x32, 0x0D, 0x0A, 0x1A, 0x0A];
let page = black_jbig2_page();
let header = |flags: u8| [&ID[..], &[flags], &1u32.to_be_bytes()].concat();
let (page_header, page_data) = page.split_at(11);
let end_of_file = [0, 0, 0, 1, 51, 0, 1, 0, 0, 0, 0];
assert!(jbig2_image_draws(&page), "without a header");
assert!(jbig2_image_draws(&[&header(0x01)[..], &page].concat()), "sequential");
assert!(jbig2_image_draws(&[&header(0x00)[..], page_header, &end_of_file, page_data].concat()), "random access");
assert!(!jbig2_image_draws(&[&header(0x11)[..], &page].concat()), "reserved flag bits");
}
/// The JBIG2 images of a page share one allowance, down to the step, and the next page starts with
/// its own. The images differ by an extension segment: one image decoded again isn't decoded twice.
#[test]
+1 -1
View File
File diff suppressed because one or more lines are too long
+18 -3
View File
@@ -88,6 +88,19 @@ pub(crate) struct File<'a> {
/// (D.4.1)
const FILE_HEADER_ID: [u8; 8] = [0x97, 0x4A, 0x42, 0x32, 0x0D, 0x0A, 0x1A, 0x0A];
/// PdfCraft patch (17): an embedded stream that starts with a file header (D.4), which some
/// producers write into a PDF's `JBIG2Decode` data although ISO 32000-1 7.4.7 has the embedded
/// organization (D.3) there. Returns the reader past the header and the organization the header
/// declares, or `None` when the data doesn't start with the header's ID string.
pub(crate) fn after_file_header(data: &[u8]) -> Result<Option<(Reader<'_>, FileOrganization)>> {
if !data.starts_with(&FILE_HEADER_ID) {
return Ok(None);
}
let mut reader = Reader::new(data);
let header = parse_file_header(&mut reader)?;
Ok(Some((reader, header.organization)))
}
/// Parse a standalone JBIG2 file.
pub(crate) fn parse_file(data: &[u8]) -> Result<File<'_>> {
let mut reader = Reader::new(data);
@@ -170,7 +183,9 @@ fn parse_segments<'a>(
FileOrganization::Sequential => {
parse_segments_sequential(reader, &mut segments, &mut SegmentCount::default())?;
}
FileOrganization::RandomAccess => parse_segments_random_access(reader, &mut segments)?,
FileOrganization::RandomAccess => {
parse_segments_random_access(reader, &mut segments, &mut SegmentCount::default())?;
}
}
Ok(segments)
@@ -213,12 +228,12 @@ pub(crate) fn parse_segments_sequential<'a>(
/// is followed by a sequence of segments headers; the last segment header is
/// followed by the data for the first segment, then the data for the second
/// segment, and so on." (D.2)
fn parse_segments_random_access<'a>(
pub(crate) fn parse_segments_random_access<'a>(
reader: &mut Reader<'a>,
segments: &mut Vec<Segment<'a>>,
count: &mut SegmentCount,
) -> Result<()> {
let mut headers = Vec::new();
let mut count = SegmentCount::default();
loop {
if reader.at_end() {
+19 -4
View File
@@ -75,7 +75,10 @@ pub use error::{
SegmentError, SymbolError, TemplateError,
};
use crate::file::{SegmentCount, parse_segments_sequential};
use crate::file::{
FileOrganization, SegmentCount, after_file_header, parse_segments_random_access,
parse_segments_sequential,
};
use bitmap::Bitmap;
use decode::CombinationOperator;
use decode::generic;
@@ -117,7 +120,9 @@ impl<'a> Image<'a> {
/// Parse an embedded JBIG2 image with optional global segments.
///
/// The file is expected to use the embedded organization defined in
/// Annex D.3.
/// Annex D.3. PdfCraft patch (17): data that starts with a file header (D.4)
/// is read past it, in the organization it declares, as Acrobat and PDFBox
/// draw such streams; its segments still count together with the globals'.
pub fn new_embedded(data: &'a [u8], globals: Option<&'a [u8]>) -> Result<Self> {
let mut segments = Vec::new();
let mut count = SegmentCount::default();
@@ -126,8 +131,18 @@ impl<'a> Image<'a> {
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
};
let mut reader = Reader::new(data);
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
match after_file_header(data)? {
Some((mut reader, FileOrganization::RandomAccess)) => {
parse_segments_random_access(&mut reader, &mut segments, &mut count)?;
}
Some((mut reader, FileOrganization::Sequential)) => {
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
}
None => {
let mut reader = Reader::new(data);
parse_segments_sequential(&mut reader, &mut segments, &mut count)?;
}
}
segments.sort_by_key(|seg| seg.header.segment_number);