Skip to main content

stet_pdf_reader/
filters.rs

1// stet-pdf-reader
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Stream decode filter chain for PDF streams.
6
7use crate::error::PdfError;
8use crate::objects::PdfDict;
9
10/// Ceiling on the decompressed size of a stream that declares nothing about
11/// its own uncompressed contents.
12///
13/// The decompression filters are amplifiers, and `decode_stream` applies them
14/// in sequence, so without a bound the amplification is unbounded *and*
15/// multiplicative. A single Deflate pass tops out near 1032:1 on a run of
16/// zeros — a limit of the format, not a decision anyone made — but nesting the
17/// filter three deep squares and cubes that: a 707-byte file with
18/// `/Filter [/FlateDecode /FlateDecode /FlateDecode]` measured a 2058 MB peak
19/// RSS here, and under a constrained address space it aborted with
20/// `memory allocation of N bytes failed` and dumped core. That is not a
21/// failure a caller can catch — Rust aborts on allocation failure — so it has
22/// to be prevented rather than handled.
23///
24/// # Calibration
25///
26/// This bound only has to cover streams that describe *nothing* about their
27/// decompressed length, because a stream that does declare a length gets that
28/// instead — see [`DecodeBudget::for_stream`]. What is left is content
29/// streams, object and cross-reference streams, embedded font programs, ICC
30/// profiles, and sampled-function tables. The largest of those:
31///
32/// | Stream | Decompressed |
33/// |---|---|
34/// | Type 0 sampled function, `/Size [4096 4096]`, 4 outputs @ 16 bps | 134 MB |
35/// | Heavy vector-art content stream | ~100 MB |
36/// | Embedded CFF or TrueType font program | < 30 MB |
37///
38/// 512 MiB leaves roughly 4x headroom over the largest, while turning the
39/// bomb above into an error instead of an abort.
40pub const MAX_DECODED_STREAM_BYTES: usize = 512 * 1024 * 1024;
41
42/// How much a stream is allowed to decompress to.
43///
44/// The ceiling starts at [`MAX_DECODED_STREAM_BYTES`] and is *raised* — never
45/// lowered — by whatever the stream dictionary declares about its own
46/// uncompressed size. Raising rather than replacing is what keeps this from
47/// rejecting files stet renders correctly today: a stream that declares
48/// nothing, or declares something small, still gets the full general
49/// allowance.
50#[derive(Debug, Clone, Copy)]
51pub struct DecodeBudget {
52    limit: usize,
53}
54
55impl Default for DecodeBudget {
56    fn default() -> Self {
57        Self {
58            limit: MAX_DECODED_STREAM_BYTES,
59        }
60    }
61}
62
63impl DecodeBudget {
64    /// A budget with an explicit ceiling.
65    pub const fn new(limit: usize) -> Self {
66        Self { limit }
67    }
68
69    /// The ceiling, in bytes.
70    pub const fn limit(&self) -> usize {
71        self.limit
72    }
73
74    /// Derive a budget from a stream dictionary.
75    ///
76    /// Two dictionary shapes declare an uncompressed size, and a file that
77    /// legitimately carries a stream larger than the general allowance will be
78    /// one of them:
79    ///
80    /// - An **image XObject** declares `/Width`, `/Height`,
81    ///   `/BitsPerComponent` and `/ColorSpace`, which give the raster size
82    ///   exactly. A 60x40 inch grand-format image at 1200 dpi is 3.46 Gpx, and
83    ///   as 8-bit CMYK that is a legitimate 13.8 GB stream. Refusing it would
84    ///   be precisely the prepress regression an earlier corpus-derived image
85    ///   cap already caused once.
86    /// - An **embedded file** declares `/Params << /Size n >>` (PDF 32000-1
87    ///   7.11.4.2), the attachment's uncompressed length. Attachments are only
88    ///   decoded when a caller explicitly asks for one by name.
89    ///
90    /// Both declared values are themselves bounded — image dimensions by
91    /// [`stet_graphics::image_limits`], attachment size by the file's own
92    /// claim — so a lie is a *bounded* lie, which is the property the general
93    /// ceiling exists to guarantee.
94    pub fn for_stream(dict: &PdfDict) -> Self {
95        let declared = declared_image_bytes(dict)
96            .or_else(|| declared_embedded_file_bytes(dict))
97            .unwrap_or(0);
98        Self {
99            limit: declared.max(MAX_DECODED_STREAM_BYTES),
100        }
101    }
102
103    /// Fail if `produced` bytes exceeds the ceiling.
104    ///
105    /// Called from inside each decompression loop rather than on the finished
106    /// buffer: checking afterwards would mean the allocation the ceiling
107    /// exists to prevent has already happened.
108    fn check(&self, produced: usize) -> Result<(), PdfError> {
109        if produced > self.limit {
110            return Err(PdfError::DecompressionError(format!(
111                "decompressed stream exceeds the {} byte limit",
112                self.limit
113            )));
114        }
115        Ok(())
116    }
117
118    /// Clamp a `Vec::with_capacity` hint to the ceiling.
119    ///
120    /// The hints below are guesses scaled from the compressed length. Left
121    /// unclamped, a guess for a stream that will be refused anyway still
122    /// performs the allocation first.
123    fn reserve_hint(&self, want: usize) -> usize {
124        want.min(self.limit)
125    }
126}
127
128/// Raster size in bytes for a dictionary that describes an image, if it does.
129///
130/// Component count is read from a directly-present `/ColorSpace` name and
131/// otherwise assumed to be 4. Guessing high only widens the allowance, and the
132/// pixel count it multiplies is already bounded, so an unresolvable colour
133/// space cannot turn into an unbounded budget.
134fn declared_image_bytes(dict: &PdfDict) -> Option<usize> {
135    use stet_graphics::image_limits::{
136        validate_bits_per_component, validate_image_dimension, validate_image_size,
137    };
138
139    let width = validate_image_dimension(dict.get_int(b"Width"))?;
140    let height = validate_image_dimension(dict.get_int(b"Height"))?;
141    let pixels = validate_image_size(width, height)?;
142    let bpc = validate_bits_per_component(dict.get_int(b"BitsPerComponent"))? as usize;
143
144    let components = match dict.get_name(b"ColorSpace") {
145        Some(b"DeviceGray" | b"G" | b"CalGray") => 1,
146        Some(b"DeviceRGB" | b"RGB" | b"CalRGB" | b"Lab") => 3,
147        _ => 4,
148    };
149
150    // Rows are padded to a byte boundary, so compute per row rather than
151    // dividing a single product — a 1-bit 9-pixel-wide image is 2 bytes a row,
152    // not 1.125. Saturating, not checked: an image at the top of the permitted
153    // range multiplied by components and depth genuinely can exceed `usize` on
154    // a 32-bit target, and saturating there yields `usize::MAX`, which is the
155    // widest allowance rather than a refusal.
156    let row_bits = (width as usize)
157        .saturating_mul(components)
158        .saturating_mul(bpc);
159    let row_bytes = row_bits.div_ceil(8);
160    Some(row_bytes.saturating_mul(pixels / width as usize))
161}
162
163/// Declared uncompressed length of an embedded file, if the dict carries one.
164fn declared_embedded_file_bytes(dict: &PdfDict) -> Option<usize> {
165    let size = dict.get_dict(b"Params")?.get_int(b"Size")?;
166    usize::try_from(size).ok()
167}
168
169/// A single decode filter.
170#[derive(Debug, Clone, PartialEq)]
171pub enum Filter {
172    FlateDecode,
173    LZWDecode,
174    ASCIIHexDecode,
175    ASCII85Decode,
176    RunLengthDecode,
177    DCTDecode,
178    CCITTFaxDecode,
179    JPXDecode,
180    JBIG2Decode,
181}
182
183/// Parse the /Filter and /DecodeParms entries from a stream dict.
184/// Pass a resolver to dereference indirect `/Filter` or `/DecodeParms` values.
185/// `None` is acceptable during bootstrap (xref stream parsing) where no resolver
186/// exists yet and indirect references don't occur.
187pub fn parse_filters(
188    dict: &PdfDict,
189    resolver: Option<&crate::resolver::Resolver>,
190) -> Result<(Vec<Filter>, Vec<Option<PdfDict>>), PdfError> {
191    let filter_obj = match dict.get(b"Filter") {
192        Some(obj) => obj,
193        None => return Ok((Vec::new(), Vec::new())),
194    };
195
196    // Resolve indirect Filter reference if needed
197    let resolved_filter;
198    let filter_obj = if let crate::objects::PdfObj::Ref(_, _) = filter_obj {
199        if let Some(r) = resolver {
200            resolved_filter = r.deref(filter_obj).unwrap_or_else(|_| filter_obj.clone());
201            &resolved_filter
202        } else {
203            filter_obj
204        }
205    } else {
206        filter_obj
207    };
208
209    let filter_names: Vec<&[u8]> = match filter_obj {
210        crate::objects::PdfObj::Name(n) => vec![n.as_slice()],
211        crate::objects::PdfObj::Array(arr) => {
212            // Array elements may also be indirect references
213            arr.iter()
214                .filter_map(|o| {
215                    if let Some(n) = o.as_name() {
216                        return Some(n);
217                    }
218                    None
219                })
220                .collect()
221        }
222        _ => return Ok((Vec::new(), Vec::new())),
223    };
224
225    let mut filters = Vec::new();
226    for name in &filter_names {
227        filters.push(filter_from_name(name)?);
228    }
229
230    // Parse DecodeParms (single dict or array of dicts/refs)
231    let dp_obj = dict.get(b"DecodeParms");
232    let resolved_dp;
233    let dp_obj = match dp_obj {
234        Some(crate::objects::PdfObj::Ref(_, _)) if resolver.is_some() => {
235            resolved_dp = resolver.unwrap().deref(dp_obj.unwrap()).ok();
236            resolved_dp.as_ref()
237        }
238        other => other,
239    };
240
241    let parms = match dp_obj {
242        Some(crate::objects::PdfObj::Dict(d)) => vec![Some(d.clone())],
243        Some(crate::objects::PdfObj::Array(arr)) => arr
244            .iter()
245            .map(|o| match o {
246                crate::objects::PdfObj::Dict(d) => Some(d.clone()),
247                crate::objects::PdfObj::Ref(_, _) if resolver.is_some() => resolver
248                    .unwrap()
249                    .deref(o)
250                    .ok()
251                    .and_then(|r| r.as_dict().cloned()),
252                _ => None,
253            })
254            .collect(),
255        _ => vec![None; filters.len()],
256    };
257
258    // Pad parms to match filters length
259    let mut parms = parms;
260    while parms.len() < filters.len() {
261        parms.push(None);
262    }
263
264    // Fill in CCITT decode hints from the image/stream dict when missing.
265    // PDF image streams always carry /Width and /Height, but malformed producers
266    // sometimes omit the matching /Columns and /Rows in /DecodeParms. Without
267    // /Rows the decoder has no target height and bails out mid-stream on
268    // damaged Group 4 data. Copy them over so the CCITT filter can cap its
269    // row count and pad short output with white scanlines.
270    for (i, filter) in filters.iter().enumerate() {
271        if *filter != Filter::CCITTFaxDecode {
272            continue;
273        }
274        let dp = parms[i].get_or_insert_with(PdfDict::new);
275        if dp.get_int(b"Columns").is_none()
276            && let Some(w) = dict.get_int(b"Width")
277        {
278            dp.insert(b"Columns".to_vec(), crate::objects::PdfObj::Int(w));
279        }
280        if dp.get_int(b"Rows").is_none()
281            && let Some(h) = dict.get_int(b"Height")
282        {
283            dp.insert(b"Rows".to_vec(), crate::objects::PdfObj::Int(h));
284        }
285    }
286
287    Ok((filters, parms))
288}
289
290fn filter_from_name(name: &[u8]) -> Result<Filter, PdfError> {
291    match name {
292        b"FlateDecode" | b"Fl" => Ok(Filter::FlateDecode),
293        b"LZWDecode" | b"LZW" => Ok(Filter::LZWDecode),
294        b"ASCIIHexDecode" | b"AHx" => Ok(Filter::ASCIIHexDecode),
295        b"ASCII85Decode" | b"A85" => Ok(Filter::ASCII85Decode),
296        b"RunLengthDecode" | b"RL" => Ok(Filter::RunLengthDecode),
297        b"DCTDecode" | b"DCT" => Ok(Filter::DCTDecode),
298        b"CCITTFaxDecode" | b"CCF" => Ok(Filter::CCITTFaxDecode),
299        b"JPXDecode" | b"JPX" => Ok(Filter::JPXDecode),
300        b"JBIG2Decode" | b"JBIG2" => Ok(Filter::JBIG2Decode),
301        // Tolerate truncated filter names from malformed PDFs
302        _ if name.starts_with(b"Flate") => Ok(Filter::FlateDecode),
303        _ if name.starts_with(b"LZW") => Ok(Filter::LZWDecode),
304        _ if name.starts_with(b"ASCIIHex") => Ok(Filter::ASCIIHexDecode),
305        _ if name.starts_with(b"ASCII85") => Ok(Filter::ASCII85Decode),
306        _ if name.starts_with(b"RunLength") => Ok(Filter::RunLengthDecode),
307        _ if name.starts_with(b"CCITT") => Ok(Filter::CCITTFaxDecode),
308        _ if name.starts_with(b"JPX") => Ok(Filter::JPXDecode),
309        _ if name.starts_with(b"JBIG2") => Ok(Filter::JBIG2Decode),
310        _ => Err(PdfError::UnsupportedFilter(
311            String::from_utf8_lossy(name).into(),
312        )),
313    }
314}
315
316/// Decode raw stream data through a chain of filters.
317///
318/// Bounds the decompressed size at [`MAX_DECODED_STREAM_BYTES`]. Callers
319/// holding the stream dictionary should prefer [`decode_stream_bounded`] with
320/// [`DecodeBudget::for_stream`], which additionally allows the larger sizes a
321/// dictionary can legitimately declare.
322pub fn decode_stream(
323    raw_data: &[u8],
324    filters: &[Filter],
325    decode_parms: &[Option<PdfDict>],
326    jbig2_globals: Option<&[u8]>,
327) -> Result<Vec<u8>, PdfError> {
328    decode_stream_bounded(
329        raw_data,
330        filters,
331        decode_parms,
332        jbig2_globals,
333        DecodeBudget::default(),
334    )
335}
336
337/// Decode raw stream data through a chain of filters, under an explicit
338/// decompressed-size ceiling.
339///
340/// The budget covers every stage rather than resetting per filter, which is
341/// what stops a chain of decompressors from multiplying their amplification
342/// together.
343pub fn decode_stream_bounded(
344    raw_data: &[u8],
345    filters: &[Filter],
346    decode_parms: &[Option<PdfDict>],
347    jbig2_globals: Option<&[u8]>,
348    budget: DecodeBudget,
349) -> Result<Vec<u8>, PdfError> {
350    let mut data = raw_data.to_vec();
351
352    for (i, filter) in filters.iter().enumerate() {
353        let parms = decode_parms.get(i).and_then(|p| p.as_ref());
354        data = match filter {
355            Filter::FlateDecode => decode_flate(&data, parms, budget)?,
356            Filter::LZWDecode => decode_lzw(&data, parms, budget)?,
357            Filter::ASCIIHexDecode => decode_ascii_hex(&data)?,
358            Filter::ASCII85Decode => decode_ascii85(&data)?,
359            Filter::RunLengthDecode => decode_run_length(&data, budget)?,
360            Filter::DCTDecode => decode_dct(&data)?,
361            Filter::CCITTFaxDecode => decode_ccittfax(&data, parms)?,
362            #[cfg(feature = "jpx")]
363            Filter::JPXDecode => decode_jpx(&data)?,
364            #[cfg(not(feature = "jpx"))]
365            Filter::JPXDecode => {
366                return Err(PdfError::UnsupportedFilter("JPXDecode (disabled)".into()));
367            }
368            Filter::JBIG2Decode => decode_jbig2(&data, jbig2_globals)?,
369        };
370        // The image codecs (DCT, CCITT, JPX, JBIG2) size their own output from
371        // the dimensions in their own headers and are not covered by the
372        // incremental checks below, so verify each stage's result as well.
373        budget.check(data.len())?;
374    }
375
376    Ok(data)
377}
378
379/// FlateDecode (zlib/deflate).
380fn decode_flate(
381    data: &[u8],
382    parms: Option<&PdfDict>,
383    budget: DecodeBudget,
384) -> Result<Vec<u8>, PdfError> {
385    // Try zlib first. If it ends with an error (truncated output),
386    // also try raw deflate (skip 2-byte zlib header) and pick the longer result.
387    let (zlib_output, zlib_clean, _) = decode_flate_inner(data, true, budget);
388    let output = if zlib_clean {
389        zlib_output?
390    } else {
391        // Zlib hit an error (corrupt checksum, etc).  Try raw deflate (skip
392        // 2-byte zlib header) and prefer it only when zlib clearly truncated
393        // mid-stream.  If zlib consumed (nearly) all input, the data is
394        // complete — the error is just a bad trailing checksum, and raw
395        // deflate may decode garbage past the stream boundary.
396        let zlib_data = zlib_output.unwrap_or_default();
397        if data.len() > 2 {
398            let (raw_output, _, _) = decode_flate_inner(&data[2..], false, budget);
399            let raw_data = raw_output.unwrap_or_default();
400            if raw_data.len() > zlib_data.len()
401                && raw_data[..zlib_data.len()] == zlib_data[..]
402                && looks_like_valid_continuation(&raw_data, zlib_data.len())
403            {
404                // Raw produced more data, the shared prefix matches, and the
405                // extra bytes look like valid content — zlib truncated early
406                // due to a checksum error; use the fuller raw output.
407                raw_data
408            } else if !zlib_data.is_empty() {
409                zlib_data
410            } else if !raw_data.is_empty() {
411                raw_data
412            } else {
413                return Err(PdfError::DecompressionError(
414                    "flate: decompression failed".into(),
415                ));
416            }
417        } else if !zlib_data.is_empty() {
418            zlib_data
419        } else {
420            return Err(PdfError::DecompressionError(
421                "flate: decompression failed".into(),
422            ));
423        }
424    };
425
426    // Apply predictor if specified
427    if let Some(parms) = parms {
428        let predictor = parms.get_int(b"Predictor").unwrap_or(1);
429        if predictor > 1 {
430            return apply_predictor(&output, parms, predictor);
431        }
432    }
433
434    Ok(output)
435}
436
437/// Check whether the extra bytes (past `start`) in `data` look like valid
438/// stream content rather than garbage from decoding past a stream boundary.
439/// Checks a sample of bytes for printable ASCII / whitespace, which is typical
440/// for PDF content streams but not for accidentally-decoded binary data.
441fn looks_like_valid_continuation(data: &[u8], start: usize) -> bool {
442    if start >= data.len() {
443        return false;
444    }
445    // Sample the first 64 bytes of the continuation
446    let sample = &data[start..data.len().min(start + 64)];
447    let printable = sample
448        .iter()
449        .filter(|&&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
450        .count();
451    // If >80% of sampled bytes are printable, it's likely valid content
452    printable * 5 >= sample.len() * 4
453}
454
455/// Inner flate decompression. `zlib` = true uses zlib wrapper, false uses raw deflate.
456/// Returns (Result<data>, clean) where clean=true means StreamEnd was reached normally.
457/// Returns (decompressed_data, clean_finish, bytes_consumed).
458///
459/// A budget overrun is reported as `(Err, clean = true, _)`. The `clean` flag
460/// is what suppresses the raw-deflate retry in the caller, and suppressing it
461/// is right here: the stream is not truncated, it is too large, and decoding
462/// it a second way would allocate just as much again before failing the same
463/// way. It also keeps the overrun from being mistaken for a checksum error and
464/// silently downgraded to a truncated-but-usable result.
465fn decode_flate_inner(
466    data: &[u8],
467    zlib: bool,
468    budget: DecodeBudget,
469) -> (Result<Vec<u8>, PdfError>, bool, usize) {
470    use flate2::Decompress;
471
472    let mut decompressor = Decompress::new(zlib);
473    let mut output = Vec::with_capacity(budget.reserve_hint(data.len().saturating_mul(3)));
474    let mut buf = [0u8; 8192];
475    let mut input_offset = 0;
476
477    loop {
478        let before_in = decompressor.total_in() as usize;
479        let before_out = decompressor.total_out() as usize;
480        let result = decompressor.decompress(
481            &data[input_offset..],
482            &mut buf,
483            flate2::FlushDecompress::None,
484        );
485
486        let consumed = decompressor.total_in() as usize - before_in;
487        let produced = decompressor.total_out() as usize - before_out;
488        input_offset += consumed;
489        output.extend_from_slice(&buf[..produced]);
490
491        if let Err(e) = budget.check(output.len()) {
492            return (Err(e), true, input_offset);
493        }
494
495        match result {
496            Ok(status) => match status {
497                flate2::Status::StreamEnd => return (Ok(output), true, input_offset),
498                flate2::Status::Ok | flate2::Status::BufError => {
499                    if consumed == 0 && produced == 0 {
500                        return (Ok(output), true, input_offset);
501                    }
502                }
503            },
504            Err(_) if !output.is_empty() => {
505                // Partial output — checksum/trailing data error.
506                return (Ok(output), false, input_offset);
507            }
508            Err(e) => {
509                return (
510                    Err(PdfError::DecompressionError(format!("flate: {e}"))),
511                    false,
512                    input_offset,
513                );
514            }
515        }
516    }
517}
518
519/// LZWDecode — native PDF-compatible LZW decoder.
520///
521/// Handles EarlyChange correctly and tolerates premature EOF (missing EOD code),
522/// which is common in real-world PDFs.
523fn decode_lzw(
524    data: &[u8],
525    parms: Option<&PdfDict>,
526    budget: DecodeBudget,
527) -> Result<Vec<u8>, PdfError> {
528    let early_change = parms.and_then(|p| p.get_int(b"EarlyChange")).unwrap_or(1) != 0;
529
530    let output = lzw_decode(data, early_change, budget)?;
531
532    // Apply predictor if specified
533    if let Some(parms) = parms {
534        let predictor = parms.get_int(b"Predictor").unwrap_or(1);
535        if predictor > 1 {
536            return apply_predictor(&output, parms, predictor);
537        }
538    }
539
540    Ok(output)
541}
542
543// --- Native PDF LZW decoder ---
544
545const LZW_CLEAR_TABLE: usize = 256;
546const LZW_EOD: usize = 257;
547const LZW_MAX_ENTRIES: usize = 4096;
548const LZW_INITIAL_SIZE: usize = 258;
549
550/// Decode an LZW-compressed byte stream per the PDF spec.
551///
552/// Stops with an error once `budget` is exceeded. LZW amplifies less than
553/// Deflate per pass — table entries cap at 4096 codes — but it amplifies
554/// without bound across a chain, and it is the second decompressor a nested
555/// bomb can reach for.
556fn lzw_decode(data: &[u8], early_change: bool, budget: DecodeBudget) -> Result<Vec<u8>, PdfError> {
557    let failed = || PdfError::DecompressionError("lzw: decode failed".into());
558
559    let mut table = LzwTable::new(early_change);
560    let mut bit_size = table.code_length();
561    let mut reader = LzwBitReader::new(data);
562    let mut decoded = Vec::new();
563    let mut prev: Option<usize> = None;
564
565    loop {
566        let next = match reader.read(bit_size) {
567            Some(code) => code as usize,
568            None => {
569                // Premature EOF — missing EOD code. Return what we have.
570                return Ok(decoded);
571            }
572        };
573
574        match next {
575            LZW_CLEAR_TABLE => {
576                table.clear();
577                prev = None;
578                bit_size = table.code_length();
579            }
580            LZW_EOD => return Ok(decoded),
581            new => {
582                if new > table.size() {
583                    // Invalid code — return partial data if we have any
584                    if decoded.is_empty() {
585                        return Err(failed());
586                    }
587                    return Ok(decoded);
588                }
589
590                if new < table.size() {
591                    let entry = table.get(new).ok_or_else(failed)?;
592                    let first_byte = entry[0];
593                    decoded.extend_from_slice(entry);
594
595                    if let Some(prev_code) = prev {
596                        table.register(prev_code, first_byte);
597                    }
598                } else if new == table.size() && prev.is_some() {
599                    // KwKwK case: code references the entry about to be created
600                    let prev_code = prev.unwrap();
601                    let prev_entry = table.get(prev_code).ok_or_else(failed)?;
602                    let first_byte = prev_entry[0];
603
604                    let new_entry = table.register(prev_code, first_byte).ok_or_else(failed)?;
605                    decoded.extend_from_slice(new_entry);
606                } else {
607                    if decoded.is_empty() {
608                        return Err(failed());
609                    }
610                    return Ok(decoded);
611                }
612
613                budget.check(decoded.len())?;
614
615                bit_size = table.code_length();
616                prev = Some(new);
617            }
618        }
619    }
620}
621
622/// LZW string table.
623struct LzwTable {
624    early_change: bool,
625    entries: Vec<Option<Vec<u8>>>,
626}
627
628impl LzwTable {
629    fn new(early_change: bool) -> Self {
630        let mut entries: Vec<_> = (0..=255u8).map(|b| Some(vec![b])).collect();
631        entries.push(None); // 256 = CLEAR_TABLE
632        entries.push(None); // 257 = EOD
633        Self {
634            early_change,
635            entries,
636        }
637    }
638
639    fn push(&mut self, entry: Vec<u8>) -> Option<&[u8]> {
640        if self.entries.len() >= LZW_MAX_ENTRIES {
641            None
642        } else {
643            self.entries.push(Some(entry));
644            self.entries.last()?.as_deref()
645        }
646    }
647
648    fn register(&mut self, prev: usize, new_byte: u8) -> Option<&[u8]> {
649        let prev_entry = self.get(prev)?;
650        let mut new_entry = Vec::with_capacity(prev_entry.len() + 1);
651        new_entry.extend(prev_entry);
652        new_entry.push(new_byte);
653        self.push(new_entry)
654    }
655
656    fn get(&self, index: usize) -> Option<&[u8]> {
657        self.entries.get(index)?.as_deref()
658    }
659
660    fn clear(&mut self) {
661        self.entries.truncate(LZW_INITIAL_SIZE);
662    }
663
664    fn size(&self) -> usize {
665        self.entries.len()
666    }
667
668    fn code_length(&self) -> u8 {
669        let adjusted = self.entries.len() + if self.early_change { 1 } else { 0 };
670        if adjusted >= 2048 {
671            12
672        } else if adjusted >= 1024 {
673            11
674        } else if adjusted >= 512 {
675            10
676        } else {
677            9
678        }
679    }
680}
681
682/// MSB-first bit reader for LZW.
683struct LzwBitReader<'a> {
684    data: &'a [u8],
685    bit_pos: usize,
686}
687
688impl<'a> LzwBitReader<'a> {
689    fn new(data: &'a [u8]) -> Self {
690        Self { data, bit_pos: 0 }
691    }
692
693    fn read(&mut self, bit_size: u8) -> Option<u32> {
694        let byte_pos = self.bit_pos / 8;
695        if byte_pos >= self.data.len() {
696            return None;
697        }
698        let bit_offset = self.bit_pos % 8;
699        let end_byte = (self.bit_pos + bit_size as usize - 1) / 8;
700
701        // Read up to 8 bytes into a u64 for extraction
702        let mut buf = [0u8; 8];
703        for (i, b) in buf.iter_mut().enumerate().take(end_byte - byte_pos + 1) {
704            *b = *self.data.get(byte_pos + i)?;
705        }
706        let bits = u64::from_be_bytes(buf);
707        let shift = 64 - bit_offset - bit_size as usize;
708        let mask = (1u64 << bit_size) - 1;
709        let value = ((bits >> shift) & mask) as u32;
710
711        self.bit_pos += bit_size as usize;
712        Some(value)
713    }
714}
715
716/// ASCIIHexDecode.
717fn decode_ascii_hex(data: &[u8]) -> Result<Vec<u8>, PdfError> {
718    let mut result = Vec::with_capacity(data.len() / 2);
719    let mut high: Option<u8> = None;
720
721    for &b in data {
722        if b == b'>' {
723            break;
724        }
725        if b.is_ascii_whitespace() {
726            continue;
727        }
728        let nibble = hex_digit(b)
729            .ok_or_else(|| PdfError::DecompressionError(format!("invalid hex digit: 0x{b:02x}")))?;
730        match high {
731            None => high = Some(nibble),
732            Some(h) => {
733                result.push(h << 4 | nibble);
734                high = None;
735            }
736        }
737    }
738    if let Some(h) = high {
739        result.push(h << 4);
740    }
741
742    Ok(result)
743}
744
745/// ASCII85Decode.
746fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PdfError> {
747    let mut result = Vec::with_capacity(data.len() * 4 / 5);
748    let mut tuple: u64 = 0;
749    let mut count = 0u8;
750
751    for &b in data {
752        if b == b'~' {
753            break; // ~> end marker
754        }
755        if b.is_ascii_whitespace() {
756            continue;
757        }
758        if b == b'z' && count == 0 {
759            result.extend_from_slice(&[0, 0, 0, 0]);
760            continue;
761        }
762        if !(b'!'..=b'u').contains(&b) {
763            continue; // skip invalid
764        }
765        tuple = tuple * 85 + (b - b'!') as u64;
766        count += 1;
767        if count == 5 {
768            result.push((tuple >> 24) as u8);
769            result.push((tuple >> 16) as u8);
770            result.push((tuple >> 8) as u8);
771            result.push(tuple as u8);
772            tuple = 0;
773            count = 0;
774        }
775    }
776
777    // Handle remainder
778    if count > 0 {
779        for _ in count..5 {
780            tuple = tuple * 85 + 84; // pad with 'u'
781        }
782        for i in 0..(count - 1) {
783            result.push((tuple >> (24 - i * 8)) as u8);
784        }
785    }
786
787    Ok(result)
788}
789
790/// RunLengthDecode (PackBits).
791///
792/// Amplifies at most 128:1 on its own — two input bytes expand to 128 — which
793/// is modest until it sits on top of a Deflate stage, where the two multiply.
794fn decode_run_length(data: &[u8], budget: DecodeBudget) -> Result<Vec<u8>, PdfError> {
795    let mut result = Vec::new();
796    let mut i = 0;
797
798    while i < data.len() {
799        budget.check(result.len())?;
800        let length_byte = data[i];
801        i += 1;
802        if length_byte < 128 {
803            // Copy next (length_byte + 1) bytes literally
804            let count = length_byte as usize + 1;
805            if i + count > data.len() {
806                break;
807            }
808            result.extend_from_slice(&data[i..i + count]);
809            i += count;
810        } else if length_byte > 128 {
811            // Repeat next byte (257 - length_byte) times
812            if i >= data.len() {
813                break;
814            }
815            let count = 257 - length_byte as usize;
816            let val = data[i];
817            i += 1;
818            for _ in 0..count {
819                result.push(val);
820            }
821        } else {
822            // 128 = EOD
823            break;
824        }
825    }
826
827    Ok(result)
828}
829
830/// DCTDecode (JPEG).
831/// For PDF image streams, DCTDecode returns raw pixel data.
832/// However, when used as a filter in a filter chain, the JPEG data
833/// is typically the final representation — return the raw JPEG bytes
834/// since the image decoder will handle them. For standalone streams,
835/// decode the JPEG to raw pixels.
836fn decode_dct(data: &[u8]) -> Result<Vec<u8>, PdfError> {
837    use jpeg_decoder::Decoder;
838
839    // wasm32: prefer zune-jpeg over jpeg-decoder as the primary decoder.
840    // jpeg-decoder's wasm SIMD IDCT path traps (raw `unreachable`, bypasses
841    // std::panic::set_hook) on JPEGs whose bitstream ends early — the native
842    // scalar reader returns a clean `Err("failed to fill whole buffer")`
843    // which the fallback chain below catches, but on wasm32 the SIMD path
844    // writes past the end of an intermediate buffer before the stream check
845    // fires, tripping wasm's bounds check. Starting with zune avoids that
846    // code path entirely for the most common JPEGs. jpeg-decoder is still
847    // tried below as a secondary fallback (e.g. for Adobe YCCK streams
848    // where zune's color transform would give wrong results).
849    #[cfg(target_arch = "wasm32")]
850    if let Some(pixels) = decode_dct_via_zune(data) {
851        return Ok(pixels);
852    }
853
854    let mut decoder = Decoder::new(data);
855
856    // Work around jpeg_decoder bug: it checks component IDs (1,2,3) → YCbCr
857    // before checking Adobe APP14 ColorTransform. When ColorTransform=0 (raw
858    // RGB) is present but component IDs are (1,2,3), the decoder incorrectly
859    // applies YCbCr→RGB conversion to already-RGB data. Detect this case and
860    // override with ColorTransform::RGB.
861    if has_adobe_rgb_marker(data) || is_raw_rgb_jpeg(data) {
862        decoder.set_color_transform(jpeg_decoder::ColorTransform::RGB);
863    } else if needs_ycck_override(data) {
864        decoder.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
865    }
866
867    let pixels = match decoder.decode() {
868        Ok(p) => p,
869        Err(e) => {
870            // jpeg_decoder doesn't support 2-component JPEGs (DeviceN spot
871            // color images). Fall back to zune-jpeg which handles arbitrary
872            // component counts.
873            if let Some(pixels) = decode_dct_zune(data) {
874                return Ok(pixels);
875            }
876            // Some JPEGs use DNL (Define Number of Lines) markers to specify
877            // the height after encoding. Patch the SOF header with the DNL
878            // height and retry.
879            if let Some(patched) = patch_jpeg_dnl_height(data) {
880                return decode_dct(&patched);
881            }
882            // Truncated JPEGs: try tolerant decode that returns partial data.
883            if let Some(pixels) = decode_dct_tolerant(data) {
884                return Ok(pixels);
885            }
886            // Last resort: append EOI marker to truncated JPEG and retry.
887            // jpeg-decoder may succeed when the stream is terminated properly.
888            {
889                let mut padded = data.to_vec();
890                // Strip any partial marker at end, then add EOI
891                if padded.last() == Some(&0xFF) {
892                    padded.pop();
893                }
894                padded.extend_from_slice(&[0xFF, 0xD9]);
895                let mut retry_dec = Decoder::new(&padded[..]);
896                if has_adobe_rgb_marker(&padded) || is_raw_rgb_jpeg(&padded) {
897                    retry_dec.set_color_transform(jpeg_decoder::ColorTransform::RGB);
898                } else if needs_ycck_override(&padded) {
899                    retry_dec.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
900                }
901                if let Ok(pixels) = retry_dec.decode() {
902                    // Apply same CMYK inversion as the normal path
903                    if let Some(info) = retry_dec.info()
904                        && info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
905                    {
906                        let mut result = pixels;
907                        for b in result.iter_mut() {
908                            *b = 255 - *b;
909                        }
910                        return Ok(result);
911                    }
912                    return Ok(pixels);
913                }
914            }
915            return Err(PdfError::DecompressionError(format!("DCTDecode: {e}")));
916        }
917    };
918
919    // For 4-component (CMYK) JPEG, the jpeg_decoder applies a CMYK color
920    // transform that inverts all channels (255-x). However, for PDF streams the
921    // raw JPEG data is already in the correct byte order for the PDF /Decode
922    // array to process. Undo the decoder's inversion so the PDF renderer gets
923    // the original sample values.
924    if let Some(info) = decoder.info()
925        && info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
926    {
927        let mut result = pixels;
928        for b in result.iter_mut() {
929            *b = 255 - *b;
930        }
931        return Ok(result);
932    }
933
934    Ok(pixels)
935}
936
937/// Run a closure that may panic, suppressing the panic message and returning
938/// `None` on panic.  Used for third-party JPEG decoders that can panic on
939/// malformed input.
940fn catch_silent<F, T>(f: F) -> Option<T>
941where
942    F: FnOnce() -> Option<T> + std::panic::UnwindSafe,
943{
944    let prev = std::panic::take_hook();
945    std::panic::set_hook(Box::new(|_| {}));
946    let result = std::panic::catch_unwind(f).ok().flatten();
947    std::panic::set_hook(prev);
948    result
949}
950
951/// Primary JPEG decoder on wasm32. Uses zune-jpeg directly (no `catch_silent`
952/// wrapper — `catch_unwind` is a no-op under `panic=abort` and the hook swap
953/// would clobber the WASM panic hook). Picks the output colorspace from the
954/// SOF component count so 1/3/4-component JPEGs all decode to their natural
955/// format. Returns `None` if zune-jpeg can't decode — caller falls back to
956/// jpeg-decoder.
957#[cfg(target_arch = "wasm32")]
958fn decode_dct_via_zune(data: &[u8]) -> Option<Vec<u8>> {
959    use zune_jpeg::JpegDecoder;
960    let n_comps = jpeg_dimensions_and_components(data)
961        .map(|(_, _, n)| n)
962        .unwrap_or(3);
963    let out_cs = match n_comps {
964        1 => zune_core::colorspace::ColorSpace::Luma,
965        4 => zune_core::colorspace::ColorSpace::CMYK,
966        _ => zune_core::colorspace::ColorSpace::RGB,
967    };
968    let options = zune_core::options::DecoderOptions::default().jpeg_set_out_colorspace(out_cs);
969    let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), options);
970    decoder.decode().ok()
971}
972
973/// Fallback JPEG decoder using zune-jpeg for component counts that
974/// jpeg_decoder doesn't support (e.g., 2-component DeviceN images).
975fn decode_dct_zune(data: &[u8]) -> Option<Vec<u8>> {
976    use zune_jpeg::JpegDecoder;
977    // Request raw 2-component output (LumaA) to avoid unwanted color
978    // conversion. PDF DeviceN images need the original channel values
979    // for the tinting function.
980    let data = data.to_vec();
981    catch_silent(move || {
982        let options = zune_core::options::DecoderOptions::default()
983            .jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::LumaA);
984        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
985        decoder.decode().ok()
986    })
987}
988
989/// Tolerant JPEG decoder for truncated streams.
990/// Returns partial pixel data for whatever MCU rows decoded successfully.
991/// Applies the same CMYK channel inversion as the primary decoder path.
992fn decode_dct_tolerant(data: &[u8]) -> Option<Vec<u8>> {
993    // Detect component count from SOF to set the right output colorspace.
994    // Without this, zune-jpeg converts CMYK to RGB, producing wrong data.
995    let n_comps = jpeg_dimensions_and_components(data)
996        .map(|(_, _, n)| n)
997        .unwrap_or(3);
998    let data = data.to_vec();
999    catch_silent(move || {
1000        use zune_jpeg::JpegDecoder;
1001        let out_cs = match n_comps {
1002            1 => zune_core::colorspace::ColorSpace::Luma,
1003            4 => zune_core::colorspace::ColorSpace::CMYK,
1004            _ => zune_core::colorspace::ColorSpace::RGB,
1005        };
1006        let options = zune_core::options::DecoderOptions::default()
1007            .set_strict_mode(false)
1008            .jpeg_set_out_colorspace(out_cs);
1009        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
1010        decoder.decode().ok()
1011    })
1012}
1013
1014/// Patch a JPEG that uses DNL (Define Number of Lines, marker 0xFFDC) to specify
1015/// its height. Finds the DNL marker, extracts the height, writes it into the SOF
1016/// header, and strips the DNL marker from the scan data so standard decoders can
1017/// parse it.
1018fn patch_jpeg_dnl_height(data: &[u8]) -> Option<Vec<u8>> {
1019    // Find DNL marker (0xFF 0xDC) and extract height
1020    let dnl_height = {
1021        let mut pos = 0;
1022        let mut found = None;
1023        while pos + 4 < data.len() {
1024            if data[pos] == 0xFF && data[pos + 1] == 0xDC {
1025                // DNL: FF DC 00 04 <height_hi> <height_lo>
1026                if pos + 5 < data.len() {
1027                    let h = ((data[pos + 4] as u16) << 8) | data[pos + 5] as u16;
1028                    found = Some((pos, h));
1029                }
1030                break;
1031            }
1032            pos += 1;
1033        }
1034        found
1035    };
1036    let (dnl_pos, height) = dnl_height?;
1037    if height == 0 {
1038        return None;
1039    }
1040
1041    // Find SOF marker (0xFFC0..0xFFC3) and patch height field
1042    let mut patched = data.to_vec();
1043    let mut pos = 2; // skip SOI
1044    while pos + 8 < patched.len() {
1045        if patched[pos] != 0xFF {
1046            pos += 1;
1047            continue;
1048        }
1049        let marker = patched[pos + 1];
1050        if (0xC0..=0xC3).contains(&marker) {
1051            // SOF: FF Cn LL LL PP HH HH WW WW ...
1052            // Height is at offset +5 (2 bytes, big-endian)
1053            patched[pos + 5] = (height >> 8) as u8;
1054            patched[pos + 6] = (height & 0xFF) as u8;
1055            break;
1056        }
1057        if marker == 0xDA {
1058            break; // SOS — stop before scan data
1059        }
1060        // Skip marker segment
1061        if pos + 3 < patched.len() {
1062            let seg_len = ((patched[pos + 2] as usize) << 8) | patched[pos + 3] as usize;
1063            pos += 2 + seg_len;
1064        } else {
1065            break;
1066        }
1067    }
1068
1069    // Remove the DNL marker (6 bytes: FF DC 00 04 HH HH)
1070    if dnl_pos + 6 <= patched.len() {
1071        patched.drain(dnl_pos..dnl_pos + 6);
1072    }
1073
1074    Some(patched)
1075}
1076
1077/// Extract image dimensions from a JPEG's SOF marker.
1078/// Returns `(width, height)` if found.
1079///
1080/// Patch the SOF height field in raw JPEG data.
1081/// Used when the SOF header has a streaming-encoder placeholder height (e.g.
1082/// 60000) that exceeds the PDF dict's authoritative /Height value.
1083pub fn patch_jpeg_sof_height(data: &mut [u8], new_height: u16) {
1084    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
1085        return;
1086    }
1087    let mut pos = 2;
1088    while pos + 4 < data.len() {
1089        if data[pos] != 0xFF {
1090            pos += 1;
1091            continue;
1092        }
1093        let marker = data[pos + 1];
1094        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
1095            if pos + 6 < data.len() {
1096                data[pos + 5] = (new_height >> 8) as u8;
1097                data[pos + 6] = (new_height & 0xFF) as u8;
1098            }
1099            return;
1100        }
1101        if marker == 0xDA {
1102            return; // SOS — too late
1103        }
1104        let seg_len = if pos + 3 < data.len() {
1105            ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize
1106        } else {
1107            return;
1108        };
1109        pos += 2 + seg_len;
1110    }
1111}
1112
1113/// Extract width, height, and component count from a JPEG SOF header.
1114pub(crate) fn jpeg_dimensions_and_components(data: &[u8]) -> Option<(u32, u32, u8)> {
1115    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
1116        return None;
1117    }
1118    let mut pos = 2;
1119    while pos + 4 < data.len() {
1120        if data[pos] != 0xFF {
1121            pos += 1;
1122            continue;
1123        }
1124        let marker = data[pos + 1];
1125        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
1126            if pos + 9 < data.len() {
1127                let h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
1128                let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
1129                let n = data[pos + 9];
1130                return Some((w, h, n));
1131            }
1132        }
1133        if marker == 0xDA {
1134            break;
1135        }
1136        let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
1137        pos += 2 + seg_len;
1138    }
1139    None
1140}
1141
1142/// When the JPEG uses DNL (Define Number of Lines, marker 0xFFDC) — indicated by
1143/// a dummy SOF height of 0 or 0xFFFF — scans the bitstream for the DNL marker
1144/// and returns its height instead.
1145pub fn jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
1146    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
1147        return None;
1148    }
1149    let mut pos = 2;
1150    while pos + 4 < data.len() {
1151        if data[pos] != 0xFF {
1152            pos += 1;
1153            continue;
1154        }
1155        let marker = data[pos + 1];
1156        // SOF markers: 0xC0-0xCF except 0xC4 (DHT), 0xC8 (JPG), 0xCC (DAC)
1157        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
1158            if pos + 9 < data.len() {
1159                let mut h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
1160                let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
1161                // SOF height 0 or 0xFFFF means "defined by DNL marker later"
1162                if h == 0 || h == 0xFFFF {
1163                    if let Some(dnl_h) = find_dnl_height(data) {
1164                        h = dnl_h as u32;
1165                    }
1166                }
1167                return Some((w, h));
1168            }
1169        }
1170        if marker == 0xDA {
1171            break; // SOS — no more markers
1172        }
1173        let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
1174        pos += 2 + seg_len;
1175    }
1176    None
1177}
1178
1179/// Scan JPEG data for a DNL (Define Number of Lines) marker and return its height.
1180fn find_dnl_height(data: &[u8]) -> Option<u16> {
1181    let mut pos = 0;
1182    while pos + 5 < data.len() {
1183        if data[pos] == 0xFF && data[pos + 1] == 0xDC && pos + 5 < data.len() {
1184            return Some(((data[pos + 4] as u16) << 8) | data[pos + 5] as u16);
1185        }
1186        pos += 1;
1187    }
1188    None
1189}
1190
1191/// Check if a JPEG has Adobe APP14 ColorTransform=0 AND uniform sampling factors,
1192/// confirming the data is truly raw RGB (not YCbCr mislabeled with ColorTransform=0).
1193/// YCbCr JPEGs use chroma subsampling (e.g., Y=2×2, Cb/Cr=1×1) while RGB JPEGs
1194/// use uniform sampling (all components 1×1).
1195fn has_adobe_rgb_marker(data: &[u8]) -> bool {
1196    let mut has_ct0 = false;
1197    let mut uniform_sampling = false;
1198    let mut i = 2; // skip SOI
1199    while i + 4 < data.len() {
1200        if data[i] != 0xFF {
1201            break;
1202        }
1203        let marker = data[i + 1];
1204        if marker == 0xDA {
1205            break; // SOS — done with headers
1206        }
1207        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1208        if i + 2 + len > data.len() {
1209            break;
1210        }
1211        // APP14 (Adobe) marker: check ColorTransform
1212        // Segment layout: length(2) + "Adobe"(5) + version(2) + flags0(2) + flags1(2) + CT(1) = 14
1213        if marker == 0xEE && len >= 14 {
1214            let color_transform = data[i + 2 + 13];
1215            has_ct0 = color_transform == 0;
1216        }
1217        // SOF0/SOF2: check sampling factors
1218        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
1219            let ncomp = data[i + 9] as usize;
1220            if ncomp == 3 && i + 10 + ncomp * 3 <= data.len() {
1221                let s0 = data[i + 11]; // component 0 sampling
1222                let s1 = data[i + 14]; // component 1 sampling
1223                let s2 = data[i + 17]; // component 2 sampling
1224                uniform_sampling = s0 == s1 && s1 == s2;
1225            }
1226        }
1227        i += 2 + len;
1228    }
1229    has_ct0 && uniform_sampling
1230}
1231
1232/// Detect raw RGB JPEGs that have no APP14/JFIF markers and non-standard
1233/// component IDs (e.g. 0,1,2 instead of the YCbCr standard 1,2,3).
1234/// These JPEGs store raw RGB data — applying YCbCr→RGB conversion produces
1235/// completely wrong colors (e.g. blue → magenta).
1236fn is_raw_rgb_jpeg(data: &[u8]) -> bool {
1237    let mut has_jfif = false;
1238    let mut has_adobe = false;
1239    let mut non_standard_ids = false;
1240    let mut uniform_sampling = false;
1241    let mut n_components = 0u8;
1242    let mut i = 2; // skip SOI
1243    while i + 4 < data.len() {
1244        if data[i] != 0xFF {
1245            break;
1246        }
1247        let marker = data[i + 1];
1248        if marker == 0xDA {
1249            break;
1250        }
1251        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1252        if i + 2 + len > data.len() {
1253            break;
1254        }
1255        if marker == 0xE0 && len >= 7 && &data[i + 4..i + 9] == b"JFIF\x00" {
1256            has_jfif = true;
1257        }
1258        if marker == 0xEE && len >= 7 && &data[i + 4..i + 9] == b"Adobe" {
1259            has_adobe = true;
1260        }
1261        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
1262            n_components = data[i + 9];
1263            if n_components == 3 && i + 10 + 9 <= data.len() {
1264                let id0 = data[i + 10];
1265                let id1 = data[i + 13];
1266                let id2 = data[i + 16];
1267                // Standard YCbCr uses IDs (1,2,3). Anything else suggests raw RGB.
1268                non_standard_ids = !(id0 == 1 && id1 == 2 && id2 == 3);
1269                let s0 = data[i + 11];
1270                let s1 = data[i + 14];
1271                let s2 = data[i + 17];
1272                uniform_sampling = s0 == s1 && s1 == s2;
1273            }
1274        }
1275        i += 2 + len;
1276    }
1277    // Raw RGB: 3 components, non-standard IDs, uniform sampling, no JFIF/Adobe markers
1278    n_components == 3 && non_standard_ids && uniform_sampling && !has_jfif && !has_adobe
1279}
1280
1281/// Work around jpeg_decoder bug: it checks `"Adobe\0"` (6 bytes) in APP14
1282/// but the spec defines only 5-byte `"Adobe"`. The 6th byte is the high byte
1283/// of the version field. When version >= 256 (high byte != 0), jpeg_decoder
1284/// misses the APP14 marker entirely and misidentifies YCCK as plain CMYK.
1285/// Returns true when the last APP14 has ColorTransform=2 (YCCK) and jpeg_decoder
1286/// would fail to detect it.
1287fn needs_ycck_override(data: &[u8]) -> bool {
1288    let mut last_ct = None;
1289    let mut decoder_would_miss = false;
1290    let mut n_components = 0u8;
1291    let mut i = 2; // skip SOI
1292    while i + 4 < data.len() {
1293        if data[i] != 0xFF {
1294            break;
1295        }
1296        let marker = data[i + 1];
1297        if marker == 0xDA {
1298            break; // SOS
1299        }
1300        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1301        if i + 2 + len > data.len() {
1302            break;
1303        }
1304        // APP14 (Adobe): "Adobe"(5) + version(2) + flags0(2) + flags1(2) + CT(1)
1305        if marker == 0xEE && len >= 14 && &data[i + 4..i + 9] == b"Adobe" {
1306            let ct = data[i + 2 + 13];
1307            last_ct = Some(ct);
1308            // jpeg_decoder checks data[0..6] == "Adobe\0", so byte index 5
1309            // (= data[i+9], the version high byte) must be 0 for it to detect.
1310            decoder_would_miss = data[i + 9] != 0;
1311        }
1312        // SOF0/SOF2: get number of components
1313        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
1314            n_components = data[i + 9];
1315        }
1316        i += 2 + len;
1317    }
1318    // Only override when: last APP14 says YCCK, jpeg_decoder would miss it,
1319    // and the JPEG has 4 components (CMYK/YCCK domain).
1320    last_ct == Some(2) && decoder_would_miss && n_components == 4
1321}
1322
1323/// CCITTFaxDecode (Group 3 / Group 4 fax compression).
1324fn decode_ccittfax(data: &[u8], parms: Option<&PdfDict>) -> Result<Vec<u8>, PdfError> {
1325    use crate::objects::PdfObj;
1326
1327    let k = parms.and_then(|p| p.get_int(b"K")).unwrap_or(0) as i32;
1328    let columns = parms.and_then(|p| p.get_int(b"Columns")).unwrap_or(1728) as u16;
1329    let rows_limit = parms.and_then(|p| p.get_int(b"Rows")).unwrap_or(0) as u32;
1330    let end_of_block = parms
1331        .and_then(|p| match p.get(b"EndOfBlock") {
1332            Some(PdfObj::Bool(b)) => Some(*b),
1333            _ => None,
1334        })
1335        .unwrap_or(true);
1336    let black_is1 = parms
1337        .and_then(|p| match p.get(b"BlackIs1") {
1338            Some(PdfObj::Bool(b)) => Some(*b),
1339            _ => None,
1340        })
1341        .unwrap_or(false);
1342
1343    let encoded_byte_align = parms
1344        .and_then(|p| match p.get(b"EncodedByteAlign") {
1345            Some(PdfObj::Bool(b)) => Some(*b),
1346            _ => None,
1347        })
1348        .unwrap_or(false);
1349
1350    let encoding = if k < 0 {
1351        hayro_ccitt::EncodingMode::Group4
1352    } else if k == 0 {
1353        hayro_ccitt::EncodingMode::Group3_1D
1354    } else {
1355        hayro_ccitt::EncodingMode::Group3_2D { k: k as u32 }
1356    };
1357
1358    let settings = hayro_ccitt::DecodeSettings {
1359        columns: columns as u32,
1360        rows: if rows_limit > 0 { rows_limit } else { u32::MAX },
1361        end_of_block,
1362        end_of_line: false,
1363        rows_are_byte_aligned: encoded_byte_align,
1364        encoding,
1365        invert_black: false,
1366    };
1367
1368    decode_ccitt_hayro(data, &settings, black_is1)
1369}
1370
1371/// A byte-oriented CCITT pixel decoder used by hayro-ccitt.
1372/// Packs decoded pixels into bytes (MSB first), with `black_is1` polarity control.
1373struct CcittByteDecoder {
1374    output: Vec<u8>,
1375    current_byte: u8,
1376    bit_pos: u8,
1377    black_is1: bool,
1378}
1379
1380impl CcittByteDecoder {
1381    fn new(black_is1: bool) -> Self {
1382        Self {
1383            output: Vec::new(),
1384            current_byte: 0,
1385            bit_pos: 0,
1386            black_is1,
1387        }
1388    }
1389
1390    fn flush_byte(&mut self) {
1391        if self.bit_pos > 0 {
1392            // Shift remaining bits to MSB position and pad
1393            let remaining = 8 - self.bit_pos;
1394            self.current_byte <<= remaining;
1395            if !self.black_is1 {
1396                // Pad unfilled bits as white (1)
1397                self.current_byte |= (1u8 << remaining) - 1;
1398            }
1399            self.output.push(self.current_byte);
1400            self.current_byte = 0;
1401            self.bit_pos = 0;
1402        }
1403    }
1404}
1405
1406impl hayro_ccitt::Decoder for CcittByteDecoder {
1407    fn push_pixel(&mut self, white: bool) {
1408        // black_is1=true: black=1, white=0
1409        // black_is1=false: black=0, white=1
1410        let bit = if self.black_is1 { !white } else { white };
1411        self.current_byte = (self.current_byte << 1) | (bit as u8);
1412        self.bit_pos += 1;
1413        if self.bit_pos == 8 {
1414            self.output.push(self.current_byte);
1415            self.current_byte = 0;
1416            self.bit_pos = 0;
1417        }
1418    }
1419
1420    fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) {
1421        // If there are partial bits pending, we can't directly push bytes —
1422        // the bit boundary wouldn't align. Fall back to pixel-by-pixel.
1423        if self.bit_pos != 0 {
1424            for _ in 0..chunk_count * 8 {
1425                self.push_pixel(white);
1426            }
1427            return;
1428        }
1429        let byte = if (self.black_is1 && !white) || (!self.black_is1 && white) {
1430            0xFF
1431        } else {
1432            0x00
1433        };
1434        for _ in 0..chunk_count {
1435            self.output.push(byte);
1436        }
1437    }
1438
1439    fn next_line(&mut self) {
1440        self.flush_byte();
1441    }
1442}
1443
1444/// Decode CCITT data using hayro-ccitt (supports Group 3 and Group 4), with
1445/// a fall-through to the `fax` crate when hayro rejects the stream with a
1446/// hard error (Overflow, InvalidCode, LineLengthMismatch). The `fax` crate is
1447/// more lenient with malformed Group 4 streams produced by old Acrobat
1448/// Distiller versions, where hayro's strict position-arithmetic checks can
1449/// bail out mid-stream even though the image is still decodable.
1450fn decode_ccitt_hayro(
1451    data: &[u8],
1452    settings: &hayro_ccitt::DecodeSettings,
1453    black_is1: bool,
1454) -> Result<Vec<u8>, PdfError> {
1455    let mut decoder = CcittByteDecoder::new(black_is1);
1456    let hayro_err = hayro_ccitt::decode(data, &mut decoder, settings).err();
1457
1458    // If hayro failed with anything other than a soft EOF, try `fax` as a
1459    // fallback. Keep whichever decoder produced more byte output.
1460    if let Some(e) = hayro_err
1461        && e != hayro_ccitt::DecodeError::UnexpectedEof
1462    {
1463        let fallback = decode_ccitt_fax(data, settings, black_is1);
1464        use std::sync::atomic::{AtomicBool, Ordering};
1465        static WARNED: AtomicBool = AtomicBool::new(false);
1466        if fallback.len() > decoder.output.len() {
1467            if !WARNED.swap(true, Ordering::Relaxed) {
1468                eprintln!(
1469                    "[CCITT] hayro-ccitt error: {} — fell back to `fax` crate",
1470                    e
1471                );
1472            }
1473            return Ok(fallback);
1474        }
1475        if !WARNED.swap(true, Ordering::Relaxed) {
1476            eprintln!("[CCITT] decode warning: {} (using partial data)", e);
1477        }
1478    }
1479    Ok(decoder.output)
1480}
1481
1482/// Decode CCITT data using the `fax` crate as a fallback. Returns a byte-packed
1483/// buffer with the same polarity/layout as the hayro path.
1484fn decode_ccitt_fax(
1485    data: &[u8],
1486    settings: &hayro_ccitt::DecodeSettings,
1487    black_is1: bool,
1488) -> Vec<u8> {
1489    let width = settings.columns as u16;
1490    let row_bytes = settings.columns.div_ceil(8) as usize;
1491    let mut out: Vec<u8> = Vec::new();
1492    // Byte value for a full chunk of "white" and "black" pixels after polarity.
1493    // black_is1=false (PDF default): 0=black, 1=white → white row = 0xFF, black = 0x00
1494    // black_is1=true: 0=white, 1=black → white row = 0x00, black = 0xFF
1495    let white_byte: u8 = if black_is1 { 0x00 } else { 0xFF };
1496    let black_byte: u8 = !white_byte;
1497
1498    let rows_limit = if settings.rows == u32::MAX || settings.rows == 0 {
1499        None
1500    } else {
1501        Some(settings.rows.min(u16::MAX as u32) as u16)
1502    };
1503
1504    let mut emit_row = |transitions: &[u16]| {
1505        // Rebuild one packed row from the transition list.
1506        let mut row = vec![white_byte; row_bytes];
1507        // Row starts white; each transition flips color starting at that index.
1508        let mut color_white = true;
1509        let mut cursor: u16 = 0;
1510        // Add the sentinel `width` transition so we close the final run.
1511        let iter = transitions.iter().copied().chain(std::iter::once(width));
1512        for next in iter {
1513            let end = next.min(width);
1514            if !color_white && end > cursor {
1515                fill_bits(&mut row, cursor as usize, end as usize, black_byte != 0);
1516            }
1517            color_white = !color_white;
1518            cursor = end;
1519            if cursor >= width {
1520                break;
1521            }
1522        }
1523        out.extend_from_slice(&row);
1524    };
1525
1526    match settings.encoding {
1527        hayro_ccitt::EncodingMode::Group4 => {
1528            let _ = fax::decoder::decode_g4(data.iter().copied(), width, rows_limit, &mut emit_row);
1529        }
1530        hayro_ccitt::EncodingMode::Group3_1D | hayro_ccitt::EncodingMode::Group3_2D { .. } => {
1531            let _ = fax::decoder::decode_g3(data.iter().copied(), &mut emit_row);
1532        }
1533    }
1534
1535    // Pad truncated output with white scanlines so downstream image handling
1536    // sees the full-height buffer. Without this, a Group 4 stream that the
1537    // decoder can't finish (malformed PDF) would produce a buffer short by
1538    // thousands of bytes; the image code fills the missing rows with zeros,
1539    // which lands as a solid black rectangle covering part of the page.
1540    if let Some(target_rows) = rows_limit {
1541        let expected = row_bytes * target_rows as usize;
1542        if out.len() < expected {
1543            out.resize(expected, white_byte);
1544        }
1545    }
1546
1547    out
1548}
1549
1550/// Flip bits in a byte-packed (MSB-first) row between `[start, end)` to black.
1551/// `start`/`end` are pixel indices; the buffer is pre-filled with the "white"
1552/// polarity, so this routine only needs to set the black-colored runs.
1553fn fill_bits(row: &mut [u8], start: usize, end: usize, black_is_one: bool) {
1554    if end <= start {
1555        return;
1556    }
1557    for x in start..end {
1558        let byte = x / 8;
1559        let bit = 0x80u8 >> (x % 8);
1560        if black_is_one {
1561            row[byte] |= bit;
1562        } else {
1563            row[byte] &= !bit;
1564        }
1565    }
1566}
1567
1568/// JBIG2Decode.
1569fn decode_jbig2(data: &[u8], globals: Option<&[u8]>) -> Result<Vec<u8>, PdfError> {
1570    // Native builds run the decode on a sidecar thread with a 2-second
1571    // watchdog, guarding against malformed streams that hang the decoder
1572    // (e.g. issue15942.pdf). wasm32-unknown-unknown has no thread support,
1573    // so the watchdog is skipped there and we call the decoder directly —
1574    // a hanging stream will hang the page, but normal streams (like those
1575    // in pdf_samples/1321.pdf) will now decode instead of panicking at
1576    // `std::thread::spawn`.
1577    #[cfg(not(target_arch = "wasm32"))]
1578    let image = {
1579        let data_owned = data.to_vec();
1580        let globals_owned = globals.map(|g| g.to_vec());
1581        let (tx, rx) = std::sync::mpsc::channel();
1582        std::thread::spawn(move || {
1583            let result = hayro_jbig2::decode_embedded(&data_owned, globals_owned.as_deref());
1584            let _ = tx.send(result);
1585        });
1586        // Scale timeout with data size: 5s base + 5s per MB of compressed data.
1587        // Large scanned-document pages (e.g. 19k×25k bilevel at 2MB) need more
1588        // than the original 2s, while the watchdog still catches malformed
1589        // streams that hang the decoder indefinitely.
1590        let timeout_secs = 5 + (data.len() as u64 / (1024 * 1024)) * 5;
1591        rx.recv_timeout(std::time::Duration::from_secs(timeout_secs))
1592            .map_err(|_| PdfError::DecompressionError("JBIG2: decode timed out".into()))?
1593            .map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?
1594    };
1595
1596    #[cfg(target_arch = "wasm32")]
1597    let image = hayro_jbig2::decode_embedded(data, globals)
1598        .map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?;
1599
1600    // Convert Vec<bool> to packed bytes (8 pixels/byte, MSB first)
1601    // JBIG2: true = black, false = white
1602    // PDF DeviceGray: 0 = black, 1 = white
1603    // So: start all-white (0xFF), clear bits for black pixels
1604    let row_bytes = (image.width as usize).div_ceil(8);
1605    let mut packed = vec![0xFFu8; row_bytes * image.height as usize];
1606    for y in 0..image.height as usize {
1607        for x in 0..image.width as usize {
1608            if image.data[y * image.width as usize + x] {
1609                packed[y * row_bytes + x / 8] &= !(0x80 >> (x % 8));
1610            }
1611        }
1612    }
1613    Ok(packed)
1614}
1615
1616/// JPXDecode (JPEG 2000).
1617///
1618/// Uses hayro-jpeg2000 to decode JP2 or raw J2K codestreams into interleaved pixel data.
1619#[cfg(feature = "jpx")]
1620fn decode_jpx(data: &[u8]) -> Result<Vec<u8>, PdfError> {
1621    if data.is_empty() {
1622        return Ok(Vec::new());
1623    }
1624
1625    let image = hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default())
1626        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1627
1628    image
1629        .decode()
1630        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))
1631}
1632
1633/// JPXDecode without resolving the JP2-internal palette.
1634///
1635/// Some Adobe-generated JP2 files declare 4-bit palette column precision but
1636/// store 8-bit values.  hayro-jpeg2000's palette resolution rescales based on
1637/// the declared precision, corrupting the colors.  When the PDF provides its
1638/// own Indexed color space, we skip the JP2 palette and let the PDF lookup
1639/// table handle it.
1640///
1641/// Returns `(decoded_data, original_bit_depth)`.  The original bit depth is
1642/// needed to un-normalize hayro's 8-bit output back to raw palette indices
1643/// (hayro rescales sub-8-bit data to 0-255).
1644#[cfg(feature = "jpx")]
1645pub fn decode_jpx_no_palette(data: &[u8]) -> Result<(Vec<u8>, u8), PdfError> {
1646    if data.is_empty() {
1647        return Ok((Vec::new(), 8));
1648    }
1649
1650    let settings = hayro_jpeg2000::DecodeSettings {
1651        resolve_palette_indices: false,
1652        ..Default::default()
1653    };
1654    let image = hayro_jpeg2000::Image::new(data, &settings)
1655        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1656    let bit_depth = image.original_bit_depth();
1657
1658    let pixels = image
1659        .decode()
1660        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1661    Ok((pixels, bit_depth))
1662}
1663
1664/// Query the number of color channels (excluding alpha) and whether alpha is
1665/// present in a JPEG 2000 image, without fully decoding the pixel data.
1666/// Returns `(color_channels, has_alpha)`.
1667#[cfg(feature = "jpx")]
1668pub fn jpx_color_info(data: &[u8]) -> Option<(u8, bool)> {
1669    let image =
1670        hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
1671    Some((image.color_space().num_channels(), image.has_alpha()))
1672}
1673
1674/// Extract image dimensions from a JPEG 2000 stream without full decode.
1675/// Returns `(width, height)`.
1676#[cfg(feature = "jpx")]
1677pub fn jpx_dimensions(data: &[u8]) -> Option<(u32, u32)> {
1678    let image =
1679        hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
1680    Some((image.width(), image.height()))
1681}
1682
1683/// Decode filters preceding JPXDecode in a filter chain (e.g. ASCIIHexDecode).
1684/// Returns the raw JP2/J2K data ready for `jpx_dimensions` / `jpx_color_info`.
1685pub fn decode_pre_jpx(raw: &[u8], dict: &crate::objects::PdfDict) -> Vec<u8> {
1686    let (filters, parms) = parse_filters(dict, None).unwrap_or_default();
1687    // Apply all filters except JPXDecode
1688    let pre_count = filters
1689        .iter()
1690        .take_while(|f| !matches!(f, Filter::JPXDecode))
1691        .count();
1692    if pre_count == 0 {
1693        return raw.to_vec();
1694    }
1695    let pre_parms: Vec<_> = parms.into_iter().take(pre_count).collect();
1696    decode_stream(raw, &filters[..pre_count], &pre_parms, None).unwrap_or_else(|_| raw.to_vec())
1697}
1698
1699/// Largest accepted `/Columns` in `/DecodeParms`.
1700///
1701/// For an image stream this is the image width, so it is held to the same
1702/// ceiling images are; for an xref stream it is a handful of bytes.
1703const MAX_PREDICTOR_COLUMNS: i64 = 100_000;
1704
1705/// Largest accepted `/Colors` in `/DecodeParms`.
1706///
1707/// PDF 32000-1 Table 10 gives 1, 2, 3, or 4. DeviceN can carry more
1708/// components, so this allows the 32 that `/DeviceN` itself is capped at
1709/// rather than the literal table value.
1710const MAX_PREDICTOR_COLORS: i64 = 32;
1711
1712/// Validate one `/DecodeParms` integer, substituting `default` when absent.
1713///
1714/// Returns `None` for a value outside `1..=max`. These come straight from the
1715/// file and feed the predictor's row-size arithmetic; a negative one becomes
1716/// enormous under `as usize`, and a zero makes `row_bytes` zero, which reaches
1717/// `slice::chunks(0)` — a panic in release builds as well as debug.
1718fn validate_decode_parm(value: Option<i64>, default: i64, max: i64) -> Option<usize> {
1719    let v = value.unwrap_or(default);
1720    if v >= 1 && v <= max {
1721        usize::try_from(v).ok()
1722    } else {
1723        None
1724    }
1725}
1726
1727/// Apply PNG or TIFF predictor to decoded data.
1728///
1729/// A malformed `/DecodeParms` yields the data unchanged rather than an error:
1730/// the predictor is a reversible transform layered on top of an already
1731/// decoded stream, so passing it through leaves the caller with the same bytes
1732/// it would have had if `/Predictor` were absent, which is the more useful
1733/// outcome for a damaged file than failing the whole stream.
1734fn apply_predictor(data: &[u8], parms: &PdfDict, predictor: i64) -> Result<Vec<u8>, PdfError> {
1735    let (Some(columns), Some(colors), Some(bpc)) = (
1736        validate_decode_parm(parms.get_int(b"Columns"), 1, MAX_PREDICTOR_COLUMNS),
1737        validate_decode_parm(parms.get_int(b"Colors"), 1, MAX_PREDICTOR_COLORS),
1738        validate_decode_parm(
1739            parms.get_int(b"BitsPerComponent"),
1740            8,
1741            stet_graphics::image_limits::MAX_BITS_PER_COMPONENT,
1742        ),
1743    ) else {
1744        return Ok(data.to_vec());
1745    };
1746
1747    // Bounded above by 100_000 * 32 * 16, so these cannot overflow; the
1748    // checked forms document that rather than relying on the reader to
1749    // re-derive it.
1750    let Some(bytes_per_pixel) = colors.checked_mul(bpc).map(|b| b.div_ceil(8)) else {
1751        return Ok(data.to_vec());
1752    };
1753    let Some(row_bytes) = columns
1754        .checked_mul(colors)
1755        .and_then(|c| c.checked_mul(bpc))
1756        .map(|b| b.div_ceil(8))
1757    else {
1758        return Ok(data.to_vec());
1759    };
1760    // Every predictor below either chunks by `row_bytes` or divides by
1761    // `row_bytes + 1`; neither is meaningful at zero.
1762    if row_bytes == 0 || bytes_per_pixel == 0 {
1763        return Ok(data.to_vec());
1764    }
1765
1766    if predictor == 2 {
1767        // TIFF horizontal differencing
1768        if bpc < 8 {
1769            // Sub-byte samples: operate at sample level, not byte level
1770            apply_tiff_predictor_subbyte(data, columns, colors, bpc, row_bytes)
1771        } else if bpc == 16 {
1772            // 16-bit samples: add as 16-bit values, not byte-by-byte
1773            apply_tiff_predictor_16bit(data, columns, colors, row_bytes)
1774        } else {
1775            apply_tiff_predictor(data, row_bytes, bytes_per_pixel)
1776        }
1777    } else if predictor >= 10 {
1778        // PNG predictors
1779        apply_png_predictor(data, row_bytes, bytes_per_pixel)
1780    } else {
1781        Ok(data.to_vec())
1782    }
1783}
1784
1785/// TIFF predictor 2 for sub-byte samples (BPC = 1, 2, or 4).
1786/// Operates at the individual sample level within packed bytes.
1787fn apply_tiff_predictor_subbyte(
1788    data: &[u8],
1789    columns: usize,
1790    colors: usize,
1791    bpc: usize,
1792    row_bytes: usize,
1793) -> Result<Vec<u8>, PdfError> {
1794    let samples_per_row = columns * colors;
1795    let mask = (1u8 << bpc) - 1; // e.g., 1 for bpc=1, 3 for bpc=2, 15 for bpc=4
1796    let mut result = Vec::with_capacity(data.len());
1797
1798    for row in data.chunks(row_bytes) {
1799        let mut out_row = vec![0u8; row.len()];
1800        // Copy the raw bytes first, then undo differencing at sample level
1801        out_row[..row.len()].copy_from_slice(row);
1802
1803        // Extract all samples, undo differencing, re-pack
1804        let mut prev = vec![0u8; colors];
1805        for col in 0..columns {
1806            for c in 0..colors {
1807                let sample_idx = col * colors + c;
1808                if sample_idx >= samples_per_row {
1809                    break;
1810                }
1811                let bit_offset = sample_idx * bpc;
1812                let byte_idx = bit_offset / 8;
1813                let bit_pos = 8 - bpc - (bit_offset % 8); // MSB-first packing
1814                if byte_idx >= row.len() {
1815                    break;
1816                }
1817                let encoded = (row[byte_idx] >> bit_pos) & mask;
1818                let decoded = (encoded.wrapping_add(prev[c])) & mask;
1819                prev[c] = decoded;
1820                // Write back
1821                out_row[byte_idx] = (out_row[byte_idx] & !(mask << bit_pos)) | (decoded << bit_pos);
1822            }
1823        }
1824        result.extend_from_slice(&out_row);
1825    }
1826
1827    Ok(result)
1828}
1829
1830/// TIFF predictor 2 for 16-bit samples.
1831///
1832/// Each sample is 2 bytes (big-endian). The byte-level predictor doesn't
1833/// propagate carry between high and low bytes, producing wrong results.
1834fn apply_tiff_predictor_16bit(
1835    data: &[u8],
1836    columns: usize,
1837    colors: usize,
1838    row_bytes: usize,
1839) -> Result<Vec<u8>, PdfError> {
1840    let mut result = Vec::with_capacity(data.len());
1841
1842    for row in data.chunks(row_bytes) {
1843        let mut out_row = vec![0u8; row.len()];
1844        let mut prev = vec![0u16; colors];
1845
1846        for col in 0..columns {
1847            for c in 0..colors {
1848                let byte_idx = (col * colors + c) * 2;
1849                if byte_idx + 1 >= row.len() {
1850                    break;
1851                }
1852                let encoded = u16::from_be_bytes([row[byte_idx], row[byte_idx + 1]]);
1853                let decoded = encoded.wrapping_add(prev[c]);
1854                prev[c] = decoded;
1855                let [hi, lo] = decoded.to_be_bytes();
1856                out_row[byte_idx] = hi;
1857                out_row[byte_idx + 1] = lo;
1858            }
1859        }
1860        result.extend_from_slice(&out_row);
1861    }
1862
1863    Ok(result)
1864}
1865
1866/// TIFF predictor 2: horizontal differencing.
1867fn apply_tiff_predictor(
1868    data: &[u8],
1869    row_bytes: usize,
1870    bytes_per_pixel: usize,
1871) -> Result<Vec<u8>, PdfError> {
1872    let mut result = Vec::with_capacity(data.len());
1873
1874    for row in data.chunks(row_bytes) {
1875        let mut out_row = vec![0u8; row.len()];
1876        for i in 0..row.len() {
1877            let left = if i >= bytes_per_pixel {
1878                out_row[i - bytes_per_pixel]
1879            } else {
1880                0
1881            };
1882            out_row[i] = row[i].wrapping_add(left);
1883        }
1884        result.extend_from_slice(&out_row);
1885    }
1886
1887    Ok(result)
1888}
1889
1890/// PNG predictors (10-15): per-row predictor byte.
1891fn apply_png_predictor(
1892    data: &[u8],
1893    row_bytes: usize,
1894    bytes_per_pixel: usize,
1895) -> Result<Vec<u8>, PdfError> {
1896    // Each row has a leading predictor byte + row_bytes data bytes
1897    let stride = row_bytes + 1;
1898
1899    // Detect data that lacks predictor bytes despite DecodeParms claiming them.
1900    // If data divides evenly into row_bytes but NOT into stride, the stream
1901    // was written without per-row predictor prefixes — return as-is.
1902    if row_bytes > 0
1903        && !data.is_empty()
1904        && data.len().is_multiple_of(row_bytes)
1905        && !data.len().is_multiple_of(stride)
1906    {
1907        return Ok(data.to_vec());
1908    }
1909
1910    let num_rows = data.len() / stride;
1911    let mut result = Vec::with_capacity(num_rows * row_bytes);
1912    let mut prev_row = vec![0u8; row_bytes];
1913
1914    for row_idx in 0..num_rows {
1915        let row_start = row_idx * stride;
1916        if row_start >= data.len() {
1917            break;
1918        }
1919        let filter_type = data[row_start];
1920        let row_data = &data[row_start + 1..std::cmp::min(row_start + stride, data.len())];
1921        let mut out_row = vec![0u8; row_data.len()];
1922
1923        match filter_type {
1924            0 => {
1925                // None
1926                out_row.copy_from_slice(row_data);
1927            }
1928            1 => {
1929                // Sub
1930                for i in 0..row_data.len() {
1931                    let left = if i >= bytes_per_pixel {
1932                        out_row[i - bytes_per_pixel]
1933                    } else {
1934                        0
1935                    };
1936                    out_row[i] = row_data[i].wrapping_add(left);
1937                }
1938            }
1939            2 => {
1940                // Up
1941                for i in 0..row_data.len() {
1942                    let up = if i < prev_row.len() { prev_row[i] } else { 0 };
1943                    out_row[i] = row_data[i].wrapping_add(up);
1944                }
1945            }
1946            3 => {
1947                // Average
1948                for i in 0..row_data.len() {
1949                    let left = if i >= bytes_per_pixel {
1950                        out_row[i - bytes_per_pixel] as u16
1951                    } else {
1952                        0
1953                    };
1954                    let up = if i < prev_row.len() {
1955                        prev_row[i] as u16
1956                    } else {
1957                        0
1958                    };
1959                    out_row[i] = row_data[i].wrapping_add(((left + up) / 2) as u8);
1960                }
1961            }
1962            4 => {
1963                // Paeth
1964                for i in 0..row_data.len() {
1965                    let left = if i >= bytes_per_pixel {
1966                        out_row[i - bytes_per_pixel]
1967                    } else {
1968                        0
1969                    };
1970                    let up = if i < prev_row.len() { prev_row[i] } else { 0 };
1971                    let up_left = if i >= bytes_per_pixel && i - bytes_per_pixel < prev_row.len() {
1972                        prev_row[i - bytes_per_pixel]
1973                    } else {
1974                        0
1975                    };
1976                    out_row[i] = row_data[i].wrapping_add(paeth(left, up, up_left));
1977                }
1978            }
1979            _ => {
1980                // Unknown predictor type — pass through
1981                out_row.copy_from_slice(row_data);
1982            }
1983        }
1984
1985        prev_row[..out_row.len()].copy_from_slice(&out_row);
1986        result.extend_from_slice(&out_row);
1987    }
1988
1989    Ok(result)
1990}
1991
1992/// Paeth predictor function.
1993fn paeth(a: u8, b: u8, c: u8) -> u8 {
1994    let a = a as i16;
1995    let b = b as i16;
1996    let c = c as i16;
1997    let p = a + b - c;
1998    let pa = (p - a).abs();
1999    let pb = (p - b).abs();
2000    let pc = (p - c).abs();
2001    if pa <= pb && pa <= pc {
2002        a as u8
2003    } else if pb <= pc {
2004        b as u8
2005    } else {
2006        c as u8
2007    }
2008}
2009
2010fn hex_digit(b: u8) -> Option<u8> {
2011    match b {
2012        b'0'..=b'9' => Some(b - b'0'),
2013        b'a'..=b'f' => Some(b - b'a' + 10),
2014        b'A'..=b'F' => Some(b - b'A' + 10),
2015        _ => None,
2016    }
2017}
2018
2019#[cfg(test)]
2020mod tests {
2021    use super::*;
2022
2023    #[test]
2024    fn flate_round_trip() {
2025        use flate2::Compression;
2026        use flate2::write::ZlibEncoder;
2027        use std::io::Write;
2028
2029        let original = b"Hello, PDF world! This is a test of FlateDecode.";
2030        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
2031        enc.write_all(original).unwrap();
2032        let compressed = enc.finish().unwrap();
2033
2034        let decoded = decode_flate(&compressed, None, DecodeBudget::default()).unwrap();
2035        assert_eq!(&decoded, original);
2036    }
2037
2038    #[test]
2039    fn ascii_hex_decode() {
2040        let decoded = decode_ascii_hex(b"48656C6C6F>").unwrap();
2041        assert_eq!(&decoded, b"Hello");
2042    }
2043
2044    #[test]
2045    fn ascii_hex_odd_digits() {
2046        let decoded = decode_ascii_hex(b"ABC>").unwrap();
2047        assert_eq!(decoded, vec![0xAB, 0xC0]);
2048    }
2049
2050    #[test]
2051    fn ascii85_decode() {
2052        // "Hello" in ASCII85 = 87cURD]j7
2053        // Full encoding: <~87cURD]j7BEbo7~>  (for "Hello, World")
2054        // Simple test: encode "test" = FCfN8
2055        let decoded = decode_ascii85(b"FCfN8~>").unwrap();
2056        assert_eq!(&decoded, b"test");
2057    }
2058
2059    #[test]
2060    fn ascii85_z_shortcut() {
2061        let decoded = decode_ascii85(b"z~>").unwrap();
2062        assert_eq!(decoded, vec![0, 0, 0, 0]);
2063    }
2064
2065    #[test]
2066    fn run_length_decode() {
2067        // 2 = copy 3 bytes, then 253 = repeat next byte 4 times, then 128 = EOD
2068        let data = vec![2, b'A', b'B', b'C', 253, b'X', 128];
2069        let decoded = decode_run_length(&data, DecodeBudget::default()).unwrap();
2070        assert_eq!(&decoded, b"ABCXXXX");
2071    }
2072
2073    #[test]
2074    fn png_predictor_none() {
2075        // Row of 3 bytes, predictor type 0 (none)
2076        let data = vec![0, 10, 20, 30];
2077        let result = apply_png_predictor(&data, 3, 1).unwrap();
2078        assert_eq!(result, vec![10, 20, 30]);
2079    }
2080
2081    #[test]
2082    fn png_predictor_sub() {
2083        // Row of 3 bytes, predictor type 1 (sub), bpp=1
2084        // input: [5, 3, 4] -> output: [5, 8, 12]
2085        let data = vec![1, 5, 3, 4];
2086        let result = apply_png_predictor(&data, 3, 1).unwrap();
2087        assert_eq!(result, vec![5, 8, 12]);
2088    }
2089
2090    #[test]
2091    fn png_predictor_up() {
2092        // Two rows, predictor type 2 (up)
2093        // Row 0: [0, 10, 20, 30]  (type 0 = none)
2094        // Row 1: [2, 5, 5, 5]    (type 2 = up)
2095        let data = vec![0, 10, 20, 30, 2, 5, 5, 5];
2096        let result = apply_png_predictor(&data, 3, 1).unwrap();
2097        assert_eq!(result, vec![10, 20, 30, 15, 25, 35]);
2098    }
2099
2100    #[test]
2101    fn filter_chain() {
2102        use flate2::Compression;
2103        use flate2::write::ZlibEncoder;
2104        use std::io::Write;
2105
2106        let original = b"filter chain test data";
2107        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
2108        enc.write_all(original).unwrap();
2109        let compressed = enc.finish().unwrap();
2110
2111        // Encode as ASCII hex
2112        let mut hex = String::new();
2113        for b in &compressed {
2114            hex.push_str(&format!("{b:02X}"));
2115        }
2116        hex.push('>');
2117
2118        let filters = vec![Filter::ASCIIHexDecode, Filter::FlateDecode];
2119        let parms = vec![None, None];
2120        let decoded = decode_stream(hex.as_bytes(), &filters, &parms, None).unwrap();
2121        assert_eq!(&decoded, original);
2122    }
2123
2124    // --- Decompression-bomb ceiling ---
2125
2126    fn zlib(data: &[u8]) -> Vec<u8> {
2127        use flate2::Compression;
2128        use flate2::write::ZlibEncoder;
2129        use std::io::Write;
2130        let mut enc = ZlibEncoder::new(Vec::new(), Compression::best());
2131        enc.write_all(data).unwrap();
2132        enc.finish().unwrap()
2133    }
2134
2135    /// The measured attack: a few hundred bytes of nested `/FlateDecode`
2136    /// reaching gigabytes. Before the budget this peaked at 2058 MB of RSS
2137    /// from a 707-byte file, and aborted outright once the address space was
2138    /// too small to satisfy it.
2139    ///
2140    /// The budget here is deliberately tiny so the test costs nothing; the
2141    /// property under test is that the *chain* shares one ceiling, so nesting
2142    /// cannot multiply past it.
2143    #[test]
2144    fn nested_flate_chain_is_refused_rather_than_expanded() {
2145        let mut data = zlib(&vec![0u8; 4 << 20]);
2146        let mut filters = vec![Filter::FlateDecode];
2147        for _ in 0..3 {
2148            data = zlib(&data);
2149            filters.push(Filter::FlateDecode);
2150        }
2151        assert!(
2152            data.len() < 1024,
2153            "the bomb must stay small: {}",
2154            data.len()
2155        );
2156
2157        let parms = vec![None; filters.len()];
2158        let err =
2159            decode_stream_bounded(&data, &filters, &parms, None, DecodeBudget::new(64 * 1024))
2160                .unwrap_err();
2161        assert!(
2162            matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")),
2163            "expected a budget refusal, got {err:?}"
2164        );
2165    }
2166
2167    /// RunLength stacked on Flate: 128:1 on top of ~1000:1. The RunLength
2168    /// decoder grows a byte at a time, so its check has to sit inside the
2169    /// loop, not on the finished buffer.
2170    #[test]
2171    fn run_length_on_flate_is_refused() {
2172        let rle = b"\x81\x00".repeat(64 << 10); // -> 8 MB expanded
2173        let data = zlib(&rle);
2174        let filters = [Filter::FlateDecode, Filter::RunLengthDecode];
2175        let parms = vec![None; filters.len()];
2176        let err =
2177            decode_stream_bounded(&data, &filters, &parms, None, DecodeBudget::new(64 * 1024))
2178                .unwrap_err();
2179        assert!(matches!(err, PdfError::DecompressionError(_)), "{err:?}");
2180    }
2181
2182    /// A budget overrun must not be mistaken for the truncated-stream case
2183    /// that `decode_flate` recovers from by retrying as raw deflate. If it
2184    /// were, the bomb would come back as a silently truncated success.
2185    #[test]
2186    fn flate_budget_overrun_is_an_error_not_a_truncation() {
2187        let data = zlib(&vec![0u8; 4 << 20]);
2188        let err = decode_flate(&data, None, DecodeBudget::new(4096)).unwrap_err();
2189        assert!(matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")));
2190    }
2191
2192    #[test]
2193    fn lzw_output_is_bounded() {
2194        // A cleared table followed by literal codes: enough output to pass a
2195        // 32-byte ceiling without needing a real LZW compressor.
2196        let mut bits = Vec::new();
2197        let mut acc: u32 = 0;
2198        let mut nbits = 0;
2199        for code in std::iter::once(LZW_CLEAR_TABLE).chain(std::iter::repeat_n(0usize, 512)) {
2200            acc = (acc << 9) | code as u32;
2201            nbits += 9;
2202            while nbits >= 8 {
2203                bits.push((acc >> (nbits - 8)) as u8);
2204                nbits -= 8;
2205            }
2206        }
2207        let err = decode_lzw(&bits, None, DecodeBudget::new(32)).unwrap_err();
2208        assert!(matches!(err, PdfError::DecompressionError(ref m) if m.contains("exceeds")));
2209    }
2210
2211    /// A stream comfortably under the ceiling is unaffected — the ceiling must
2212    /// not be reachable by ordinary content.
2213    #[test]
2214    fn ordinary_streams_are_unaffected() {
2215        let original = b"q 1 0 0 1 10 10 cm BT /F1 12 Tf (hello) Tj ET Q".repeat(1000);
2216        let data = zlib(&original);
2217        let decoded = decode_stream_bounded(
2218            &data,
2219            &[Filter::FlateDecode],
2220            &[None],
2221            None,
2222            DecodeBudget::default(),
2223        )
2224        .unwrap();
2225        assert_eq!(decoded, original);
2226    }
2227
2228    // --- Budget derivation ---
2229
2230    fn dict_from(src: &[u8]) -> PdfDict {
2231        let mut lexer = crate::lexer::Lexer::new(src);
2232        match crate::lexer::parse_object(&mut lexer).unwrap() {
2233            crate::objects::PdfObj::Dict(d) => d,
2234            other => panic!("expected a dict, got {other:?}"),
2235        }
2236    }
2237
2238    #[test]
2239    fn a_dict_declaring_nothing_gets_the_general_ceiling() {
2240        let budget = DecodeBudget::for_stream(&dict_from(b"<</Type/ObjStm/N 4/First 20>>"));
2241        assert_eq!(budget.limit(), MAX_DECODED_STREAM_BYTES);
2242    }
2243
2244    /// A grand-format image is a legitimately multi-gigabyte stream, and the
2245    /// general ceiling must give way to what the dictionary declares. An
2246    /// earlier corpus-derived cap rejected exactly this class of file.
2247    #[test]
2248    fn a_declared_image_raster_raises_the_ceiling() {
2249        // 60x40 inch at 1200 dpi, 8-bit CMYK: 72000 x 48000 x 4 = 13.8 GB.
2250        let budget = DecodeBudget::for_stream(&dict_from(
2251            b"<</Subtype/Image/Width 72000/Height 48000/BitsPerComponent 8/ColorSpace/DeviceCMYK>>",
2252        ));
2253        assert_eq!(budget.limit(), 72_000usize * 48_000 * 4);
2254    }
2255
2256    /// Declaring a *small* image must not shrink the allowance below the
2257    /// general ceiling — the dictionary raises the bound, never lowers it.
2258    #[test]
2259    fn a_small_declared_image_does_not_lower_the_ceiling() {
2260        let budget = DecodeBudget::for_stream(&dict_from(
2261            b"<</Subtype/Image/Width 8/Height 8/BitsPerComponent 8/ColorSpace/DeviceGray>>",
2262        ));
2263        assert_eq!(budget.limit(), MAX_DECODED_STREAM_BYTES);
2264    }
2265
2266    /// Sub-byte depths pad each row to a byte boundary, so the raster is
2267    /// computed per row rather than from a single product.
2268    #[test]
2269    fn sub_byte_rows_are_padded_to_a_byte_boundary() {
2270        let bytes = declared_image_bytes(&dict_from(
2271            b"<</Width 9/Height 4/BitsPerComponent 1/ColorSpace/DeviceGray>>",
2272        ))
2273        .unwrap();
2274        assert_eq!(bytes, 2 * 4);
2275    }
2276
2277    /// Dimensions outside `stet_graphics::image_limits` are not a licence to
2278    /// raise the ceiling — they fall back to the general allowance.
2279    #[test]
2280    fn out_of_range_dimensions_do_not_raise_the_ceiling() {
2281        for src in [
2282            &b"<</Width 999999999/Height 999999999/BitsPerComponent 8>>"[..],
2283            &b"<</Width -1/Height 10/BitsPerComponent 8>>"[..],
2284            &b"<</Width 10/Height 10/BitsPerComponent 999>>"[..],
2285        ] {
2286            assert_eq!(
2287                DecodeBudget::for_stream(&dict_from(src)).limit(),
2288                MAX_DECODED_STREAM_BYTES,
2289                "{}",
2290                String::from_utf8_lossy(src)
2291            );
2292        }
2293    }
2294
2295    /// An attachment declares its uncompressed length in `/Params /Size`
2296    /// (PDF 32000-1 7.11.4.2), and a large one is legitimate.
2297    #[test]
2298    fn an_embedded_file_size_raises_the_ceiling() {
2299        let budget = DecodeBudget::for_stream(&dict_from(
2300            b"<</Type/EmbeddedFile/Params<</Size 2000000000>>>>",
2301        ));
2302        assert_eq!(budget.limit(), 2_000_000_000);
2303    }
2304}