mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M7: a Thai edit refuses only a read past its allowance, and reads its subsets' metrics within it
CodeRabbit's review of 674a20899:
- The Thai edit refused when nothing was left of its allowance, so an
edit whose reads spent it to the byte was refused. It now refuses when
a read was refused for want of it (`DecodeBudget::refused`).
- `LegacyThaiFont::from_pdf` read its metrics with `from_dict`, outside
the edit's allowance, and so did the edit for each subset it measures.
Both now read them within it (`from_dict_within`), and metrics read
past it adapt nothing: `from_pdf` refuses the font before reading its
program, and the edit leaves the subset unread and is refused. The
edit's scan of every font on the page for its face name stays outside
the allowance, as on `main`: charged, each alias of a font would pay
again, and other faces would spend what the face's subsets need.
`MAX_THAI_PROGRAMS` says what it pays for.
- When the `/CIDToGIDMap` a THSarabunPSK subset's Thai mappings are
rebuilt from was left unread for want of the editor's allowance,
`composite_glyph_maps` gave nothing, and a subset with a Latin-only
cmap of its own was listed as it was, without its Thai mappings. The
editor now lists no copy of it, as for metrics read past the
allowance.
Tests: `a_thai_edit_that_spends_its_allowance_to_the_byte_goes_through`
(`pdfcraft-edit`: a subset costing exactly the 64 MiB is edited, and
one three bytes past it is refused),
`a_legacy_fonts_metrics_are_paid_for_from_the_edits_allowance` and
`a_map_left_unread_for_the_allowance_lists_no_copy` (`pdfcraft-fonts`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
8c5333bfe7
commit
4fca5783a7
4 files changed
+94
-11
No files matched your search
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Option<Self>, 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<u8> = (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> <ffff> endcodespacerange \
|
||||
|
||||
Reference in new issue
Block a user