diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index b51bb0a657..e526f2a8e6 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -115,7 +115,7 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg let Some(object) = descriptor.get(key) else { continue }; let object = doc.resolve(object); let Object::Stream(stream) = object.as_ref() else { continue }; - let Ok(mut bytes) = budget.decode_within(stream, MAX_SFNT) else { continue }; + let Ok(mut bytes) = budget.decode_within(&doc.with_direct_parms(stream), MAX_SFNT) else { continue }; let Ok(face) = skrifa::FontRef::from_index(&bytes, 0) else { continue }; if face.metrics(skrifa::instance::Size::unscaled(), skrifa::instance::LocationRef::default()).units_per_em == 0 { continue; @@ -305,25 +305,31 @@ const MAX_CID_TO_GID: usize = 1 << 17; /// The streams one reading of a font decodes, against the caller's allowance, and whether one was /// left unread because that allowance was spent. struct Reads<'a> { + doc: &'a Document, budget: &'a mut DecodeBudget, refused: bool, } impl<'a> Reads<'a> { - fn new(budget: &'a mut DecodeBudget) -> Self { - Self { budget, refused: false } + fn new(doc: &'a Document, budget: &'a mut DecodeBudget) -> Self { + Self { doc, budget, refused: false } } - /// `stream` decoded within `max` and the allowance; `None` when it can't be, noting when the - /// allowance is why. A stream past `max` is unread as before allowances, not refused. + /// `stream` decoded within `max` and the allowance, its `/DecodeParms` read where they are given + /// by reference too ([`Document::with_direct_parms`]); `None` when it can't be, noting when the + /// allowance is why. /// - /// `max` bounds what the stream decodes to. Its filter stages are paid for together, up to - /// `max` for each, the most a decode without an allowance lets them make: a map of 65,536 CIDs + /// `max` bounds what the stream decodes to, and its filter stages together may make up to `max` + /// for each of them, as much as a decode without an allowance lets them: a map of 65,536 CIDs /// behind ASCIIHex and Flate makes all of `MAX_CID_TO_GID` in its last stage, with the hex - /// stage's output on top. + /// stage's output on top. One stage may so make more than `max`, which a decode without an + /// allowance stops, so a stream that one leaves unread can read here, never the other way + /// round. A stream past `max` is unread, not refused, when what is left can pay for its stages; + /// when it can't, the stream spends it and counts as refused. fn decode(&mut self, stream: &Stream, max: usize) -> Option> { + let stream = self.doc.with_direct_parms(stream); let stages = stream.filters().len().max(1); - match self.budget.decode_within(stream, max.saturating_mul(stages)) { + match self.budget.decode_within(&stream, max.saturating_mul(stages)) { Ok(data) if data.len() <= max => Some(data), Ok(_) => None, Err(_) => { @@ -690,7 +696,7 @@ impl Metrics { /// it reads (a page's, say): each stream pays for what it decodes, usable or not. A stream the /// allowance can't pay for is left unread, and [`Metrics::past_allowance`] says so. pub fn from_dict_within(doc: &Document, font: &Dict, budget: &mut DecodeBudget) -> Self { - let mut reads = Reads::new(budget); + let mut reads = Reads::new(doc, budget); let mut m = Self::read_metrics(doc, font, &mut reads); let base = font .name(b"BaseFont") diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index dbcf367bd4..3e914816bb 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -334,6 +334,55 @@ fn a_full_cid_map_reads_behind_any_number_of_filters() { } } +/// `data` in rows of `columns` bytes behind Flate and a PNG predictor, each row tagged None, with +/// its `/DecodeParms` given by reference or direct. +fn predicted(doc: &mut Document, data: &[u8], columns: usize, by_reference: bool) -> Object { + let rows: Vec = data.chunks(columns).flat_map(|row| std::iter::once(0).chain(row.iter().copied())).collect(); + let mut s = Stream::flate(Dict::new(), &rows); + let parms: Dict = + [(b"Predictor".to_vec(), Object::Int(12)), (b"Columns".to_vec(), Object::Int(i64::try_from(columns).unwrap()))].into_iter().collect(); + let parms = if by_reference { Object::Ref(doc.add(Object::Dict(parms))) } else { Object::Dict(parms) }; + s.dict.set(b"DecodeParms".to_vec(), parms); + Object::Ref(doc.add(Object::Stream(s))) +} + +/// A stream whose `/DecodeParms` are given by reference reads as it does with them direct: a PNG +/// predictor's tag bytes are taken out of the map, the CMap and the editor's program, not left in. +#[test] +fn parameters_given_by_reference_are_read() { + let read = |by_reference: bool| { + let mut doc = Document::new_empty(); + let (mut f, _) = subset_type0(&mut doc, b""); + let map: Vec = (0..1000u16).flat_map(u16::to_be_bytes).collect(); + let map = predicted(&mut doc, &map, 2, by_reference); + let mut cmap = b"1 beginbfchar <0001> <0058> endbfchar".to_vec(); + cmap.resize(48, b' '); + let unicode = predicted(&mut doc, &cmap, 16, by_reference); + let Some(Object::Array(mut descendants)) = f.get(b"DescendantFonts").cloned() else { panic!("descendant") }; + if let Some(Object::Dict(d)) = descendants.get_mut(0) { + d.set(b"CIDToGIDMap".to_vec(), map); + } + f.set(b"DescendantFonts".to_vec(), Object::Array(descendants)); + f.set(b"ToUnicode".to_vec(), unicode); + let m = Metrics::from_dict(&doc, &f); + (m.outlined_cids.clone(), m.text_of(1).map(Cow::into_owned)) + }; + let direct = read(false); + assert!(!direct.0.is_empty()); + assert_eq!(direct.1.as_deref(), Some("X")); + assert_eq!(read(true), direct); + let mut program = include_bytes!("../../../../assets/fonts/Inter-Regular.ttf").to_vec(); + program.resize(program.len().next_multiple_of(4), 0); + for by_reference in [false, true] { + let mut doc = Document::new_empty(); + let file = predicted(&mut doc, &program, 4, by_reference); + let mut descriptor = Dict::new(); + descriptor.set(b"FontFile2".to_vec(), file); + let simple = font(&mut doc, vec![("FontDescriptor", Object::Dict(descriptor))]); + assert_eq!(embedded_sfnt(&doc, &simple).as_deref(), Some(program.as_slice()), "by reference: {by_reference}"); + } +} + /// Fonts that each name one large ToUnicode share the allowance they are read within: it pays for /// as many readings as it holds, and the rest are left unread, where each reading used to decode /// it afresh. @@ -732,7 +781,7 @@ fn malformed_cmaps_never_panic() { let mut enc = Dict::new(); enc.set(b"Differences".to_vec(), Object::Array(vec![Object::Int(i64::MAX), Object::name("A"), Object::name("B")])); let f = font(&mut doc, vec![("Subtype", Object::name("Type1")), ("Encoding", Object::Dict(enc))]); - assert_eq!(unicode_map(&doc, &f, false, &mut Reads::new(&mut DecodeBudget::new(usize::MAX, 0))).text(u32::MAX).as_deref(), Some("B")); + assert_eq!(unicode_map(&doc, &f, false, &mut Reads::new(&doc, &mut DecodeBudget::new(usize::MAX, 0))).text(u32::MAX).as_deref(), Some("B")); } #[test]