PDF/.ai import: dictionary values the reader can't parse no longer crash the ink and font scans

The reader's Dict::entries unwraps every value it reads, so it panics on
a value it cannot parse, such as the name /#zz or the number 12-.5
(vendor/hayro-syntax/src/object/dict.rs:104 at craft-libs ca50d63b).
Besides the nesting check in pages.rs, two loops of the import call it:

- Colors::visit (import_color.rs) looks for the file's Separation and
  DeviceN spaces when a page paints in one. craft_pdf's interpret_page
  catches the panic, and the import fails with "PDF page interpretation
  failed internally".
- Walker::note_fonts (import_scan.rs) notes the fonts of each page. The
  import does not catch that panic.

On main the nesting check in pages.rs panics on such a value first
(#2133 fixes it); with it fixed, the import reaches these two loops.

Colors::visit now reads the values with Dict::keys and get_raw, sorted
by key as before, and leaves out a value the reader cannot parse.
note_fonts reads the keys and looks each font up by its key, as before.
This commit is contained in:
dotCooCoo
2026-10-16 01:34:10 +07:00
committed by bflatastic
parent a69ddea930
commit 9e90464f20
4 changed files with 47 additions and 2 deletions
+8 -1
View File
@@ -309,7 +309,7 @@ impl<'p> Colors<'p> {
}
}
Object::Dict(d) => {
for (_, v) in d.entries() {
for v in values(&d) {
if let Some(v) = direct(v) {
self.visit(v, depth + 1);
}
@@ -386,6 +386,13 @@ pub(crate) fn direct(m: MaybeRef<Object<'_>>) -> Option<Object<'_>> {
}
}
/// The values of `d` that the reader can parse, sorted by key as `Dict::entries` sorts them.
pub(crate) fn values<'a>(d: &craft_pdf::syntax::object::Dict<'a>) -> impl Iterator<Item = MaybeRef<Object<'a>>> {
let mut keys: Vec<Name<'a>> = d.keys().collect();
keys.sort_by(|a, b| a.as_ref().cmp(b.as_ref()));
keys.into_iter().filter_map(|k| d.get_raw::<Object<'a>>(k.as_ref()))
}
fn alt_model(o: &Object<'_>) -> Option<Alt> {
match o {
Object::Name(n) => match n.as_str() {
+1 -1
View File
@@ -415,7 +415,7 @@ impl Walker<'_, '_> {
}
fn note_fonts(&mut self, res: &Resources<'_>) {
for (key, _) in res.fonts.entries() {
for key in res.fonts.keys() {
if let Some(f) = res.fonts.get::<Dict<'_>>(key.as_ref())
&& let Some(name) = f.get::<Name<'_>>(b"BaseFont")
{
+21
View File
@@ -216,6 +216,27 @@ fn colour_spaces_are_read_from_the_interpreters_debug_form() {
assert_eq!(crate::import_color::split_color("Color { color_space: X, components: [NaN], opacity: 1.0 }"), None);
}
/// A dictionary value the reader can't parse (a name with a broken `#` escape, a number with a
/// stray `-`) is left out when the file's ink spaces are looked for, and the inks after it are found.
#[test]
fn inks_are_found_past_dictionary_values_the_reader_cannot_parse() {
use craft_pdf::interpret::Function;
use craft_pdf::syntax::object::{FromBytes, Object};
let tint = "<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 0 1 0] /N 1 >>";
let key = format!("{:?}", Function::new(&Object::from_bytes(tint.as_bytes()).unwrap()).unwrap());
for junk in ["/#zz", "12-.5"] {
let page = page_with(&format!("/A {junk} /CS0 [/Separation /Gold /DeviceCMYK {tint}]"), &[("CS0", "1")]);
// Opened by the reader alone, without the checks of `pages::open`.
let pdf = craft_pdf::syntax::Pdf::new(pdf(&[page], None)).unwrap();
let mut c = Colors::new(&pdf, vec![]);
let space = c.space_of(&format!("ColorSpace(Separation(Separation {{ tint_transform: {key} }}))"), 1);
assert_eq!(space, Space::Inks(0), "{junk}");
let gold = c.native(space, &[1.0]).unwrap();
assert_eq!(gold.link, Some(("Gold".to_string(), 1.0)), "{junk}");
assert!(near(gold.color, Color::cmyk(0.0, 0.0, 1.0, 0.0)), "{junk}: {:?}", gold.color);
}
}
/// Spot colours defined in Lab can have an ICC Lab profile as their alternate space, `[/ICCBased
/// profile]` with 3 components: the tint transform gives Lab values, which are not RGB (#1032). The
/// profile is told by its `Alternate` (`[/Lab …]`), else by its header's data colour space.
+17
View File
@@ -321,6 +321,23 @@ fn font_names_resolve_to_available_families() {
assert_eq!((family.as_str(), style.as_str(), found), ("Missing Sans Pro", "Bold Italic", false));
}
/// A value of the page's font dictionary that the reader can't parse (a name with a broken `#`
/// escape, a number with a stray `-`) is left out when the page's fonts are noted, and the fonts
/// after it are noted.
#[test]
fn fonts_are_noted_past_font_resources_the_reader_cannot_parse() {
for junk in ["/#zz", "12-.5"] {
let resources = format!("/Font << /A {junk} /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>");
let bytes = one_page("BT /F1 24 Tf 10 30 Td (Hello) Tj ET", &resources, &[]);
// Opened by the reader alone, without the checks of `pages::open`.
let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap();
let mut fonts = crate::import_scan::FontNames::new();
let mut ocgs = crate::import_scan::Ocgs::read(&pdf);
crate::import_scan::scan_page(&pdf.pages()[0], &mut ocgs, false, &mut fonts, pdf.decode_limits().max_decoded_stream()).unwrap();
assert_eq!(fonts.into_values().collect::<Vec<_>>(), [("Helvetica".to_string(), false)], "{junk}");
}
}
// ---------- meshes, stops and extend ----------
fn hex(bytes: &[u8]) -> String {