mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 04:11:34 +00:00
M7: a font's streams are read with /DecodeParms given by reference
A /CIDToGIDMap, CMap or program whose /DecodeParms are an indirect object, or an array holding one, was decoded without them, as on main: behind a PNG predictor its tag bytes stayed in, so the map read wrong and the CMap or program not at all. Each of the five metrics reads and the editor's program read now go through Document::with_direct_parms, which shares the stream's data and copies its dictionary only when a parameter is indirect. Reads::decode's comment now says what the stage bound does: a stage may make more than the stream's limit where a decode without an allowance stops it, and a stream past its limit counts as refused when what is left can't pay for its stages. Found by the Legal forms review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ade9877370
commit
fd4b698c71
+16
-10
@@ -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<Vec<u8>> {
|
||||
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")
|
||||
|
||||
@@ -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<u8> = 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<u8> = (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]
|
||||
|
||||
Reference in New Issue
Block a user