181 lines
8.9 KiB
Rust
181 lines
8.9 KiB
Rust
//! Fonts the editor meets in the wild (PDF-03).
|
|
|
|
use std::path::Path;
|
|
|
|
use ely_pdf_engine::{Doc, Worker, Writing, Wrote};
|
|
|
|
fn worker() -> Worker {
|
|
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
|
|
}
|
|
|
|
fn page_text(worker: &mut Worker, doc: Doc) -> String {
|
|
worker.text(doc, 0).expect("reads").iter().map(|c| c.c).collect()
|
|
}
|
|
|
|
/// A rewrite of block `block` as `text` in `font`.
|
|
fn rewrite(worker: &mut Worker, doc: Doc, block: usize, text: &str, font: &str) -> Writing {
|
|
let read = worker.text_blocks(doc, 0).expect("blocks")[block].clone();
|
|
Writing {
|
|
block: Some(block),
|
|
expected: read.text,
|
|
text: text.into(),
|
|
font: font.into(),
|
|
size: read.size,
|
|
rgb: read.rgb,
|
|
bx: [read.rect[0], read.rect[1], read.rect[2] - read.rect[0]],
|
|
}
|
|
}
|
|
|
|
/// A page whose text uses a CMap-encoded CID font.
|
|
fn cmap_font_pdf() -> Vec<u8> {
|
|
let content = "BT /F1 24 Tf 72 700 Td <00480069> Tj ET";
|
|
one_font_page(content, &[
|
|
"<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>".to_string(),
|
|
"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor 7 0 R >>".to_string(),
|
|
"<< /Type /FontDescriptor /FontName /STSong-Light /Flags 4 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 880 /Descent -120 /CapHeight 700 /StemV 80 >>".to_string(),
|
|
])
|
|
}
|
|
|
|
/// A page drawing `content`; font objects start at 5.
|
|
fn one_font_page(content: &str, font: &[String]) -> Vec<u8> {
|
|
page_with("<< /F1 5 0 R >>", content, font)
|
|
}
|
|
|
|
/// A page with font resources `fonts`; objects from 5.
|
|
fn page_with(fonts: &str, content: &str, font: &[String]) -> Vec<u8> {
|
|
let mut objects = vec![
|
|
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
|
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font {fonts} >> /Contents 4 0 R >>"),
|
|
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
|
];
|
|
objects.extend_from_slice(font);
|
|
let mut pdf = b"%PDF-1.7\n".to_vec();
|
|
let mut offsets = Vec::new();
|
|
for (i, body) in objects.iter().enumerate() {
|
|
offsets.push(pdf.len());
|
|
pdf.extend(format!("{} 0 obj\n{body}\nendobj\n", i + 1).bytes());
|
|
}
|
|
let xref = pdf.len();
|
|
pdf.extend(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).bytes());
|
|
for offset in offsets {
|
|
pdf.extend(format!("{offset:010} 00000 n \n").bytes());
|
|
}
|
|
pdf.extend(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).bytes());
|
|
pdf
|
|
}
|
|
|
|
#[test]
|
|
fn a_font_whose_codes_ely_cannot_write_says_so() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let path = dir.path().join("cmap.pdf");
|
|
std::fs::write(&path, cmap_font_pdf()).expect("writes");
|
|
let mut worker = worker();
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
assert_eq!(page_text(&mut worker, doc), "Hi");
|
|
let writing = rewrite(&mut worker, doc, 0, "Yo", "original");
|
|
assert_eq!(worker.put_text(doc, 0, &writing, 2.0).expect("answers"), Wrote::Unwritable);
|
|
assert_eq!(page_text(&mut worker, doc), "Hi", "nothing changed");
|
|
let helvetica = rewrite(&mut worker, doc, 0, "Yo", "helvetica");
|
|
assert!(matches!(worker.put_text(doc, 0, &helvetica, 0.0).expect("writes"), Wrote::Done(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_ligature_block_edits_like_any_other() {
|
|
// Code 1 is one ligature glyph, in either Unicode form.
|
|
let mut forms = vec![("00660069".to_string(), "find".to_string()), ("FB01".into(), "find".into()), ("FB03".into(), "ffind".into())];
|
|
// Long mappings, to MuPDF's cap of 256: none cut short.
|
|
forms.push(("0066".repeat(48), format!("{}nd", "f".repeat(48))));
|
|
forms.push(("FB03".repeat(256), format!("{}nd", "ffi".repeat(256))));
|
|
for (unicode, text) in forms {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let path = dir.path().join("lig.pdf");
|
|
let cmap = format!("/CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /Lig def 1 begincodespacerange <00> <FF> endcodespacerange 3 beginbfchar <01> <{unicode}> <6E> <006E> <64> <0064> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end");
|
|
let content = "BT /F1 24 Tf 72 700 Td <016E64> Tj ET";
|
|
let pdf = one_font_page(content, &[
|
|
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [1 /fi] >> /ToUnicode 6 0 R >>".to_string(),
|
|
format!("<< /Length {} >>\nstream\n{cmap}\nendstream", cmap.len()),
|
|
]);
|
|
std::fs::write(&path, pdf).expect("writes");
|
|
let mut worker = worker();
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
let block = worker.text_blocks(doc, 0).expect("blocks").into_iter().next().expect("a block");
|
|
assert_eq!(block.text, text);
|
|
assert!(block.editable, "{unicode}: {block:?}");
|
|
let writing = rewrite(&mut worker, doc, 0, "Yo", "helvetica");
|
|
let Wrote::Done(mark) = worker.put_text(doc, 0, &writing, 0.0).expect("writes") else {
|
|
panic!("{unicode}: the ligature block was not rewritten");
|
|
};
|
|
assert_eq!(page_text(&mut worker, doc.at(&mark).expect("a mark")), "Yo");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_control_character_in_text_still_reads() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let path = dir.path().join("control.pdf");
|
|
// Code 1 names U+0001, as broken encodings do.
|
|
let content = "BT /F1 24 Tf 72 700 Td <41014243> Tj ET";
|
|
let pdf = one_font_page(content, &[
|
|
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding << /Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [1 /uni0001] >> >>".to_string(),
|
|
]);
|
|
std::fs::write(&path, pdf).expect("writes");
|
|
let mut worker = worker();
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
let text = page_text(&mut worker, doc);
|
|
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
|
|
assert_eq!(blocks.len(), 1);
|
|
assert_eq!(blocks[0].text, text, "the same text as the page reads");
|
|
}
|
|
|
|
#[test]
|
|
fn a_font_name_with_a_stray_byte_still_reads() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let path = dir.path().join("name.pdf");
|
|
// #E9 alone is not UTF-8.
|
|
let content = "BT /F1 24 Tf 72 700 Td (ABC) Tj ET";
|
|
let pdf = one_font_page(content, &[
|
|
"<< /Type /Font /Subtype /Type1 /BaseFont /Review#E9Font /Encoding /WinAnsiEncoding >>".to_string(),
|
|
]);
|
|
std::fs::write(&path, pdf).expect("writes");
|
|
let mut worker = worker();
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
|
|
assert_eq!((blocks[0].text.as_str(), blocks[0].font.as_str()), ("ABC", "Review\u{FFFD}Font"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_font_name_in_surrogate_halves_still_reads() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let content = "BT /F1 24 Tf 72 700 Td (ABC) Tj ET";
|
|
let mut worker = worker();
|
|
// A pair joins; a lone half is no character.
|
|
for (base, want) in [("Review#ED#A0#BD#ED#B8#80Font", "Review\u{1F600}Font"), ("Lone#ED#A0#BDFont", "Lone\u{FFFD}Font")] {
|
|
let path = dir.path().join(format!("{want}.pdf"));
|
|
let font = format!("<< /Type /Font /Subtype /Type1 /BaseFont /{base} /Encoding /WinAnsiEncoding >>");
|
|
std::fs::write(&path, one_font_page(content, &[font])).expect("writes");
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
let blocks = worker.text_blocks(doc, 0).expect("the blocks parse");
|
|
assert_eq!(blocks[0].font, want);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_font_written_into_the_page_says_it_cannot_write() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let path = dir.path().join("direct.pdf");
|
|
// The font dictionary sits in the page itself.
|
|
let fonts = "<< /F1 << /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /Identity-H /DescendantFonts [5 0 R] >> >>";
|
|
let pdf = page_with(fonts, "BT /F1 24 Tf 72 700 Td <00480069> Tj ET", &[
|
|
"<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor 6 0 R >>".to_string(),
|
|
"<< /Type /FontDescriptor /FontName /STSong-Light /Flags 4 /FontBBox [0 -200 1000 900] /ItalicAngle 0 /Ascent 880 /Descent -120 /CapHeight 700 /StemV 80 >>".to_string(),
|
|
]);
|
|
std::fs::write(&path, pdf).expect("writes");
|
|
let mut worker = worker();
|
|
let doc = worker.open(&path).expect("opens").doc;
|
|
let writing = rewrite(&mut worker, doc, 0, "Yo", "original");
|
|
assert_eq!(worker.put_text(doc, 0, &writing, 2.0).expect("answers"), Wrote::Unwritable);
|
|
let helvetica = rewrite(&mut worker, doc, 0, "Yo", "helvetica");
|
|
assert!(matches!(worker.put_text(doc, 0, &helvetica, 0.0).expect("writes"), Wrote::Done(_)));
|
|
}
|