diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 57009ce99e..466e61c7f9 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -14295,6 +14295,29 @@ fn legacy_thai_subsets_are_read_within_one_budget_per_edit() { assert_eq!(pdfcraft_cos::write_full(&distinct, &Default::default()).unwrap(), before, "a refused edit changes nothing"); } +/// An edit whose THSarabunPSK subset spends the edit's allowance to the byte goes through: only a +/// read refused for want of it refuses the edit. The subset is read four times over its program +/// (the edit's metrics read, `LegacyThaiFont`'s, the editor's program read and its metrics read) +/// and three times over its ToUnicode, so a program 192 bytes short of 16 MiB and a ToUnicode of +/// 256 bytes cost exactly the 64 MiB; a byte more of ToUnicode is three past it. +#[test] +fn a_thai_edit_that_spends_its_allowance_to_the_byte_goes_through() { + let mut program = legacy_thai_program(); + program.resize((16 << 20) - 192, 0); + let edit = |unicode_len: usize| { + let mut unicode = LEGACY_THAI_UNICODE.to_vec(); + unicode.resize(unicode_len, b' '); + let mut doc = text_page("BT /F1 16 Tf 50 650 Td <00d6> Tj ET"); + let font = legacy_thai_font(&mut doc, "ABCDEF+THSarabunPSK", &program, &unicode); + doc.set(pdfcraft_cos::ObjRef::new(5, 0), Object::Dict(font)); + let style = BlockStyle { width: Some(400.0), ..Default::default() }; + text::rewrite_block(&mut doc, 0, 0, Some("กขค"), &style).map(|_| ()).map_err(|e| e.to_string()) + }; + assert_eq!(edit(256), Ok(())); + let error = edit(257).unwrap_err(); + assert!(error.contains("THSarabunPSK") && error.contains("MiB of font data"), "{error}"); +} + /// A THSarabunPSK-named subset that has OpenType Thai mark positioning is a modern face: it is /// shaped with its own layout like any source font, not by the legacy shaper (which would ask /// for Windows PUA alternates such a face doesn't need). diff --git a/crates/edit/src/text_regions/styled.rs b/crates/edit/src/text_regions/styled.rs index a7c7166f35..9c2732c35f 100644 --- a/crates/edit/src/text_regions/styled.rs +++ b/crates/edit/src/text_regions/styled.rs @@ -117,10 +117,10 @@ struct LegacyFace { } /// What one edit may decode of its THSarabunPSK subsets' font data: their programs (each at most -/// 16 MiB), which it keeps, and what is read to adapt their programs' cmaps: their CMaps and glyph -/// maps, and a subset's program again, for which of its glyphs have outlines. A page may name many -/// of them. The metrics read to find and measure the subsets (`Metrics::from_dict`) aren't charged -/// to it. +/// 16 MiB), which it keeps, and the streams of the metrics read to measure them and to adapt their +/// programs' cmaps: their CMaps and glyph maps, and a subset's program again, for which of its +/// glyphs have outlines. A page may name many of them. The metrics read to find the subsets among +/// the page's fonts aren't charged to it. const MAX_THAI_PROGRAMS: usize = 64 << 20; struct PreparedCharacters { @@ -903,6 +903,9 @@ fn legacy_thai_font( } let family = pdfcraft_fonts::embedding::face_key(&source.base_font); let mut candidates = vec![source.font.clone()]; + // Every name on the page is read here for its face name alone, aliases of one font and other + // faces' fonts too, with no total, as before: charged to the edit's allowance, each alias would + // pay again and other faces would spend what the face's subsets need. candidates.extend(fonts.iter().filter_map(|(name, font)| { let font = doc.dict(font)?; (pdfcraft_fonts::embedding::face_key(&Metrics::from_dict(doc, &font).base_font) == family).then(|| String::from_utf8_lossy(name).into_owned()) @@ -917,9 +920,15 @@ fn legacy_thai_font( let face = match object.and_then(|o| cache.values().find(|face| face.object == Some(o))) { Some(alias) => LegacyFace { glyphs: alias.glyphs.clone(), metrics: alias.metrics.clone(), object, ..*alias }, None => { - let metrics = Metrics::from_dict(doc, &font); - let space = source_space(doc, fonts, &metrics); - let glyphs = pdfcraft_fonts::pdf::LegacyThaiFont::from_pdf(doc, &font, space, budget).ok().flatten().map(std::rc::Rc::new); + // Read within the edit's allowance; past it the subset goes unread, and the edit is + // refused below. + let metrics = Metrics::from_dict_within(doc, &font, budget); + let (space, glyphs) = if metrics.past_allowance { + (0.0, None) + } else { + let space = source_space(doc, fonts, &metrics); + (space, pdfcraft_fonts::pdf::LegacyThaiFont::from_pdf(doc, &font, space, budget).ok().flatten().map(std::rc::Rc::new)) + }; let style = FontStyle::of(doc, &font, &metrics); LegacyFace { glyphs, metrics, style, object, space } } @@ -927,7 +936,9 @@ fn legacy_thai_font( cache.insert(name, face); } // A subset left unread would count as unusable, silently changing which subset draws the text. - if budget.remaining() == 0 { + // A read refused for want of the allowance is what refuses the edit: reads that spent it to the + // byte refused nothing. + if budget.refused() > 0 { return Err(EditError::Invalid(format!( "the page's {} subsets need more than {} MiB of font data for one edit; choose a replacement font for this text", source.base_font.rsplit('+').next().unwrap_or(&source.base_font), diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index b67152684e..f68761e02e 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -156,8 +156,13 @@ pub fn embedded_sfnt_within(doc: &Document, font: &Dict, budget: &mut DecodeBudg } let mut mappings = std::collections::BTreeMap::new(); if metrics.composite { + let refused = budget.refused(); if let Some((map, _)) = composite_glyph_maps(doc, font, &bytes, &metrics, budget) { mappings = map; + } else if budget.refused() > refused { + // The map the cmap is rebuilt from was left unread for want of the allowance: no + // copy is listed, adapted or as it is, as for metrics read past it. + continue; } if own_cmap { let charmap = face.charmap(); diff --git a/crates/fonts/src/pdf/thai.rs b/crates/fonts/src/pdf/thai.rs index f34111cd5d..84075f87e8 100644 --- a/crates/fonts/src/pdf/thai.rs +++ b/crates/fonts/src/pdf/thai.rs @@ -298,13 +298,17 @@ impl LegacyThaiFont { /// Thai glyphs are not in the legacy order: the nominal and alternate marks this shaper /// assumes can't be trusted there, so it is shaped as an ordinary source font. /// - /// Its streams are decoded against `budget`, which the caller shares between every font it - /// reads: what they keep is the programs decoded and the editor cmaps added to them. + /// Its streams, its metrics' too, are decoded against `budget`, which the caller shares + /// between every font it reads: what they keep is the programs decoded and the editor cmaps + /// added to them. Metrics read past it adapt nothing: the font is refused. pub fn from_pdf(doc: &Document, font: &Dict, space: f64, budget: &mut DecodeBudget) -> Result, String> { - let m = Metrics::from_dict(doc, font); + let m = Metrics::from_dict_within(doc, font, budget); if !m.base_font.rsplit('+').next().unwrap_or(&m.base_font).starts_with("THSarabunPSK") { return Ok(None); } + if m.past_allowance { + return Err("the source Thai font's metrics are past the edit's allowance".into()); + } if !space.is_finite() || space < 0.0 { return Err("the source space width is invalid".into()); } @@ -532,6 +536,46 @@ mod tests { /// An intact legacy map: nominal Thai and two tone alternates. const INTACT: &[u8] = b"2 beginbfrange <00d6> <010f> <0e01> <0110> <012a> <0e3f> endbfrange 2 beginbfchar <0160> <0e48> <0161> <0e49> endbfchar"; + /// A THSarabunPSK subset whose own cmap is Latin-only has its Thai mappings rebuilt from its + /// `/CIDToGIDMap`. With that map left unread for want of the editor's allowance, the editor + /// lists no copy, rather than the program without its Thai mappings. + #[test] + fn a_map_left_unread_for_the_allowance_lists_no_copy() { + let mut doc = Document::new_empty(); + let program = include_bytes!("../../../../assets/fonts/Inter-Regular.ttf"); + let file = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), program)))); + let map: Vec = (0u16..512).flat_map(u16::to_be_bytes).collect(); + let cid_to_gid = Object::Ref(doc.add(Object::Stream(Stream::flate(Dict::new(), &map)))); + let font = thai_font_over(&mut doc, file, cid_to_gid, INTACT); + let mut budget = DecodeBudget::new(usize::MAX / 2, 0); + assert!(crate::pdf::embedded_sfnt_within(&doc, &font, &mut budget).is_some()); + let cost = usize::MAX / 2 - budget.remaining(); + // The map the cmap is rebuilt from is read last: a byte short of the cost, it is refused. + let mut short = DecodeBudget::new(cost - 1, 0); + assert!(crate::pdf::embedded_sfnt_within(&doc, &font, &mut short).is_none(), "the program is listed without its Thai mappings"); + assert_eq!(short.refused(), 1); + } + + /// `LegacyThaiFont::from_pdf` reads its metrics within the edit's allowance too: the subset's + /// program, for its outlined glyphs, and its ToUnicode are paid for on top of what the + /// editor's read of it pays. Metrics read past the allowance refuse the font before its + /// program is read. + #[test] + fn a_legacy_fonts_metrics_are_paid_for_from_the_edits_allowance() { + let mut doc = Document::new_empty(); + let program = thai_ordered_program(); + let font = thai_font(&mut doc, &program, INTACT); + let mut editor = DecodeBudget::new(usize::MAX / 2, 0); + assert!(crate::pdf::embedded_sfnt_within(&doc, &font, &mut editor).is_some()); + let editor_cost = usize::MAX / 2 - editor.remaining(); + let mut budget = DecodeBudget::new(usize::MAX / 2, 0); + LegacyThaiFont::from_pdf(&doc, &font, 0.25, &mut budget).unwrap(); + assert_eq!(usize::MAX / 2 - budget.remaining(), editor_cost + program.len() + INTACT.len()); + let mut short = DecodeBudget::new(program.len() + INTACT.len() - 1, 0); + let error = LegacyThaiFont::from_pdf(&doc, &font, 0.25, &mut short).err().unwrap_or_default(); + assert!(error.contains("past the edit's allowance"), "{error}"); + } + /// The exporter's damaged map: intact anchors, Latin/control destinations for the other Thai /// codes, and the oversized range first. const DAMAGED: &[u8] = b"begincmap 1 begincodespacerange <0000> endcodespacerange \