mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 13:54:09 +00:00
Review of the whole change found reads it fixes that no test failed without: each was put back to main's read alone, and the tests passed. Each now has a case that fails without it: - the proof's `Tf` size (text starting outside the region reaches into it only at its size) and font (an undecodable font's text leaves the region unverifiable), and the strings a `TJ` carries; - the proof's `l`, `c`, `v` and `y` points after a surplus, from a point outside the region, and the walk's `m`, `l`, `c`, `v` and `y` points beside a mark, each alone; - the property list a second page sharing the resources still names; - a Type 3 font an ExtGState's `/Font` sets, kept by the page and its glyph by the font, and one without resources of its own, which keeps the page's pattern its glyph may paint with; - a Type 3 font only a soft mask's group selects, the mask's ExtGState named after a surplus; - the named property lists read once, with each `BDC` naming another entry, so that it is the lists, not each name's answer, that are kept. And the walk's `cs` says why it tells a Pattern space by the name `/Pattern` alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
99 lines
4.8 KiB
Rust
99 lines
4.8 KiB
Rust
//! Work regressions for what a redaction looks up at operators a stream can hold thousands of: the
|
|
//! tags cleanup's named property lists at each `BDC` are not read whole each time. A test binary of
|
|
//! its own, so that none of the crate's other tests shares its process, and its tests run one at a
|
|
//! time. Each test compares two timings of its own, the least of three runs each, with many
|
|
//! entries or operators and with one, not a fixed time. The bound leaves room for slower machines:
|
|
//! each regression took seconds against milliseconds.
|
|
|
|
use std::sync::{Arc, Mutex, PoisonError};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use pdfcraft_annot::{Meta, NewAnnotation, Shape, Style, add_annotation, rect_quad};
|
|
use pdfcraft_cos::Document;
|
|
use pdfcraft_redact::apply;
|
|
use pdfcraft_testkit::{Pdf, stream_obj};
|
|
|
|
/// One page with `resources` and `extra` objects (from object 5 on), drawing a word under a mark
|
|
/// (so the redaction rewrites the page) and then `ops`.
|
|
fn page(resources: &str, ops: &[u8], extra: Vec<Vec<u8>>) -> Document {
|
|
let content = [b"BT /F1 10 Tf 20 260 Td (SECRET) Tj ET ".as_slice(), ops].concat();
|
|
let mut objs = vec![
|
|
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
|
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
|
format!(
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 4 0 R /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> {resources} >> >>"
|
|
)
|
|
.into_bytes(),
|
|
stream_obj("", content),
|
|
];
|
|
objs.extend(extra);
|
|
let mut doc = Document::open(Arc::new(Pdf::new().objs(objs).build())).unwrap();
|
|
let shape = Shape::Redact { quads: vec![rect_quad([0.0, 200.0, 300.0, 300.0])], overlay: String::new(), look: Default::default() };
|
|
let style = Style::default_for(&shape);
|
|
add_annotation(&mut doc, &NewAnnotation { page: 0, shape, style, contents: String::new(), author: "Synthetic test".into() }, &Meta::default())
|
|
.unwrap();
|
|
doc
|
|
}
|
|
|
|
/// One page whose `/Properties` holds `entries` entries, drawing `ops`.
|
|
fn properties(entries: usize, ops: &[u8]) -> Document {
|
|
let lists: String = (0..entries).map(|i| format!("/M{i} << /Lang (en) >> ")).collect();
|
|
page(&format!("/Properties << {lists}>>"), ops, Vec::new())
|
|
}
|
|
|
|
/// One page whose property list `/M0` is object 5, which gives its `MCID` as object 6 and holds
|
|
/// 400,000 more entries: larger than the document keeps, so that reading it again parses it again,
|
|
/// all of it, once another object has been read since. The page draws `ops`.
|
|
fn oversized(ops: &[u8]) -> Document {
|
|
let more: String = (0..400_000).map(|i| format!("/J{i} /DeviceGray ")).collect();
|
|
page("/Properties << /M0 5 0 R >>", ops, vec![format!("<< /MCID 6 0 R {more}>>").into_bytes(), b"0".to_vec()])
|
|
}
|
|
|
|
/// The tests' lock: they run one at a time, so that none is timed while another works.
|
|
static SERIAL: Mutex<()> = Mutex::new(());
|
|
|
|
fn one_at_a_time() -> std::sync::MutexGuard<'static, ()> {
|
|
SERIAL.lock().unwrap_or_else(PoisonError::into_inner)
|
|
}
|
|
|
|
/// How long redacting a document `make` builds takes, the least of three runs.
|
|
fn redaction(make: impl Fn() -> Document) -> Duration {
|
|
(0..3)
|
|
.map(|_| {
|
|
let mut doc = make();
|
|
let start = Instant::now();
|
|
let report = apply(&mut doc, None).unwrap();
|
|
let took = start.elapsed();
|
|
assert_eq!(report.glyphs, 6, "the word under the mark goes");
|
|
took
|
|
})
|
|
.min()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Whether `many` took about as long as `one`.
|
|
fn about_as_long(many: Duration, one: Duration) -> bool {
|
|
many < one * 10 + Duration::from_secs(1)
|
|
}
|
|
|
|
/// Written when the tags cleanup resolved the page's `/Properties` at every named `BDC`: 20,000 of
|
|
/// them with 5,000 entries took 28 s against 0.07 s with one entry (optimized). Each `BDC` here
|
|
/// names another entry, so that it is the lists, not each name's answer, that are read once.
|
|
#[test]
|
|
fn named_property_lists_are_read_once_for_a_page() {
|
|
let _serial = one_at_a_time();
|
|
let ops: Vec<u8> = (0..5_000).map(|i| format!("/P /M{i} BDC EMC ")).collect::<String>().into_bytes();
|
|
let (one, many) = (redaction(|| properties(1, &ops)), redaction(|| properties(5_000, &ops)));
|
|
assert!(about_as_long(many, one), "5,000 entries {many:?}, one {one:?}");
|
|
}
|
|
|
|
/// Written when the tags cleanup read a named property list's `MCID` by reference at each `BDC`: a
|
|
/// list larger than the document keeps was parsed again at each, 50 of them taking 12.3 s against
|
|
/// 0.44 s with the `MCID` given directly (optimized).
|
|
#[test]
|
|
fn an_oversized_property_list_is_read_once_for_a_stream() {
|
|
let _serial = one_at_a_time();
|
|
let (one, many) = (redaction(|| oversized(b"/P /M0 BDC EMC ")), redaction(|| oversized(&b"/P /M0 BDC EMC ".repeat(100))));
|
|
assert!(about_as_long(many, one), "100 operators {many:?}, one {one:?}");
|
|
}
|