#![allow(clippy::expect_used)]
use std::time::{Duration, Instant};
use pdfrum::{
Affine, Deadline, DiagKind, Document, Error, LimitExceeded, Limits, OpenOptions, Operation,
PageIndex, RenderOptions, RenderSession, VelloCpuBackend,
};
const HELLO: &str = "tests/fixtures/hello_world.pdf";
const GUIDE: &str = "../../benches/corpus/text_quick_start.pdf";
fn open_with(limits: Limits) -> Document {
Document::open_with(HELLO, &{
let mut __o = OpenOptions::default();
__o.limits = limits;
__o
})
.expect("open")
}
fn open_guide(deadline: Deadline) -> pdfrum::Result<Document> {
Document::open_with(GUIDE, &{
let mut __o = OpenOptions::default();
__o.limits = Limits {
deadline: Some(deadline),
..Limits::default()
};
__o
})
}
fn capped(max_render_pixels: u64) -> Document {
open_with(Limits {
max_render_pixels: Some(max_render_pixels),
..Limits::default()
})
}
fn huge() -> RenderOptions {
let mut options = RenderOptions::default();
options.transform = Affine::scale(100.0);
options
}
#[test]
fn a_render_above_the_pixel_cap_is_refused_before_anything_is_allocated() {
let doc = capped(100_000_000);
let page = doc.page(0).expect("page");
let started = Instant::now();
let error = page
.render(&VelloCpuBackend::new(), &huge())
.expect_err("400 megapixels is above a 100-megapixel cap");
assert!(
started.elapsed() < Duration::from_secs(2),
"the refusal took {:?}: something was allocated or decoded first",
started.elapsed()
);
match error {
Error::Limit(LimitExceeded::RenderPixels {
width,
height,
allowed,
}) => {
assert_eq!((width, height, allowed), (20_000, 20_000, 100_000_000));
}
other => panic!("expected the pixel cap, got {other:?}"),
}
let message = error.to_string();
assert!(
message.contains("20000 x 20000 px") && message.contains("cap of 100 megapixels"),
"a person can act on the message: {message}"
);
}
#[test]
fn a_prepared_page_is_refused_at_the_draw() {
let doc = capped(100_000_000);
let page = doc.page(0).expect("page");
let mut session = RenderSession::new();
let prepared = page.prepare(&huge(), &mut session);
assert!(matches!(
prepared.render_on(&VelloCpuBackend::new(), &mut session),
Err(Error::Limit(LimitExceeded::RenderPixels { .. }))
));
assert!(matches!(
prepared.render(&VelloCpuBackend::new()),
Err(Error::Limit(LimitExceeded::RenderPixels { .. }))
));
}
#[test]
fn a_render_under_the_cap_succeeds() {
let doc = capped(100_000_000);
let page = doc.page(0).expect("page");
let pixmap = page
.render(&VelloCpuBackend::new(), &RenderOptions::default())
.expect("40000 px is under the cap");
assert_eq!((pixmap.width(), pixmap.height()), (200, 200));
let doc = capped(40_000);
let page = doc.page(0).expect("page");
assert!(
page.render(&VelloCpuBackend::new(), &RenderOptions::default())
.is_ok()
);
let doc = capped(39_999);
let page = doc.page(0).expect("page");
assert!(matches!(
page.render(&VelloCpuBackend::new(), &RenderOptions::default()),
Err(Error::Limit(LimitExceeded::RenderPixels {
width: 200,
height: 200,
allowed: 39_999
}))
));
}
#[test]
fn a_deadline_already_passed_refuses_the_open() {
let error = open_guide(Deadline::after(Duration::ZERO)).expect_err("no time at all");
assert!(matches!(
error,
Error::Limit(LimitExceeded::Time {
during: Operation::Open,
page: None,
..
})
));
assert_eq!(
error.to_string(),
"time limit of 0 s exceeded while opening the document; allow more time or do less"
);
assert!(matches!(
open_guide(Deadline::at(Instant::now())),
Err(Error::Limit(LimitExceeded::Time {
during: Operation::Open,
..
}))
));
}
#[test]
fn a_generous_deadline_changes_nothing() {
let doc = open_guide(Deadline::after(Duration::from_hours(1))).expect("an hour is plenty");
let plain = Document::open(GUIDE).expect("open");
let (page, reference) = (doc.page(0).expect("page"), plain.page(0).expect("page"));
let backend = VelloCpuBackend::new();
let options = RenderOptions::default();
assert_eq!(
page.render(&backend, &options).expect("render").data(),
reference.render(&backend, &options).expect("render").data()
);
assert_eq!(page.text().to_string(), reference.text().to_string());
assert!(!page.text().to_string().is_empty());
assert!(!doc.all_diagnostics().contains(&DiagKind::TimeLimitReached));
}
#[test]
fn a_deadline_that_passes_after_the_open_stops_every_later_operation() {
let doc = open_guide(Deadline::after(Duration::from_millis(200))).expect("opens in time");
let page = doc.page(0).expect("loads in time");
std::thread::sleep(Duration::from_millis(250));
let error = page
.render(&VelloCpuBackend::new(), &RenderOptions::default())
.expect_err("out of time");
assert!(matches!(
error,
Error::Limit(LimitExceeded::Time {
during: Operation::Render,
page: Some(index),
..
}) if index == PageIndex::FIRST
));
assert_eq!(
error.to_string(),
"time limit of 200 ms exceeded while rendering page 0; allow more time or do less"
);
assert_eq!(page.text().char_count(), 0);
assert!(doc.all_diagnostics().contains(&DiagKind::TimeLimitReached));
assert!(matches!(
doc.page(0),
Err(Error::Limit(LimitExceeded::Time {
during: Operation::PageLoad,
page: Some(index),
..
})) if index == PageIndex::FIRST
));
assert_eq!(doc.pages().count(), 0);
}
#[test]
fn a_caller_can_stop_the_work_by_hand() {
let stop = Deadline::manual();
let doc = open_guide(stop.clone()).expect("open");
let page = doc.page(0).expect("page");
assert!(
page.render(&VelloCpuBackend::new(), &RenderOptions::default())
.is_ok(),
"nothing is raised yet"
);
stop.stop();
let error = page
.render(&VelloCpuBackend::new(), &RenderOptions::default())
.expect_err("stopped");
assert!(matches!(
error,
Error::Limit(LimitExceeded::Stopped {
during: Operation::Render,
page: Some(index),
}) if index == PageIndex::FIRST
));
assert_eq!(
error.to_string(),
"stopped by the caller while rendering page 0"
);
assert_eq!(page.text().char_count(), 0);
assert!(matches!(
doc.page(0),
Err(Error::Limit(LimitExceeded::Stopped {
during: Operation::PageLoad,
..
}))
));
}
#[test]
fn a_caller_can_stop_the_work_from_another_thread() {
let stop = Deadline::manual();
let doc = open_guide(stop.clone()).expect("open");
let page = doc.page(0).expect("page");
let raiser = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
stop.stop();
});
let backend = VelloCpuBackend::new();
let error = loop {
if let Err(error) = page.render(&backend, &RenderOptions::default()) {
break error;
}
};
raiser.join().expect("the raiser exits");
assert!(matches!(
error,
Error::Limit(LimitExceeded::Stopped {
during: Operation::Render,
..
})
));
}
#[test]
fn the_default_limits_cap_nothing() {
let doc = open_with(Limits::default());
let page = doc.page(0).expect("page");
let pixmap = page
.render(&VelloCpuBackend::new(), &RenderOptions::scaled(4.0))
.expect("no cap by default");
assert_eq!((pixmap.width(), pixmap.height()), (800, 800));
}