mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 10:46:38 +00:00
Read each THSarabunPSK subset of an edit once, within one budget
Every resource name of a page's Thai subsets decoded and kept its own copy of the font program, so many names of one large program could hold about 2 GiB for a single edit. Names of one font object now share its program, the subsets of an edit are decoded within a shared 64 MiB allowance, and an edit that needs more is refused with the reason, changing nothing.
This commit is contained in:
@@ -13112,6 +13112,47 @@ fn editing_legacy_thai_shapes_a_run_across_subsets_of_the_same_face() {
|
||||
assert_eq!(pdfcraft_cos::write_full(&doc, &Default::default()).unwrap(), before, "a refused edit changes nothing");
|
||||
}
|
||||
|
||||
/// Every resource name of one THSarabunPSK font object shares its program, decoded once. Distinct
|
||||
/// subsets of the face are read within one budget for the edit: past it the edit is refused with
|
||||
/// the reason, and the document is unchanged.
|
||||
#[test]
|
||||
fn legacy_thai_subsets_are_read_within_one_budget_per_edit() {
|
||||
// Padding after the tables: the program reads the same, at 12 MiB.
|
||||
let mut large = legacy_thai_program();
|
||||
large.resize(12 << 20, 0);
|
||||
let fixture = |extra: &dyn Fn(&mut Document) -> Object| {
|
||||
let mut doc = text_page("BT /F1 16 Tf 50 650 Td <00d6> Tj ET");
|
||||
let font = legacy_thai_font(&mut doc, "ABCDEF+THSarabunPSK", &large, LEGACY_THAI_UNICODE);
|
||||
doc.set(pdfcraft_cos::ObjRef::new(5, 0), Object::Dict(font));
|
||||
let names: Vec<_> = (0..8).map(|i| (format!("T{i}").into_bytes(), extra(&mut doc))).collect();
|
||||
doc.update_dict(pdfcraft_cos::ObjRef::new(3, 0), |page| {
|
||||
let Some(Object::Dict(mut resources)) = page.get(b"Resources").cloned() else { panic!("fixture") };
|
||||
let Some(Object::Dict(mut fonts)) = resources.get(b"Font").cloned() else { panic!("fixture") };
|
||||
for (name, font) in names {
|
||||
fonts.set(name, font);
|
||||
}
|
||||
resources.set(b"Font".to_vec(), Object::Dict(fonts));
|
||||
page.set(b"Resources".to_vec(), Object::Dict(resources));
|
||||
})
|
||||
.unwrap();
|
||||
doc
|
||||
};
|
||||
let style = BlockStyle { width: Some(400.0), ..Default::default() };
|
||||
// Eight more names of F1's object: 108 MiB if each name decoded its own copy.
|
||||
let mut aliases = fixture(&|_| Object::Ref(pdfcraft_cos::ObjRef::new(5, 0)));
|
||||
text::rewrite_block(&mut aliases, 0, 0, Some("กขค"), &style).expect("the aliases share one program");
|
||||
assert_eq!(text::text_lines(&reopen(&aliases), 0).unwrap().iter().map(|l| l.text.as_str()).collect::<String>(), "กขค");
|
||||
// Eight more font objects over the program: each is read, and together they are too many.
|
||||
let mut distinct = fixture(&|doc| {
|
||||
let font = doc.get(pdfcraft_cos::ObjRef::new(5, 0)).as_ref().clone();
|
||||
Object::Ref(doc.add(font))
|
||||
});
|
||||
let before = pdfcraft_cos::write_full(&distinct, &Default::default()).unwrap();
|
||||
let error = text::rewrite_block(&mut distinct, 0, 0, Some("กขค"), &style).unwrap_err().to_string();
|
||||
assert!(error.contains("THSarabunPSK") && error.contains("MiB") && error.contains("choose a replacement font"), "{error}");
|
||||
assert_eq!(pdfcraft_cos::write_full(&distinct, &Default::default()).unwrap(), before, "a refused edit changes nothing");
|
||||
}
|
||||
|
||||
/// 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).
|
||||
|
||||
@@ -105,12 +105,19 @@ impl FontResolver<'_> {
|
||||
}
|
||||
|
||||
struct LegacyFace {
|
||||
glyphs: Option<pdfcraft_fonts::pdf::LegacyThaiFont>,
|
||||
/// Shared by every resource name of one font object: its program is decoded and kept once.
|
||||
glyphs: Option<std::rc::Rc<pdfcraft_fonts::pdf::LegacyThaiFont>>,
|
||||
metrics: Metrics,
|
||||
/// Weight and slant ([`FontStyle::of`]).
|
||||
style: FontStyle,
|
||||
/// The font object the resource names, when it is given by reference.
|
||||
object: Option<pdfcraft_cos::ObjRef>,
|
||||
}
|
||||
|
||||
/// What one edit may decode, and so keep, of the font programs of its THSarabunPSK subsets
|
||||
/// (each at most 16 MiB): a page may name many of them.
|
||||
const MAX_THAI_PROGRAMS: usize = 64 << 20;
|
||||
|
||||
struct PreparedCharacters {
|
||||
values: Vec<StyledChar>,
|
||||
substituted: Option<String>,
|
||||
@@ -155,6 +162,7 @@ impl CharacterPreparation<'_> {
|
||||
let Self { doc, first, lines, draft, spans, style, overrides, fonts, metrics, max_glyphs } = self;
|
||||
let thai_choice = builtin_thai_choice(fonts);
|
||||
let mut thai = ThaiShaping::default();
|
||||
let mut thai_programs = pdfcraft_cos::DecodeBudget::new(MAX_THAI_PROGRAMS, 0);
|
||||
let mut chars = Vec::new();
|
||||
let mut substituted = None;
|
||||
let mut fallback = FallbackBuilder::new(max_glyphs);
|
||||
@@ -197,7 +205,7 @@ impl CharacterPreparation<'_> {
|
||||
}),
|
||||
};
|
||||
match chosen {
|
||||
Some(chosen) => legacy_thai_font(resolver.doc, resolver.fonts, &chosen, span, &mut thai.legacy)?,
|
||||
Some(chosen) => legacy_thai_font(resolver.doc, resolver.fonts, &chosen, span, &mut thai.legacy, &mut thai_programs)?,
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
@@ -823,13 +831,15 @@ fn apply_range_styles<'a>(
|
||||
}
|
||||
|
||||
/// Select one existing subset of the exact source face for a Thai span. Never replace its
|
||||
/// family/weight to make an edit succeed; missing glyphs need an explicit font choice.
|
||||
/// family/weight to make an edit succeed; missing glyphs need an explicit font choice. The
|
||||
/// subsets' programs are read within `budget`, shared by the edit's spans.
|
||||
fn legacy_thai_font(
|
||||
doc: &Document,
|
||||
fonts: &Dict,
|
||||
source: &TextLine,
|
||||
span: &str,
|
||||
cache: &mut HashMap<String, LegacyFace>,
|
||||
budget: &mut pdfcraft_cos::DecodeBudget,
|
||||
) -> Result<Option<ThaiSpan>, EditError> {
|
||||
if !source.base_font.rsplit('+').next().unwrap_or(&source.base_font).starts_with("THSarabunPSK")
|
||||
|| !span.chars().any(|c| matches!(u32::from(c), 0x0e00..=0x0e7f))
|
||||
@@ -843,13 +853,33 @@ fn legacy_thai_font(
|
||||
(pdfcraft_fonts::embedding::face_key(&Metrics::from_dict(doc, &font).base_font) == family).then(|| String::from_utf8_lossy(name).into_owned())
|
||||
}));
|
||||
for name in candidates.into_iter().take(128) {
|
||||
if let std::collections::hash_map::Entry::Vacant(entry) = cache.entry(name) {
|
||||
let Some(font) = fonts.get(entry.key().as_bytes()).and_then(|f| doc.dict(f)) else { continue };
|
||||
let metrics = Metrics::from_dict(doc, &font);
|
||||
let glyphs = pdfcraft_fonts::pdf::LegacyThaiFont::from_pdf(doc, &font, source_space(doc, fonts, &metrics)).ok().flatten();
|
||||
let style = FontStyle::of(doc, &font, &metrics);
|
||||
entry.insert(LegacyFace { glyphs, metrics, style });
|
||||
if cache.contains_key(&name) {
|
||||
continue;
|
||||
}
|
||||
let Some(resource) = fonts.get(name.as_bytes()) else { continue };
|
||||
let Some(font) = doc.dict(resource) else { continue };
|
||||
let object = resource.as_ref();
|
||||
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(), style: alias.style, object },
|
||||
None => {
|
||||
let metrics = Metrics::from_dict(doc, &font);
|
||||
let glyphs = pdfcraft_fonts::pdf::LegacyThaiFont::from_pdf(doc, &font, source_space(doc, fonts, &metrics), budget)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(std::rc::Rc::new);
|
||||
let style = FontStyle::of(doc, &font, &metrics);
|
||||
LegacyFace { glyphs, metrics, style, object }
|
||||
}
|
||||
};
|
||||
cache.insert(name, face);
|
||||
}
|
||||
// A subset left unread would count as unusable, silently changing which subset draws the text.
|
||||
if budget.remaining() == 0 {
|
||||
return Err(EditError::Invalid(format!(
|
||||
"the page's {} subsets need more than {} MiB of font programs for one edit; choose a replacement font for this text",
|
||||
source.base_font.rsplit('+').next().unwrap_or(&source.base_font),
|
||||
MAX_THAI_PROGRAMS >> 20
|
||||
)));
|
||||
}
|
||||
// A face none of whose subsets can be shaped in place (no Thai, or neither positioned nor in
|
||||
// the legacy glyph order) is shaped like any other source font (`shape_source`).
|
||||
|
||||
@@ -293,7 +293,10 @@ impl LegacyThaiFont {
|
||||
/// another family, a font with no Thai, and a program without Thai mark positioning whose
|
||||
/// 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.
|
||||
pub fn from_pdf(doc: &Document, font: &Dict, space: f64) -> Result<Option<Self>, String> {
|
||||
///
|
||||
/// 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.
|
||||
pub fn from_pdf(doc: &Document, font: &Dict, space: f64, budget: &mut DecodeBudget) -> Result<Option<Self>, String> {
|
||||
let m = Metrics::from_dict(doc, font);
|
||||
if !m.base_font.rsplit('+').next().unwrap_or(&m.base_font).starts_with("THSarabunPSK") {
|
||||
return Ok(None);
|
||||
@@ -301,10 +304,9 @@ impl LegacyThaiFont {
|
||||
if !space.is_finite() || space < 0.0 {
|
||||
return Err("the source space width is invalid".into());
|
||||
}
|
||||
let bytes = super::embedded_sfnt(doc, font).ok_or("the source Thai font has no readable glyph program")?;
|
||||
let bytes = super::embedded_sfnt_within(doc, font, budget).ok_or("the source Thai font has no readable glyph program")?;
|
||||
let positioned = crate::source_shaping::has_thai_layout(&bytes);
|
||||
let (unicode, codes) = composite_glyph_maps(doc, font, &bytes, &m, &mut DecodeBudget::new(1 << 17, 0))
|
||||
.ok_or("the Thai source glyph codes cannot be recovered safely")?;
|
||||
let (unicode, codes) = composite_glyph_maps(doc, font, &bytes, &m, budget).ok_or("the Thai source glyph codes cannot be recovered safely")?;
|
||||
// A subset keeps only the glyphs its document used, so no particular letter or alternate
|
||||
// is required here: [`Self::shape`] refuses a text whose glyphs, or required alternates
|
||||
// ([`require_pua`]), the subset lacks. Without its own positioning, every Thai glyph it
|
||||
@@ -579,7 +581,8 @@ mod tests {
|
||||
let metrics = Metrics::from_dict(&doc, &font);
|
||||
let (map, _) = composite_glyph_maps(&doc, &font, &stripped, &metrics, &mut DecodeBudget::new(1 << 20, 0)).unwrap();
|
||||
assert_eq!(map.get(&0x0e01), Some(&214), "the CIDToGIDMap is read with its predictor");
|
||||
let shaped = LegacyThaiFont::from_pdf(&doc, &font, 0.25).unwrap().expect("a legacy subset").shape("ก่").unwrap();
|
||||
let shaped =
|
||||
LegacyThaiFont::from_pdf(&doc, &font, 0.25, &mut DecodeBudget::new(1 << 26, 0)).unwrap().expect("a legacy subset").shape("ก่").unwrap();
|
||||
assert_eq!(shaped.shaped.glyphs.iter().map(|g| g.id.to_u32()).collect::<Vec<_>>(), [214, 352]);
|
||||
}
|
||||
|
||||
@@ -592,7 +595,8 @@ mod tests {
|
||||
let latin = skrifa::FontRef::new(&program).unwrap().charmap().map('A').unwrap();
|
||||
let mut doc = Document::new_empty();
|
||||
let font = thai_font(&mut doc, &program, INTACT);
|
||||
let shaped = LegacyThaiFont::from_pdf(&doc, &font, 0.25).unwrap().expect("a legacy subset").shape("ก่").unwrap();
|
||||
let shaped =
|
||||
LegacyThaiFont::from_pdf(&doc, &font, 0.25, &mut DecodeBudget::new(1 << 26, 0)).unwrap().expect("a legacy subset").shape("ก่").unwrap();
|
||||
assert_eq!(shaped.shaped.glyphs.iter().map(|g| g.id.to_u32()).collect::<Vec<_>>(), [214, 352]);
|
||||
let editor = crate::pdf::embedded_sfnt(&doc, &font).unwrap();
|
||||
assert_eq!(skrifa::FontRef::new(&editor).unwrap().charmap().map('A'), Some(latin), "the program's own mappings stay");
|
||||
@@ -608,7 +612,7 @@ mod tests {
|
||||
let mode = |program: &[u8], unicode: &[u8]| {
|
||||
let mut doc = Document::new_empty();
|
||||
let font = thai_font(&mut doc, program, unicode);
|
||||
LegacyThaiFont::from_pdf(&doc, &font, 0.25).unwrap().map(|f| f.positioned)
|
||||
LegacyThaiFont::from_pdf(&doc, &font, 0.25, &mut DecodeBudget::new(1 << 26, 0)).unwrap().map(|f| f.positioned)
|
||||
};
|
||||
let stripped = without_tables(&thai_ordered_program(), &[b"cmap", b"GSUB", b"GPOS"]);
|
||||
assert_eq!(mode(&stripped, unicode), Some(false), "a legacy positional subset");
|
||||
|
||||
Reference in New Issue
Block a user