mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 23:03:27 +00:00
M12: an XFA page area keeps at most 128 content areas, so a subform split across them lays out within its budget (#5843) (#5844)
This commit is contained in:
1 parent
67a6037be1
commit
359cef8b0d
3 files changed
+88
-1
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use super::common::{clamp_opt, divide, is_row, moved, solid_extent, splittable};
|
||||
use super::items::Item;
|
||||
use super::{Ctx, Frame, Layout, Layouter, MAX_DEPTH, Node, Rect, SomIndices, Subform, XfaError, data_path, instance_count};
|
||||
use super::{Ctx, Frame, Layout, Layouter, MAX_DEPTH, MAX_MEASURES, Node, Rect, SomIndices, Subform, XfaError, data_path, instance_count};
|
||||
|
||||
impl Layouter<'_> {
|
||||
/// Place `node` as one block of size (`w`, `h`) at the cursor: in this content area when it fits,
|
||||
@@ -75,6 +75,14 @@ impl Layouter<'_> {
|
||||
let mut seg_top = top;
|
||||
let mut first_segment = true;
|
||||
loop {
|
||||
// Each pass rescans every item still to be placed. A block split across very many
|
||||
// content areas, and so across very many pages, would do this once per page, costing
|
||||
// time quadratic in its items; the still-unplaced count spends the work budget so a
|
||||
// crafted template stops rather than run for seconds before the page limit catches it.
|
||||
self.measures.set(self.measures.get().saturating_add(rest.len()));
|
||||
if self.measures.get() > MAX_MEASURES {
|
||||
return Err(XfaError::TooLarge("a subform is split across too many content areas to lay out".into()));
|
||||
}
|
||||
let (rect, _, placed) = self.cursor();
|
||||
let limit = rect.bottom() + shift;
|
||||
let mut cut = limit;
|
||||
|
||||
@@ -21,6 +21,10 @@ const MAX_IMAGE: usize = 32 << 20;
|
||||
const MAX_ITEMS: usize = 1000;
|
||||
/// Longest text run kept.
|
||||
const MAX_TEXT: usize = 1 << 20;
|
||||
/// Most content areas kept on one page area. A master page has a handful (the most in the
|
||||
/// 500-form corpus is nine); laying a splittable subform out across tens of thousands of them,
|
||||
/// which `next_area` walks before starting a new page, costs time quadratic in the paragraphs.
|
||||
const MAX_CONTENT_AREAS: usize = 128;
|
||||
|
||||
/// Parse a measurement such as `12.7mm`, `72pt`, `1in`, `2.54cm` into points. Bare numbers are
|
||||
/// points. `None` for anything else (including `em`).
|
||||
@@ -609,7 +613,19 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
|
||||
attr_measure(c, "h").unwrap_or(height).clamp(1.0, height),
|
||||
)
|
||||
})
|
||||
.take(MAX_CONTENT_AREAS + 1)
|
||||
.collect();
|
||||
if content.len() > MAX_CONTENT_AREAS {
|
||||
content.truncate(MAX_CONTENT_AREAS);
|
||||
// Said once however many warnings came before: other warnings fill the shared cap of
|
||||
// 100 without limit of their own, and this one must not go silent behind them. It is
|
||||
// kept first, so whether it was said is one comparison, not a scan of every warning
|
||||
// for every page area (nothing else is put first).
|
||||
let said = format!("a page area has more than {MAX_CONTENT_AREAS} content areas; the rest are left out");
|
||||
if warnings.first() != Some(&said) {
|
||||
warnings.insert(0, said);
|
||||
}
|
||||
}
|
||||
if content.is_empty() {
|
||||
content.push(Rect::new(0.0, 0.0, width, height));
|
||||
}
|
||||
|
||||
@@ -188,6 +188,46 @@ fn the_template_parses_with_fonts_captions_and_rich_text() {
|
||||
assert!(last.break_before_page);
|
||||
}
|
||||
|
||||
/// A page area that declares far more content areas than any real master keeps only the first
|
||||
/// 128, with a warning. `split_block` walks every content area of a page before starting a new
|
||||
/// one, re-scanning the unplaced items each time, so tens of thousands of them cost time
|
||||
/// quadratic in a splittable subform's paragraphs. Capping the count bounds that walk.
|
||||
#[test]
|
||||
fn a_page_area_keeps_at_most_a_hundred_and_twenty_eight_content_areas() {
|
||||
let areas = (0..200).map(|i| format!("<contentArea x=\"0.5in\" y=\"{}pt\" w=\"7in\" h=\"1pt\"/>", i)).collect::<String>();
|
||||
let xml = format!(
|
||||
r##"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||||
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet>
|
||||
<pageArea name="front"><medium stock="letter" short="8.5in" long="11in"/>{areas}</pageArea>
|
||||
</pageSet>
|
||||
</subform>
|
||||
</template>
|
||||
</xdp:xdp>
|
||||
"##
|
||||
);
|
||||
let (tpl, warnings) = parse(&xml).unwrap();
|
||||
let ps = tpl.root.page_set.as_ref().unwrap();
|
||||
assert_eq!(ps.areas[0].content.len(), 128, "the content areas are capped");
|
||||
assert!(warnings.iter().any(|w| w.contains("content areas")), "a warning records the cap: {warnings:?}");
|
||||
// Still said after a hundred other warnings (CodeRabbit review on #5844): images that can't
|
||||
// be decoded, parsed before the page set, fill the warnings' shared cap first.
|
||||
let bad = "<draw w=\"1in\" h=\"1in\"><value><image contentType=\"image/png\">!!!</image></value></draw>".repeat(120);
|
||||
let crowded = xml.replace("<subform name=\"form\" layout=\"tb\">", &format!("<subform name=\"form\" layout=\"tb\">{bad}"));
|
||||
let (_, warnings) = parse(&crowded).unwrap();
|
||||
assert!(warnings.len() > 100, "the other warnings came first: {}", warnings.len());
|
||||
assert!(warnings.iter().any(|w| w.contains("content areas")), "the cap is still recorded");
|
||||
// Many capped page areas say it once, found without scanning every warning (it comes first).
|
||||
let page_area = |n: usize| format!(r#"<pageArea name="p{n}"><medium stock="letter" short="8.5in" long="11in"/>{areas}</pageArea>"#);
|
||||
let many: String = (0..50).map(page_area).collect();
|
||||
let crowded = crowded.replacen(&page_area(0).replace("p0", "front"), &many, 1);
|
||||
let (_, warnings) = parse(&crowded).unwrap();
|
||||
assert_eq!(warnings.iter().filter(|w| w.contains("content areas")).count(), 1, "said once");
|
||||
assert!(warnings[0].contains("content areas"), "kept first: {:?}", warnings.first());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rich_text_max_length_reaches_the_field_and_layout() {
|
||||
for (attribute, expected) in [
|
||||
@@ -678,6 +718,29 @@ fn empty_repeating_subforms_spend_the_layout_budget() {
|
||||
assert!(layout_xml(&tpl).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_split_across_very_many_pages_spends_the_layout_budget() {
|
||||
// One-point content areas slice a flowed subform's paragraphs one line to a page, so the
|
||||
// block is split across hundreds of pages; split_block rescans the still-unplaced
|
||||
// paragraphs once per page, which is quadratic in their number. Fewer paragraphs than this
|
||||
// lay out (188 pages here), so the page limit does not catch it; the rescans spend the
|
||||
// work budget instead.
|
||||
let areas: String = (0..128).map(|i| format!("<contentArea x=\"0.5in\" y=\"{i}pt\" w=\"7in\" h=\"1pt\"/>")).collect();
|
||||
let paras: String = (0..2500).map(|i| format!("<p>Paragraph number {i} with a few words to fill a line.</p>")).collect();
|
||||
let tpl = format!(
|
||||
"<template><subform name=\"form\" layout=\"tb\"><pageSet><pageArea name=\"p\">\
|
||||
<medium stock=\"letter\" short=\"8.5in\" long=\"11in\"/>{areas}</pageArea></pageSet>\
|
||||
<subform name=\"body\" layout=\"tb\"><field name=\"t\" w=\"7in\"><ui><textEdit multiLine=\"1\"/></ui>\
|
||||
<value><exData contentType=\"text/html\"><body xmlns=\"http://www.w3.org/1999/xhtml\">{paras}</body></exData></value>\
|
||||
</field></subform></subform></template>"
|
||||
);
|
||||
match layout_xml(&tpl) {
|
||||
// It says what ran out, not that the form is nested too deeply (CodeRabbit review on #5844).
|
||||
Err(XfaError::TooLarge(m)) => assert!(m.contains("split across"), "{m}"),
|
||||
other => panic!("a block split across very many pages must be refused: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_packet_stream_listed_many_times_is_read_once() {
|
||||
let mut doc = Document::open(Arc::new(shell("<template><subform/></template>"))).unwrap();
|
||||
|
||||
Reference in new issue
Block a user