Skip to main content

rpdfium_parser/
filter.rs

1// Derived from PDFium's cpdf_stream_acc.cpp / cpdf_page_module.cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Filter chain resolution — maps stream dictionary `/Filter` and `/DecodeParms`
7//! entries to codec types.
8
9use std::collections::HashMap;
10
11use rpdfium_codec::{DecodeFilter, FilterParams};
12use rpdfium_core::Name;
13
14use crate::object::Object;
15
16/// Parse the `/Filter` and `/DecodeParms` entries from a stream dictionary
17/// into a sequence of `(DecodeFilter, FilterParams)` pairs suitable for
18/// [`rpdfium_codec::apply_filter_chain`].
19///
20/// Returns an empty `Vec` when no `/Filter` key is present.
21pub fn resolve_filter_chain(dict: &HashMap<Name, Object>) -> Vec<(DecodeFilter, FilterParams)> {
22    let filter_obj = match dict.get(&Name::filter()) {
23        Some(obj) => obj,
24        None => return Vec::new(),
25    };
26
27    let decode_parms_obj = dict.get(&Name::decode_parms());
28
29    // /Filter can be a single Name or an Array of Names
30    let filter_names: Vec<&Name> = match filter_obj {
31        Object::Name(n) => vec![n],
32        Object::Array(arr) => arr.iter().filter_map(|o| o.as_name()).collect(),
33        _ => return Vec::new(),
34    };
35
36    // /DecodeParms can be a single Dict, an Array of Dicts, or absent
37    let params_list: Vec<Option<&HashMap<Name, Object>>> = match decode_parms_obj {
38        Some(Object::Dictionary(d)) => vec![Some(d)],
39        Some(Object::Array(arr)) => arr
40            .iter()
41            .map(|o| match o {
42                Object::Dictionary(d) => Some(d),
43                Object::Null => None,
44                _ => None,
45            })
46            .collect(),
47        _ => Vec::new(),
48    };
49
50    filter_names
51        .iter()
52        .enumerate()
53        .filter_map(|(i, name)| {
54            let filter = name_to_filter(name)?;
55            let params = params_list
56                .get(i)
57                .and_then(|opt| *opt)
58                .map(extract_filter_params)
59                .unwrap_or_default();
60            Some((filter, params))
61        })
62        .collect()
63}
64
65/// Map a PDF filter name (including standard abbreviations) to a [`DecodeFilter`].
66///
67/// Returns `None` for unrecognised names.
68pub fn name_to_filter(name: &Name) -> Option<DecodeFilter> {
69    match name.as_bytes() {
70        // Full names
71        b"FlateDecode" => Some(DecodeFilter::Flate),
72        b"LZWDecode" => Some(DecodeFilter::LZW),
73        b"ASCII85Decode" => Some(DecodeFilter::ASCII85),
74        b"ASCIIHexDecode" => Some(DecodeFilter::ASCIIHex),
75        b"RunLengthDecode" => Some(DecodeFilter::RunLength),
76        b"DCTDecode" => Some(DecodeFilter::DCT),
77        b"CCITTFaxDecode" => Some(DecodeFilter::CCITTFax),
78        b"JBIG2Decode" => Some(DecodeFilter::JBIG2),
79        b"JPXDecode" => Some(DecodeFilter::JPX),
80        // Standard abbreviations (PDF Spec, Table 6)
81        b"Fl" => Some(DecodeFilter::Flate),
82        b"LZW" => Some(DecodeFilter::LZW),
83        b"A85" => Some(DecodeFilter::ASCII85),
84        b"AHx" => Some(DecodeFilter::ASCIIHex),
85        b"RL" => Some(DecodeFilter::RunLength),
86        b"DCT" => Some(DecodeFilter::DCT),
87        b"CCF" => Some(DecodeFilter::CCITTFax),
88        b"JBIG2" => Some(DecodeFilter::JBIG2),
89        b"JPX" => Some(DecodeFilter::JPX),
90        _ => None,
91    }
92}
93
94/// Extract [`FilterParams`] from a `/DecodeParms` dictionary.
95pub fn extract_filter_params(dict: &HashMap<Name, Object>) -> FilterParams {
96    FilterParams {
97        predictor: dict
98            .get(&Name::predictor())
99            .and_then(|o| o.as_i64())
100            .map(|v| v as i32),
101        columns: dict
102            .get(&Name::columns())
103            .and_then(|o| o.as_i64())
104            .map(|v| v as i32),
105        colors: dict
106            .get(&Name::colors())
107            .and_then(|o| o.as_i64())
108            .map(|v| v as i32),
109        bits_per_component: dict
110            .get(&Name::bits_per_component())
111            .and_then(|o| o.as_i64())
112            .map(|v| v as i32),
113        early_change: dict
114            .get(&Name::early_change())
115            .and_then(|o| o.as_i64())
116            .map(|v| v != 0),
117        k: dict
118            .get(&Name::k())
119            .and_then(|o| o.as_i64())
120            .map(|v| v as i32),
121        rows: dict
122            .get(&Name::rows())
123            .and_then(|o| o.as_i64())
124            .map(|v| v as i32),
125        end_of_line: dict.get(&Name::end_of_line()).and_then(|o| o.as_bool()),
126        encoded_byte_align: dict
127            .get(&Name::encoded_byte_align())
128            .and_then(|o| o.as_bool()),
129        black_is_1: dict.get(&Name::black_is_1()).and_then(|o| o.as_bool()),
130        // JBIG2 globals require stream resolution, handled at a higher level.
131        jbig2_globals: None,
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_no_filter_returns_empty() {
141        let dict = HashMap::new();
142        assert!(resolve_filter_chain(&dict).is_empty());
143    }
144
145    #[test]
146    fn test_single_filter_name() {
147        let mut dict = HashMap::new();
148        dict.insert(Name::filter(), Object::Name(Name::flate_decode()));
149
150        let chain = resolve_filter_chain(&dict);
151        assert_eq!(chain.len(), 1);
152        assert_eq!(chain[0].0, DecodeFilter::Flate);
153    }
154
155    #[test]
156    fn test_filter_array() {
157        let mut dict = HashMap::new();
158        dict.insert(
159            Name::filter(),
160            Object::Array(vec![
161                Object::Name(Name::flate_decode()),
162                Object::Name(Name::ascii85_decode()),
163            ]),
164        );
165
166        let chain = resolve_filter_chain(&dict);
167        assert_eq!(chain.len(), 2);
168        assert_eq!(chain[0].0, DecodeFilter::Flate);
169        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
170    }
171
172    #[test]
173    fn test_abbreviated_filter_names() {
174        assert_eq!(name_to_filter(&Name::from("Fl")), Some(DecodeFilter::Flate));
175        assert_eq!(
176            name_to_filter(&Name::from("A85")),
177            Some(DecodeFilter::ASCII85)
178        );
179        assert_eq!(
180            name_to_filter(&Name::from("AHx")),
181            Some(DecodeFilter::ASCIIHex)
182        );
183        assert_eq!(
184            name_to_filter(&Name::from("RL")),
185            Some(DecodeFilter::RunLength)
186        );
187        assert_eq!(name_to_filter(&Name::from("LZW")), Some(DecodeFilter::LZW));
188        assert_eq!(name_to_filter(&Name::from("DCT")), Some(DecodeFilter::DCT));
189        assert_eq!(
190            name_to_filter(&Name::from("CCF")),
191            Some(DecodeFilter::CCITTFax)
192        );
193        assert_eq!(
194            name_to_filter(&Name::from("JBIG2")),
195            Some(DecodeFilter::JBIG2)
196        );
197        assert_eq!(name_to_filter(&Name::from("JPX")), Some(DecodeFilter::JPX));
198    }
199
200    #[test]
201    fn test_full_filter_names() {
202        assert_eq!(
203            name_to_filter(&Name::flate_decode()),
204            Some(DecodeFilter::Flate)
205        );
206        assert_eq!(name_to_filter(&Name::lzw_decode()), Some(DecodeFilter::LZW));
207        assert_eq!(
208            name_to_filter(&Name::ascii85_decode()),
209            Some(DecodeFilter::ASCII85)
210        );
211        assert_eq!(
212            name_to_filter(&Name::ascii_hex_decode()),
213            Some(DecodeFilter::ASCIIHex)
214        );
215        assert_eq!(
216            name_to_filter(&Name::run_length_decode()),
217            Some(DecodeFilter::RunLength)
218        );
219        assert_eq!(name_to_filter(&Name::dct_decode()), Some(DecodeFilter::DCT));
220        assert_eq!(
221            name_to_filter(&Name::ccitt_fax_decode()),
222            Some(DecodeFilter::CCITTFax)
223        );
224        assert_eq!(
225            name_to_filter(&Name::jbig2_decode()),
226            Some(DecodeFilter::JBIG2)
227        );
228        assert_eq!(name_to_filter(&Name::jpx_decode()), Some(DecodeFilter::JPX));
229    }
230
231    #[test]
232    fn test_unknown_filter_name_returns_none() {
233        assert_eq!(name_to_filter(&Name::from("UnknownFilter")), None);
234    }
235
236    #[test]
237    fn test_single_decode_parms_dict() {
238        let mut params_dict = HashMap::new();
239        params_dict.insert(Name::predictor(), Object::Integer(12));
240        params_dict.insert(Name::columns(), Object::Integer(800));
241
242        let mut dict = HashMap::new();
243        dict.insert(Name::filter(), Object::Name(Name::flate_decode()));
244        dict.insert(Name::decode_parms(), Object::Dictionary(params_dict));
245
246        let chain = resolve_filter_chain(&dict);
247        assert_eq!(chain.len(), 1);
248        assert_eq!(chain[0].0, DecodeFilter::Flate);
249        assert_eq!(chain[0].1.predictor, Some(12));
250        assert_eq!(chain[0].1.columns, Some(800));
251    }
252
253    #[test]
254    fn test_decode_parms_array_with_null() {
255        let mut params_dict = HashMap::new();
256        params_dict.insert(Name::predictor(), Object::Integer(15));
257
258        let mut dict = HashMap::new();
259        dict.insert(
260            Name::filter(),
261            Object::Array(vec![
262                Object::Name(Name::flate_decode()),
263                Object::Name(Name::ascii85_decode()),
264            ]),
265        );
266        dict.insert(
267            Name::decode_parms(),
268            Object::Array(vec![Object::Dictionary(params_dict), Object::Null]),
269        );
270
271        let chain = resolve_filter_chain(&dict);
272        assert_eq!(chain.len(), 2);
273        assert_eq!(chain[0].1.predictor, Some(15));
274        // Second filter should have default params (null in array)
275        assert_eq!(chain[1].1.predictor, None);
276    }
277
278    #[test]
279    fn test_extract_all_filter_params() {
280        let mut d = HashMap::new();
281        d.insert(Name::predictor(), Object::Integer(12));
282        d.insert(Name::columns(), Object::Integer(100));
283        d.insert(Name::colors(), Object::Integer(3));
284        d.insert(Name::bits_per_component(), Object::Integer(8));
285        d.insert(Name::early_change(), Object::Integer(0));
286        d.insert(Name::k(), Object::Integer(-1));
287        d.insert(Name::rows(), Object::Integer(50));
288        d.insert(Name::end_of_line(), Object::Boolean(true));
289        d.insert(Name::encoded_byte_align(), Object::Boolean(false));
290        d.insert(Name::black_is_1(), Object::Boolean(true));
291
292        let params = extract_filter_params(&d);
293        assert_eq!(params.predictor, Some(12));
294        assert_eq!(params.columns, Some(100));
295        assert_eq!(params.colors, Some(3));
296        assert_eq!(params.bits_per_component, Some(8));
297        assert_eq!(params.early_change, Some(false));
298        assert_eq!(params.k, Some(-1));
299        assert_eq!(params.rows, Some(50));
300        assert_eq!(params.end_of_line, Some(true));
301        assert_eq!(params.encoded_byte_align, Some(false));
302        assert_eq!(params.black_is_1, Some(true));
303    }
304
305    #[test]
306    fn test_extract_empty_params() {
307        let d = HashMap::new();
308        let params = extract_filter_params(&d);
309        assert_eq!(params.predictor, None);
310        assert_eq!(params.columns, None);
311    }
312
313    #[test]
314    fn test_filter_with_no_decode_parms() {
315        let mut dict = HashMap::new();
316        dict.insert(Name::filter(), Object::Name(Name::ascii_hex_decode()));
317        // No /DecodeParms key
318
319        let chain = resolve_filter_chain(&dict);
320        assert_eq!(chain.len(), 1);
321        assert_eq!(chain[0].0, DecodeFilter::ASCIIHex);
322        assert_eq!(chain[0].1.predictor, None);
323    }
324
325    #[test]
326    fn test_non_name_filter_value_returns_empty() {
327        let mut dict = HashMap::new();
328        dict.insert(Name::filter(), Object::Integer(42));
329
330        assert!(resolve_filter_chain(&dict).is_empty());
331    }
332
333    /// DCT filter not at the end of the chain — chain should be preserved in order.
334    #[test]
335    fn test_dct_not_at_end_of_chain() {
336        let mut dict = HashMap::new();
337        dict.insert(
338            Name::filter(),
339            Object::Array(vec![
340                Object::Name(Name::dct_decode()),
341                Object::Name(Name::flate_decode()),
342            ]),
343        );
344
345        let chain = resolve_filter_chain(&dict);
346        assert_eq!(chain.len(), 2);
347        assert_eq!(chain[0].0, DecodeFilter::DCT);
348        assert_eq!(chain[1].0, DecodeFilter::Flate);
349    }
350
351    /// JPX followed by ASCII85 — chain should be preserved in order.
352    #[test]
353    fn test_jpx_followed_by_ascii85() {
354        let mut dict = HashMap::new();
355        dict.insert(
356            Name::filter(),
357            Object::Array(vec![
358                Object::Name(Name::jpx_decode()),
359                Object::Name(Name::ascii85_decode()),
360            ]),
361        );
362
363        let chain = resolve_filter_chain(&dict);
364        assert_eq!(chain.len(), 2);
365        assert_eq!(chain[0].0, DecodeFilter::JPX);
366        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
367    }
368
369    /// Triple filter chain — all three filters should be preserved in order.
370    #[test]
371    fn test_triple_filter_chain() {
372        let mut dict = HashMap::new();
373        dict.insert(
374            Name::filter(),
375            Object::Array(vec![
376                Object::Name(Name::flate_decode()),
377                Object::Name(Name::ascii85_decode()),
378                Object::Name(Name::ascii_hex_decode()),
379            ]),
380        );
381
382        let chain = resolve_filter_chain(&dict);
383        assert_eq!(chain.len(), 3);
384        assert_eq!(chain[0].0, DecodeFilter::Flate);
385        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
386        assert_eq!(chain[2].0, DecodeFilter::ASCIIHex);
387    }
388
389    // -----------------------------------------------------------------------
390    // Tests ported from upstream fpdf_parser_decode_unittest.cpp
391    // -----------------------------------------------------------------------
392
393    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
394    ///
395    /// No filter key → empty chain.
396    #[test]
397    fn test_parser_decode_get_decoder_array_no_filter() {
398        let dict = HashMap::new();
399        let chain = resolve_filter_chain(&dict);
400        assert!(chain.is_empty());
401    }
402
403    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
404    ///
405    /// Wrong filter type (String instead of Name) → empty chain.
406    #[test]
407    fn test_parser_decode_get_decoder_array_wrong_filter_type() {
408        let mut dict = HashMap::new();
409        dict.insert(
410            Name::filter(),
411            Object::String(rpdfium_core::PdfString::from_bytes(b"RL".to_vec())),
412        );
413        let chain = resolve_filter_chain(&dict);
414        assert!(chain.is_empty());
415    }
416
417    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
418    ///
419    /// Single filter name → one-element chain.
420    #[test]
421    fn test_parser_decode_get_decoder_array_single_name() {
422        let mut dict = HashMap::new();
423        dict.insert(Name::filter(), Object::Name(Name::from("RL")));
424        let chain = resolve_filter_chain(&dict);
425        assert_eq!(chain.len(), 1);
426        assert_eq!(chain[0].0, DecodeFilter::RunLength);
427    }
428
429    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
430    ///
431    /// Empty filter array → empty chain.
432    #[test]
433    fn test_parser_decode_get_decoder_array_empty_array() {
434        let mut dict = HashMap::new();
435        dict.insert(Name::filter(), Object::Array(vec![]));
436        let chain = resolve_filter_chain(&dict);
437        assert!(chain.is_empty());
438    }
439
440    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
441    ///
442    /// Valid 1-element filter array.
443    #[test]
444    fn test_parser_decode_get_decoder_array_one_element() {
445        let mut dict = HashMap::new();
446        dict.insert(
447            Name::filter(),
448            Object::Array(vec![Object::Name(Name::from("FlateDecode"))]),
449        );
450        let chain = resolve_filter_chain(&dict);
451        assert_eq!(chain.len(), 1);
452        assert_eq!(chain[0].0, DecodeFilter::Flate);
453    }
454
455    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
456    ///
457    /// Valid 2-element filter array.
458    #[test]
459    fn test_parser_decode_get_decoder_array_two_elements() {
460        let mut dict = HashMap::new();
461        dict.insert(
462            Name::filter(),
463            Object::Array(vec![
464                Object::Name(Name::from("AHx")),
465                Object::Name(Name::from("LZWDecode")),
466            ]),
467        );
468        let chain = resolve_filter_chain(&dict);
469        assert_eq!(chain.len(), 2);
470        assert_eq!(chain[0].0, DecodeFilter::ASCIIHex);
471        assert_eq!(chain[1].0, DecodeFilter::LZW);
472    }
473
474    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
475    ///
476    /// Single known filter is valid.
477    #[test]
478    fn test_parser_decode_validate_pipeline_single_known() {
479        let mut dict = HashMap::new();
480        dict.insert(Name::filter(), Object::Name(Name::from("FlateDecode")));
481        let chain = resolve_filter_chain(&dict);
482        assert_eq!(chain.len(), 1);
483        assert_eq!(chain[0].0, DecodeFilter::Flate);
484    }
485
486    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
487    ///
488    /// Unknown filter name is silently skipped (not an error).
489    #[test]
490    fn test_parser_decode_validate_pipeline_unknown_filter_skipped() {
491        let mut dict = HashMap::new();
492        dict.insert(Name::filter(), Object::Name(Name::from("FooBar")));
493        // rpdfium silently skips unknown filters (name_to_filter returns None)
494        let chain = resolve_filter_chain(&dict);
495        assert!(chain.is_empty());
496    }
497
498    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
499    ///
500    /// Valid 2-decoder pipeline: AHx + LZWDecode.
501    #[test]
502    fn test_parser_decode_validate_pipeline_two_decoders() {
503        let mut dict = HashMap::new();
504        dict.insert(
505            Name::filter(),
506            Object::Array(vec![
507                Object::Name(Name::from("AHx")),
508                Object::Name(Name::from("LZWDecode")),
509            ]),
510        );
511        let chain = resolve_filter_chain(&dict);
512        assert_eq!(chain.len(), 2);
513        assert_eq!(chain[0].0, DecodeFilter::ASCIIHex);
514        assert_eq!(chain[1].0, DecodeFilter::LZW);
515    }
516
517    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
518    ///
519    /// Valid 5-decoder pipeline.
520    #[test]
521    fn test_parser_decode_validate_pipeline_five_decoders() {
522        let mut dict = HashMap::new();
523        dict.insert(
524            Name::filter(),
525            Object::Array(vec![
526                Object::Name(Name::from("ASCII85Decode")),
527                Object::Name(Name::from("A85")),
528                Object::Name(Name::from("RunLengthDecode")),
529                Object::Name(Name::from("FlateDecode")),
530                Object::Name(Name::from("RL")),
531            ]),
532        );
533        let chain = resolve_filter_chain(&dict);
534        assert_eq!(chain.len(), 5);
535        assert_eq!(chain[0].0, DecodeFilter::ASCII85);
536        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
537        assert_eq!(chain[2].0, DecodeFilter::RunLength);
538        assert_eq!(chain[3].0, DecodeFilter::Flate);
539        assert_eq!(chain[4].0, DecodeFilter::RunLength);
540    }
541
542    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
543    ///
544    /// 5-decoder pipeline with an image decoder at the end.
545    #[test]
546    fn test_parser_decode_validate_pipeline_image_at_end() {
547        let mut dict = HashMap::new();
548        dict.insert(
549            Name::filter(),
550            Object::Array(vec![
551                Object::Name(Name::from("RunLengthDecode")),
552                Object::Name(Name::from("ASCII85Decode")),
553                Object::Name(Name::from("FlateDecode")),
554                Object::Name(Name::from("LZW")),
555                Object::Name(Name::from("DCTDecode")),
556            ]),
557        );
558        let chain = resolve_filter_chain(&dict);
559        assert_eq!(chain.len(), 5);
560        assert_eq!(chain[4].0, DecodeFilter::DCT);
561    }
562
563    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
564    ///
565    /// Two duplicate decoders (ASCII85Decode + ASCII85Decode) — valid pipeline.
566    #[test]
567    fn test_parser_decode_validate_pipeline_duplicate_decoders() {
568        let mut dict = HashMap::new();
569        dict.insert(
570            Name::filter(),
571            Object::Array(vec![
572                Object::Name(Name::from("ASCII85Decode")),
573                Object::Name(Name::from("ASCII85Decode")),
574            ]),
575        );
576        let chain = resolve_filter_chain(&dict);
577        assert_eq!(chain.len(), 2);
578        assert_eq!(chain[0].0, DecodeFilter::ASCII85);
579        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
580    }
581
582    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
583    ///
584    /// Two image decoders in pipeline — upstream rejects this as invalid.
585    /// rpdfium does not perform pipeline validation (it just maps names),
586    /// so both decoders are returned. Validation happens at decode time.
587    #[test]
588    fn test_parser_decode_validate_pipeline_two_image_decoders() {
589        let mut dict = HashMap::new();
590        dict.insert(
591            Name::filter(),
592            Object::Array(vec![
593                Object::Name(Name::from("DCTDecode")),
594                Object::Name(Name::from("CCITTFaxDecode")),
595            ]),
596        );
597        // rpdfium does not reject this at filter chain resolution time
598        let chain = resolve_filter_chain(&dict);
599        assert_eq!(chain.len(), 2);
600        assert_eq!(chain[0].0, DecodeFilter::DCT);
601        assert_eq!(chain[1].0, DecodeFilter::CCITTFax);
602    }
603
604    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
605    ///
606    /// Image decoder not at end of pipeline — upstream rejects.
607    /// rpdfium does not validate pipeline ordering.
608    #[test]
609    fn test_parser_decode_validate_pipeline_image_not_at_end() {
610        let mut dict = HashMap::new();
611        dict.insert(
612            Name::filter(),
613            Object::Array(vec![
614                Object::Name(Name::from("DCTDecode")),
615                Object::Name(Name::from("FlateDecode")),
616            ]),
617        );
618        let chain = resolve_filter_chain(&dict);
619        assert_eq!(chain.len(), 2);
620        assert_eq!(chain[0].0, DecodeFilter::DCT);
621        assert_eq!(chain[1].0, DecodeFilter::Flate);
622    }
623
624    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
625    ///
626    /// Invalid pipeline with image decoder in the middle — upstream rejects.
627    /// rpdfium does not validate pipeline ordering.
628    #[test]
629    fn test_parser_decode_validate_pipeline_image_in_middle() {
630        let mut dict = HashMap::new();
631        dict.insert(
632            Name::filter(),
633            Object::Array(vec![
634                Object::Name(Name::from("FlateDecode")),
635                Object::Name(Name::from("FlateDecode")),
636                Object::Name(Name::from("DCTDecode")),
637                Object::Name(Name::from("FlateDecode")),
638                Object::Name(Name::from("FlateDecode")),
639            ]),
640        );
641        let chain = resolve_filter_chain(&dict);
642        assert_eq!(chain.len(), 5);
643        assert_eq!(chain[2].0, DecodeFilter::DCT);
644    }
645
646    /// Upstream: TEST(ParserDecodeTest, GetDecoderArray)
647    ///
648    /// Invalid 2-element filter array (two image decoders) — upstream rejects.
649    /// rpdfium does not validate at this level.
650    #[test]
651    fn test_parser_decode_get_decoder_array_invalid_two_image() {
652        let mut dict = HashMap::new();
653        dict.insert(
654            Name::filter(),
655            Object::Array(vec![
656                Object::Name(Name::from("DCTDecode")),
657                Object::Name(Name::from("CCITTFaxDecode")),
658            ]),
659        );
660        // rpdfium does not validate pipeline ordering at resolve time
661        let chain = resolve_filter_chain(&dict);
662        assert_eq!(chain.len(), 2);
663    }
664
665    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
666    ///
667    /// Wrong type element in middle of 5-decoder pipeline — upstream rejects.
668    /// rpdfium's filter_map silently skips non-Name elements.
669    #[test]
670    fn test_parser_decode_validate_pipeline_wrong_type_in_middle() {
671        let mut dict = HashMap::new();
672        dict.insert(
673            Name::filter(),
674            Object::Array(vec![
675                Object::Name(Name::from("ASCII85Decode")),
676                Object::Name(Name::from("A85")),
677                Object::Name(Name::from("RunLengthDecode")),
678                Object::Name(Name::from("FlateDecode")),
679                Object::String(rpdfium_core::PdfString::from_bytes(b"RL".to_vec())),
680            ]),
681        );
682        // The String "RL" is skipped by as_name() filter
683        let chain = resolve_filter_chain(&dict);
684        assert_eq!(chain.len(), 4);
685        assert_eq!(chain[0].0, DecodeFilter::ASCII85);
686        assert_eq!(chain[1].0, DecodeFilter::ASCII85);
687        assert_eq!(chain[2].0, DecodeFilter::RunLength);
688        assert_eq!(chain[3].0, DecodeFilter::Flate);
689    }
690
691    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipeline)
692    ///
693    /// String element (wrong type) in filter array — skipped by filter_map.
694    #[test]
695    fn test_parser_decode_validate_pipeline_wrong_type_in_array() {
696        let mut dict = HashMap::new();
697        dict.insert(
698            Name::filter(),
699            Object::Array(vec![Object::String(rpdfium_core::PdfString::from_bytes(
700                b"FlateEncode".to_vec(),
701            ))]),
702        );
703        // Non-Name objects in the array are skipped by as_name() filter
704        let chain = resolve_filter_chain(&dict);
705        assert!(chain.is_empty());
706    }
707
708    /// Upstream: TEST(ParserDecodeTest, ValidateDecoderPipelineWithIndirectObjects)
709    ///
710    /// In upstream, filter names can come from indirect references that are
711    /// resolved before pipeline validation.  rpdfium's `resolve_filter_chain`
712    /// operates on already-resolved `Object` values (no `Object::Reference` in
713    /// the dict), so indirect-object resolution is handled at a higher layer
714    /// (ObjectStore).  This test verifies the equivalent behavior: a Name
715    /// obtained after resolution works just like an inline Name.
716    #[test]
717    fn test_parser_decode_validate_pipeline_with_indirect_objects() {
718        // Simulate the post-resolution case: all Names are direct
719        // Valid 2-decoder pipeline (equivalent of indirect FlateDecode + direct LZW)
720        let mut dict = HashMap::new();
721        dict.insert(
722            Name::filter(),
723            Object::Array(vec![
724                Object::Name(Name::from("FlateDecode")),
725                Object::Name(Name::from("LZW")),
726            ]),
727        );
728        let chain = resolve_filter_chain(&dict);
729        assert_eq!(chain.len(), 2);
730        assert_eq!(chain[0].0, DecodeFilter::Flate);
731        assert_eq!(chain[1].0, DecodeFilter::LZW);
732
733        // Valid 5-decoder pipeline with image decoder at the end
734        let mut dict2 = HashMap::new();
735        dict2.insert(
736            Name::filter(),
737            Object::Array(vec![
738                Object::Name(Name::from("RunLengthDecode")),
739                Object::Name(Name::from("ASCII85Decode")),
740                Object::Name(Name::from("FlateDecode")),
741                Object::Name(Name::from("LZW")),
742                Object::Name(Name::from("DCTDecode")),
743            ]),
744        );
745        let chain2 = resolve_filter_chain(&dict2);
746        assert_eq!(chain2.len(), 5);
747        assert_eq!(chain2[4].0, DecodeFilter::DCT);
748    }
749}