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/// A single decode filter.
11#[derive(Debug, Clone, PartialEq)]
12pub enum Filter {
13    FlateDecode,
14    LZWDecode,
15    ASCIIHexDecode,
16    ASCII85Decode,
17    RunLengthDecode,
18    DCTDecode,
19    CCITTFaxDecode,
20    JPXDecode,
21    JBIG2Decode,
22}
23
24/// Parse the /Filter and /DecodeParms entries from a stream dict.
25/// Pass a resolver to dereference indirect `/Filter` or `/DecodeParms` values.
26/// `None` is acceptable during bootstrap (xref stream parsing) where no resolver
27/// exists yet and indirect references don't occur.
28pub fn parse_filters(
29    dict: &PdfDict,
30    resolver: Option<&crate::resolver::Resolver>,
31) -> Result<(Vec<Filter>, Vec<Option<PdfDict>>), PdfError> {
32    let filter_obj = match dict.get(b"Filter") {
33        Some(obj) => obj,
34        None => return Ok((Vec::new(), Vec::new())),
35    };
36
37    // Resolve indirect Filter reference if needed
38    let resolved_filter;
39    let filter_obj = if let crate::objects::PdfObj::Ref(_, _) = filter_obj {
40        if let Some(r) = resolver {
41            resolved_filter = r.deref(filter_obj).unwrap_or_else(|_| filter_obj.clone());
42            &resolved_filter
43        } else {
44            filter_obj
45        }
46    } else {
47        filter_obj
48    };
49
50    let filter_names: Vec<&[u8]> = match filter_obj {
51        crate::objects::PdfObj::Name(n) => vec![n.as_slice()],
52        crate::objects::PdfObj::Array(arr) => {
53            // Array elements may also be indirect references
54            arr.iter()
55                .filter_map(|o| {
56                    if let Some(n) = o.as_name() {
57                        return Some(n);
58                    }
59                    None
60                })
61                .collect()
62        }
63        _ => return Ok((Vec::new(), Vec::new())),
64    };
65
66    let mut filters = Vec::new();
67    for name in &filter_names {
68        filters.push(filter_from_name(name)?);
69    }
70
71    // Parse DecodeParms (single dict or array of dicts/refs)
72    let dp_obj = dict.get(b"DecodeParms");
73    let resolved_dp;
74    let dp_obj = match dp_obj {
75        Some(crate::objects::PdfObj::Ref(_, _)) if resolver.is_some() => {
76            resolved_dp = resolver.unwrap().deref(dp_obj.unwrap()).ok();
77            resolved_dp.as_ref()
78        }
79        other => other,
80    };
81
82    let parms = match dp_obj {
83        Some(crate::objects::PdfObj::Dict(d)) => vec![Some(d.clone())],
84        Some(crate::objects::PdfObj::Array(arr)) => arr
85            .iter()
86            .map(|o| match o {
87                crate::objects::PdfObj::Dict(d) => Some(d.clone()),
88                crate::objects::PdfObj::Ref(_, _) if resolver.is_some() => resolver
89                    .unwrap()
90                    .deref(o)
91                    .ok()
92                    .and_then(|r| r.as_dict().cloned()),
93                _ => None,
94            })
95            .collect(),
96        _ => vec![None; filters.len()],
97    };
98
99    // Pad parms to match filters length
100    let mut parms = parms;
101    while parms.len() < filters.len() {
102        parms.push(None);
103    }
104
105    // Fill in CCITT decode hints from the image/stream dict when missing.
106    // PDF image streams always carry /Width and /Height, but malformed producers
107    // sometimes omit the matching /Columns and /Rows in /DecodeParms. Without
108    // /Rows the decoder has no target height and bails out mid-stream on
109    // damaged Group 4 data. Copy them over so the CCITT filter can cap its
110    // row count and pad short output with white scanlines.
111    for (i, filter) in filters.iter().enumerate() {
112        if *filter != Filter::CCITTFaxDecode {
113            continue;
114        }
115        let dp = parms[i].get_or_insert_with(PdfDict::new);
116        if dp.get_int(b"Columns").is_none()
117            && let Some(w) = dict.get_int(b"Width")
118        {
119            dp.insert(b"Columns".to_vec(), crate::objects::PdfObj::Int(w));
120        }
121        if dp.get_int(b"Rows").is_none()
122            && let Some(h) = dict.get_int(b"Height")
123        {
124            dp.insert(b"Rows".to_vec(), crate::objects::PdfObj::Int(h));
125        }
126    }
127
128    Ok((filters, parms))
129}
130
131fn filter_from_name(name: &[u8]) -> Result<Filter, PdfError> {
132    match name {
133        b"FlateDecode" | b"Fl" => Ok(Filter::FlateDecode),
134        b"LZWDecode" | b"LZW" => Ok(Filter::LZWDecode),
135        b"ASCIIHexDecode" | b"AHx" => Ok(Filter::ASCIIHexDecode),
136        b"ASCII85Decode" | b"A85" => Ok(Filter::ASCII85Decode),
137        b"RunLengthDecode" | b"RL" => Ok(Filter::RunLengthDecode),
138        b"DCTDecode" | b"DCT" => Ok(Filter::DCTDecode),
139        b"CCITTFaxDecode" | b"CCF" => Ok(Filter::CCITTFaxDecode),
140        b"JPXDecode" | b"JPX" => Ok(Filter::JPXDecode),
141        b"JBIG2Decode" | b"JBIG2" => Ok(Filter::JBIG2Decode),
142        // Tolerate truncated filter names from malformed PDFs
143        _ if name.starts_with(b"Flate") => Ok(Filter::FlateDecode),
144        _ if name.starts_with(b"LZW") => Ok(Filter::LZWDecode),
145        _ if name.starts_with(b"ASCIIHex") => Ok(Filter::ASCIIHexDecode),
146        _ if name.starts_with(b"ASCII85") => Ok(Filter::ASCII85Decode),
147        _ if name.starts_with(b"RunLength") => Ok(Filter::RunLengthDecode),
148        _ if name.starts_with(b"CCITT") => Ok(Filter::CCITTFaxDecode),
149        _ if name.starts_with(b"JPX") => Ok(Filter::JPXDecode),
150        _ if name.starts_with(b"JBIG2") => Ok(Filter::JBIG2Decode),
151        _ => Err(PdfError::UnsupportedFilter(
152            String::from_utf8_lossy(name).into(),
153        )),
154    }
155}
156
157/// Decode raw stream data through a chain of filters.
158pub fn decode_stream(
159    raw_data: &[u8],
160    filters: &[Filter],
161    decode_parms: &[Option<PdfDict>],
162    jbig2_globals: Option<&[u8]>,
163) -> Result<Vec<u8>, PdfError> {
164    let mut data = raw_data.to_vec();
165
166    for (i, filter) in filters.iter().enumerate() {
167        let parms = decode_parms.get(i).and_then(|p| p.as_ref());
168        data = match filter {
169            Filter::FlateDecode => decode_flate(&data, parms)?,
170            Filter::LZWDecode => decode_lzw(&data, parms)?,
171            Filter::ASCIIHexDecode => decode_ascii_hex(&data)?,
172            Filter::ASCII85Decode => decode_ascii85(&data)?,
173            Filter::RunLengthDecode => decode_run_length(&data)?,
174            Filter::DCTDecode => decode_dct(&data)?,
175            Filter::CCITTFaxDecode => decode_ccittfax(&data, parms)?,
176            #[cfg(feature = "jpx")]
177            Filter::JPXDecode => decode_jpx(&data)?,
178            #[cfg(not(feature = "jpx"))]
179            Filter::JPXDecode => {
180                return Err(PdfError::UnsupportedFilter("JPXDecode (disabled)".into()));
181            }
182            Filter::JBIG2Decode => decode_jbig2(&data, jbig2_globals)?,
183        };
184    }
185
186    Ok(data)
187}
188
189/// FlateDecode (zlib/deflate).
190fn decode_flate(data: &[u8], parms: Option<&PdfDict>) -> Result<Vec<u8>, PdfError> {
191    // Try zlib first. If it ends with an error (truncated output),
192    // also try raw deflate (skip 2-byte zlib header) and pick the longer result.
193    let (zlib_output, zlib_clean, _) = decode_flate_inner(data, true);
194    let output = if zlib_clean {
195        zlib_output?
196    } else {
197        // Zlib hit an error (corrupt checksum, etc).  Try raw deflate (skip
198        // 2-byte zlib header) and prefer it only when zlib clearly truncated
199        // mid-stream.  If zlib consumed (nearly) all input, the data is
200        // complete — the error is just a bad trailing checksum, and raw
201        // deflate may decode garbage past the stream boundary.
202        let zlib_data = zlib_output.unwrap_or_default();
203        if data.len() > 2 {
204            let (raw_output, _, _) = decode_flate_inner(&data[2..], false);
205            let raw_data = raw_output.unwrap_or_default();
206            if raw_data.len() > zlib_data.len()
207                && raw_data[..zlib_data.len()] == zlib_data[..]
208                && looks_like_valid_continuation(&raw_data, zlib_data.len())
209            {
210                // Raw produced more data, the shared prefix matches, and the
211                // extra bytes look like valid content — zlib truncated early
212                // due to a checksum error; use the fuller raw output.
213                raw_data
214            } else if !zlib_data.is_empty() {
215                zlib_data
216            } else if !raw_data.is_empty() {
217                raw_data
218            } else {
219                return Err(PdfError::DecompressionError(
220                    "flate: decompression failed".into(),
221                ));
222            }
223        } else if !zlib_data.is_empty() {
224            zlib_data
225        } else {
226            return Err(PdfError::DecompressionError(
227                "flate: decompression failed".into(),
228            ));
229        }
230    };
231
232    // Apply predictor if specified
233    if let Some(parms) = parms {
234        let predictor = parms.get_int(b"Predictor").unwrap_or(1);
235        if predictor > 1 {
236            return apply_predictor(&output, parms, predictor);
237        }
238    }
239
240    Ok(output)
241}
242
243/// Check whether the extra bytes (past `start`) in `data` look like valid
244/// stream content rather than garbage from decoding past a stream boundary.
245/// Checks a sample of bytes for printable ASCII / whitespace, which is typical
246/// for PDF content streams but not for accidentally-decoded binary data.
247fn looks_like_valid_continuation(data: &[u8], start: usize) -> bool {
248    if start >= data.len() {
249        return false;
250    }
251    // Sample the first 64 bytes of the continuation
252    let sample = &data[start..data.len().min(start + 64)];
253    let printable = sample
254        .iter()
255        .filter(|&&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
256        .count();
257    // If >80% of sampled bytes are printable, it's likely valid content
258    printable * 5 >= sample.len() * 4
259}
260
261/// Inner flate decompression. `zlib` = true uses zlib wrapper, false uses raw deflate.
262/// Returns (Result<data>, clean) where clean=true means StreamEnd was reached normally.
263/// Returns (decompressed_data, clean_finish, bytes_consumed).
264fn decode_flate_inner(data: &[u8], zlib: bool) -> (Result<Vec<u8>, PdfError>, bool, usize) {
265    use flate2::Decompress;
266
267    let mut decompressor = Decompress::new(zlib);
268    let mut output = Vec::with_capacity(data.len() * 3);
269    let mut buf = [0u8; 8192];
270    let mut input_offset = 0;
271
272    loop {
273        let before_in = decompressor.total_in() as usize;
274        let before_out = decompressor.total_out() as usize;
275        let result = decompressor.decompress(
276            &data[input_offset..],
277            &mut buf,
278            flate2::FlushDecompress::None,
279        );
280
281        let consumed = decompressor.total_in() as usize - before_in;
282        let produced = decompressor.total_out() as usize - before_out;
283        input_offset += consumed;
284        output.extend_from_slice(&buf[..produced]);
285
286        match result {
287            Ok(status) => match status {
288                flate2::Status::StreamEnd => return (Ok(output), true, input_offset),
289                flate2::Status::Ok | flate2::Status::BufError => {
290                    if consumed == 0 && produced == 0 {
291                        return (Ok(output), true, input_offset);
292                    }
293                }
294            },
295            Err(_) if !output.is_empty() => {
296                // Partial output — checksum/trailing data error.
297                return (Ok(output), false, input_offset);
298            }
299            Err(e) => {
300                return (
301                    Err(PdfError::DecompressionError(format!("flate: {e}"))),
302                    false,
303                    input_offset,
304                );
305            }
306        }
307    }
308}
309
310/// LZWDecode — native PDF-compatible LZW decoder.
311///
312/// Handles EarlyChange correctly and tolerates premature EOF (missing EOD code),
313/// which is common in real-world PDFs.
314fn decode_lzw(data: &[u8], parms: Option<&PdfDict>) -> Result<Vec<u8>, PdfError> {
315    let early_change = parms.and_then(|p| p.get_int(b"EarlyChange")).unwrap_or(1) != 0;
316
317    let output = lzw_decode(data, early_change)
318        .ok_or_else(|| PdfError::DecompressionError("lzw: decode failed".into()))?;
319
320    // Apply predictor if specified
321    if let Some(parms) = parms {
322        let predictor = parms.get_int(b"Predictor").unwrap_or(1);
323        if predictor > 1 {
324            return apply_predictor(&output, parms, predictor);
325        }
326    }
327
328    Ok(output)
329}
330
331// --- Native PDF LZW decoder ---
332
333const LZW_CLEAR_TABLE: usize = 256;
334const LZW_EOD: usize = 257;
335const LZW_MAX_ENTRIES: usize = 4096;
336const LZW_INITIAL_SIZE: usize = 258;
337
338/// Decode an LZW-compressed byte stream per the PDF spec.
339fn lzw_decode(data: &[u8], early_change: bool) -> Option<Vec<u8>> {
340    let mut table = LzwTable::new(early_change);
341    let mut bit_size = table.code_length();
342    let mut reader = LzwBitReader::new(data);
343    let mut decoded = Vec::new();
344    let mut prev: Option<usize> = None;
345
346    loop {
347        let next = match reader.read(bit_size) {
348            Some(code) => code as usize,
349            None => {
350                // Premature EOF — missing EOD code. Return what we have.
351                return Some(decoded);
352            }
353        };
354
355        match next {
356            LZW_CLEAR_TABLE => {
357                table.clear();
358                prev = None;
359                bit_size = table.code_length();
360            }
361            LZW_EOD => return Some(decoded),
362            new => {
363                if new > table.size() {
364                    // Invalid code — return partial data if we have any
365                    if decoded.is_empty() {
366                        return None;
367                    }
368                    return Some(decoded);
369                }
370
371                if new < table.size() {
372                    let entry = table.get(new)?;
373                    let first_byte = entry[0];
374                    decoded.extend_from_slice(entry);
375
376                    if let Some(prev_code) = prev {
377                        table.register(prev_code, first_byte);
378                    }
379                } else if new == table.size() && prev.is_some() {
380                    // KwKwK case: code references the entry about to be created
381                    let prev_code = prev.unwrap();
382                    let prev_entry = table.get(prev_code)?;
383                    let first_byte = prev_entry[0];
384
385                    let new_entry = table.register(prev_code, first_byte)?;
386                    decoded.extend_from_slice(new_entry);
387                } else {
388                    if decoded.is_empty() {
389                        return None;
390                    }
391                    return Some(decoded);
392                }
393
394                bit_size = table.code_length();
395                prev = Some(new);
396            }
397        }
398    }
399}
400
401/// LZW string table.
402struct LzwTable {
403    early_change: bool,
404    entries: Vec<Option<Vec<u8>>>,
405}
406
407impl LzwTable {
408    fn new(early_change: bool) -> Self {
409        let mut entries: Vec<_> = (0..=255u8).map(|b| Some(vec![b])).collect();
410        entries.push(None); // 256 = CLEAR_TABLE
411        entries.push(None); // 257 = EOD
412        Self {
413            early_change,
414            entries,
415        }
416    }
417
418    fn push(&mut self, entry: Vec<u8>) -> Option<&[u8]> {
419        if self.entries.len() >= LZW_MAX_ENTRIES {
420            None
421        } else {
422            self.entries.push(Some(entry));
423            self.entries.last()?.as_deref()
424        }
425    }
426
427    fn register(&mut self, prev: usize, new_byte: u8) -> Option<&[u8]> {
428        let prev_entry = self.get(prev)?;
429        let mut new_entry = Vec::with_capacity(prev_entry.len() + 1);
430        new_entry.extend(prev_entry);
431        new_entry.push(new_byte);
432        self.push(new_entry)
433    }
434
435    fn get(&self, index: usize) -> Option<&[u8]> {
436        self.entries.get(index)?.as_deref()
437    }
438
439    fn clear(&mut self) {
440        self.entries.truncate(LZW_INITIAL_SIZE);
441    }
442
443    fn size(&self) -> usize {
444        self.entries.len()
445    }
446
447    fn code_length(&self) -> u8 {
448        let adjusted = self.entries.len() + if self.early_change { 1 } else { 0 };
449        if adjusted >= 2048 {
450            12
451        } else if adjusted >= 1024 {
452            11
453        } else if adjusted >= 512 {
454            10
455        } else {
456            9
457        }
458    }
459}
460
461/// MSB-first bit reader for LZW.
462struct LzwBitReader<'a> {
463    data: &'a [u8],
464    bit_pos: usize,
465}
466
467impl<'a> LzwBitReader<'a> {
468    fn new(data: &'a [u8]) -> Self {
469        Self { data, bit_pos: 0 }
470    }
471
472    fn read(&mut self, bit_size: u8) -> Option<u32> {
473        let byte_pos = self.bit_pos / 8;
474        if byte_pos >= self.data.len() {
475            return None;
476        }
477        let bit_offset = self.bit_pos % 8;
478        let end_byte = (self.bit_pos + bit_size as usize - 1) / 8;
479
480        // Read up to 8 bytes into a u64 for extraction
481        let mut buf = [0u8; 8];
482        for (i, b) in buf.iter_mut().enumerate().take(end_byte - byte_pos + 1) {
483            *b = *self.data.get(byte_pos + i)?;
484        }
485        let bits = u64::from_be_bytes(buf);
486        let shift = 64 - bit_offset - bit_size as usize;
487        let mask = (1u64 << bit_size) - 1;
488        let value = ((bits >> shift) & mask) as u32;
489
490        self.bit_pos += bit_size as usize;
491        Some(value)
492    }
493}
494
495/// ASCIIHexDecode.
496fn decode_ascii_hex(data: &[u8]) -> Result<Vec<u8>, PdfError> {
497    let mut result = Vec::with_capacity(data.len() / 2);
498    let mut high: Option<u8> = None;
499
500    for &b in data {
501        if b == b'>' {
502            break;
503        }
504        if b.is_ascii_whitespace() {
505            continue;
506        }
507        let nibble = hex_digit(b)
508            .ok_or_else(|| PdfError::DecompressionError(format!("invalid hex digit: 0x{b:02x}")))?;
509        match high {
510            None => high = Some(nibble),
511            Some(h) => {
512                result.push(h << 4 | nibble);
513                high = None;
514            }
515        }
516    }
517    if let Some(h) = high {
518        result.push(h << 4);
519    }
520
521    Ok(result)
522}
523
524/// ASCII85Decode.
525fn decode_ascii85(data: &[u8]) -> Result<Vec<u8>, PdfError> {
526    let mut result = Vec::with_capacity(data.len() * 4 / 5);
527    let mut tuple: u64 = 0;
528    let mut count = 0u8;
529
530    for &b in data {
531        if b == b'~' {
532            break; // ~> end marker
533        }
534        if b.is_ascii_whitespace() {
535            continue;
536        }
537        if b == b'z' && count == 0 {
538            result.extend_from_slice(&[0, 0, 0, 0]);
539            continue;
540        }
541        if !(b'!'..=b'u').contains(&b) {
542            continue; // skip invalid
543        }
544        tuple = tuple * 85 + (b - b'!') as u64;
545        count += 1;
546        if count == 5 {
547            result.push((tuple >> 24) as u8);
548            result.push((tuple >> 16) as u8);
549            result.push((tuple >> 8) as u8);
550            result.push(tuple as u8);
551            tuple = 0;
552            count = 0;
553        }
554    }
555
556    // Handle remainder
557    if count > 0 {
558        for _ in count..5 {
559            tuple = tuple * 85 + 84; // pad with 'u'
560        }
561        for i in 0..(count - 1) {
562            result.push((tuple >> (24 - i * 8)) as u8);
563        }
564    }
565
566    Ok(result)
567}
568
569/// RunLengthDecode (PackBits).
570fn decode_run_length(data: &[u8]) -> Result<Vec<u8>, PdfError> {
571    let mut result = Vec::new();
572    let mut i = 0;
573
574    while i < data.len() {
575        let length_byte = data[i];
576        i += 1;
577        if length_byte < 128 {
578            // Copy next (length_byte + 1) bytes literally
579            let count = length_byte as usize + 1;
580            if i + count > data.len() {
581                break;
582            }
583            result.extend_from_slice(&data[i..i + count]);
584            i += count;
585        } else if length_byte > 128 {
586            // Repeat next byte (257 - length_byte) times
587            if i >= data.len() {
588                break;
589            }
590            let count = 257 - length_byte as usize;
591            let val = data[i];
592            i += 1;
593            for _ in 0..count {
594                result.push(val);
595            }
596        } else {
597            // 128 = EOD
598            break;
599        }
600    }
601
602    Ok(result)
603}
604
605/// DCTDecode (JPEG).
606/// For PDF image streams, DCTDecode returns raw pixel data.
607/// However, when used as a filter in a filter chain, the JPEG data
608/// is typically the final representation — return the raw JPEG bytes
609/// since the image decoder will handle them. For standalone streams,
610/// decode the JPEG to raw pixels.
611fn decode_dct(data: &[u8]) -> Result<Vec<u8>, PdfError> {
612    use jpeg_decoder::Decoder;
613
614    // wasm32: prefer zune-jpeg over jpeg-decoder as the primary decoder.
615    // jpeg-decoder's wasm SIMD IDCT path traps (raw `unreachable`, bypasses
616    // std::panic::set_hook) on JPEGs whose bitstream ends early — the native
617    // scalar reader returns a clean `Err("failed to fill whole buffer")`
618    // which the fallback chain below catches, but on wasm32 the SIMD path
619    // writes past the end of an intermediate buffer before the stream check
620    // fires, tripping wasm's bounds check. Starting with zune avoids that
621    // code path entirely for the most common JPEGs. jpeg-decoder is still
622    // tried below as a secondary fallback (e.g. for Adobe YCCK streams
623    // where zune's color transform would give wrong results).
624    #[cfg(target_arch = "wasm32")]
625    if let Some(pixels) = decode_dct_via_zune(data) {
626        return Ok(pixels);
627    }
628
629    let mut decoder = Decoder::new(data);
630
631    // Work around jpeg_decoder bug: it checks component IDs (1,2,3) → YCbCr
632    // before checking Adobe APP14 ColorTransform. When ColorTransform=0 (raw
633    // RGB) is present but component IDs are (1,2,3), the decoder incorrectly
634    // applies YCbCr→RGB conversion to already-RGB data. Detect this case and
635    // override with ColorTransform::RGB.
636    if has_adobe_rgb_marker(data) || is_raw_rgb_jpeg(data) {
637        decoder.set_color_transform(jpeg_decoder::ColorTransform::RGB);
638    } else if needs_ycck_override(data) {
639        decoder.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
640    }
641
642    let pixels = match decoder.decode() {
643        Ok(p) => p,
644        Err(e) => {
645            // jpeg_decoder doesn't support 2-component JPEGs (DeviceN spot
646            // color images). Fall back to zune-jpeg which handles arbitrary
647            // component counts.
648            if let Some(pixels) = decode_dct_zune(data) {
649                return Ok(pixels);
650            }
651            // Some JPEGs use DNL (Define Number of Lines) markers to specify
652            // the height after encoding. Patch the SOF header with the DNL
653            // height and retry.
654            if let Some(patched) = patch_jpeg_dnl_height(data) {
655                return decode_dct(&patched);
656            }
657            // Truncated JPEGs: try tolerant decode that returns partial data.
658            if let Some(pixels) = decode_dct_tolerant(data) {
659                return Ok(pixels);
660            }
661            // Last resort: append EOI marker to truncated JPEG and retry.
662            // jpeg-decoder may succeed when the stream is terminated properly.
663            {
664                let mut padded = data.to_vec();
665                // Strip any partial marker at end, then add EOI
666                if padded.last() == Some(&0xFF) {
667                    padded.pop();
668                }
669                padded.extend_from_slice(&[0xFF, 0xD9]);
670                let mut retry_dec = Decoder::new(&padded[..]);
671                if has_adobe_rgb_marker(&padded) || is_raw_rgb_jpeg(&padded) {
672                    retry_dec.set_color_transform(jpeg_decoder::ColorTransform::RGB);
673                } else if needs_ycck_override(&padded) {
674                    retry_dec.set_color_transform(jpeg_decoder::ColorTransform::YCCK);
675                }
676                if let Ok(pixels) = retry_dec.decode() {
677                    // Apply same CMYK inversion as the normal path
678                    if let Some(info) = retry_dec.info()
679                        && info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
680                    {
681                        let mut result = pixels;
682                        for b in result.iter_mut() {
683                            *b = 255 - *b;
684                        }
685                        return Ok(result);
686                    }
687                    return Ok(pixels);
688                }
689            }
690            return Err(PdfError::DecompressionError(format!("DCTDecode: {e}")));
691        }
692    };
693
694    // For 4-component (CMYK) JPEG, the jpeg_decoder applies a CMYK color
695    // transform that inverts all channels (255-x). However, for PDF streams the
696    // raw JPEG data is already in the correct byte order for the PDF /Decode
697    // array to process. Undo the decoder's inversion so the PDF renderer gets
698    // the original sample values.
699    if let Some(info) = decoder.info()
700        && info.pixel_format == jpeg_decoder::PixelFormat::CMYK32
701    {
702        let mut result = pixels;
703        for b in result.iter_mut() {
704            *b = 255 - *b;
705        }
706        return Ok(result);
707    }
708
709    Ok(pixels)
710}
711
712/// Run a closure that may panic, suppressing the panic message and returning
713/// `None` on panic.  Used for third-party JPEG decoders that can panic on
714/// malformed input.
715fn catch_silent<F, T>(f: F) -> Option<T>
716where
717    F: FnOnce() -> Option<T> + std::panic::UnwindSafe,
718{
719    let prev = std::panic::take_hook();
720    std::panic::set_hook(Box::new(|_| {}));
721    let result = std::panic::catch_unwind(f).ok().flatten();
722    std::panic::set_hook(prev);
723    result
724}
725
726/// Primary JPEG decoder on wasm32. Uses zune-jpeg directly (no `catch_silent`
727/// wrapper — `catch_unwind` is a no-op under `panic=abort` and the hook swap
728/// would clobber the WASM panic hook). Picks the output colorspace from the
729/// SOF component count so 1/3/4-component JPEGs all decode to their natural
730/// format. Returns `None` if zune-jpeg can't decode — caller falls back to
731/// jpeg-decoder.
732#[cfg(target_arch = "wasm32")]
733fn decode_dct_via_zune(data: &[u8]) -> Option<Vec<u8>> {
734    use zune_jpeg::JpegDecoder;
735    let n_comps = jpeg_dimensions_and_components(data)
736        .map(|(_, _, n)| n)
737        .unwrap_or(3);
738    let out_cs = match n_comps {
739        1 => zune_core::colorspace::ColorSpace::Luma,
740        4 => zune_core::colorspace::ColorSpace::CMYK,
741        _ => zune_core::colorspace::ColorSpace::RGB,
742    };
743    let options = zune_core::options::DecoderOptions::default().jpeg_set_out_colorspace(out_cs);
744    let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(data), options);
745    decoder.decode().ok()
746}
747
748/// Fallback JPEG decoder using zune-jpeg for component counts that
749/// jpeg_decoder doesn't support (e.g., 2-component DeviceN images).
750fn decode_dct_zune(data: &[u8]) -> Option<Vec<u8>> {
751    use zune_jpeg::JpegDecoder;
752    // Request raw 2-component output (LumaA) to avoid unwanted color
753    // conversion. PDF DeviceN images need the original channel values
754    // for the tinting function.
755    let data = data.to_vec();
756    catch_silent(move || {
757        let options = zune_core::options::DecoderOptions::default()
758            .jpeg_set_out_colorspace(zune_core::colorspace::ColorSpace::LumaA);
759        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
760        decoder.decode().ok()
761    })
762}
763
764/// Tolerant JPEG decoder for truncated streams.
765/// Returns partial pixel data for whatever MCU rows decoded successfully.
766/// Applies the same CMYK channel inversion as the primary decoder path.
767fn decode_dct_tolerant(data: &[u8]) -> Option<Vec<u8>> {
768    // Detect component count from SOF to set the right output colorspace.
769    // Without this, zune-jpeg converts CMYK to RGB, producing wrong data.
770    let n_comps = jpeg_dimensions_and_components(data)
771        .map(|(_, _, n)| n)
772        .unwrap_or(3);
773    let data = data.to_vec();
774    catch_silent(move || {
775        use zune_jpeg::JpegDecoder;
776        let out_cs = match n_comps {
777            1 => zune_core::colorspace::ColorSpace::Luma,
778            4 => zune_core::colorspace::ColorSpace::CMYK,
779            _ => zune_core::colorspace::ColorSpace::RGB,
780        };
781        let options = zune_core::options::DecoderOptions::default()
782            .set_strict_mode(false)
783            .jpeg_set_out_colorspace(out_cs);
784        let mut decoder = JpegDecoder::new_with_options(std::io::Cursor::new(&data), options);
785        decoder.decode().ok()
786    })
787}
788
789/// Patch a JPEG that uses DNL (Define Number of Lines, marker 0xFFDC) to specify
790/// its height. Finds the DNL marker, extracts the height, writes it into the SOF
791/// header, and strips the DNL marker from the scan data so standard decoders can
792/// parse it.
793fn patch_jpeg_dnl_height(data: &[u8]) -> Option<Vec<u8>> {
794    // Find DNL marker (0xFF 0xDC) and extract height
795    let dnl_height = {
796        let mut pos = 0;
797        let mut found = None;
798        while pos + 4 < data.len() {
799            if data[pos] == 0xFF && data[pos + 1] == 0xDC {
800                // DNL: FF DC 00 04 <height_hi> <height_lo>
801                if pos + 5 < data.len() {
802                    let h = ((data[pos + 4] as u16) << 8) | data[pos + 5] as u16;
803                    found = Some((pos, h));
804                }
805                break;
806            }
807            pos += 1;
808        }
809        found
810    };
811    let (dnl_pos, height) = dnl_height?;
812    if height == 0 {
813        return None;
814    }
815
816    // Find SOF marker (0xFFC0..0xFFC3) and patch height field
817    let mut patched = data.to_vec();
818    let mut pos = 2; // skip SOI
819    while pos + 8 < patched.len() {
820        if patched[pos] != 0xFF {
821            pos += 1;
822            continue;
823        }
824        let marker = patched[pos + 1];
825        if (0xC0..=0xC3).contains(&marker) {
826            // SOF: FF Cn LL LL PP HH HH WW WW ...
827            // Height is at offset +5 (2 bytes, big-endian)
828            patched[pos + 5] = (height >> 8) as u8;
829            patched[pos + 6] = (height & 0xFF) as u8;
830            break;
831        }
832        if marker == 0xDA {
833            break; // SOS — stop before scan data
834        }
835        // Skip marker segment
836        if pos + 3 < patched.len() {
837            let seg_len = ((patched[pos + 2] as usize) << 8) | patched[pos + 3] as usize;
838            pos += 2 + seg_len;
839        } else {
840            break;
841        }
842    }
843
844    // Remove the DNL marker (6 bytes: FF DC 00 04 HH HH)
845    if dnl_pos + 6 <= patched.len() {
846        patched.drain(dnl_pos..dnl_pos + 6);
847    }
848
849    Some(patched)
850}
851
852/// Extract image dimensions from a JPEG's SOF marker.
853/// Returns `(width, height)` if found.
854///
855/// Patch the SOF height field in raw JPEG data.
856/// Used when the SOF header has a streaming-encoder placeholder height (e.g.
857/// 60000) that exceeds the PDF dict's authoritative /Height value.
858pub fn patch_jpeg_sof_height(data: &mut [u8], new_height: u16) {
859    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
860        return;
861    }
862    let mut pos = 2;
863    while pos + 4 < data.len() {
864        if data[pos] != 0xFF {
865            pos += 1;
866            continue;
867        }
868        let marker = data[pos + 1];
869        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
870            if pos + 6 < data.len() {
871                data[pos + 5] = (new_height >> 8) as u8;
872                data[pos + 6] = (new_height & 0xFF) as u8;
873            }
874            return;
875        }
876        if marker == 0xDA {
877            return; // SOS — too late
878        }
879        let seg_len = if pos + 3 < data.len() {
880            ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize
881        } else {
882            return;
883        };
884        pos += 2 + seg_len;
885    }
886}
887
888/// Extract width, height, and component count from a JPEG SOF header.
889pub(crate) fn jpeg_dimensions_and_components(data: &[u8]) -> Option<(u32, u32, u8)> {
890    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
891        return None;
892    }
893    let mut pos = 2;
894    while pos + 4 < data.len() {
895        if data[pos] != 0xFF {
896            pos += 1;
897            continue;
898        }
899        let marker = data[pos + 1];
900        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
901            if pos + 9 < data.len() {
902                let h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
903                let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
904                let n = data[pos + 9];
905                return Some((w, h, n));
906            }
907        }
908        if marker == 0xDA {
909            break;
910        }
911        let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
912        pos += 2 + seg_len;
913    }
914    None
915}
916
917/// When the JPEG uses DNL (Define Number of Lines, marker 0xFFDC) — indicated by
918/// a dummy SOF height of 0 or 0xFFFF — scans the bitstream for the DNL marker
919/// and returns its height instead.
920pub fn jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
921    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
922        return None;
923    }
924    let mut pos = 2;
925    while pos + 4 < data.len() {
926        if data[pos] != 0xFF {
927            pos += 1;
928            continue;
929        }
930        let marker = data[pos + 1];
931        // SOF markers: 0xC0-0xCF except 0xC4 (DHT), 0xC8 (JPG), 0xCC (DAC)
932        if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC {
933            if pos + 9 < data.len() {
934                let mut h = ((data[pos + 5] as u32) << 8) | data[pos + 6] as u32;
935                let w = ((data[pos + 7] as u32) << 8) | data[pos + 8] as u32;
936                // SOF height 0 or 0xFFFF means "defined by DNL marker later"
937                if h == 0 || h == 0xFFFF {
938                    if let Some(dnl_h) = find_dnl_height(data) {
939                        h = dnl_h as u32;
940                    }
941                }
942                return Some((w, h));
943            }
944        }
945        if marker == 0xDA {
946            break; // SOS — no more markers
947        }
948        let seg_len = ((data[pos + 2] as usize) << 8) | data[pos + 3] as usize;
949        pos += 2 + seg_len;
950    }
951    None
952}
953
954/// Scan JPEG data for a DNL (Define Number of Lines) marker and return its height.
955fn find_dnl_height(data: &[u8]) -> Option<u16> {
956    let mut pos = 0;
957    while pos + 5 < data.len() {
958        if data[pos] == 0xFF && data[pos + 1] == 0xDC && pos + 5 < data.len() {
959            return Some(((data[pos + 4] as u16) << 8) | data[pos + 5] as u16);
960        }
961        pos += 1;
962    }
963    None
964}
965
966/// Check if a JPEG has Adobe APP14 ColorTransform=0 AND uniform sampling factors,
967/// confirming the data is truly raw RGB (not YCbCr mislabeled with ColorTransform=0).
968/// YCbCr JPEGs use chroma subsampling (e.g., Y=2×2, Cb/Cr=1×1) while RGB JPEGs
969/// use uniform sampling (all components 1×1).
970fn has_adobe_rgb_marker(data: &[u8]) -> bool {
971    let mut has_ct0 = false;
972    let mut uniform_sampling = false;
973    let mut i = 2; // skip SOI
974    while i + 4 < data.len() {
975        if data[i] != 0xFF {
976            break;
977        }
978        let marker = data[i + 1];
979        if marker == 0xDA {
980            break; // SOS — done with headers
981        }
982        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
983        if i + 2 + len > data.len() {
984            break;
985        }
986        // APP14 (Adobe) marker: check ColorTransform
987        // Segment layout: length(2) + "Adobe"(5) + version(2) + flags0(2) + flags1(2) + CT(1) = 14
988        if marker == 0xEE && len >= 14 {
989            let color_transform = data[i + 2 + 13];
990            has_ct0 = color_transform == 0;
991        }
992        // SOF0/SOF2: check sampling factors
993        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
994            let ncomp = data[i + 9] as usize;
995            if ncomp == 3 && i + 10 + ncomp * 3 <= data.len() {
996                let s0 = data[i + 11]; // component 0 sampling
997                let s1 = data[i + 14]; // component 1 sampling
998                let s2 = data[i + 17]; // component 2 sampling
999                uniform_sampling = s0 == s1 && s1 == s2;
1000            }
1001        }
1002        i += 2 + len;
1003    }
1004    has_ct0 && uniform_sampling
1005}
1006
1007/// Detect raw RGB JPEGs that have no APP14/JFIF markers and non-standard
1008/// component IDs (e.g. 0,1,2 instead of the YCbCr standard 1,2,3).
1009/// These JPEGs store raw RGB data — applying YCbCr→RGB conversion produces
1010/// completely wrong colors (e.g. blue → magenta).
1011fn is_raw_rgb_jpeg(data: &[u8]) -> bool {
1012    let mut has_jfif = false;
1013    let mut has_adobe = false;
1014    let mut non_standard_ids = false;
1015    let mut uniform_sampling = false;
1016    let mut n_components = 0u8;
1017    let mut i = 2; // skip SOI
1018    while i + 4 < data.len() {
1019        if data[i] != 0xFF {
1020            break;
1021        }
1022        let marker = data[i + 1];
1023        if marker == 0xDA {
1024            break;
1025        }
1026        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1027        if i + 2 + len > data.len() {
1028            break;
1029        }
1030        if marker == 0xE0 && len >= 7 && &data[i + 4..i + 9] == b"JFIF\x00" {
1031            has_jfif = true;
1032        }
1033        if marker == 0xEE && len >= 7 && &data[i + 4..i + 9] == b"Adobe" {
1034            has_adobe = true;
1035        }
1036        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
1037            n_components = data[i + 9];
1038            if n_components == 3 && i + 10 + 9 <= data.len() {
1039                let id0 = data[i + 10];
1040                let id1 = data[i + 13];
1041                let id2 = data[i + 16];
1042                // Standard YCbCr uses IDs (1,2,3). Anything else suggests raw RGB.
1043                non_standard_ids = !(id0 == 1 && id1 == 2 && id2 == 3);
1044                let s0 = data[i + 11];
1045                let s1 = data[i + 14];
1046                let s2 = data[i + 17];
1047                uniform_sampling = s0 == s1 && s1 == s2;
1048            }
1049        }
1050        i += 2 + len;
1051    }
1052    // Raw RGB: 3 components, non-standard IDs, uniform sampling, no JFIF/Adobe markers
1053    n_components == 3 && non_standard_ids && uniform_sampling && !has_jfif && !has_adobe
1054}
1055
1056/// Work around jpeg_decoder bug: it checks `"Adobe\0"` (6 bytes) in APP14
1057/// but the spec defines only 5-byte `"Adobe"`. The 6th byte is the high byte
1058/// of the version field. When version >= 256 (high byte != 0), jpeg_decoder
1059/// misses the APP14 marker entirely and misidentifies YCCK as plain CMYK.
1060/// Returns true when the last APP14 has ColorTransform=2 (YCCK) and jpeg_decoder
1061/// would fail to detect it.
1062fn needs_ycck_override(data: &[u8]) -> bool {
1063    let mut last_ct = None;
1064    let mut decoder_would_miss = false;
1065    let mut n_components = 0u8;
1066    let mut i = 2; // skip SOI
1067    while i + 4 < data.len() {
1068        if data[i] != 0xFF {
1069            break;
1070        }
1071        let marker = data[i + 1];
1072        if marker == 0xDA {
1073            break; // SOS
1074        }
1075        let len = u16::from_be_bytes([data[i + 2], data[i + 3]]) as usize;
1076        if i + 2 + len > data.len() {
1077            break;
1078        }
1079        // APP14 (Adobe): "Adobe"(5) + version(2) + flags0(2) + flags1(2) + CT(1)
1080        if marker == 0xEE && len >= 14 && &data[i + 4..i + 9] == b"Adobe" {
1081            let ct = data[i + 2 + 13];
1082            last_ct = Some(ct);
1083            // jpeg_decoder checks data[0..6] == "Adobe\0", so byte index 5
1084            // (= data[i+9], the version high byte) must be 0 for it to detect.
1085            decoder_would_miss = data[i + 9] != 0;
1086        }
1087        // SOF0/SOF2: get number of components
1088        if (marker == 0xC0 || marker == 0xC2) && i + 9 < data.len() {
1089            n_components = data[i + 9];
1090        }
1091        i += 2 + len;
1092    }
1093    // Only override when: last APP14 says YCCK, jpeg_decoder would miss it,
1094    // and the JPEG has 4 components (CMYK/YCCK domain).
1095    last_ct == Some(2) && decoder_would_miss && n_components == 4
1096}
1097
1098/// CCITTFaxDecode (Group 3 / Group 4 fax compression).
1099fn decode_ccittfax(data: &[u8], parms: Option<&PdfDict>) -> Result<Vec<u8>, PdfError> {
1100    use crate::objects::PdfObj;
1101
1102    let k = parms.and_then(|p| p.get_int(b"K")).unwrap_or(0) as i32;
1103    let columns = parms.and_then(|p| p.get_int(b"Columns")).unwrap_or(1728) as u16;
1104    let rows_limit = parms.and_then(|p| p.get_int(b"Rows")).unwrap_or(0) as u32;
1105    let end_of_block = parms
1106        .and_then(|p| match p.get(b"EndOfBlock") {
1107            Some(PdfObj::Bool(b)) => Some(*b),
1108            _ => None,
1109        })
1110        .unwrap_or(true);
1111    let black_is1 = parms
1112        .and_then(|p| match p.get(b"BlackIs1") {
1113            Some(PdfObj::Bool(b)) => Some(*b),
1114            _ => None,
1115        })
1116        .unwrap_or(false);
1117
1118    let encoded_byte_align = parms
1119        .and_then(|p| match p.get(b"EncodedByteAlign") {
1120            Some(PdfObj::Bool(b)) => Some(*b),
1121            _ => None,
1122        })
1123        .unwrap_or(false);
1124
1125    let encoding = if k < 0 {
1126        hayro_ccitt::EncodingMode::Group4
1127    } else if k == 0 {
1128        hayro_ccitt::EncodingMode::Group3_1D
1129    } else {
1130        hayro_ccitt::EncodingMode::Group3_2D { k: k as u32 }
1131    };
1132
1133    let settings = hayro_ccitt::DecodeSettings {
1134        columns: columns as u32,
1135        rows: if rows_limit > 0 { rows_limit } else { u32::MAX },
1136        end_of_block,
1137        end_of_line: false,
1138        rows_are_byte_aligned: encoded_byte_align,
1139        encoding,
1140        invert_black: false,
1141    };
1142
1143    decode_ccitt_hayro(data, &settings, black_is1)
1144}
1145
1146/// A byte-oriented CCITT pixel decoder used by hayro-ccitt.
1147/// Packs decoded pixels into bytes (MSB first), with `black_is1` polarity control.
1148struct CcittByteDecoder {
1149    output: Vec<u8>,
1150    current_byte: u8,
1151    bit_pos: u8,
1152    black_is1: bool,
1153}
1154
1155impl CcittByteDecoder {
1156    fn new(black_is1: bool) -> Self {
1157        Self {
1158            output: Vec::new(),
1159            current_byte: 0,
1160            bit_pos: 0,
1161            black_is1,
1162        }
1163    }
1164
1165    fn flush_byte(&mut self) {
1166        if self.bit_pos > 0 {
1167            // Shift remaining bits to MSB position and pad
1168            let remaining = 8 - self.bit_pos;
1169            self.current_byte <<= remaining;
1170            if !self.black_is1 {
1171                // Pad unfilled bits as white (1)
1172                self.current_byte |= (1u8 << remaining) - 1;
1173            }
1174            self.output.push(self.current_byte);
1175            self.current_byte = 0;
1176            self.bit_pos = 0;
1177        }
1178    }
1179}
1180
1181impl hayro_ccitt::Decoder for CcittByteDecoder {
1182    fn push_pixel(&mut self, white: bool) {
1183        // black_is1=true: black=1, white=0
1184        // black_is1=false: black=0, white=1
1185        let bit = if self.black_is1 { !white } else { white };
1186        self.current_byte = (self.current_byte << 1) | (bit as u8);
1187        self.bit_pos += 1;
1188        if self.bit_pos == 8 {
1189            self.output.push(self.current_byte);
1190            self.current_byte = 0;
1191            self.bit_pos = 0;
1192        }
1193    }
1194
1195    fn push_pixel_chunk(&mut self, white: bool, chunk_count: u32) {
1196        // If there are partial bits pending, we can't directly push bytes —
1197        // the bit boundary wouldn't align. Fall back to pixel-by-pixel.
1198        if self.bit_pos != 0 {
1199            for _ in 0..chunk_count * 8 {
1200                self.push_pixel(white);
1201            }
1202            return;
1203        }
1204        let byte = if (self.black_is1 && !white) || (!self.black_is1 && white) {
1205            0xFF
1206        } else {
1207            0x00
1208        };
1209        for _ in 0..chunk_count {
1210            self.output.push(byte);
1211        }
1212    }
1213
1214    fn next_line(&mut self) {
1215        self.flush_byte();
1216    }
1217}
1218
1219/// Decode CCITT data using hayro-ccitt (supports Group 3 and Group 4), with
1220/// a fall-through to the `fax` crate when hayro rejects the stream with a
1221/// hard error (Overflow, InvalidCode, LineLengthMismatch). The `fax` crate is
1222/// more lenient with malformed Group 4 streams produced by old Acrobat
1223/// Distiller versions, where hayro's strict position-arithmetic checks can
1224/// bail out mid-stream even though the image is still decodable.
1225fn decode_ccitt_hayro(
1226    data: &[u8],
1227    settings: &hayro_ccitt::DecodeSettings,
1228    black_is1: bool,
1229) -> Result<Vec<u8>, PdfError> {
1230    let mut decoder = CcittByteDecoder::new(black_is1);
1231    let hayro_err = hayro_ccitt::decode(data, &mut decoder, settings).err();
1232
1233    // If hayro failed with anything other than a soft EOF, try `fax` as a
1234    // fallback. Keep whichever decoder produced more byte output.
1235    if let Some(e) = hayro_err
1236        && e != hayro_ccitt::DecodeError::UnexpectedEof
1237    {
1238        let fallback = decode_ccitt_fax(data, settings, black_is1);
1239        use std::sync::atomic::{AtomicBool, Ordering};
1240        static WARNED: AtomicBool = AtomicBool::new(false);
1241        if fallback.len() > decoder.output.len() {
1242            if !WARNED.swap(true, Ordering::Relaxed) {
1243                eprintln!(
1244                    "[CCITT] hayro-ccitt error: {} — fell back to `fax` crate",
1245                    e
1246                );
1247            }
1248            return Ok(fallback);
1249        }
1250        if !WARNED.swap(true, Ordering::Relaxed) {
1251            eprintln!("[CCITT] decode warning: {} (using partial data)", e);
1252        }
1253    }
1254    Ok(decoder.output)
1255}
1256
1257/// Decode CCITT data using the `fax` crate as a fallback. Returns a byte-packed
1258/// buffer with the same polarity/layout as the hayro path.
1259fn decode_ccitt_fax(
1260    data: &[u8],
1261    settings: &hayro_ccitt::DecodeSettings,
1262    black_is1: bool,
1263) -> Vec<u8> {
1264    let width = settings.columns as u16;
1265    let row_bytes = settings.columns.div_ceil(8) as usize;
1266    let mut out: Vec<u8> = Vec::new();
1267    // Byte value for a full chunk of "white" and "black" pixels after polarity.
1268    // black_is1=false (PDF default): 0=black, 1=white → white row = 0xFF, black = 0x00
1269    // black_is1=true: 0=white, 1=black → white row = 0x00, black = 0xFF
1270    let white_byte: u8 = if black_is1 { 0x00 } else { 0xFF };
1271    let black_byte: u8 = !white_byte;
1272
1273    let rows_limit = if settings.rows == u32::MAX || settings.rows == 0 {
1274        None
1275    } else {
1276        Some(settings.rows.min(u16::MAX as u32) as u16)
1277    };
1278
1279    let mut emit_row = |transitions: &[u16]| {
1280        // Rebuild one packed row from the transition list.
1281        let mut row = vec![white_byte; row_bytes];
1282        // Row starts white; each transition flips color starting at that index.
1283        let mut color_white = true;
1284        let mut cursor: u16 = 0;
1285        // Add the sentinel `width` transition so we close the final run.
1286        let iter = transitions.iter().copied().chain(std::iter::once(width));
1287        for next in iter {
1288            let end = next.min(width);
1289            if !color_white && end > cursor {
1290                fill_bits(&mut row, cursor as usize, end as usize, black_byte != 0);
1291            }
1292            color_white = !color_white;
1293            cursor = end;
1294            if cursor >= width {
1295                break;
1296            }
1297        }
1298        out.extend_from_slice(&row);
1299    };
1300
1301    match settings.encoding {
1302        hayro_ccitt::EncodingMode::Group4 => {
1303            let _ = fax::decoder::decode_g4(data.iter().copied(), width, rows_limit, &mut emit_row);
1304        }
1305        hayro_ccitt::EncodingMode::Group3_1D | hayro_ccitt::EncodingMode::Group3_2D { .. } => {
1306            let _ = fax::decoder::decode_g3(data.iter().copied(), &mut emit_row);
1307        }
1308    }
1309
1310    // Pad truncated output with white scanlines so downstream image handling
1311    // sees the full-height buffer. Without this, a Group 4 stream that the
1312    // decoder can't finish (malformed PDF) would produce a buffer short by
1313    // thousands of bytes; the image code fills the missing rows with zeros,
1314    // which lands as a solid black rectangle covering part of the page.
1315    if let Some(target_rows) = rows_limit {
1316        let expected = row_bytes * target_rows as usize;
1317        if out.len() < expected {
1318            out.resize(expected, white_byte);
1319        }
1320    }
1321
1322    out
1323}
1324
1325/// Flip bits in a byte-packed (MSB-first) row between `[start, end)` to black.
1326/// `start`/`end` are pixel indices; the buffer is pre-filled with the "white"
1327/// polarity, so this routine only needs to set the black-colored runs.
1328fn fill_bits(row: &mut [u8], start: usize, end: usize, black_is_one: bool) {
1329    if end <= start {
1330        return;
1331    }
1332    for x in start..end {
1333        let byte = x / 8;
1334        let bit = 0x80u8 >> (x % 8);
1335        if black_is_one {
1336            row[byte] |= bit;
1337        } else {
1338            row[byte] &= !bit;
1339        }
1340    }
1341}
1342
1343/// JBIG2Decode.
1344fn decode_jbig2(data: &[u8], globals: Option<&[u8]>) -> Result<Vec<u8>, PdfError> {
1345    // Native builds run the decode on a sidecar thread with a 2-second
1346    // watchdog, guarding against malformed streams that hang the decoder
1347    // (e.g. issue15942.pdf). wasm32-unknown-unknown has no thread support,
1348    // so the watchdog is skipped there and we call the decoder directly —
1349    // a hanging stream will hang the page, but normal streams (like those
1350    // in pdf_samples/1321.pdf) will now decode instead of panicking at
1351    // `std::thread::spawn`.
1352    #[cfg(not(target_arch = "wasm32"))]
1353    let image = {
1354        let data_owned = data.to_vec();
1355        let globals_owned = globals.map(|g| g.to_vec());
1356        let (tx, rx) = std::sync::mpsc::channel();
1357        std::thread::spawn(move || {
1358            let result = hayro_jbig2::decode_embedded(&data_owned, globals_owned.as_deref());
1359            let _ = tx.send(result);
1360        });
1361        // Scale timeout with data size: 5s base + 5s per MB of compressed data.
1362        // Large scanned-document pages (e.g. 19k×25k bilevel at 2MB) need more
1363        // than the original 2s, while the watchdog still catches malformed
1364        // streams that hang the decoder indefinitely.
1365        let timeout_secs = 5 + (data.len() as u64 / (1024 * 1024)) * 5;
1366        rx.recv_timeout(std::time::Duration::from_secs(timeout_secs))
1367            .map_err(|_| PdfError::DecompressionError("JBIG2: decode timed out".into()))?
1368            .map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?
1369    };
1370
1371    #[cfg(target_arch = "wasm32")]
1372    let image = hayro_jbig2::decode_embedded(data, globals)
1373        .map_err(|e| PdfError::DecompressionError(format!("JBIG2: {e}")))?;
1374
1375    // Convert Vec<bool> to packed bytes (8 pixels/byte, MSB first)
1376    // JBIG2: true = black, false = white
1377    // PDF DeviceGray: 0 = black, 1 = white
1378    // So: start all-white (0xFF), clear bits for black pixels
1379    let row_bytes = (image.width as usize).div_ceil(8);
1380    let mut packed = vec![0xFFu8; row_bytes * image.height as usize];
1381    for y in 0..image.height as usize {
1382        for x in 0..image.width as usize {
1383            if image.data[y * image.width as usize + x] {
1384                packed[y * row_bytes + x / 8] &= !(0x80 >> (x % 8));
1385            }
1386        }
1387    }
1388    Ok(packed)
1389}
1390
1391/// JPXDecode (JPEG 2000).
1392///
1393/// Uses hayro-jpeg2000 to decode JP2 or raw J2K codestreams into interleaved pixel data.
1394#[cfg(feature = "jpx")]
1395fn decode_jpx(data: &[u8]) -> Result<Vec<u8>, PdfError> {
1396    if data.is_empty() {
1397        return Ok(Vec::new());
1398    }
1399
1400    let image = hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default())
1401        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1402
1403    image
1404        .decode()
1405        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))
1406}
1407
1408/// JPXDecode without resolving the JP2-internal palette.
1409///
1410/// Some Adobe-generated JP2 files declare 4-bit palette column precision but
1411/// store 8-bit values.  hayro-jpeg2000's palette resolution rescales based on
1412/// the declared precision, corrupting the colors.  When the PDF provides its
1413/// own Indexed color space, we skip the JP2 palette and let the PDF lookup
1414/// table handle it.
1415///
1416/// Returns `(decoded_data, original_bit_depth)`.  The original bit depth is
1417/// needed to un-normalize hayro's 8-bit output back to raw palette indices
1418/// (hayro rescales sub-8-bit data to 0-255).
1419#[cfg(feature = "jpx")]
1420pub fn decode_jpx_no_palette(data: &[u8]) -> Result<(Vec<u8>, u8), PdfError> {
1421    if data.is_empty() {
1422        return Ok((Vec::new(), 8));
1423    }
1424
1425    let settings = hayro_jpeg2000::DecodeSettings {
1426        resolve_palette_indices: false,
1427        ..Default::default()
1428    };
1429    let image = hayro_jpeg2000::Image::new(data, &settings)
1430        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1431    let bit_depth = image.original_bit_depth();
1432
1433    let pixels = image
1434        .decode()
1435        .map_err(|e| PdfError::DecompressionError(format!("JPXDecode: {e}")))?;
1436    Ok((pixels, bit_depth))
1437}
1438
1439/// Query the number of color channels (excluding alpha) and whether alpha is
1440/// present in a JPEG 2000 image, without fully decoding the pixel data.
1441/// Returns `(color_channels, has_alpha)`.
1442#[cfg(feature = "jpx")]
1443pub fn jpx_color_info(data: &[u8]) -> Option<(u8, bool)> {
1444    let image =
1445        hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
1446    Some((image.color_space().num_channels(), image.has_alpha()))
1447}
1448
1449/// Extract image dimensions from a JPEG 2000 stream without full decode.
1450/// Returns `(width, height)`.
1451#[cfg(feature = "jpx")]
1452pub fn jpx_dimensions(data: &[u8]) -> Option<(u32, u32)> {
1453    let image =
1454        hayro_jpeg2000::Image::new(data, &hayro_jpeg2000::DecodeSettings::default()).ok()?;
1455    Some((image.width(), image.height()))
1456}
1457
1458/// Decode filters preceding JPXDecode in a filter chain (e.g. ASCIIHexDecode).
1459/// Returns the raw JP2/J2K data ready for `jpx_dimensions` / `jpx_color_info`.
1460pub fn decode_pre_jpx(raw: &[u8], dict: &crate::objects::PdfDict) -> Vec<u8> {
1461    let (filters, parms) = parse_filters(dict, None).unwrap_or_default();
1462    // Apply all filters except JPXDecode
1463    let pre_count = filters
1464        .iter()
1465        .take_while(|f| !matches!(f, Filter::JPXDecode))
1466        .count();
1467    if pre_count == 0 {
1468        return raw.to_vec();
1469    }
1470    let pre_parms: Vec<_> = parms.into_iter().take(pre_count).collect();
1471    decode_stream(raw, &filters[..pre_count], &pre_parms, None).unwrap_or_else(|_| raw.to_vec())
1472}
1473
1474/// Apply PNG or TIFF predictor to decoded data.
1475fn apply_predictor(data: &[u8], parms: &PdfDict, predictor: i64) -> Result<Vec<u8>, PdfError> {
1476    let columns = parms.get_int(b"Columns").unwrap_or(1) as usize;
1477    let colors = parms.get_int(b"Colors").unwrap_or(1) as usize;
1478    let bpc = parms.get_int(b"BitsPerComponent").unwrap_or(8) as usize;
1479
1480    let bytes_per_pixel = (colors * bpc).div_ceil(8);
1481    let row_bytes = (columns * colors * bpc).div_ceil(8);
1482
1483    if predictor == 2 {
1484        // TIFF horizontal differencing
1485        if bpc < 8 {
1486            // Sub-byte samples: operate at sample level, not byte level
1487            apply_tiff_predictor_subbyte(data, columns, colors, bpc, row_bytes)
1488        } else if bpc == 16 {
1489            // 16-bit samples: add as 16-bit values, not byte-by-byte
1490            apply_tiff_predictor_16bit(data, columns, colors, row_bytes)
1491        } else {
1492            apply_tiff_predictor(data, row_bytes, bytes_per_pixel)
1493        }
1494    } else if predictor >= 10 {
1495        // PNG predictors
1496        apply_png_predictor(data, row_bytes, bytes_per_pixel)
1497    } else {
1498        Ok(data.to_vec())
1499    }
1500}
1501
1502/// TIFF predictor 2 for sub-byte samples (BPC = 1, 2, or 4).
1503/// Operates at the individual sample level within packed bytes.
1504fn apply_tiff_predictor_subbyte(
1505    data: &[u8],
1506    columns: usize,
1507    colors: usize,
1508    bpc: usize,
1509    row_bytes: usize,
1510) -> Result<Vec<u8>, PdfError> {
1511    let samples_per_row = columns * colors;
1512    let mask = (1u8 << bpc) - 1; // e.g., 1 for bpc=1, 3 for bpc=2, 15 for bpc=4
1513    let mut result = Vec::with_capacity(data.len());
1514
1515    for row in data.chunks(row_bytes) {
1516        let mut out_row = vec![0u8; row.len()];
1517        // Copy the raw bytes first, then undo differencing at sample level
1518        out_row[..row.len()].copy_from_slice(row);
1519
1520        // Extract all samples, undo differencing, re-pack
1521        let mut prev = vec![0u8; colors];
1522        for col in 0..columns {
1523            for c in 0..colors {
1524                let sample_idx = col * colors + c;
1525                if sample_idx >= samples_per_row {
1526                    break;
1527                }
1528                let bit_offset = sample_idx * bpc;
1529                let byte_idx = bit_offset / 8;
1530                let bit_pos = 8 - bpc - (bit_offset % 8); // MSB-first packing
1531                if byte_idx >= row.len() {
1532                    break;
1533                }
1534                let encoded = (row[byte_idx] >> bit_pos) & mask;
1535                let decoded = (encoded.wrapping_add(prev[c])) & mask;
1536                prev[c] = decoded;
1537                // Write back
1538                out_row[byte_idx] = (out_row[byte_idx] & !(mask << bit_pos)) | (decoded << bit_pos);
1539            }
1540        }
1541        result.extend_from_slice(&out_row);
1542    }
1543
1544    Ok(result)
1545}
1546
1547/// TIFF predictor 2 for 16-bit samples.
1548///
1549/// Each sample is 2 bytes (big-endian). The byte-level predictor doesn't
1550/// propagate carry between high and low bytes, producing wrong results.
1551fn apply_tiff_predictor_16bit(
1552    data: &[u8],
1553    columns: usize,
1554    colors: usize,
1555    row_bytes: usize,
1556) -> Result<Vec<u8>, PdfError> {
1557    let mut result = Vec::with_capacity(data.len());
1558
1559    for row in data.chunks(row_bytes) {
1560        let mut out_row = vec![0u8; row.len()];
1561        let mut prev = vec![0u16; colors];
1562
1563        for col in 0..columns {
1564            for c in 0..colors {
1565                let byte_idx = (col * colors + c) * 2;
1566                if byte_idx + 1 >= row.len() {
1567                    break;
1568                }
1569                let encoded = u16::from_be_bytes([row[byte_idx], row[byte_idx + 1]]);
1570                let decoded = encoded.wrapping_add(prev[c]);
1571                prev[c] = decoded;
1572                let [hi, lo] = decoded.to_be_bytes();
1573                out_row[byte_idx] = hi;
1574                out_row[byte_idx + 1] = lo;
1575            }
1576        }
1577        result.extend_from_slice(&out_row);
1578    }
1579
1580    Ok(result)
1581}
1582
1583/// TIFF predictor 2: horizontal differencing.
1584fn apply_tiff_predictor(
1585    data: &[u8],
1586    row_bytes: usize,
1587    bytes_per_pixel: usize,
1588) -> Result<Vec<u8>, PdfError> {
1589    let mut result = Vec::with_capacity(data.len());
1590
1591    for row in data.chunks(row_bytes) {
1592        let mut out_row = vec![0u8; row.len()];
1593        for i in 0..row.len() {
1594            let left = if i >= bytes_per_pixel {
1595                out_row[i - bytes_per_pixel]
1596            } else {
1597                0
1598            };
1599            out_row[i] = row[i].wrapping_add(left);
1600        }
1601        result.extend_from_slice(&out_row);
1602    }
1603
1604    Ok(result)
1605}
1606
1607/// PNG predictors (10-15): per-row predictor byte.
1608fn apply_png_predictor(
1609    data: &[u8],
1610    row_bytes: usize,
1611    bytes_per_pixel: usize,
1612) -> Result<Vec<u8>, PdfError> {
1613    // Each row has a leading predictor byte + row_bytes data bytes
1614    let stride = row_bytes + 1;
1615
1616    // Detect data that lacks predictor bytes despite DecodeParms claiming them.
1617    // If data divides evenly into row_bytes but NOT into stride, the stream
1618    // was written without per-row predictor prefixes — return as-is.
1619    if row_bytes > 0
1620        && !data.is_empty()
1621        && data.len().is_multiple_of(row_bytes)
1622        && !data.len().is_multiple_of(stride)
1623    {
1624        return Ok(data.to_vec());
1625    }
1626
1627    let num_rows = data.len() / stride;
1628    let mut result = Vec::with_capacity(num_rows * row_bytes);
1629    let mut prev_row = vec![0u8; row_bytes];
1630
1631    for row_idx in 0..num_rows {
1632        let row_start = row_idx * stride;
1633        if row_start >= data.len() {
1634            break;
1635        }
1636        let filter_type = data[row_start];
1637        let row_data = &data[row_start + 1..std::cmp::min(row_start + stride, data.len())];
1638        let mut out_row = vec![0u8; row_data.len()];
1639
1640        match filter_type {
1641            0 => {
1642                // None
1643                out_row.copy_from_slice(row_data);
1644            }
1645            1 => {
1646                // Sub
1647                for i in 0..row_data.len() {
1648                    let left = if i >= bytes_per_pixel {
1649                        out_row[i - bytes_per_pixel]
1650                    } else {
1651                        0
1652                    };
1653                    out_row[i] = row_data[i].wrapping_add(left);
1654                }
1655            }
1656            2 => {
1657                // Up
1658                for i in 0..row_data.len() {
1659                    let up = if i < prev_row.len() { prev_row[i] } else { 0 };
1660                    out_row[i] = row_data[i].wrapping_add(up);
1661                }
1662            }
1663            3 => {
1664                // Average
1665                for i in 0..row_data.len() {
1666                    let left = if i >= bytes_per_pixel {
1667                        out_row[i - bytes_per_pixel] as u16
1668                    } else {
1669                        0
1670                    };
1671                    let up = if i < prev_row.len() {
1672                        prev_row[i] as u16
1673                    } else {
1674                        0
1675                    };
1676                    out_row[i] = row_data[i].wrapping_add(((left + up) / 2) as u8);
1677                }
1678            }
1679            4 => {
1680                // Paeth
1681                for i in 0..row_data.len() {
1682                    let left = if i >= bytes_per_pixel {
1683                        out_row[i - bytes_per_pixel]
1684                    } else {
1685                        0
1686                    };
1687                    let up = if i < prev_row.len() { prev_row[i] } else { 0 };
1688                    let up_left = if i >= bytes_per_pixel && i - bytes_per_pixel < prev_row.len() {
1689                        prev_row[i - bytes_per_pixel]
1690                    } else {
1691                        0
1692                    };
1693                    out_row[i] = row_data[i].wrapping_add(paeth(left, up, up_left));
1694                }
1695            }
1696            _ => {
1697                // Unknown predictor type — pass through
1698                out_row.copy_from_slice(row_data);
1699            }
1700        }
1701
1702        prev_row[..out_row.len()].copy_from_slice(&out_row);
1703        result.extend_from_slice(&out_row);
1704    }
1705
1706    Ok(result)
1707}
1708
1709/// Paeth predictor function.
1710fn paeth(a: u8, b: u8, c: u8) -> u8 {
1711    let a = a as i16;
1712    let b = b as i16;
1713    let c = c as i16;
1714    let p = a + b - c;
1715    let pa = (p - a).abs();
1716    let pb = (p - b).abs();
1717    let pc = (p - c).abs();
1718    if pa <= pb && pa <= pc {
1719        a as u8
1720    } else if pb <= pc {
1721        b as u8
1722    } else {
1723        c as u8
1724    }
1725}
1726
1727fn hex_digit(b: u8) -> Option<u8> {
1728    match b {
1729        b'0'..=b'9' => Some(b - b'0'),
1730        b'a'..=b'f' => Some(b - b'a' + 10),
1731        b'A'..=b'F' => Some(b - b'A' + 10),
1732        _ => None,
1733    }
1734}
1735
1736#[cfg(test)]
1737mod tests {
1738    use super::*;
1739
1740    #[test]
1741    fn flate_round_trip() {
1742        use flate2::Compression;
1743        use flate2::write::ZlibEncoder;
1744        use std::io::Write;
1745
1746        let original = b"Hello, PDF world! This is a test of FlateDecode.";
1747        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
1748        enc.write_all(original).unwrap();
1749        let compressed = enc.finish().unwrap();
1750
1751        let decoded = decode_flate(&compressed, None).unwrap();
1752        assert_eq!(&decoded, original);
1753    }
1754
1755    #[test]
1756    fn ascii_hex_decode() {
1757        let decoded = decode_ascii_hex(b"48656C6C6F>").unwrap();
1758        assert_eq!(&decoded, b"Hello");
1759    }
1760
1761    #[test]
1762    fn ascii_hex_odd_digits() {
1763        let decoded = decode_ascii_hex(b"ABC>").unwrap();
1764        assert_eq!(decoded, vec![0xAB, 0xC0]);
1765    }
1766
1767    #[test]
1768    fn ascii85_decode() {
1769        // "Hello" in ASCII85 = 87cURD]j7
1770        // Full encoding: <~87cURD]j7BEbo7~>  (for "Hello, World")
1771        // Simple test: encode "test" = FCfN8
1772        let decoded = decode_ascii85(b"FCfN8~>").unwrap();
1773        assert_eq!(&decoded, b"test");
1774    }
1775
1776    #[test]
1777    fn ascii85_z_shortcut() {
1778        let decoded = decode_ascii85(b"z~>").unwrap();
1779        assert_eq!(decoded, vec![0, 0, 0, 0]);
1780    }
1781
1782    #[test]
1783    fn run_length_decode() {
1784        // 2 = copy 3 bytes, then 253 = repeat next byte 4 times, then 128 = EOD
1785        let data = vec![2, b'A', b'B', b'C', 253, b'X', 128];
1786        let decoded = decode_run_length(&data).unwrap();
1787        assert_eq!(&decoded, b"ABCXXXX");
1788    }
1789
1790    #[test]
1791    fn png_predictor_none() {
1792        // Row of 3 bytes, predictor type 0 (none)
1793        let data = vec![0, 10, 20, 30];
1794        let result = apply_png_predictor(&data, 3, 1).unwrap();
1795        assert_eq!(result, vec![10, 20, 30]);
1796    }
1797
1798    #[test]
1799    fn png_predictor_sub() {
1800        // Row of 3 bytes, predictor type 1 (sub), bpp=1
1801        // input: [5, 3, 4] -> output: [5, 8, 12]
1802        let data = vec![1, 5, 3, 4];
1803        let result = apply_png_predictor(&data, 3, 1).unwrap();
1804        assert_eq!(result, vec![5, 8, 12]);
1805    }
1806
1807    #[test]
1808    fn png_predictor_up() {
1809        // Two rows, predictor type 2 (up)
1810        // Row 0: [0, 10, 20, 30]  (type 0 = none)
1811        // Row 1: [2, 5, 5, 5]    (type 2 = up)
1812        let data = vec![0, 10, 20, 30, 2, 5, 5, 5];
1813        let result = apply_png_predictor(&data, 3, 1).unwrap();
1814        assert_eq!(result, vec![10, 20, 30, 15, 25, 35]);
1815    }
1816
1817    #[test]
1818    fn filter_chain() {
1819        use flate2::Compression;
1820        use flate2::write::ZlibEncoder;
1821        use std::io::Write;
1822
1823        let original = b"filter chain test data";
1824        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
1825        enc.write_all(original).unwrap();
1826        let compressed = enc.finish().unwrap();
1827
1828        // Encode as ASCII hex
1829        let mut hex = String::new();
1830        for b in &compressed {
1831            hex.push_str(&format!("{b:02X}"));
1832        }
1833        hex.push('>');
1834
1835        let filters = vec![Filter::ASCIIHexDecode, Filter::FlateDecode];
1836        let parms = vec![None, None];
1837        let decoded = decode_stream(hex.as_bytes(), &filters, &parms, None).unwrap();
1838        assert_eq!(&decoded, original);
1839    }
1840}