mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-03 06:51:50 -06:00
fix(render): a JBIG2 symbol with no rows costs a step, and a refined instance is charged first
From CodeRabbit on the work count. A symbol with no rows cost no work, though it is decoded all the same, so a stream of thousands of dictionaries of such symbols, exporting none, went uncounted. Each symbol now costs a step more than its pixels. A refined text instance was charged after it was decoded, so one could still be 2^28 pixels past what was left before the decoder refused. It's charged once its size is read, before anything is allocated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8243952569
commit
72e763ad12
@@ -3673,6 +3673,31 @@ fn zero_width_jbig2_dictionary(symbols: u32, exported: bool) -> Vec<u8> {
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// A Huffman-coded symbol dictionary of `symbols` symbols of width 1 and no rows, none exported.
|
||||
/// Only table B.5 has a negative height class delta, so it gets to no rows from one.
|
||||
fn no_row_jbig2_dictionary(symbols: u32) -> Vec<u8> {
|
||||
let mut bits = Bits::default();
|
||||
bits.put(0, 1); // height class delta 1 (B.5), a class of no symbols
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
bits.put(0, 5); // BMSIZE 0, uncompressed (B.1)
|
||||
bits.align();
|
||||
bits.put(0b1111110, 7); // height class delta -1 (B.5): -255 and 254
|
||||
bits.put(254, 8);
|
||||
bits.put(0b10, 2); // width delta 1 (B.2)
|
||||
for _ in 1..symbols {
|
||||
bits.put(0, 1); // width delta 0 (B.2)
|
||||
}
|
||||
bits.put(0b111111, 6); // end of the class (B.2)
|
||||
bits.put(0, 5); // BMSIZE 0, uncompressed (B.1)
|
||||
bits.align();
|
||||
bits.table_b1(symbols); // the first export run, not exported, is all of them
|
||||
let mut dictionary = vec![0x00, 0x05]; // Huffman, tables B.5, B.2, B.1, B.1
|
||||
dictionary.extend_from_slice(&0u32.to_be_bytes()); // exported
|
||||
dictionary.extend_from_slice(&symbols.to_be_bytes()); // new
|
||||
dictionary.extend_from_slice(&bits.bytes);
|
||||
dictionary
|
||||
}
|
||||
|
||||
/// A Huffman-coded symbol dictionary of `symbols` white symbols of width 1 and `height` rows, none
|
||||
/// exported, for `height` from 76 up.
|
||||
fn narrow_jbig2_dictionary(symbols: u32, height: u32) -> Vec<u8> {
|
||||
@@ -4083,31 +4108,33 @@ fn jbig2_decoding_work_is_counted_item_by_item() {
|
||||
data: jbig2_pattern_dictionary(32, 2),
|
||||
cost: two_patterns(1, 32),
|
||||
},
|
||||
// Each symbol costs one more than its pixels, so that one of no rows isn't free.
|
||||
Item { what: "symbols of no rows", before: vec![], spent: 0, kind: 0, refs: &[], data: no_row_jbig2_dictionary(5), cost: 5 },
|
||||
// Rows cost in a symbol of no pixels too.
|
||||
Item { what: "symbols without pixels", before: vec![], spent: 0, kind: 0, refs: &[], data: zero_width_jbig2_dictionary(3, false), cost: 12 },
|
||||
Item { what: "a tall symbol of no width", before: vec![], spent: 0, kind: 0, refs: &[], data: zero(1000), cost: 4000 },
|
||||
Item { what: "new symbols", before: vec![], spent: 0, kind: 0, refs: &[], data: square_jbig2_dictionary(80, false), cost: 6720 },
|
||||
Item { what: "referred symbols", before: symbol(), spent: 4, kind: 0, refs: &[1], data: exporting_none, cost: 1 },
|
||||
Item { what: "symbols without pixels", before: vec![], spent: 0, kind: 0, refs: &[], data: zero_width_jbig2_dictionary(3, false), cost: 15 },
|
||||
Item { what: "a tall symbol of no width", before: vec![], spent: 0, kind: 0, refs: &[], data: zero(1000), cost: 4001 },
|
||||
Item { what: "new symbols", before: vec![], spent: 0, kind: 0, refs: &[], data: square_jbig2_dictionary(80, false), cost: 6721 },
|
||||
Item { what: "referred symbols", before: symbol(), spent: 5, kind: 0, refs: &[1], data: exporting_none, cost: 1 },
|
||||
// Its symbol, and its one row and word; Huffman-coded, a table line for the symbol.
|
||||
Item { what: "immediate text", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "intermediate text", before: symbol(), spent: 4, kind: 4, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "a text region's pixels", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(32, 32, 0), cost: 75 },
|
||||
Item { what: "immediate text", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "intermediate text", before: symbol(), spent: 5, kind: 4, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
|
||||
Item { what: "a text region's pixels", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(32, 32, 0), cost: 75 },
|
||||
Item {
|
||||
what: "an intermediate text region's pixels",
|
||||
before: symbol(),
|
||||
spent: 4,
|
||||
spent: 5,
|
||||
kind: 4,
|
||||
refs: &[1],
|
||||
data: jbig2_text_region(32, 32, 0),
|
||||
cost: 75,
|
||||
},
|
||||
// The region's 13, a copy of the symbol's row and the round of the loop.
|
||||
Item { what: "an instance", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 1), cost: 15 },
|
||||
Item { what: "an instance", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 1), cost: 15 },
|
||||
// The region's 13, 80 rows of 4 words copied, the round of the loop.
|
||||
Item {
|
||||
what: "an instance of a large symbol",
|
||||
before: vec![(0, square_jbig2_dictionary(80, true))],
|
||||
spent: 6720,
|
||||
spent: 6721,
|
||||
kind: 6,
|
||||
refs: &[1],
|
||||
data: jbig2_text_region(1, 1, 1),
|
||||
@@ -4117,7 +4144,7 @@ fn jbig2_decoding_work_is_counted_item_by_item() {
|
||||
Item {
|
||||
what: "a refined instance",
|
||||
before: vec![(0, square_jbig2_dictionary(80, true))],
|
||||
spent: 6720,
|
||||
spent: 6721,
|
||||
kind: 6,
|
||||
refs: &[1],
|
||||
data: jbig2_refined_text_region(),
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-2
@@ -135,8 +135,9 @@ pub(crate) fn decode(
|
||||
if ctx.pixels > MAX_DICTIONARY_PIXELS {
|
||||
bail!(OverflowError::BitmapDimension);
|
||||
}
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`.
|
||||
let work = decode_work(symbol_width, ctx.height_class_height);
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`. One with no rows costs no pixels, but it is
|
||||
// decoded all the same, and a stream can hold thousands of dictionaries of them.
|
||||
let work = decode_work(symbol_width, ctx.height_class_height).saturating_add(1);
|
||||
*ctx.work_left = ctx.work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
// PdfCraft patch: see `room`. Rows take a word at least, so symbols of one column
|
||||
// held 270 MiB within the pixel bound, however few the dictionary exported.
|
||||
|
||||
+9
-9
@@ -151,7 +151,7 @@ pub(crate) fn decode_with(
|
||||
let symbol_id = ctx.read_symbol_id()?;
|
||||
|
||||
let symbol_bitmap =
|
||||
decode_symbol_instance_bitmap(&mut ctx, symbols, header, symbol_id)?;
|
||||
decode_symbol_instance_bitmap(&mut ctx, symbols, header, symbol_id, work_left)?;
|
||||
|
||||
let symbol_bitmap_ref: &Bitmap = match &symbol_bitmap {
|
||||
SymbolBitmap::Reference(idx) => symbols.get(*idx).ok_or(SymbolError::OutOfRange)?,
|
||||
@@ -161,14 +161,8 @@ pub(crate) fn decode_with(
|
||||
let symbol_height = symbol_bitmap_ref.height as i32;
|
||||
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`. An instance of a large symbol costs its copy,
|
||||
// and 10,000 of one took 64 s. A refined one was decoded as well. Even one with no
|
||||
// pixels goes round this loop.
|
||||
let (width, height) = (symbol_bitmap_ref.width, symbol_bitmap_ref.height);
|
||||
let copied = copy_work(width, height).saturating_add(1);
|
||||
let work = match &symbol_bitmap {
|
||||
SymbolBitmap::Reference(_) => copied,
|
||||
SymbolBitmap::Owned(_) => copied.saturating_add(decode_work(width, height)),
|
||||
};
|
||||
// and 10,000 of one took 64 s. Even one with no pixels goes round this loop.
|
||||
let work = copy_work(symbol_bitmap_ref.width, symbol_bitmap_ref.height).saturating_add(1);
|
||||
*work_left = work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
|
||||
if !header.flags.transposed
|
||||
@@ -546,6 +540,7 @@ fn decode_symbol_instance_bitmap(
|
||||
symbols: &[&Bitmap],
|
||||
header: &TextRegionHeader<'_>,
|
||||
symbol_id: usize,
|
||||
work_left: &mut u64,
|
||||
) -> Result<SymbolBitmap> {
|
||||
if !header.flags.use_refinement || ctx.read_refinement_flag()? == 0 {
|
||||
return Ok(SymbolBitmap::Reference(symbol_id));
|
||||
@@ -577,6 +572,11 @@ fn decode_symbol_instance_bitmap(
|
||||
.checked_add(rdy)
|
||||
.ok_or(OverflowError::ReferenceOffset)?;
|
||||
|
||||
// PdfCraft patch: see `bitmap::MAX_WORK`. Before it is decoded: charged after, one instance
|
||||
// could still be 2^28 pixels past what was left.
|
||||
let work = decode_work(refined_width, refined_height);
|
||||
*work_left = work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
|
||||
|
||||
let mut refined_bitmap = Bitmap::new(refined_width, refined_height)?;
|
||||
|
||||
ctx.decode_refinement_bitmap(
|
||||
|
||||
Reference in New Issue
Block a user