mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 02:41:31 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+18
-3
@@ -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() {
|
||||
|
||||
Vendored
+19
-4
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user