Files
pdfcraft/crates/engine/tests/it/document_fonts.rs
T
aiongg 9526363152 Build each crate's integration tests as one binary (#4087)
* Build each crate's integration tests as one binary

Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.

ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.

What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
  ones (content, document_links, editing, fill_sign, view), so one
  process creates one wgpu device at a time. The drop-line test in ui.rs
  and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
  CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
  names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.

* Name the one-binary test paths in docs and comments

- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
  crates/ui-egui/tests/it/main.rs.
2026-10-22 11:28:58 -04:00

99 lines
5.1 KiB
Rust

//! Document font selection and contextual glyph save/reopen acceptance on synthetic PDFs.
use crate::support;
use pdfcraft_cos::{Dict, Document, Object, SaveOptions, Stream};
use pdfcraft_engine::{BlockStyle, Edit, Session};
use pdfcraft_render::{PageRenderer, RenderRequest};
use std::sync::Arc;
fn document() -> Arc<Vec<u8>> {
let mut d = Document::new_empty();
let pages = d.add(Object::Dict(Dict::new()));
let contents = d.add(Object::Stream(Stream::flate(Dict::new(), b"BT /F1 18 Tf 30 160 Td (Source) Tj ET")));
let mut font = Dict::new();
font.set(b"Type".to_vec(), Object::name("Font"));
font.set(b"Subtype".to_vec(), Object::name("Type1"));
font.set(b"BaseFont".to_vec(), Object::name("Helvetica"));
let mut fonts = Dict::new();
fonts.set(b"F1".to_vec(), Object::Dict(font));
let mut res = Dict::new();
res.set(b"Font".to_vec(), Object::Dict(fonts));
let mut p = Dict::new();
p.set(b"Type".to_vec(), Object::name("Page"));
p.set(b"Parent".to_vec(), Object::Ref(pages));
p.set(b"Contents".to_vec(), Object::Ref(contents));
p.set(b"Resources".to_vec(), Object::Dict(res));
p.set(b"MediaBox".to_vec(), Object::Array([0, 0, 300, 240].map(Object::Int).to_vec()));
let page = d.add(Object::Dict(p));
d.update_dict(pages, |p| {
p.set(b"Type".to_vec(), Object::name("Pages"));
p.set(b"Kids".to_vec(), Object::Array(vec![Object::Ref(page)]));
p.set(b"Count".to_vec(), Object::Int(1));
})
.unwrap();
let mut root = Dict::new();
root.set(b"Type".to_vec(), Object::name("Catalog"));
root.set(b"Pages".to_vec(), Object::Ref(pages));
let root = d.add(Object::Dict(root));
d.trailer_mut().set(b"Root".to_vec(), Object::Ref(root));
Arc::new(pdfcraft_cos::write_full(&d, &SaveOptions::default()).unwrap())
}
fn pixels(bytes: Arc<Vec<u8>>) -> Vec<u8> {
let mut renderer = PageRenderer::new(bytes, Default::default());
let p = renderer.render(RenderRequest { page: 0, scale: 2.0, ..Default::default() });
assert!(p.error.is_none(), "{:?}", p.error);
assert!(p.warnings.is_empty(), "{:?}", p.warnings);
p.rgba.to_vec()
}
fn exercise(font: Vec<u8>, text: &str, expected_family: &str) {
let mut s = Session::new();
let id = s.open("source.pdf", None, document(), None).unwrap();
let original = pixels(s.get(id).unwrap().display.clone());
s.apply(id, Edit::RegisterTextFont { page: 0, bytes: Arc::new(font.clone()) }).unwrap();
let resource = s.get(id).unwrap().text_editor_fonts(0).into_iter().find(|f| f.bytes == font).unwrap().resource;
let style = BlockStyle { resource: Some(resource), width: Some(240.0), ..Default::default() };
s.apply(id, Edit::EditTextBlock { page: 0, block: 0, text: text.into(), style: style.clone() }).unwrap();
let edited = s.get(id).unwrap();
let lines = edited.text_lines(0);
assert_eq!(lines.iter().map(|l| l.text.as_str()).collect::<String>(), text);
assert!(lines.iter().all(|l| l.base_font == expected_family));
assert!((lines[0].baseline()[1] - 160.0).abs() < 0.01);
let current = pixels(edited.display.clone());
assert_ne!(current, original);
let saved = s.save_bytes(id).unwrap();
let re = s.open("saved.pdf", None, saved, None).unwrap();
assert_eq!(pixels(s.get(re).unwrap().display.clone()), current);
assert_eq!(s.get(re).unwrap().text_blocks(0)[0].editor_text, text);
let twice = format!("{text} {text}");
s.apply(re, Edit::EditTextBlock { page: 0, block: 0, text: twice.clone(), style: style.clone() }).unwrap();
assert_eq!(s.get(re).unwrap().text_blocks(0)[0].editor_text, twice);
let again = s.save_bytes(re).unwrap();
let next = s.open("twice.pdf", None, again, None).unwrap();
assert_eq!(s.get(next).unwrap().text_blocks(0)[0].editor_text, twice);
assert_eq!(pixels(s.get(next).unwrap().display.clone()), pixels(s.get(re).unwrap().display.clone()));
s.apply(re, Edit::EditTextBlock { page: 0, block: 0, text: String::new(), style }).unwrap();
assert!(s.get(re).unwrap().text_lines(0).is_empty(), "every contextual glyph is deleted");
let empty = pixels(s.get(re).unwrap().display.clone());
assert!(empty.as_chunks::<4>().0.iter().all(|p| p[..3] == [255; 3]));
s.undo(id).unwrap();
assert_eq!(pixels(s.get(id).unwrap().display.clone()), original);
}
#[test]
fn opentype_cff_document_font_preserves_program_codes_and_rendering_after_reopen() {
exercise(support::font_fixture::cff_font(), "A A", "RectFont-Regular");
}
#[test]
fn contextual_arabic_marks_ligatures_and_mixed_bidi_keep_logical_text_after_reopen() {
if let Some(path) = std::env::var_os("PDFCRAFT_TEST_ARABIC_FONT") {
exercise(std::fs::read(path).unwrap(), "سَلَام 123", "NotoSansArabic-Regular");
return;
}
let Some(font) = pdfcraft_fonts::document_arabic_font() else {
eprintln!("set CRAFT_FONTS_DIR to test full Arabic glyph programs");
return;
};
if pdfcraft_fonts::embedding::validate(font.bytes).is_err() {
eprintln!("set PDFCRAFT_TEST_ARABIC_FONT to an openly licensed static Noto Sans Arabic face");
return;
}
exercise(font.bytes.to_vec(), "سَلَام 123", "NotoSansArabic-Regular");
}