From 69d6e3d28a57d244d14d75c30cad4967c8a804ff Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 01:22:55 +0000 Subject: [PATCH] Batch fix: EPS: filter keeps its source, so an image's data source isn't skipped twice (#1909) --- crates/pdf/src/ps.rs | 20 +++++++++++--------- crates/pdf/src/tests_import.rs | 10 ++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/pdf/src/ps.rs b/crates/pdf/src/ps.rs index 984f7206..88385186 100644 --- a/crates/pdf/src/ps.rs +++ b/crates/pdf/src/ps.rs @@ -1271,31 +1271,33 @@ impl Interp { } // `source [params] /Name filter`. A SubFileDecode of the current file with a count of // 0 reads to its end-of-data string: skip that data (an embedded XMP packet, which - // otherwise ran as PostScript). Other filters stand for the file they read. + // otherwise ran as PostScript). Other filters read what their source reads. "filter" => { let name = self.pop().key(); - if name == "SubFileDecode" { + let mut skipped = false; + let source = if name == "SubFileDecode" && !matches!(self.st.last(), Some(V::Dict(_))) { let eod = match self.pop() { V::Str(s) => s.borrow().clone(), _ => vec![], }; let count = self.popn(); - if let V::File = self.pop() - && count == 0.0 - && !eod.is_empty() - { + let source = self.pop(); + if matches!(source, V::File) && count == 0.0 && !eod.is_empty() { src.pos = match find_from(src.d, &eod, src.pos) { Some(p) => p.saturating_add(eod.len()), None => src.d.len(), }; + skipped = true; } + source } else { if matches!(self.st.last(), Some(V::Dict(_))) { self.pop(); } - self.pop(); - } - self.push(V::File); + self.pop() + }; + // Data already stepped over leaves nothing for `image` to skip. + self.push(if skipped { V::Null } else { source }); } "vmstatus" => { for _ in 0..3 { diff --git a/crates/pdf/src/tests_import.rs b/crates/pdf/src/tests_import.rs index 7f46f6cf..71b93a78 100644 --- a/crates/pdf/src/tests_import.rs +++ b/crates/pdf/src/tests_import.rs @@ -45,6 +45,16 @@ fn eps_with_aliased_operators_device_queries_and_embedded_data_draws() { assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "drawing after the embedded data"); } +#[test] +fn eps_filter_on_a_string_source_does_not_skip_the_program() { + let square = "1 0 0 setrgbcolor 50 25 moveto 150 25 lineto 150 75 lineto 50 75 lineto closepath fill\n"; + let img = "gsave 1 1 8 [1 0 0 1 0 0] (FF) /ASCIIHexDecode filter image grestore\n"; + // Images aren't drawn yet, so this parses without `eps`'s nothing-skipped check. + let doc = parse(format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 100\n{img}{square}").as_bytes()).unwrap(); + assert_eq!(doc.skipped, ["image"]); + assert!(coverage(&render(&doc), 50, 25, 150, 75) > 4900.0, "drawing after an image read from a string"); +} + #[test] fn eps_text_with_bundled_fonts_and_show_variants() { // Not embedded: drawn with the bundled stand-in, like the same text in a PDF.