diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 240b1c4d55..e9a909ea5c 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -156,13 +156,13 @@ fn cids_with_outlines(doc: &Document, font: &Dict) -> Vec { let Some(program) = dict(doc, descendant.get(b"FontDescriptor")).and_then(|d| d.get(b"FontFile2").cloned()) else { return Vec::new() }; let program = doc.resolve(&program); let Object::Stream(stream) = program.as_ref() else { return Vec::new() }; - let Ok(bytes) = stream.decoded_within(16 << 20) else { return Vec::new() }; + let Ok(bytes) = doc.with_direct_parms(stream).decoded_within(16 << 20) else { return Vec::new() }; let Some(loca) = skrifa::FontRef::from_index(&bytes, 0).ok().and_then(|face| face.loca(None).ok()) else { return Vec::new() }; let outlined = |gid: u32| loca.get_raw(gid as usize).zip(loca.get_raw(gid as usize + 1)).is_some_and(|(start, end)| end > start); match descendant.get(b"CIDToGIDMap").map(|o| doc.resolve(o)).as_deref() { None | Some(Object::Name(_)) => bit_set_below_64k((0..loca.len().min(0x1_0000) as u32).filter(|&gid| outlined(gid))), Some(Object::Stream(map)) => { - let Ok(map) = map.decoded_within(1 << 17) else { return Vec::new() }; + let Ok(map) = doc.with_direct_parms(map).decoded_within(1 << 17) else { return Vec::new() }; bit_set_below_64k( map.as_chunks::<2>() .0 @@ -189,7 +189,7 @@ fn embedded_widths(doc: &Document, font: &Dict, m: &mut Metrics) { let Some(object) = descriptor.get(key) else { continue }; let object = doc.resolve(object); let Object::Stream(stream) = object.as_ref() else { continue }; - let Ok(bytes) = stream.decoded_within(16 << 20) else { continue }; + let Ok(bytes) = doc.with_direct_parms(stream).decoded_within(16 << 20) else { continue }; let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue }; let upem = f64::from(face.metrics(Size::unscaled(), LocationRef::default()).units_per_em); if upem == 0.0 { @@ -691,7 +691,7 @@ impl Metrics { m.codes = Codes::Two; if let Some(e) = font.get(b"Encoding").map(|e| doc.resolve(e)) && let Object::Stream(s) = &*e - && let Ok(data) = s.decoded_within(MAX_CMAP_BYTES) + && let Ok(data) = doc.with_direct_parms(s).decoded_within(MAX_CMAP_BYTES) { let (spaces, cids) = parse_cmap(&data); if !spaces.is_empty() { @@ -1293,7 +1293,10 @@ fn unicode_map(doc: &Document, font: &Dict, composite: bool) -> Unicode { } match font.get(b"ToUnicode").map(|t| doc.resolve(t)).as_deref() { Some(Object::Stream(s)) => { - if let Ok(data) = s.decoded_within(MAX_CMAP_BYTES) { + // With `/DecodeParms` given by reference, as every stream of the font is read here, + // and as the redaction proof reads the map: the two must get the same text, or a copy + // redaction rightly left counts as a survivor. + if let Ok(data) = doc.with_direct_parms(s).decoded_within(MAX_CMAP_BYTES) { parse_to_unicode(&data, &mut out); } } diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index c2acef191c..69c309c0e9 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -104,6 +104,18 @@ fn font(doc: &mut Document, entries: Vec<(&str, Object)>) -> Dict { d } +/// `data` behind Flate and a PNG predictor whose `/DecodeParms` is a reference: read without +/// it, every row of four bytes keeps its tag byte. +fn behind_parms_by_reference(doc: &mut Document, data: &[u8]) -> Object { + let parms = doc.add(Object::Dict(Dict::from_iter([(b"Predictor".to_vec(), Object::Int(12)), (b"Columns".to_vec(), Object::Int(4))]))); + let mut data = data.to_vec(); + data.resize(data.len().next_multiple_of(4), 0); + let rows: Vec = data.chunks(4).flat_map(|row| std::iter::once(0).chain(row.iter().copied())).collect(); + let mut stream = Stream::flate(Dict::new(), &rows); + stream.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms)); + Object::Ref(doc.add(Object::Stream(stream))) +} + #[test] fn embedded_editor_fonts_are_validated_without_installing_or_exporting() { use pdfcraft_cos::Stream; @@ -124,34 +136,46 @@ fn embedded_editor_fonts_are_validated_without_installing_or_exporting() { } /// #2130: with no `/Widths`, an embedded program's advances win over a guess from the name -/// ("Mono" would mean Courier, 600 units for every glyph). +/// ("Mono" would mean Courier, 600 units for every glyph), the program's `/DecodeParms` given +/// directly or by reference. #[test] fn a_font_without_widths_measures_with_its_embedded_program() { use pdfcraft_cos::Stream; use skrifa::MetadataProvider as _; - let mut doc = Document::new_empty(); let bytes = include_bytes!("../../../../assets/fonts/Inter-Regular.ttf"); let face = skrifa::FontRef::new(bytes).unwrap(); let (size, location) = (skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()); let upem = f64::from(face.metrics(size, location).units_per_em); let advance = |c: char| f64::from(face.glyph_metrics(size, location).advance_width(face.charmap().map(c).unwrap()).unwrap()) / upem; - let program = doc.add(Object::Stream(Stream::flate(Dict::new(), bytes))); - let mut descriptor = Dict::new(); - descriptor.set(b"FontFile2".to_vec(), Object::Ref(program)); - let f = font( - &mut doc, - vec![ - ("Subtype", Object::name("TrueType")), - ("BaseFont", Object::name("HouseMono")), - ("Encoding", Object::name("WinAnsiEncoding")), - ("FontDescriptor", Object::Dict(descriptor)), - ], - ); - let m = Metrics::from_dict(&doc, &f); - for c in ['i', 'l', 'W', ' '] { - assert!((m.width(u32::from(c)) - advance(c)).abs() < 1e-9, "{c}: {} vs {}", m.width(u32::from(c)), advance(c)); + for by_reference in [false, true] { + let mut doc = Document::new_empty(); + let program = if by_reference { + behind_parms_by_reference(&mut doc, bytes) + } else { + Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), bytes)))) + }; + let mut descriptor = Dict::new(); + descriptor.set(b"FontFile2".to_vec(), program); + let f = font( + &mut doc, + vec![ + ("Subtype", Object::name("TrueType")), + ("BaseFont", Object::name("HouseMono")), + ("Encoding", Object::name("WinAnsiEncoding")), + ("FontDescriptor", Object::Dict(descriptor)), + ], + ); + let m = Metrics::from_dict(&doc, &f); + for c in ['i', 'l', 'W', ' '] { + assert!( + (m.width(u32::from(c)) - advance(c)).abs() < 1e-9, + "{c}, by reference {by_reference}: {} vs {}", + m.width(u32::from(c)), + advance(c) + ); + } + assert!(m.width(u32::from('i')) < 0.4, "not Courier's 0.6, by reference {by_reference}"); } - assert!(m.width(u32::from('i')) < 0.4, "not Courier's 0.6"); } #[test] @@ -342,6 +366,79 @@ fn cmap_streams_decode_within_a_size_cap() { } } +#[test] +fn cmap_streams_read_their_parameters_given_by_reference() { + // Both maps behind a predictor whose `/DecodeParms` is a reference: read without it, every + // row keeps its tag byte inside the map's tokens. + let to_unicode = b"1 begincodespacerange <0000> endcodespacerange 1 beginbfrange <0024> <0026> <0041> endbfrange"; + let encoding = b"1 begincodespacerange <0000> endcodespacerange 1 begincidrange <0000> 0 endcidrange"; + let mut doc = Document::new_empty(); + let (tu, enc) = (behind_parms_by_reference(&mut doc, to_unicode), behind_parms_by_reference(&mut doc, encoding)); + let mut desc = Dict::new(); + desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2")); + let f = font( + &mut doc, + vec![("Subtype", Object::name("Type0")), ("Encoding", enc), ("DescendantFonts", Object::Array(vec![Object::Dict(desc)])), ("ToUnicode", tu)], + ); + let m = Metrics::from_dict(&doc, &f); + assert_eq!(m.decode(&[0, 0x24]), "A", "ToUnicode"); + assert!(matches!(m.codes, Codes::Ranges(_)), "Encoding"); +} + +/// A Type 0 subset over Inter with `/DW` alone, whose CIDs 1 to 7 draw "A B C D" (ToUnicode +/// gives CIDs 1 and 2), its program and its `/CIDToGIDMap` each behind a predictor whose +/// `/DecodeParms` is a reference, or behind Flate alone. +fn inter_subset(program_by_reference: bool, map_by_reference: bool) -> Metrics { + use skrifa::MetadataProvider as _; + let bytes = include_bytes!("../../../../assets/fonts/Inter-Regular.ttf"); + let face = skrifa::FontRef::new(bytes).unwrap(); + let gid = |c: char| u16::try_from(face.charmap().map(c).unwrap().to_u32()).unwrap(); + let gids: Vec = std::iter::once(0).chain("A B C D".chars().map(gid)).flat_map(u16::to_be_bytes).collect(); + let stream = |doc: &mut Document, data: &[u8], by_reference: bool| { + if by_reference { behind_parms_by_reference(doc, data) } else { Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), data)))) } + }; + let mut doc = Document::new_empty(); + let program = stream(&mut doc, bytes, program_by_reference); + let map = stream(&mut doc, &gids, map_by_reference); + let to_unicode = b"1 begincodespacerange <0000> endcodespacerange 2 beginbfchar <0001> <0041> <0002> <0020> endbfchar"; + let to_unicode = stream(&mut doc, to_unicode, false); + let mut descriptor = Dict::new(); + descriptor.set(b"FontFile2".to_vec(), program); + let mut desc = Dict::new(); + desc.set(b"Subtype".to_vec(), Object::name("CIDFontType2")); + desc.set(b"DW".to_vec(), Object::Int(600)); + desc.set(b"FontDescriptor".to_vec(), Object::Dict(descriptor)); + desc.set(b"CIDToGIDMap".to_vec(), map); + let f = font( + &mut doc, + vec![ + ("Subtype", Object::name("Type0")), + ("BaseFont", Object::name("ABCDEF+Inter")), + ("Encoding", Object::name("Identity-H")), + ("DescendantFonts", Object::Array(vec![Object::Dict(desc)])), + ("ToUnicode", to_unicode), + ], + ); + Metrics::from_dict(&doc, &f) +} + +/// A subset with `/DW` alone has a glyph for a CID when the program's glyph it maps to has an +/// outline, so the space, Inter's empty glyph, is refused. Its program, or its `/CIDToGIDMap`, +/// behind parameters given by reference says the same: read without them, no program says +/// which glyphs are empty, or the map points the CIDs at other glyphs. +#[test] +fn a_subsets_program_and_cid_map_read_their_parameters_given_by_reference() { + let direct = inter_subset(false, false); + assert!(!direct.outlined_cids.is_empty()); + assert_eq!(direct.encode("A").as_deref(), Some(&[0, 1][..])); + assert_eq!(direct.encode(" "), None, "the space's glyph is empty"); + for (program, map) in [(true, false), (false, true)] { + let m = inter_subset(program, map); + assert_eq!(m.outlined_cids, direct.outlined_cids, "program by reference: {program}, map: {map}"); + assert_eq!(m.encode(" "), None, "program by reference: {program}, map: {map}"); + } +} + #[test] fn composite_fonts_use_to_unicode() { let mut doc = Document::new_empty(); diff --git a/crates/redact/src/tests_verify.rs b/crates/redact/src/tests_verify.rs index dc3c3c89d9..28853cc4a4 100644 --- a/crates/redact/src/tests_verify.rs +++ b/crates/redact/src/tests_verify.rs @@ -350,6 +350,27 @@ fn a_survivor_behind_parameters_given_by_reference_is_found() { assert!(!proof.passed(), "{}", proof.to_text()); } +#[test] +fn a_unicode_map_behind_parameters_given_by_reference_is_read_alike() { + // The font's /ToUnicode (object 7) reads codes A..H as "SECRETVA", behind Flate and a PNG + // predictor whose `/DecodeParms` is object 8, a reference. The page draws the codes twice, + // once under the mark. Redaction takes the removed text from that map, and the proof the page + // text it allows for: read by one and not the other, the copy that stays counted as a + // survivor of a sound redaction. + let cmap = b"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CMapName /Test def\n1 begincodespacerange\n<00> \nendcodespacerange\n8 beginbfchar\n<41> <0053>\n<42> <0045>\n<43> <0043>\n<44> <0052>\n<45> <0045>\n<46> <0054>\n<47> <0056>\n<48> <0041>\nendbfchar\nendcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n"; + let mut data = cmap.to_vec(); + data.resize(data.len().next_multiple_of(4), b' '); + let rows: Vec = data.chunks(4).flat_map(|row| std::iter::once(0).chain(row.iter().copied())).collect(); + let to_unicode = stream("/Filter /FlateDecode /DecodeParms 8 0 R", Stream::flate(Dict::new(), &rows).raw.as_ref()); + let parms = b"<< /Predictor 12 /Columns 4 >>".to_vec(); + let content = b"BT /F1 10 Tf 10 200 Td (ABCDEFGH) Tj ET BT /F1 10 Tf 10 100 Td (ABCDEFGH) Tj ET"; + let mut doc = marked_content(content, Spec { extra: vec![to_unicode, parms], ..Spec::default() }); + doc.update_dict(ObjRef::new(5, 0), |font| font.set(b"ToUnicode".to_vec(), Object::Ref(ObjRef::new(7, 0)))).unwrap(); + let (result, proof) = run(&mut doc); + assert!(result.is_ok(), "{result:?}"); + assert!(proof.passed(), "{}", proof.to_text()); +} + #[test] fn a_survivor_in_an_unused_object_is_found() { // Object 7 is reachable from nothing: no catalog entry, no page resource. A reader that