M12: XFAImages: pictures kept for reuse count against the total too

A picture read from the file is kept for the next place that links it.
Those kept bytes now count against MAX_TOTAL as well: past it, a name
not read yet is neither read nor kept, so a tree with many names holds
no more than the total however many names it has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410
2026-10-30 12:50:18 +08:00
co-authored by Claude Opus 5.5
parent f75e6d9d5d
commit b1c68aa028
2 changed files with 67 additions and 6 deletions
+46 -6
View File
@@ -77,27 +77,57 @@ struct Found<'a> {
doc: &'a Document,
names: HashMap<String, Object>,
read: HashMap<String, Option<Vec<u8>>>,
/// Bytes of the pictures in `read`, and of the copies put in places: each at most
/// [`MAX_TOTAL`], however many names the tree holds.
kept: usize,
total: usize,
warnings: Vec<String>,
}
impl Found<'_> {
/// A copy of the picture linked as `href` for one more place, when the file holds it and the
/// copies so far leave room for it under [`MAX_TOTAL`].
/// pictures kept and the copies so far leave room for it under [`MAX_TOTAL`].
fn get(&mut self, href: &str) -> Option<Vec<u8>> {
if !self.read.contains_key(href) {
let data = self.load(href);
// Past the total, a picture not read yet is neither read nor kept.
let data = if self.kept >= MAX_TOTAL { None } else { self.load(href) };
let data = match data {
Some(d) if self.kept.saturating_add(d.len()) > MAX_TOTAL => {
self.over_total();
None
}
Some(d) => {
self.kept = self.kept.saturating_add(d.len());
Some(d)
}
None => {
if self.kept >= MAX_TOTAL && self.names.contains_key(href) {
self.over_total();
}
None
}
};
self.read.insert(href.to_string(), data);
}
let len = self.read.get(href).and_then(Option::as_ref).map(Vec::len)?;
if self.total.saturating_add(len) > MAX_TOTAL {
self.warnings.push("the pictures the form keeps in the file take too much memory in all; the rest are left out".into());
self.over_total();
return None;
}
self.total = self.total.saturating_add(len);
self.read.get(href).cloned().flatten()
}
fn over_total(&mut self) {
self.warnings.push("the pictures the form keeps in the file take too much memory in all; the rest are left out".into());
}
/// Bytes of the pictures read so far and kept for reuse.
#[cfg(test)]
fn kept(&self) -> usize {
self.read.values().flatten().map(Vec::len).sum()
}
fn load(&mut self, href: &str) -> Option<Vec<u8>> {
let object = self.names.get(href)?;
let object = self.doc.resolve(object);
@@ -156,12 +186,22 @@ impl Found<'_> {
/// Give the pictures `tpl` links to (and doesn't embed) the data `doc` keeps for them. A link
/// the file holds nothing for stays empty. What couldn't be read is returned as warnings.
pub fn resolve(tpl: &mut Template, doc: &Document) -> Vec<String> {
resolve_found(tpl, doc).map(|f| f.warnings).unwrap_or_default()
}
fn resolve_found<'a>(tpl: &mut Template, doc: &'a Document) -> Option<Found<'a>> {
let names = names(doc);
if names.is_empty() {
return Vec::new();
return None;
}
let mut found = Found { doc, names, read: HashMap::new(), total: 0, warnings: Vec::new() };
let mut found = Found { doc, names, read: HashMap::new(), kept: 0, total: 0, warnings: Vec::new() };
found.subform(&mut tpl.root, 0);
found.warnings.dedup();
found.warnings
Some(found)
}
/// [`resolve`], and the bytes of pictures it held for reuse at the end.
#[cfg(test)]
pub(crate) fn resolve_keeping(tpl: &mut Template, doc: &Document) -> (Vec<String>, usize) {
resolve_found(tpl, doc).map_or((Vec::new(), 0), |f| (f.warnings.clone(), f.kept()))
}
+21
View File
@@ -4246,3 +4246,24 @@ fn a_linked_picture_used_in_many_places_counts_each_place() {
assert_eq!(filled, (1 << 10) / one, "as many places as the total holds");
assert!(warnings.iter().any(|w| w.contains("too much memory")), "{warnings:?}");
}
/// Pictures read from the file and kept for reuse count against the total too (small in tests):
/// a tree with many names, each read once, holds no more than the total, however many names
/// there are; past it a name is neither read nor kept.
#[test]
fn pictures_kept_for_reuse_stay_under_the_total_whatever_the_number_of_names() {
let names: String = (0..30).map(|i| format!("(n{i}) 8 0 R ")).collect();
let kid = format!("<< /Names [{names}] >>");
let doc = Document::open(Arc::new(crate::fixtures::linked_image_shell_with(b"<< /Kids [7 0 R] >>", kid.as_bytes()))).unwrap();
let draws: String =
(0..30).map(|i| format!(r#"<draw name="d{i}" w="1in" h="1in"><value><image contentType="image/png" href="n{i}"/></value></draw>"#)).collect();
let xml = format!(
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
{draws}</subform></template>"#
);
let (mut tpl, _) = crate::parse(&xml).unwrap();
let (warnings, kept) = crate::linked_images::resolve_keeping(&mut tpl, &doc);
assert!(kept <= 1 << 10, "kept {kept} bytes");
assert!(warnings.iter().any(|w| w.contains("too much memory")), "{warnings:?}");
}