#![allow(clippy::expect_used)]
use pdfrum::{BuildContext, Document, FormSession, Modifiers, Point, UpdateKind};
fn document() -> Document {
Document::open("tests/fixtures/substituted_da_font.pdf")
.expect("the substituted_da_font fixture must open")
}
const INSIDE: Point = Point::new(100.0, 55.0);
fn hermetic_font_dir() -> Option<std::path::PathBuf> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../pdfium-c++/third_party/test_fonts/test_fonts");
if dir.is_dir() {
return Some(dir);
}
let message = format!(
"the oracle's hermetic font set is not at {}; the substituted-face \
assertions are skipped",
dir.display()
);
assert!(
std::env::var_os("PDFRUM_REQUIRE_ORACLE_FONTS").is_none(),
"PDFRUM_REQUIRE_ORACLE_FONTS is set but {message}"
);
eprintln!("note: {message}");
None
}
fn live_stream(session: &mut FormSession<'_>) -> Vec<u8> {
session.mouse_move(0, INSIDE, Modifiers::NONE);
session.mouse_down(0, INSIDE, Modifiers::NONE);
let response = session.mouse_up(0, INSIDE, Modifiers::NONE);
response
.updates
.iter()
.find_map(|update| match &update.kind {
UpdateKind::LiveEdit(ap) => Some(ap.stream.clone()),
_ => None,
})
.expect("a focused text field draws its live editor state")
}
fn caret_height(stream: &[u8]) -> f32 {
let text = String::from_utf8_lossy(stream);
let tokens: Vec<&str> = text.split_whitespace().collect();
let re = tokens
.iter()
.rposition(|token| *token == "re")
.expect("a focused field strokes a caret, which is a `re`");
tokens[re - 1]
.parse()
.expect("the caret's height is the last operand before `re`")
}
#[test]
fn a_session_built_on_a_default_context_agrees_with_the_default_constructor() {
let doc = document();
let mut plain = FormSession::new(&doc);
let mut ctx = BuildContext::new();
let mut threaded = FormSession::with_context(&doc, &mut ctx);
let (plain, threaded) = (
caret_height(&live_stream(&mut plain)),
caret_height(&live_stream(&mut threaded)),
);
assert!(
(plain - threaded).abs() < f32::EPSILON,
"two default contexts must substitute one document one way: {plain} vs {threaded}"
);
}
#[test]
fn the_hermetic_set_gives_the_caret_the_substituted_faces_height() {
let Some(font_dir) = hermetic_font_dir() else {
return;
};
let doc = document();
let mut ctx = BuildContext::with_substitution(pdfrum::SubstitutionOptions {
font_dirs: vec![font_dir],
croscore_font_names: true,
..pdfrum::SubstitutionOptions::default()
});
let mut substituting = FormSession::with_context(&doc, &mut ctx);
let substituted = caret_height(&live_stream(&mut substituting));
let mut base14 = FormSession::new(&doc);
let defaulted = caret_height(&live_stream(&mut base14));
assert!(
(substituted - 13.392).abs() < 1e-3,
"Arimo's 905/-211 at 12pt is 13.392, got {substituted}"
);
assert!(
(defaulted - 11.244).abs() < 1e-3,
"base-14 Helvetica's 718/-219 at 12pt is 11.244, got {defaulted}"
);
}
#[test]
fn a_hebrew_live_edit_sets_its_text_in_the_second_face() {
let doc = document();
let mut session = FormSession::new(&doc);
session.mouse_move(0, INSIDE, Modifiers::NONE);
session.mouse_down(0, INSIDE, Modifiers::NONE);
session.mouse_up(0, INSIDE, Modifiers::NONE);
let mut last = None;
for ch in "בחר".chars() {
let response = session.character(ch, Modifiers::NONE);
if let Some(update) = response
.updates
.iter()
.find(|update| matches!(update.kind, UpdateKind::LiveEdit(_)))
{
last = Some(update.kind.clone());
}
}
let UpdateKind::LiveEdit(ap) = last.expect("typing redraws the focused field") else {
unreachable!("filtered to LiveEdit above")
};
let stream = String::from_utf8_lossy(&ap.stream).into_owned();
assert!(
stream.contains("/_B1"),
"the Hebrew run must be set in the second face, got:\n{stream}"
);
let resources = format!("{:?}", ap.resources);
assert!(
resources.contains("_B1"),
"the second face must be declared in the appearance's own resources, got:\n{resources}"
);
let advances: Vec<f32> = stream
.lines()
.filter_map(|line| line.strip_suffix(" Td"))
.filter_map(|operands| {
let mut parts = operands.split_whitespace();
let x: f32 = parts.next()?.parse().ok()?;
parts.next().filter(|y| *y == "0")?;
Some(x)
})
.collect();
assert!(
!advances.is_empty(),
"a three-character run steps between its characters, got:\n{stream}"
);
for advance in advances {
assert!(
(advance.abs() - 3.0).abs() < 1e-3,
"each Hebrew character advances by the second face's 250/1000 at \
12pt, which is 3.0 and not the /DA font's 8.664; got {advance}"
);
}
}