quick_search/vendor/pdf-extract/src/lib.rs

2443 lines
101 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extern crate lopdf;
use encoding_rs::UTF_16BE;
use lopdf::content::{Content, Operation};
pub use lopdf::*;
use euclid::*;
use lopdf::encryption::DecryptionError;
use std::fmt::{Debug, Formatter};
extern crate encoding_rs;
extern crate euclid;
extern crate type1_encoding_parser;
extern crate unicode_normalization;
use euclid::vec2;
use unicode_normalization::UnicodeNormalization;
use std::fmt;
use std::str;
use std::fs::File;
use std::slice::Iter;
use std::collections::HashMap;
use std::rc::Rc;
use std::marker::PhantomData;
use std::result::Result;
use log::{warn, error, debug};
mod core_fonts;
mod glyphnames;
mod zapfglyphnames;
mod encodings;
pub struct Space;
pub type Transform = Transform2D<f64, Space, Space>;
#[derive(Debug)]
pub enum OutputError
{
FormatError(std::fmt::Error),
IoError(std::io::Error),
PdfError(lopdf::Error),
/// LOCAL PATCH (QuickSearch): the document spent [`MAX_PDF_WORK`] and
/// rendering stopped. Whatever was output before the stop stands.
Budget,
}
impl std::fmt::Display for OutputError
{
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
OutputError::FormatError(e) => write!(f, "Formating error: {}", e),
OutputError::IoError(e) => write!(f, "IO error: {}", e),
OutputError::PdfError(e) => write!(f, "PDF error: {}", e),
OutputError::Budget => write!(f, "work budget of {} bytes spent", MAX_PDF_WORK),
}
}
}
impl std::error::Error for OutputError{}
impl From<std::fmt::Error> for OutputError {
fn from(e: std::fmt::Error) -> Self {
OutputError::FormatError(e)
}
}
impl From<std::io::Error> for OutputError {
fn from(e: std::io::Error) -> Self {
OutputError::IoError(e)
}
}
impl From<lopdf::Error> for OutputError {
fn from(e: lopdf::Error) -> Self {
OutputError::PdfError(e)
}
}
macro_rules! dlog {
($($e:expr),*) => { {} }
//($($t:tt)*) => { println!($($t)*) }
}
fn get_info(doc: &Document) -> Option<&Dictionary> {
match doc.trailer.get(b"Info") {
Ok(&Object::Reference(ref id)) => {
match doc.get_object(*id) {
Ok(&Object::Dictionary(ref info)) => { return Some(info); }
_ => {}
}
}
_ => {}
}
None
}
fn get_catalog(doc: &Document) -> &Dictionary {
match doc.trailer.get(b"Root").unwrap() {
&Object::Reference(ref id) => {
match doc.get_object(*id) {
Ok(&Object::Dictionary(ref catalog)) => { return catalog; }
_ => {}
}
}
_ => {}
}
panic!();
}
fn get_pages(doc: &Document) -> &Dictionary {
let catalog = get_catalog(doc);
match catalog.get(b"Pages").unwrap() {
&Object::Reference(ref id) => {
match doc.get_object(*id) {
Ok(&Object::Dictionary(ref pages)) => { return pages; }
other => {dlog!("pages: {:?}", other)}
}
}
other => { dlog!("pages: {:?}", other)}
}
dlog!("catalog {:?}", catalog);
panic!();
}
#[allow(non_upper_case_globals)]
const PDFDocEncoding: &'static [u16] = &[
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008,
0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, 0x0010, 0x0011,
0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, 0x02d8, 0x02c7, 0x02c6,
0x02d9, 0x02dd, 0x02db, 0x02da, 0x02dc, 0x0020, 0x0021, 0x0022, 0x0023,
0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c,
0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035,
0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e,
0x003f, 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f, 0x0050,
0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058, 0x0059,
0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062,
0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b,
0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074,
0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d,
0x007e, 0x0000, 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x0192,
0x2044, 0x2039, 0x203a, 0x2212, 0x2030, 0x201e, 0x201c, 0x201d, 0x2018,
0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x0141, 0x0152, 0x0160, 0x0178,
0x017d, 0x0131, 0x0142, 0x0153, 0x0161, 0x017e, 0x0000, 0x20ac, 0x00a1,
0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, 0x00a8, 0x00a9, 0x00aa,
0x00ab, 0x00ac, 0x0000, 0x00ae, 0x00af, 0x00b0, 0x00b1, 0x00b2, 0x00b3,
0x00b4, 0x00b5, 0x00b6, 0x00b7, 0x00b8, 0x00b9, 0x00ba, 0x00bb, 0x00bc,
0x00bd, 0x00be, 0x00bf, 0x00c0, 0x00c1, 0x00c2, 0x00c3, 0x00c4, 0x00c5,
0x00c6, 0x00c7, 0x00c8, 0x00c9, 0x00ca, 0x00cb, 0x00cc, 0x00cd, 0x00ce,
0x00cf, 0x00d0, 0x00d1, 0x00d2, 0x00d3, 0x00d4, 0x00d5, 0x00d6, 0x00d7,
0x00d8, 0x00d9, 0x00da, 0x00db, 0x00dc, 0x00dd, 0x00de, 0x00df, 0x00e0,
0x00e1, 0x00e2, 0x00e3, 0x00e4, 0x00e5, 0x00e6, 0x00e7, 0x00e8, 0x00e9,
0x00ea, 0x00eb, 0x00ec, 0x00ed, 0x00ee, 0x00ef, 0x00f0, 0x00f1, 0x00f2,
0x00f3, 0x00f4, 0x00f5, 0x00f6, 0x00f7, 0x00f8, 0x00f9, 0x00fa, 0x00fb,
0x00fc, 0x00fd, 0x00fe, 0x00ff];
fn pdf_to_utf8(s: &[u8]) -> String {
if s.len() > 2 && s[0] == 0xfe && s[1] == 0xff {
return UTF_16BE.decode_without_bom_handling_and_without_replacement(&s[2..]).unwrap().to_string()
} else {
let r : Vec<u8> = s.iter().map(|x| *x).flat_map(|x| {
let k = PDFDocEncoding[x as usize];
vec![(k>>8) as u8, k as u8].into_iter()}).collect();
return UTF_16BE.decode_without_bom_handling_and_without_replacement(&r).unwrap().to_string()
}
}
fn to_utf8(encoding: &[u16], s: &[u8]) -> String {
if s.len() > 2 && s[0] == 0xfe && s[1] == 0xff {
return UTF_16BE.decode_without_bom_handling_and_without_replacement(&s[2..]).unwrap().to_string()
} else {
let r : Vec<u8> = s.iter().map(|x| *x).flat_map(|x| {
let k = encoding[x as usize];
vec![(k>>8) as u8, k as u8].into_iter()}).collect();
return UTF_16BE.decode_without_bom_handling_and_without_replacement(&r).unwrap().to_string()
}
}
fn maybe_deref<'a>(doc: &'a Document, o: &'a Object) -> &'a Object {
match o {
&Object::Reference(r) => doc.get_object(r).expect("missing object reference"),
_ => o
}
}
fn maybe_get_obj<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<&'a Object> {
dict.get(key).map(|o| maybe_deref(doc, o)).ok()
}
// an intermediate trait that can be used to chain conversions that may have failed
trait FromOptObj<'a> {
fn from_opt_obj(doc: &'a Document, obj: Option<&'a Object>, key: &[u8]) -> Self;
}
// conditionally convert to Self returns None if the conversion failed
trait FromObj<'a> where Self: std::marker::Sized {
fn from_obj(doc: &'a Document, obj: &'a Object) -> Option<Self>;
}
impl<'a, T: FromObj<'a>> FromOptObj<'a> for Option<T> {
fn from_opt_obj(doc: &'a Document, obj: Option<&'a Object>, _key: &[u8]) -> Self {
obj.and_then(|x| T::from_obj(doc,x))
}
}
impl<'a, T: FromObj<'a>> FromOptObj<'a> for T {
fn from_opt_obj(doc: &'a Document, obj: Option<&'a Object>, key: &[u8]) -> Self {
T::from_obj(doc, obj.expect(&String::from_utf8_lossy(key))).expect("wrong type")
}
}
// we follow the same conventions as pdfium for when to support indirect objects:
// on arrays, streams and dicts
impl<'a, T: FromObj<'a>> FromObj<'a> for Vec<T> {
fn from_obj(doc: &'a Document, obj: &'a Object) -> Option<Self> {
maybe_deref(doc, obj).as_array().map(|x| x.iter()
.map(|x| T::from_obj(doc, x).expect("wrong type"))
.collect()).ok()
}
}
impl<'a> FromObj<'a> for f64 {
fn from_obj(_doc: &Document, obj: &Object) -> Option<Self> {
match obj {
&Object::Integer(i) => Some(i as f64),
&Object::Real(f) => Some(f.into()),
_ => None
}
}
}
impl<'a> FromObj<'a> for i64 {
fn from_obj(_doc: &Document, obj: &Object) -> Option<Self> {
match obj {
&Object::Integer(i) => Some(i),
_ => None
}
}
}
impl<'a> FromObj<'a> for &'a Dictionary {
fn from_obj(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
maybe_deref(doc, obj).as_dict().ok()
}
}
impl<'a> FromObj<'a> for &'a Stream {
fn from_obj(doc: &'a Document, obj: &'a Object) -> Option<&'a Stream> {
maybe_deref(doc, obj).as_stream().ok()
}
}
impl<'a> FromObj<'a> for &'a Object {
fn from_obj(doc: &'a Document, obj: &'a Object) -> Option<&'a Object> {
Some(maybe_deref(doc, obj))
}
}
fn get<'a, T: FromOptObj<'a>>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> T {
T::from_opt_obj(doc, dict.get(key).ok(), key)
}
fn maybe_get<'a, T: FromObj<'a>>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<T> {
maybe_get_obj(doc, dict, key).and_then(|o| T::from_obj(doc, o))
}
fn get_name_string<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> String {
pdf_to_utf8(dict.get(key).map(|o| maybe_deref(doc, o)).unwrap_or_else(|_| panic!("deref")).as_name().expect("name"))
}
#[allow(dead_code)]
fn maybe_get_name_string<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<String> {
maybe_get_obj(doc, dict, key).and_then(|n| n.as_name().ok()).map(|n| pdf_to_utf8(n))
}
fn maybe_get_name<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<&'a [u8]> {
maybe_get_obj(doc, dict, key).and_then(|n| n.as_name().ok())
}
fn maybe_get_array<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> Option<&'a Vec<Object>> {
maybe_get_obj(doc, dict, key).and_then(|n| n.as_array().ok())
}
#[derive(Clone)]
struct PdfSimpleFont<'a> {
font: &'a Dictionary,
doc: &'a Document,
encoding: Option<Vec<u16>>,
unicode_map: Option<UnicodeMap>,
widths: HashMap<CharCode, f64>, // should probably just use i32 here
missing_width: f64,
}
/// LOCAL PATCH (QuickSearch): a simple font's unicode map in layers, looked
/// up in this order: its ToUnicode CMap, its CFF font file's glyph names,
/// then what its `Differences` add for codes neither maps. The first two are
/// shared with every font naming the same stream (see [`FontParts`]).
/// Upstream merged the three into one map per font, so each font sharing a
/// large CMap copied it; the order keeps the merge's precedence.
#[derive(Clone, Debug)]
struct UnicodeMap {
to_unicode: Option<Rc<HashMap<u32, String>>>,
cff: Option<Rc<HashMap<u32, String>>>,
differences: HashMap<u32, String>,
}
impl UnicodeMap {
fn get(&self, code: &u32) -> Option<&String> {
self.to_unicode.as_ref().and_then(|map| map.get(code))
.or_else(|| self.cff.as_ref().and_then(|map| map.get(code)))
.or_else(|| self.differences.get(code))
}
}
#[derive(Clone)]
struct PdfType3Font<'a> {
font: &'a Dictionary,
doc: &'a Document,
encoding: Option<Vec<u16>>,
unicode_map: Option<Rc<HashMap<CharCode, String>>>,
widths: HashMap<CharCode, f64>, // should probably just use i32 here
}
fn make_font<'a>(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> Rc<dyn PdfFont + 'a> {
let subtype = get_name_string(doc, font, b"Subtype");
dlog!("MakeFont({})", subtype);
if subtype == "Type0" {
Rc::new(PdfCIDFont::new(doc, font, parts))
} else if subtype == "Type3" {
Rc::new(PdfType3Font::new(doc, font, parts))
} else {
Rc::new(PdfSimpleFont::new(doc, font, parts))
}
}
fn is_core_font(name: &str) -> bool {
match name {
"Courier-Bold" |
"Courier-BoldOblique" |
"Courier-Oblique" |
"Courier" |
"Helvetica-Bold" |
"Helvetica-BoldOblique" |
"Helvetica-Oblique" |
"Helvetica" |
"Symbol" |
"Times-Bold" |
"Times-BoldItalic" |
"Times-Italic" |
"Times-Roman" |
"ZapfDingbats" => true,
_ => false,
}
}
fn encoding_to_unicode_table(name: &[u8]) -> Vec<u16> {
let encoding = match &name[..] {
b"MacRomanEncoding" => encodings::MAC_ROMAN_ENCODING,
b"MacExpertEncoding" => encodings::MAC_EXPERT_ENCODING,
b"WinAnsiEncoding" => encodings::WIN_ANSI_ENCODING,
_ => panic!("unexpected encoding {:?}", pdf_to_utf8(name))
};
let encoding_table = encoding.iter()
.map(|x| if let &Some(x) = x { glyphnames::name_to_unicode(x).unwrap() } else { 0 })
.collect();
encoding_table
}
/* "Glyphs in the font are selected by single-byte character codes obtained from a string that
is shown by the text-showing operators. Logically, these codes index into a table of 256
glyphs; the mapping from codes to glyphs is called the font’s encoding. Each font program
has a built-in encoding. Under some circumstances, the encoding can be altered by means
described in Section 5.5.5, “Character Encoding.”
*/
impl<'a> PdfSimpleFont<'a> {
fn new(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> PdfSimpleFont<'a> {
let base_name = get_name_string(doc, font, b"BaseFont");
let subtype = get_name_string(doc, font, b"Subtype");
let encoding: Option<&Object> = get(doc, font, b"Encoding");
dlog!("base_name {} {} enc:{:?} {:?}", base_name, subtype, encoding, font);
let descriptor: Option<&Dictionary> = get(doc, font, b"FontDescriptor");
let mut type1_file = None;
let mut cff = None;
if let Some(descriptor) = descriptor {
dlog!("descriptor {:?}", descriptor);
if subtype == "Type1" {
let file = maybe_get_obj(doc, descriptor, b"FontFile");
match file {
// LOCAL PATCH (QuickSearch): read below, only if the font
// has no `/Encoding` of its own; it was parsed here
// whether or not anything used it.
Some(&Object::Stream(ref s)) => { type1_file = Some(s); }
_ => { dlog!("font file {:?}", file) }
}
}
// LOCAL PATCH (QuickSearch): a TrueType `FontFile2` was inflated
// here and dropped unread.
let font_file3 = get::<Option<&Object>>(doc, descriptor, b"FontFile3");
match font_file3 {
Some(&Object::Stream(ref s)) => {
let subtype = get_name_string(doc, &s.dict, b"Subtype");
dlog!("font file {}, {:?}", subtype, s);
// LOCAL PATCH (QuickSearch): parsed once per font file
// (see FontParts); a file of another subtype was inflated
// here and dropped unread.
if subtype == "Type1C" {
cff = Some(parse_once(&mut parts.cff, &mut parts.parsed, s, |s| {
let table = cff_parser::Table::parse(s).unwrap();
let encoding = table.encoding.get_code_to_sid_table(&table.charset);
let mapping: HashMap<u32, String> = encoding.into_iter().filter_map(|(cid, sid)| {
let name = cff_parser::string_by_id(&table, sid).unwrap();
if name == ".notdef" {
return None;
}
let unicode = glyphnames::name_to_unicode(&name).or_else(|| {
zapfglyphnames::zapfdigbats_names_to_unicode(name)
});
if unicode.is_none() {
warn!("Couldn't find unicode for {}", name);
return None;
}
let str = String::from_utf16(&[unicode.unwrap()]).unwrap();
Some((cid as u32, str))
}).collect();
Rc::new(mapping)
}));
}
}
None => {}
_ => { dlog!("unexpected") }
}
let charset = maybe_get_obj(doc, descriptor, b"CharSet");
let _charset = match charset {
Some(&Object::String(ref s, _)) => { Some(pdf_to_utf8(&s)) }
_ => { None }
};
//dlog!("charset {:?}", charset);
}
// LOCAL PATCH (QuickSearch): layered, not merged; see UnicodeMap.
let to_unicode = get_unicode_map(doc, font, parts);
let mut unicode_map = if to_unicode.is_some() || cff.is_some() {
Some(UnicodeMap { to_unicode, cff, differences: HashMap::new() })
} else {
None
};
let mut encoding_table = None;
match encoding {
Some(&Object::Name(ref encoding_name)) => {
dlog!("encoding {:?}", pdf_to_utf8(encoding_name));
encoding_table = Some(encoding_to_unicode_table(encoding_name));
}
Some(&Object::Dictionary(ref encoding)) => {
//dlog!("Encoding {:?}", encoding);
let mut table = if let Some(base_encoding) = maybe_get_name(doc, encoding, b"BaseEncoding") {
dlog!("BaseEncoding {:?}", base_encoding);
encoding_to_unicode_table(base_encoding)
} else {
Vec::from(PDFDocEncoding)
};
let differences = maybe_get_array(doc, encoding, b"Differences");
if let Some(differences) = differences {
dlog!("Differences");
let mut code = 0;
for o in differences {
let o = maybe_deref(doc, o);
match o {
&Object::Integer(i) => { code = i; },
&Object::Name(ref n) => {
let name = pdf_to_utf8(&n);
// XXX: names of Type1 fonts can map to arbitrary strings instead of real
// unicode names, so we should probably handle this differently
let unicode = glyphnames::name_to_unicode(&name);
if let Some(unicode) = unicode{
table[code as usize] = unicode;
if let Some(ref mut unicode_map) = unicode_map {
let be = [unicode];
match unicode_map.get(&(code as u32)) {
// If there's a unicode table entry missing use one based on the name
None => { unicode_map.differences.insert(code as u32, String::from_utf16(&be).unwrap()); }
Some(e) => {
if e != &String::from_utf16(&be).unwrap() {
let normal_match = e.nfkc().eq(String::from_utf16(&be).unwrap().nfkc());
if !normal_match {
warn!("Unicode mismatch {} {} {:?} {:?} {:?}", normal_match, name, e, String::from_utf16(&be), be);
}
}
}
}
}
} else {
match unicode_map {
Some(ref mut unicode_map) if base_name.contains("FontAwesome") => {
// the fontawesome tex package will use glyph names that don't have a corresponding unicode
// code point, so we'll use an empty string instead. See issue #76
match unicode_map.get(&(code as u32)) {
None => { unicode_map.differences.insert(code as u32, "".to_owned()); }
Some(_) => {
panic!("unexpected entry in unicode map")
}
}
}
_ => {
warn!("unknown glyph name '{}' for font {}", name, base_name);
}
}
}
dlog!("{} = {} ({:?})", code, name, unicode);
if let Some(ref mut unicode_map) = unicode_map {
// The unicode map might not have the code in it, but the code might
// not be used so we don't want to panic here.
// An example of this is the 'suppress' character in the TeX Latin Modern font.
// This shows up in https://arxiv.org/pdf/2405.01295v1.pdf
dlog!("{} {:?}", code, unicode_map.get(&(code as u32)));
}
code += 1;
}
_ => { panic!("wrong type {:?}", o); }
}
}
}
// "Type" is optional
let name = encoding.get(b"Type").and_then(|x| x.as_name()).and_then(|x| Ok(pdf_to_utf8(x)));
dlog!("name: {}", name);
encoding_table = Some(table);
}
None => {
if let Some(type1_file) = type1_file {
// LOCAL PATCH (QuickSearch): the table, not the parse, is
// what is kept per font file (see FontParts), so a font
// sharing the file costs no pass over its glyph names.
encoding_table = Some(parse_once(&mut parts.type1, &mut parts.parsed, type1_file, |s| {
let type1_encoding = type1_encoding_parser::get_encoding_map(s).expect("encoding");
let mut table = Vec::from(PDFDocEncoding);
dlog!("type1encoding");
for (code, name) in type1_encoding {
let unicode = glyphnames::name_to_unicode(&pdf_to_utf8(&name));
if let Some(unicode) = unicode {
table[code as usize] = unicode;
} else {
dlog!("unknown character {}", pdf_to_utf8(&name));
}
}
table
}))
} else if subtype == "TrueType" {
encoding_table = Some(encodings::WIN_ANSI_ENCODING.iter()
.map(|x| if let &Some(x) = x { glyphnames::name_to_unicode(x).unwrap() } else { 0 })
.collect());
}
}
_ => { panic!() }
}
let mut width_map = HashMap::new();
/* "Ordinarily, a font dictionary that refers to one of the standard fonts
should omit the FirstChar, LastChar, Widths, and FontDescriptor entries.
However, it is permissible to override a standard font by including these
entries and embedding the font program in the PDF file."
Note: some PDFs include a descriptor but still don't include these entries */
// If we have widths prefer them over the core font widths. Needed for https://dkp.de/wp-content/uploads/parteitage/Sozialismusvorstellungen-der-DKP.pdf
if let (Some(first_char), Some(last_char), Some(widths)) = (maybe_get::<i64>(doc, font, b"FirstChar"), maybe_get::<i64>(doc, font, b"LastChar"), maybe_get::<Vec<f64>>(doc, font, b"Widths")) {
// Some PDF's don't have these like fips-197.pdf
let mut i: i64 = 0;
dlog!("first_char {:?}, last_char: {:?}, widths: {} {:?}", first_char, last_char, widths.len(), widths);
for w in widths {
width_map.insert((first_char + i) as CharCode, w);
i += 1;
}
assert_eq!(first_char + i - 1, last_char);
} else {
let name = if is_core_font(&base_name) {
&base_name
} else {
warn!("no widths and not core font {:?}", base_name);
// This situation is handled differently by different readers
// but basically we try to substitute the best font that we can.
// Poppler/Xpdf:
// this is technically an error -- the Widths entry is required
// for all but the Base-14 fonts -- but certain PDF generators
// apparently don't include widths for Arial and TimesNewRoman
// Pdfium: CFX_FontMapper::FindSubstFont
// mupdf: pdf_load_substitute_font
// We can try to do a better job guessing at a font by looking at the flags
// or the basename but for now we'll just use Helvetica
"Helvetica"
};
for font_metrics in core_fonts::metrics().iter() {
if font_metrics.0 == base_name {
if let Some(ref encoding) = encoding_table {
dlog!("has encoding");
for w in font_metrics.2 {
let c = glyphnames::name_to_unicode(w.2).unwrap();
for i in 0..encoding.len() {
if encoding[i] == c {
width_map.insert(i as CharCode, w.1 as f64);
}
}
}
} else {
// Instead of using the encoding from the core font we'll just look up all
// of the character names. We should probably verify that this produces the
// same result.
let mut table = vec![0; 256];
for w in font_metrics.2 {
dlog!("{} {}", w.0, w.2);
// -1 is "not encoded"
if w.0 != -1 {
table[w.0 as usize] = if base_name == "ZapfDingbats" {
zapfglyphnames::zapfdigbats_names_to_unicode(w.2).unwrap_or_else(|| panic!("bad name {:?}", w))
} else {
glyphnames::name_to_unicode(w.2).unwrap()
}
}
}
let encoding = &table[..];
for w in font_metrics.2 {
width_map.insert(w.0 as CharCode, w.1 as f64);
// -1 is "not encoded"
}
encoding_table = Some(encoding.to_vec());
}
/* "Ordinarily, a font dictionary that refers to one of the standard fonts
should omit the FirstChar, LastChar, Widths, and FontDescriptor entries.
However, it is permissible to override a standard font by including these
entries and embedding the font program in the PDF file."
Note: some PDFs include a descriptor but still don't include these entries */
// assert!(maybe_get_obj(doc, font, b"FirstChar").is_none());
// assert!(maybe_get_obj(doc, font, b"LastChar").is_none());
// assert!(maybe_get_obj(doc, font, b"Widths").is_none());
}
}
}
let missing_width = get::<Option<f64>>(doc, font, b"MissingWidth").unwrap_or(0.);
PdfSimpleFont {doc, font, widths: width_map, encoding: encoding_table, missing_width, unicode_map}
}
#[allow(dead_code)]
fn get_type(&self) -> String {
get_name_string(self.doc, self.font, b"Type")
}
#[allow(dead_code)]
fn get_basefont(&self) -> String {
get_name_string(self.doc, self.font, b"BaseFont")
}
#[allow(dead_code)]
fn get_subtype(&self) -> String {
get_name_string(self.doc, self.font, b"Subtype")
}
#[allow(dead_code)]
fn get_widths(&self) -> Option<&Vec<Object>> {
maybe_get_obj(self.doc, self.font, b"Widths").map(|widths| widths.as_array().expect("Widths should be an array"))
}
/* For type1: This entry is obsolescent and its use is no longer recommended. (See
* implementation note 42 in Appendix H.) */
#[allow(dead_code)]
fn get_name(&self) -> Option<String> {
maybe_get_name_string(self.doc, self.font, b"Name")
}
#[allow(dead_code)]
fn get_descriptor(&self) -> Option<PdfFontDescriptor> {
maybe_get_obj(self.doc, self.font, b"FontDescriptor").and_then(|desc| desc.as_dict().ok()).map(|desc| PdfFontDescriptor{desc: desc, doc: self.doc})
}
}
impl<'a> PdfType3Font<'a> {
fn new(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> PdfType3Font<'a> {
let unicode_map = get_unicode_map(doc, font, parts);
let encoding: Option<&Object> = get(doc, font, b"Encoding");
let encoding_table;
match encoding {
Some(&Object::Name(ref encoding_name)) => {
dlog!("encoding {:?}", pdf_to_utf8(encoding_name));
encoding_table = Some(encoding_to_unicode_table(encoding_name));
}
Some(&Object::Dictionary(ref encoding)) => {
//dlog!("Encoding {:?}", encoding);
let mut table = if let Some(base_encoding) = maybe_get_name(doc, encoding, b"BaseEncoding") {
dlog!("BaseEncoding {:?}", base_encoding);
encoding_to_unicode_table(base_encoding)
} else {
Vec::from(PDFDocEncoding)
};
let differences = maybe_get_array(doc, encoding, b"Differences");
if let Some(differences) = differences {
dlog!("Differences");
let mut code = 0;
for o in differences {
match o {
&Object::Integer(i) => { code = i; },
&Object::Name(ref n) => {
let name = pdf_to_utf8(&n);
// XXX: names of Type1 fonts can map to arbitrary strings instead of real
// unicode names, so we should probably handle this differently
let unicode = glyphnames::name_to_unicode(&name);
if let Some(unicode) = unicode{
table[code as usize] = unicode;
}
dlog!("{} = {} ({:?})", code, name, unicode);
if let Some(ref unicode_map) = unicode_map {
dlog!("{} {:?}", code, unicode_map.get(&(code as u32)));
}
code += 1;
}
_ => { panic!("wrong type"); }
}
}
}
let name_encoded = encoding.get(b"Type");
if let Ok(Object::Name(name)) = name_encoded {
dlog!("name: {}", pdf_to_utf8(name));
} else {
dlog!("name not found");
}
encoding_table = Some(table);
}
_ => { panic!() }
}
let first_char: i64 = get(doc, font, b"FirstChar");
let last_char: i64 = get(doc, font, b"LastChar");
let widths: Vec<f64> = get(doc, font, b"Widths");
let mut width_map = HashMap::new();
let mut i = 0;
dlog!("first_char {:?}, last_char: {:?}, widths: {} {:?}", first_char, last_char, widths.len(), widths);
for w in widths {
width_map.insert((first_char + i) as CharCode, w);
i += 1;
}
assert_eq!(first_char + i - 1, last_char);
PdfType3Font {doc, font, widths: width_map, encoding: encoding_table, unicode_map}
}
}
type CharCode = u32;
struct PdfFontIter<'a>
{
i: Iter<'a, u8>,
font: &'a dyn PdfFont,
}
impl<'a> Iterator for PdfFontIter<'a> {
type Item = (CharCode, u8);
fn next(&mut self) -> Option<(CharCode, u8)> {
self.font.next_char(&mut self.i)
}
}
trait PdfFont : Debug {
fn get_width(&self, id: CharCode) -> f64;
fn next_char(&self, iter: &mut Iter<u8>) -> Option<(CharCode, u8)>;
fn decode_char(&self, char: CharCode) -> String;
/*fn char_codes<'a>(&'a self, chars: &'a [u8]) -> PdfFontIter {
let p = self;
PdfFontIter{i: chars.iter(), font: p as &PdfFont}
}*/
}
impl<'a> dyn PdfFont + 'a {
fn char_codes(&'a self, chars: &'a [u8]) -> PdfFontIter {
PdfFontIter{i: chars.iter(), font: self}
}
fn decode(&self, chars: &[u8]) -> String {
let strings = self.char_codes(chars).map(|x| self.decode_char(x.0)).collect::<Vec<_>>();
strings.join("")
}
}
impl<'a> PdfFont for PdfSimpleFont<'a> {
fn get_width(&self, id: CharCode) -> f64 {
// LOCAL PATCH (QuickSearch): a miss collected and sorted every width
// to feed a `dlog!` that expands to nothing.
self.widths.get(&id).copied().unwrap_or(self.missing_width)
}
/*fn decode(&self, chars: &[u8]) -> String {
let encoding = self.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
to_utf8(encoding, chars)
}*/
fn next_char(&self, iter: &mut Iter<u8>) -> Option<(CharCode, u8)> {
iter.next().map(|x| (*x as CharCode, 1))
}
fn decode_char(&self, char: CharCode) -> String {
let slice = [char as u8];
if let Some(ref unicode_map) = self.unicode_map {
let s = unicode_map.get(&char);
let s = match s {
None => {
debug!("missing char {:?} in unicode map {:?} for {:?}", char, unicode_map, self.font);
// some pdf's like http://arxiv.org/pdf/2312.00064v1 are missing entries in their unicode map but do have
// entries in the encoding.
let encoding = self.encoding.as_ref().map(|x| &x[..]).expect("missing unicode map and encoding");
let s = to_utf8(encoding, &slice);
debug!("falling back to encoding {} -> {:?}", char, s);
s
}
Some(s) => { s.clone() }
};
return s
}
let encoding = self.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
//dlog!("char_code {:?} {:?}", char, self.encoding);
let s = to_utf8(encoding, &slice);
s
}
}
impl<'a> fmt::Debug for PdfSimpleFont<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.font.fmt(f)
}
}
impl<'a> PdfFont for PdfType3Font<'a> {
fn get_width(&self, id: CharCode) -> f64 {
let width = self.widths.get(&id);
if let Some(width) = width {
return *width;
} else {
panic!("missing width for {} {:?}", id, self.font);
}
}
/*fn decode(&self, chars: &[u8]) -> String {
let encoding = self.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
to_utf8(encoding, chars)
}*/
fn next_char(&self, iter: &mut Iter<u8>) -> Option<(CharCode, u8)> {
iter.next().map(|x| (*x as CharCode, 1))
}
fn decode_char(&self, char: CharCode) -> String {
let slice = [char as u8];
if let Some(ref unicode_map) = self.unicode_map {
let s = unicode_map.get(&char);
let s = match s {
None => {
debug!("missing char {:?} in unicode map {:?} for {:?}", char, unicode_map, self.font);
// some pdf's like http://arxiv.org/pdf/2312.00577v1 are missing entries in their unicode map but do have
// entries in the encoding.
let encoding = self.encoding.as_ref().map(|x| &x[..]).expect("missing unicode map and encoding");
let s = to_utf8(encoding, &slice);
debug!("falling back to encoding {} -> {:?}", char, s);
s
}
Some(s) => { s.clone() }
};
return s
}
let encoding = self.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
//dlog!("char_code {:?} {:?}", char, self.encoding);
let s = to_utf8(encoding, &slice);
s
}
}
impl<'a> fmt::Debug for PdfType3Font<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.font.fmt(f)
}
}
struct PdfCIDFont<'a> {
font: &'a Dictionary,
#[allow(dead_code)]
doc: &'a Document,
#[allow(dead_code)]
encoding: Rc<ByteMapping>,
to_unicode: Option<Rc<HashMap<u32, String>>>,
widths: HashMap<CharCode, f64>, // should probably just use i32 here
default_width: Option<f64>, // only used for CID fonts and we should probably brake out the different font types
}
/// LOCAL PATCH (QuickSearch): the code-to-CID mapping of a Type0 font's
/// `/Encoding`, which adobe-cmap-parser used to supply. `width` is in bytes;
/// that parser stored half of it, so an embedded two-byte CMap was read one
/// byte at a time.
struct ByteMapping {
/// Scanned for every character shown, up to four times (once per code
/// width), so at most [`MAX_CODESPACE_RANGES`].
codespace: Vec<CodeRange>,
/// Sorted by `src_code_lo`, so a code's range is found by binary search.
cid: Vec<CIDRange>,
}
struct CodeRange {
width: u32,
start: u32,
end: u32,
}
struct CIDRange {
src_code_lo: u32,
src_code_hi: u32,
dst_cid_lo: u32,
/// LOCAL PATCH (QuickSearch): the highest `src_code_hi` of this range and
/// of every range sorted before it (see PdfCIDFont::cid).
reach: u32,
}
/// LOCAL PATCH (QuickSearch): the widest `beginbfrange` range read, in codes;
/// a wider one is skipped. A full two-byte range is exactly this wide.
const MAX_CMAP_RANGE: u32 = 1 << 16;
/// LOCAL PATCH (QuickSearch): the most entries one CMap defines, or CID
/// ranges an `/Encoding` defines. Reading stops there and keeps what it has.
const MAX_CMAP_ENTRIES: usize = 1 << 17;
/// LOCAL PATCH (QuickSearch): the most codespace ranges an `/Encoding`
/// defines; later ones are dropped. The spec allows 100 per
/// `begincodespacerange` block, and real CMaps define a handful.
const MAX_CODESPACE_RANGES: usize = 256;
/// LOCAL PATCH (QuickSearch): the longest ToUnicode destination string read,
/// in bytes, the PDF spec's limit; an entry mapping to a longer one is
/// skipped.
const MAX_CMAP_DST: usize = 512;
/// LOCAL PATCH (QuickSearch): the most text one ToUnicode CMap maps, in
/// destination bytes summed over its entries. A range copies its destination
/// for every code it spans, so the caps on entries and on a destination's
/// length still admit 64 MiB. Reading stops there and keeps what it has, as
/// at [`MAX_CMAP_ENTRIES`].
const MAX_CMAP_TEXT: usize = 16 << 20;
/// LOCAL PATCH (QuickSearch): a CMap's operations, tokenized by lopdf's
/// content parser, which replaces adobe-cmap-parser: that one panicked on a
/// CMap it could not parse and expanded `beginbfrange` code by code, so a
/// 4-byte full range was 2^32 inserts. Each `end...` operator receives the
/// entries before it as operands. `None` if the tokenizer rejects the
/// stream; one it reads only partway yields what it read.
fn cmap_operations(content: &[u8]) -> Option<Vec<Operation>> {
Content::decode(content).ok().map(|c| c.operations)
}
/// A CMap code: the string's bytes, big-endian.
fn cmap_code(bytes: &[u8]) -> u32 {
bytes.iter().fold(0, |code, &b| (code << 8) | b as u32)
}
/// A CMap destination string as UTF-16 code units. An odd last byte is a
/// unit of its own. `None` past [`MAX_CMAP_DST`] bytes.
fn cmap_utf16(bytes: &[u8]) -> Option<Vec<u16>> {
if bytes.len() > MAX_CMAP_DST {
return None;
}
Some(bytes.chunks(2).map(|unit| cmap_code(unit) as u16).collect())
}
/// Map `code` to the UTF-16 text `units`, spending one of the `left` entries
/// and the text's bytes from `text_left`; false once either is spent.
/// Invalid UTF-16, such as a lone surrogate, maps nothing.
fn put_unicode(map: &mut HashMap<u32, String>, left: &mut usize, text_left: &mut usize, code: u32, units: &[u16]) -> bool {
let bytes = 2 * units.len();
if *left == 0 || *text_left < bytes {
return false;
}
*left -= 1;
*text_left -= bytes;
if let Ok(s) = String::from_utf16(units) {
map.insert(code, s);
}
true
}
// "It must use the beginbfchar, endbfchar, beginbfrange, and endbfrange operators to
// define the mapping from character codes to Unicode character sequences expressed in
// UTF-16BE encoding."
fn to_unicode_map(content: &[u8]) -> Option<HashMap<u32, String>> {
let mut map = HashMap::new();
let mut left = MAX_CMAP_ENTRIES;
let mut text_left = MAX_CMAP_TEXT;
'cmap: for op in cmap_operations(content)? {
match op.operator.as_str() {
"endbfchar" => {
for entry in op.operands.chunks_exact(2) {
if let (Ok(code), Ok(Some(dst))) = (entry[0].as_str(), entry[1].as_str().map(cmap_utf16)) {
if !put_unicode(&mut map, &mut left, &mut text_left, cmap_code(code), &dst) {
break 'cmap;
}
}
}
}
"endbfrange" => {
for entry in op.operands.chunks_exact(3) {
let (Ok(lo), Ok(hi)) = (entry[0].as_str(), entry[1].as_str()) else { continue };
let (lo, hi) = (cmap_code(lo), cmap_code(hi));
if hi < lo || hi - lo >= MAX_CMAP_RANGE {
continue;
}
match &entry[2] {
// Each code after the first advances the last unit by one.
Object::String(dst, _) => {
let Some(mut units) = cmap_utf16(dst) else { continue };
for code in lo..=hi {
if !put_unicode(&mut map, &mut left, &mut text_left, code, &units) {
break 'cmap;
}
if let Some(last) = units.last_mut() {
*last = last.wrapping_add(1);
}
}
}
Object::Array(dsts) => {
for (code, dst) in (lo..=hi).zip(dsts) {
if let Ok(Some(dst)) = dst.as_str().map(cmap_utf16) {
if !put_unicode(&mut map, &mut left, &mut text_left, code, &dst) {
break 'cmap;
}
}
}
}
_ => {}
}
}
}
_ => {}
}
}
Some(map)
}
/// A Type0 font's `/Encoding` CMap. One the tokenizer rejects maps no codes,
/// so the font's strings decode to nothing.
fn byte_mapping(content: &[u8]) -> ByteMapping {
let mut mapping = ByteMapping { codespace: Vec::new(), cid: Vec::new() };
for op in cmap_operations(content).unwrap_or_default() {
match op.operator.as_str() {
"endcodespacerange" => {
for entry in op.operands.chunks_exact(2) {
if let (Ok(start), Ok(end)) = (entry[0].as_str(), entry[1].as_str()) {
mapping.codespace.push(CodeRange { width: start.len() as u32, start: cmap_code(start), end: cmap_code(end) });
}
}
}
"endcidrange" => {
for entry in op.operands.chunks_exact(3) {
if let (Ok(lo), Ok(hi), Ok(cid)) = (entry[0].as_str(), entry[1].as_str(), entry[2].as_i64()) {
mapping.cid.push(CIDRange { src_code_lo: cmap_code(lo), src_code_hi: cmap_code(hi), dst_cid_lo: cid as u32, reach: 0 });
}
}
}
_ => {}
}
}
mapping.codespace.truncate(MAX_CODESPACE_RANGES);
mapping.cid.truncate(MAX_CMAP_ENTRIES);
mapping.cid.sort_by_key(|range| range.src_code_lo);
let mut reach = 0;
for range in &mut mapping.cid {
reach = reach.max(range.src_code_hi);
range.reach = reach;
}
mapping
}
/// LOCAL PATCH (QuickSearch): what fonts parse out of their sub-streams,
/// keyed by the stream's address as `Processor.font_table` keys fonts, so
/// font dictionaries naming one ToUnicode CMap, `/Encoding` CMap or font file
/// parse it once per document and share the result. Upstream parsed it again
/// for each font dictionary, uncharged. `parsed` adds up the decoded bytes of
/// the parses since the `Tf` that last built a font, which charges them.
#[derive(Default)]
struct FontParts {
to_unicode: HashMap<usize, Option<Rc<HashMap<u32, String>>>>,
encodings: HashMap<usize, Rc<ByteMapping>>,
/// A Type1 font file's built-in encoding, as the table it gives.
type1: HashMap<usize, Vec<u16>>,
/// A Type1C font file's glyph names, as unicode.
cff: HashMap<usize, Rc<HashMap<u32, String>>>,
parsed: u64,
}
/// LOCAL PATCH (QuickSearch): `parse` of `stream`'s decoded content, from
/// `cache` or parsed now, adding the decoded length to `parsed`.
fn parse_once<T: Clone>(cache: &mut HashMap<usize, T>, parsed: &mut u64, stream: &Stream, parse: impl FnOnce(&[u8]) -> T) -> T {
cache.entry(std::ptr::from_ref::<Stream>(stream) as usize).or_insert_with(|| {
let content = get_contents(stream);
*parsed += content.len() as u64;
parse(&content)
}).clone()
}
fn get_unicode_map<'a>(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> Option<Rc<HashMap<u32, String>>> {
let to_unicode = maybe_get_obj(doc, font, b"ToUnicode");
dlog!("ToUnicode: {:?}", to_unicode);
match to_unicode {
Some(&Object::Stream(ref stream)) => {
parse_once(&mut parts.to_unicode, &mut parts.parsed, stream, |s| to_unicode_map(s).map(Rc::new))
}
None => None,
Some(&Object::Name(ref name)) => {
let name = pdf_to_utf8(name);
if name != "Identity-H" {
todo!("unsupported ToUnicode name: {:?}", name);
}
None
}
_ => { panic!("unsupported cmap {:?}", to_unicode)}
}
}
impl<'a> PdfCIDFont<'a> {
fn new(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> PdfCIDFont<'a> {
let base_name = get_name_string(doc, font, b"BaseFont");
let descendants = maybe_get_array(doc, font, b"DescendantFonts").expect("Descendant fonts required");
let ciddict = maybe_deref(doc, &descendants[0]).as_dict().expect("should be CID dict");
let encoding = maybe_get_obj(doc, font, b"Encoding").expect("Encoding required in type0 fonts");
dlog!("base_name {} {:?}", base_name, font);
let encoding = match encoding {
&Object::Name(ref name) => {
let name = pdf_to_utf8(name);
dlog!("encoding {:?}", name);
if name == "Identity-H" || name == "Identity-V" {
Rc::new(ByteMapping { codespace: vec![CodeRange{width: 2, start: 0, end: 0xffff }], cid: vec![CIDRange{ src_code_lo: 0, src_code_hi: 0xffff, dst_cid_lo: 0, reach: 0xffff }]})
} else {
panic!("unsupported encoding {}", name);
}
}
&Object::Stream(ref stream) => {
parse_once(&mut parts.encodings, &mut parts.parsed, stream, |s| Rc::new(byte_mapping(s)))
}
_ => { panic!("unsupported encoding {:?}", encoding)}
};
// Sometimes a Type0 font might refer to the same underlying data as regular font. In this case we may be able to extract some encoding
// data.
// We should also look inside the truetype data to see if there's a cmap table. It will help us convert as well.
// This won't work if the cmap has been subsetted. A better approach might be to hash glyph contents and use that against
// a global library of glyph hashes
let unicode_map = get_unicode_map(doc, font, parts);
dlog!("descendents {:?} {:?}", descendants, ciddict);
let font_dict = maybe_get_obj(doc, ciddict, b"FontDescriptor").expect("required");
dlog!("{:?}", font_dict);
let _f = font_dict.as_dict().expect("must be dict");
let default_width = get::<Option<i64>>(doc, ciddict, b"DW").unwrap_or(1000);
let w: Option<Vec<&Object>> = get(doc, ciddict, b"W");
dlog!("widths {:?}", w);
let mut widths = HashMap::new();
let mut i = 0;
if let Some(w) = w {
while i < w.len() {
if let &Object::Array(ref wa) = w[i+1] {
let cid = w[i].as_i64().expect("id should be num");
let mut j = 0;
dlog!("wa: {:?} -> {:?}", cid, wa);
for w in wa {
widths.insert((cid + j) as CharCode, as_num(w) );
j += 1;
}
i += 2;
} else {
let c_first = w[i].as_i64().expect("first should be num");
let c_last = w[i].as_i64().expect("last should be num");
let c_width = as_num(&w[i]);
for id in c_first..c_last {
widths.insert(id as CharCode, c_width);
}
i += 3;
}
}
}
PdfCIDFont{doc, font, widths, to_unicode: unicode_map, encoding, default_width: Some(default_width as f64) }
}
}
impl<'a> PdfCIDFont<'a> {
/// LOCAL PATCH (QuickSearch): the CID `/Encoding` maps `code` to: its
/// offset into its range from the range's first CID. Upstream added the
/// code itself to that CID, which only an Identity CMap (every range from
/// code 0 to CID 0) gets right. The ranges are sorted (see byte_mapping),
/// so the one to try is the last starting at or below the code; upstream
/// scanned every range for every character. When that range ends below
/// the code, an earlier, wider one may still hold it (`<0000> <FFFF>`
/// around a nested `<0020> <007E>`): the first whose `reach` gets there
/// does, since `reach` only grows.
fn cid(&self, code: u32) -> Option<u32> {
let cid = &self.encoding.cid;
let below = &cid[..cid.partition_point(|range| range.src_code_lo <= code)];
let last = below.last()?;
let range = if code <= last.src_code_hi {
last
} else {
below.get(below.partition_point(|range| range.reach < code))?
};
(code - range.src_code_lo).checked_add(range.dst_cid_lo)
}
}
impl<'a> PdfFont for PdfCIDFont<'a> {
/// LOCAL PATCH (QuickSearch): `id` is the character code (see
/// next_char); `/W` is keyed by CID.
fn get_width(&self, id: CharCode) -> f64 {
let width = self.cid(id).and_then(|cid| self.widths.get(&cid));
if let Some(width) = width {
dlog!("GetWidth {} -> {}", id, *width);
return *width;
} else {
dlog!("missing width for {} falling back to default_width", id);
return self.default_width.unwrap();
}
}/*
fn decode(&self, chars: &[u8]) -> String {
self.char_codes(chars);
//let utf16 = Vec::new();
let encoding = self.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
to_utf8(encoding, chars)
}*/
fn next_char(&self, iter: &mut Iter<u8>) -> Option<(CharCode, u8)> {
let mut c = *iter.next()? as u32;
let mut code = None;
'outer: for width in 1..=4 {
for range in &self.encoding.codespace {
if c as u32 >= range.start && c as u32 <= range.end && range.width == width {
code = Some((c as u32, width));
break 'outer;
}
}
let next = *iter.next()?;
c = ((c as u32) << 8) | next as u32;
}
let code = code?;
// LOCAL PATCH (QuickSearch): the code itself, not its CID: ToUnicode
// maps codes, and the word-spacing test in show_text reads code 32.
// Upstream returned the CID, so a font whose `/Encoding` is not an
// Identity CMap looked its ToUnicode entries up under the wrong key.
// A code no CID range maps still ends the string, as upstream.
self.cid(code.0)?;
Some((code.0, code.1 as u8))
}
fn decode_char(&self, char: CharCode) -> String {
let s = self.to_unicode.as_ref().and_then(|x| x.get(&char));
if let Some(s) = s {
s.clone()
} else {
dlog!("Unknown character {:?} in {:?} {:?}", char, self.font, self.to_unicode);
"".to_string()
}
}
}
impl<'a> fmt::Debug for PdfCIDFont<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.font.fmt(f)
}
}
#[derive(Copy, Clone)]
struct PdfFontDescriptor<'a> {
desc: &'a Dictionary,
doc: &'a Document
}
impl<'a> PdfFontDescriptor<'a> {
#[allow(dead_code)]
fn get_file(&self) -> Option<&'a Object> {
maybe_get_obj(self.doc, self.desc, b"FontFile")
}
}
impl<'a> fmt::Debug for PdfFontDescriptor<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.desc.fmt(f)
}
}
fn as_num(o: &Object) -> f64 {
match o {
&Object::Integer(i) => { i as f64 }
&Object::Real(f) => { f.into() }
_ => { panic!("not a number") }
}
}
#[derive(Clone)]
struct TextState<'a>
{
font: Option<Rc<dyn PdfFont + 'a>>,
font_size: f64,
character_spacing: f64,
word_spacing: f64,
horizontal_scaling: f64,
leading: f64,
rise: f64,
tm: Transform,
}
// XXX: We'd ideally implement this without having to copy the uncompressed data
fn get_contents(contents: &Stream) -> Vec<u8> {
if contents.filters().is_ok() {
contents.decompressed_content().unwrap_or_else(|_|contents.content.clone())
} else {
contents.content.clone()
}
}
/// LOCAL PATCH (QuickSearch): colour, colour space and soft mask are gone,
/// with everything that resolved them. Nothing read them, and resolving them
/// panicked on legitimate documents (an Indexed colour space, a `gs` naming
/// no ExtGState), which lost the whole document's text.
#[derive(Clone)]
struct GraphicsState<'a>
{
ctm: Transform,
ts: TextState<'a>,
line_width: f64,
}
fn show_text(gs: &mut GraphicsState, s: &[u8],
_tlm: &Transform,
_flip_ctm: &Transform,
output: &mut dyn OutputDev) -> Result<(), OutputError> {
let ts = &mut gs.ts;
let font = ts.font.as_ref().unwrap();
//let encoding = font.encoding.as_ref().map(|x| &x[..]).unwrap_or(&PDFDocEncoding);
dlog!("{:?}", font.decode(s));
dlog!("{:?}", font.decode(s).as_bytes());
dlog!("{:?}", s);
output.begin_word()?;
for (c, length) in font.char_codes(s) {
// 5.3.3 Text Space Details
let tsm = Transform2D::row_major(ts.horizontal_scaling,
0.,
0.,
1.0,
0.,
ts.rise);
// Trm = Tsm × Tm × CTM
let trm = tsm.post_transform(&ts.tm.post_transform(&gs.ctm));
//dlog!("ctm: {:?} tm {:?}", gs.ctm, tm);
//dlog!("current pos: {:?}", position);
// 5.9 Extraction of Text Content
//dlog!("w: {}", font.widths[&(*c as i64)]);
let w0 = font.get_width(c) / 1000.;
let mut spacing = ts.character_spacing;
// "Word spacing is applied to every occurrence of the single-byte character code 32 in a
// string when using a simple font or a composite font that defines code 32 as a
// single-byte code. It does not apply to occurrences of the byte value 32 in
// multiple-byte codes."
let is_space = c == 32 && length == 1;
if is_space { spacing += ts.word_spacing }
output.output_character(&trm, w0, spacing, ts.font_size, &font.decode_char(c))?;
let tj = 0.;
let ty = 0.;
let tx = ts.horizontal_scaling * ((w0 - tj/1000.)* ts.font_size + spacing);
dlog!("horizontal {} adjust {} {} {} {}", ts.horizontal_scaling, tx, w0, ts.font_size, spacing);
// dlog!("w0: {}, tx: {}", w0, tx);
ts.tm = ts.tm.pre_transform(&Transform2D::create_translation(tx, ty));
let _trm = ts.tm.pre_transform(&gs.ctm);
//dlog!("post pos: {:?}", trm);
}
output.end_word()?;
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct MediaBox {
pub llx: f64,
pub lly: f64,
pub urx: f64,
pub ury: f64
}
#[derive(Debug)]
pub enum PathOp {
MoveTo(f64, f64),
LineTo(f64, f64),
// XXX: is it worth distinguishing the different kinds of curve ops?
CurveTo(f64, f64, f64, f64, f64, f64),
Rect(f64, f64, f64, f64),
Close,
}
#[derive(Debug)]
pub struct Path {
pub ops: Vec<PathOp>
}
impl Path {
fn new() -> Path {
Path { ops: Vec::new() }
}
fn current_point(&self) -> (f64, f64) {
match self.ops.last().unwrap() {
&PathOp::MoveTo(x, y) => { (x, y) }
&PathOp::LineTo(x, y) => { (x, y) }
&PathOp::CurveTo(_, _, _, _, x, y) => { (x, y) }
_ => { panic!() }
}
}
}
/// LOCAL PATCH (QuickSearch): the saved graphics states one content stream
/// holds. A `q` past this is counted instead of saved, and the `Q` matching
/// it only uncounts, so `q` and `Q` stay balanced; upstream saved a clone of
/// the state for every `q`, without limit.
const MAX_GS_STACK: usize = 256;
struct Processor<'a> {
/// LOCAL PATCH (QuickSearch): fonts keyed by their dictionary's address,
/// which is stable while the document is borrowed, not by resource name.
/// Names are per resource dictionary: keyed by name, a later page's `/F1`
/// decoded with an earlier page's, and every alias parsed its font again.
font_table: HashMap<usize, Rc<dyn PdfFont + 'a>>,
/// LOCAL PATCH (QuickSearch): what those fonts parsed out of their
/// sub-streams.
parts: FontParts,
/// LOCAL PATCH (QuickSearch): nesting of `Do` (Form XObject) recursion.
xobject_depth: u32,
/// LOCAL PATCH (QuickSearch): the work this document has spent; see
/// [`MAX_PDF_WORK`].
work: u64,
_none: PhantomData<&'a ()>,
}
impl<'a> Processor<'a> {
fn new() -> Processor<'a> {
Processor { font_table: HashMap::new(), parts: FontParts::default(), xobject_depth: 0, work: 0, _none: PhantomData }
}
/// LOCAL PATCH (QuickSearch): spend `work` of the document's budget; past
/// [`MAX_PDF_WORK`], rendering stops with [`OutputError::Budget`].
fn charge(&mut self, work: u64) -> Result<(), OutputError> {
self.work += work;
if self.work > MAX_PDF_WORK {
return Err(OutputError::Budget);
}
Ok(())
}
/// LOCAL PATCH (QuickSearch): `Document::get_page_content` within the
/// budget. The page's streams are concatenated newline-separated as lopdf
/// does, so graphics and text state carry from one stream into the next.
/// `/Contents` may name one stream any number of times, and lopdf inflated
/// every copy: here each copy is charged its encoded bytes before it is
/// inflated, and the buffer ends the document once `process_stream`
/// could not afford it.
fn page_content(&mut self, doc: &Document, page_id: ObjectId) -> Result<Vec<u8>, OutputError> {
let mut content = Vec::new();
for id in doc.get_page_contents(page_id) {
if let Ok(stream) = doc.get_object(id).and_then(Object::as_stream) {
self.charge(stream.content.len() as u64)?;
content.extend_from_slice(&get_contents(stream));
content.push(b'\n');
if self.work + content.len() as u64 > MAX_PDF_WORK {
return Err(OutputError::Budget);
}
}
}
Ok(content)
}
/// LOCAL PATCH (QuickSearch): the font for `font_dict`, from the table or
/// built now. Building is charged what the font reads before it is built
/// (see [`font_work`]), then the decoded bytes of the sub-streams it was
/// first to parse (see [`FontParts`]). Kept out of `process_stream`,
/// whose frame every nested `Do` stacks again.
fn font(&mut self, doc: &'a Document, font_dict: &'a Dictionary) -> Result<Rc<dyn PdfFont + 'a>, OutputError> {
let key = std::ptr::from_ref::<Dictionary>(font_dict) as usize;
if let Some(font) = self.font_table.get(&key) {
return Ok(font.clone());
}
self.charge(font_work(doc, font_dict))?;
let font = make_font(doc, font_dict, &mut self.parts);
self.font_table.insert(key, font.clone());
let parsed = std::mem::take(&mut self.parts.parsed);
self.charge(parsed)?;
Ok(font)
}
fn process_stream(&mut self, doc: &'a Document, content: Vec<u8>, resources: &'a Dictionary, media_box: &MediaBox, output: &mut dyn OutputDev, page_num: u32) -> Result<(), OutputError> {
// LOCAL PATCH (QuickSearch): charge the stream before decoding it.
self.charge(content.len() as u64 + MIN_STREAM_WORK)?;
let content = Content::decode(&content).unwrap();
let mut gs: GraphicsState = GraphicsState {
ts: TextState {
font: None,
font_size: std::f64::NAN,
character_spacing: 0.,
word_spacing: 0.,
horizontal_scaling: 100. / 100.,
leading: 0.,
rise: 0.,
tm: Transform2D::identity(),
},
line_width: 1.,
ctm: Transform2D::identity(),
};
//let mut ts = &mut gs.ts;
let mut gs_stack = Vec::new();
// LOCAL PATCH (QuickSearch): `q`s past MAX_GS_STACK not yet matched.
let mut gs_overflow = 0usize;
let mut mc_stack = Vec::new();
// XXX: replace tlm with a point for text start
let mut tlm = Transform2D::identity();
let mut path = Path::new();
let flip_ctm = Transform2D::row_major(1., 0., 0., -1., 0., media_box.ury - media_box.lly);
dlog!("MediaBox {:?}", media_box);
for operation in &content.operations {
//dlog!("op: {:?}", operation);
match operation.operator.as_ref() {
"BT" => {
tlm = Transform2D::identity();
gs.ts.tm = tlm;
}
"ET" => {
tlm = Transform2D::identity();
gs.ts.tm = tlm;
}
"cm" => {
assert!(operation.operands.len() == 6);
let m = Transform2D::row_major(as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3]),
as_num(&operation.operands[4]),
as_num(&operation.operands[5]));
gs.ctm = gs.ctm.pre_transform(&m);
dlog!("matrix {:?}", gs.ctm);
}
// LOCAL PATCH (QuickSearch): colour is not tracked; see GraphicsState.
"CS" | "cs" | "SC" | "SCN" | "sc" | "scn" | "G" | "g" | "RG" | "rg" | "K" | "k" => {
dlog!("unhandled color operation {:?}", operation);
}
"TJ" => {
match operation.operands[0] {
Object::Array(ref array) => {
for e in array {
match e {
&Object::String(ref s, _) => {
show_text(&mut gs, s, &tlm, &flip_ctm, output)?;
}
&Object::Integer(i) => {
let ts = &mut gs.ts;
let w0 = 0.;
let tj = i as f64;
let ty = 0.;
let tx = ts.horizontal_scaling * ((w0 - tj / 1000.) * ts.font_size);
ts.tm = ts.tm.pre_transform(&Transform2D::create_translation(tx, ty));
dlog!("adjust text by: {} {:?}", i, ts.tm);
}
&Object::Real(i) => {
let ts = &mut gs.ts;
let w0 = 0.;
let tj = i as f64;
let ty = 0.;
let tx = ts.horizontal_scaling * ((w0 - tj / 1000.) * ts.font_size);
ts.tm = ts.tm.pre_transform(&Transform2D::create_translation(tx, ty));
dlog!("adjust text by: {} {:?}", i, ts.tm);
}
_ => { dlog!("kind of {:?}", e); }
}
}
}
_ => {}
}
}
"Tj" => {
match operation.operands[0] {
Object::String(ref s, _) => {
show_text(&mut gs, s, &tlm, &flip_ctm, output)?;
}
_ => { panic!("unexpected Tj operand {:?}", operation) }
}
}
"Tc" => {
gs.ts.character_spacing = as_num(&operation.operands[0]);
}
"Tw" => {
gs.ts.word_spacing = as_num(&operation.operands[0]);
}
"Tz" => {
gs.ts.horizontal_scaling = as_num(&operation.operands[0]) / 100.;
}
"TL" => {
gs.ts.leading = as_num(&operation.operands[0]);
}
"Tf" => {
let fonts: &Dictionary = get(&doc, resources, b"Font");
let name = operation.operands[0].as_name().unwrap();
let font_dict: &Dictionary = get(doc, fonts, name);
let font = self.font(doc, font_dict)?;
{
/*let file = font.get_descriptor().and_then(|desc| desc.get_file());
if let Some(file) = file {
let file_contents = filter_data(file.as_stream().unwrap());
let mut cursor = Cursor::new(&file_contents[..]);
//let f = Font::read(&mut cursor);
//dlog!("font file: {:?}", f);
}*/
}
gs.ts.font = Some(font);
gs.ts.font_size = as_num(&operation.operands[1]);
dlog!("font {} size: {} {:?}", pdf_to_utf8(name), gs.ts.font_size, operation);
}
"Ts" => {
gs.ts.rise = as_num(&operation.operands[0]);
}
"Tm" => {
assert!(operation.operands.len() == 6);
tlm = Transform2D::row_major(as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3]),
as_num(&operation.operands[4]),
as_num(&operation.operands[5]));
gs.ts.tm = tlm;
dlog!("Tm: matrix {:?}", gs.ts.tm);
output.end_line()?;
}
"Td" => {
/* Move to the start of the next line, offset from the start of the current line by (tx , ty ).
tx and ty are numbers expressed in unscaled text space units.
More precisely, this operator performs the following assignments:
*/
assert!(operation.operands.len() == 2);
let tx = as_num(&operation.operands[0]);
let ty = as_num(&operation.operands[1]);
dlog!("translation: {} {}", tx, ty);
tlm = tlm.pre_transform(&Transform2D::create_translation(tx, ty));
gs.ts.tm = tlm;
dlog!("Td matrix {:?}", gs.ts.tm);
output.end_line()?;
}
"TD" => {
/* Move to the start of the next line, offset from the start of the current line by (tx , ty ).
As a side effect, this operator sets the leading parameter in the text state.
*/
assert!(operation.operands.len() == 2);
let tx = as_num(&operation.operands[0]);
let ty = as_num(&operation.operands[1]);
dlog!("translation: {} {}", tx, ty);
gs.ts.leading = -ty;
tlm = tlm.pre_transform(&Transform2D::create_translation(tx, ty));
gs.ts.tm = tlm;
dlog!("TD matrix {:?}", gs.ts.tm);
output.end_line()?;
}
"T*" => {
let tx = 0.0;
let ty = -gs.ts.leading;
tlm = tlm.pre_transform(&Transform2D::create_translation(tx, ty));
gs.ts.tm = tlm;
dlog!("T* matrix {:?}", gs.ts.tm);
output.end_line()?;
}
"q" => {
if gs_stack.len() < MAX_GS_STACK {
gs_stack.push(gs.clone());
} else {
gs_overflow += 1;
}
}
"Q" => {
if gs_overflow > 0 {
gs_overflow -= 1;
} else if let Some(s) = gs_stack.pop() {
gs = s;
} else {
warn!("No state to pop");
}
}
"i" => { dlog!("unhandled graphics state flattness operator {:?}", operation); }
"w" => { gs.line_width = as_num(&operation.operands[0]); }
"J" | "j" | "M" | "d" | "ri" | "gs" => { dlog!("unknown graphics state operator {:?}", operation); }
"m" => { path.ops.push(PathOp::MoveTo(as_num(&operation.operands[0]), as_num(&operation.operands[1]))) }
"l" => { path.ops.push(PathOp::LineTo(as_num(&operation.operands[0]), as_num(&operation.operands[1]))) }
"c" => {
path.ops.push(PathOp::CurveTo(
as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3]),
as_num(&operation.operands[4]),
as_num(&operation.operands[5])))
}
"v" => {
let (x, y) = path.current_point();
path.ops.push(PathOp::CurveTo(
x,
y,
as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3])))
}
"y" => {
path.ops.push(PathOp::CurveTo(
as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3])))
}
"h" => { path.ops.push(PathOp::Close) }
"re" => {
path.ops.push(PathOp::Rect(as_num(&operation.operands[0]),
as_num(&operation.operands[1]),
as_num(&operation.operands[2]),
as_num(&operation.operands[3])))
}
"s" | "f*" | "B" | "B*" | "b" => {
dlog!("unhandled path op {:?}", operation);
}
"S" => {
output.stroke(&gs.ctm, &path)?;
path.ops.clear();
}
"F" | "f" => {
output.fill(&gs.ctm, &path)?;
path.ops.clear();
}
"W" | "w*" => { dlog!("unhandled clipping operation {:?}", operation); }
"n" => {
dlog!("discard {:?}", path);
path.ops.clear();
}
"BMC" | "BDC" => {
mc_stack.push(operation);
}
"EMC" => {
mc_stack.pop();
}
"Do" => {
// `Do` process an entire subdocument, so we do a recursive call to `process_stream`
// with the subdocument content and resources
let xobject: &Dictionary = get(&doc, resources, b"XObject");
let name = operation.operands[0].as_name().unwrap();
let xf: &Stream = get(&doc, xobject, name);
// LOCAL PATCH (QuickSearch): only a Form XObject is
// content; an image's samples were inflated and parsed as
// operators. A form that draws itself, directly or
// through a ring, recursed here without bound: the depth
// counter bounds the stack; Processor.work (MAX_PDF_WORK)
// bounds the branching case, where each level draws the
// next twice. Both tests come before anything is inflated.
if maybe_get_name(doc, &xf.dict, b"Subtype") != Some(&b"Form"[..]) {
dlog!("skipping Do: not a Form XObject");
} else if self.xobject_depth >= MAX_PDF_RECURSION_DEPTH {
dlog!("skipping Do: XObject nesting past {}", MAX_PDF_RECURSION_DEPTH);
} else {
let resources = maybe_get_obj(&doc, &xf.dict, b"Resources").and_then(|n| n.as_dict().ok()).unwrap_or(resources);
// LOCAL PATCH (QuickSearch): inflating costs the
// encoded bytes, which process_stream's charge of the
// decoded ones misses: a form of empty deflate blocks
// decodes to almost nothing, however often it is drawn.
self.charge(xf.content.len() as u64)?;
let contents = get_contents(xf);
self.xobject_depth += 1;
let r = self.process_stream(&doc, contents, resources, &media_box, output, page_num);
self.xobject_depth -= 1;
r?;
}
}
_ => { dlog!("unknown operation {:?}", operation); }
}
}
Ok(())
}
}
pub trait OutputDev {
fn begin_page(&mut self, page_num: u32, media_box: &MediaBox, art_box: Option<(f64, f64, f64, f64)>)-> Result<(), OutputError>;
fn end_page(&mut self)-> Result<(), OutputError>;
fn output_character(&mut self, trm: &Transform, width: f64, spacing: f64, font_size: f64, char: &str) -> Result<(), OutputError>;
fn begin_word(&mut self)-> Result<(), OutputError>;
fn end_word(&mut self)-> Result<(), OutputError>;
fn end_line(&mut self)-> Result<(), OutputError>;
// LOCAL PATCH (QuickSearch): no colour arguments; see GraphicsState.
fn stroke(&mut self, _ctm: &Transform, _path: &Path)-> Result<(), OutputError> {Ok(())}
fn fill(&mut self, _ctm: &Transform, _path: &Path)-> Result<(), OutputError> {Ok(())}
}
pub struct HTMLOutput<'a> {
file: &'a mut dyn std::io::Write,
flip_ctm: Transform,
last_ctm: Transform,
buf_ctm: Transform,
buf_font_size: f64,
buf: String
}
fn insert_nbsp(input: &str) -> String {
let mut result = String::new();
let mut word_end = false;
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == ' ' {
if !word_end || chars.peek().filter(|x| **x != ' ').is_none() {
result += "&nbsp;";
} else {
result += " ";
}
word_end = false;
} else {
word_end = true;
result.push(c);
}
}
result
}
impl<'a> HTMLOutput<'a> {
pub fn new(file: &mut dyn std::io::Write) -> HTMLOutput {
HTMLOutput {
file,
flip_ctm: Transform2D::identity(),
last_ctm: Transform2D::identity(),
buf_ctm: Transform2D::identity(),
buf: String::new(),
buf_font_size: 0.,
}
}
fn flush_string(&mut self) -> Result<(), OutputError>{
if self.buf.len() != 0 {
let position = self.buf_ctm.post_transform(&self.flip_ctm);
let transformed_font_size_vec = self.buf_ctm.transform_vector(vec2(self.buf_font_size, self.buf_font_size));
// get the length of one sized of the square with the same area with a rectangle of size (x, y)
let transformed_font_size = (transformed_font_size_vec.x * transformed_font_size_vec.y).sqrt();
let (x, y) = (position.m31, position.m32);
warn!("flush {} {:?}", self.buf, (x,y));
write!(self.file, "<div style='position: absolute; left: {}px; top: {}px; font-size: {}px'>{}</div>\n",
x, y, transformed_font_size, insert_nbsp(&self.buf))?;
}
Ok(())
}
}
type ArtBox = (f64, f64, f64, f64);
impl<'a> OutputDev for HTMLOutput<'a> {
fn begin_page(&mut self, page_num: u32, media_box: &MediaBox, _: Option<ArtBox>) -> Result<(), OutputError> {
write!(self.file, "<meta charset='utf-8' /> ")?;
write!(self.file, "<!-- page {} -->", page_num)?;
write!(self.file, "<div id='page{}' style='position: relative; height: {}px; width: {}px; border: 1px black solid'>", page_num, media_box.ury - media_box.lly, media_box.urx - media_box.llx)?;
self.flip_ctm = Transform::row_major(1., 0., 0., -1., 0., media_box.ury - media_box.lly);
Ok(())
}
fn end_page(&mut self) -> Result<(), OutputError> {
self.flush_string()?;
self.buf = String::new();
self.last_ctm = Transform::identity();
write!(self.file, "</div>")?;
Ok(())
}
fn output_character(&mut self, trm: &Transform, width: f64, spacing: f64, font_size: f64, char: &str) -> Result<(), OutputError>{
if trm.approx_eq(&self.last_ctm) {
let position = trm.post_transform(&self.flip_ctm);
let (x, y) = (position.m31, position.m32);
warn!("accum {} {:?}", char, (x,y));
self.buf += char;
} else {
warn!("flush {} {:?} {:?} {} {} {}", char, trm, self.last_ctm, width, font_size, spacing);
self.flush_string()?;
self.buf = char.to_owned();
self.buf_font_size = font_size;
self.buf_ctm = *trm;
}
let position = trm.post_transform(&self.flip_ctm);
let transformed_font_size_vec = trm.transform_vector(vec2(font_size, font_size));
// get the length of one sized of the square with the same area with a rectangle of size (x, y)
let transformed_font_size = (transformed_font_size_vec.x * transformed_font_size_vec.y).sqrt();
let (x, y) = (position.m31, position.m32);
write!(self.file, "<div style='position: absolute; color: red; left: {}px; top: {}px; font-size: {}px'>{}</div>",
x, y, transformed_font_size, char)?;
self.last_ctm = trm.pre_transform(&Transform2D::create_translation(width * font_size + spacing, 0.));
Ok(())
}
fn begin_word(&mut self) -> Result<(), OutputError> {Ok(())}
fn end_word(&mut self) -> Result<(), OutputError> {Ok(())}
fn end_line(&mut self) -> Result<(), OutputError> {Ok(())}
}
pub struct SVGOutput<'a> {
file: &'a mut dyn std::io::Write
}
impl<'a> SVGOutput<'a> {
pub fn new(file: &mut dyn std::io::Write) -> SVGOutput {
SVGOutput{file}
}
}
impl<'a> OutputDev for SVGOutput<'a> {
fn begin_page(&mut self, _page_num: u32, media_box: &MediaBox, art_box: Option<(f64, f64, f64, f64)>) -> Result<(), OutputError> {
let ver = 1.1;
write!(self.file, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n")?;
if ver == 1.1 {
write!(self.file, r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">"#)?;
} else {
write!(self.file, r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">"#)?;
}
if let Some(art_box) = art_box {
let width = art_box.2 - art_box.0;
let height = art_box.3 - art_box.1;
let y = media_box.ury - art_box.1 - height;
write!(self.file, "<svg width=\"{}\" height=\"{}\" xmlns=\"http://www.w3.org/2000/svg\" version=\"{}\" viewBox='{} {} {} {}'>", width, height, ver, art_box.0, y, width, height)?;
} else {
let width = media_box.urx - media_box.llx;
let height = media_box.ury - media_box.lly;
write!(self.file, "<svg width=\"{}\" height=\"{}\" xmlns=\"http://www.w3.org/2000/svg\" version=\"{}\" viewBox='{} {} {} {}'>", width, height, ver, media_box.llx, media_box.lly, width, height)?;
}
write!(self.file, "\n")?;
type Mat = Transform;
let ctm = Mat::create_scale(1., -1.).post_translate(vec2(0., media_box.ury));
write!(self.file, "<g transform='matrix({}, {}, {}, {}, {}, {})'>\n",
ctm.m11,
ctm.m12,
ctm.m21,
ctm.m22,
ctm.m31,
ctm.m32,
)?;
Ok(())
}
fn end_page(&mut self) -> Result<(), OutputError>{
write!(self.file, "</g>\n")?;
write!(self.file, "</svg>")?;
Ok(())
}
fn output_character(&mut self, _trm: &Transform, _width: f64, _spacing: f64, _font_size: f64, _char: &str) -> Result<(), OutputError>{
Ok(())
}
fn begin_word(&mut self) -> Result<(), OutputError> {Ok(())}
fn end_word(&mut self) -> Result<(), OutputError> {Ok(())}
fn end_line(&mut self) -> Result<(), OutputError> {Ok(())}
fn fill(&mut self, ctm: &Transform, path: &Path) -> Result<(), OutputError>{
write!(self.file, "<g transform='matrix({}, {}, {}, {}, {}, {})'>",
ctm.m11,
ctm.m12,
ctm.m21,
ctm.m22,
ctm.m31,
ctm.m32,
)?;
/*if path.ops.len() == 1 {
if let PathOp::Rect(x, y, width, height) = path.ops[0] {
write!(self.file, "<rect x={} y={} width={} height={} />\n", x, y, width, height);
write!(self.file, "</g>");
return;
}
}*/
let mut d = Vec::new();
for op in &path.ops {
match op {
&PathOp::MoveTo(x, y) => { d.push(format!("M{} {}", x, y))}
&PathOp::LineTo(x, y) => { d.push(format!("L{} {}", x, y))},
&PathOp::CurveTo(x1, y1, x2, y2, x, y) => { d.push(format!("C{} {} {} {} {} {}", x1, y1, x2, y2, x, y))},
&PathOp::Close => { d.push(format!("Z"))},
&PathOp::Rect(x, y, width, height) => {
d.push(format!("M{} {}", x, y));
d.push(format!("L{} {}", x + width, y));
d.push(format!("L{} {}", x + width, y + height));
d.push(format!("L{} {}", x, y + height));
d.push(format!("Z"));
}
}
}
write!(self.file, "<path d='{}' />", d.join(" "))?;
write!(self.file, "</g>")?;
write!(self.file, "\n")?;
Ok(())
}
}
/*
File doesn't implement std::fmt::Write so we have
to do some gymnastics to accept a File or String
See https://github.com/rust-lang/rust/issues/51305
*/
pub trait ConvertToFmt {
type Writer: std::fmt::Write;
fn convert(self) -> Self::Writer;
}
impl<'a> ConvertToFmt for &'a mut String {
type Writer = &'a mut String;
fn convert(self) -> Self::Writer {
self
}
}
pub struct WriteAdapter<W> {
f: W,
}
impl<W: std::io::Write> std::fmt::Write for WriteAdapter<W> {
fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
self.f.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}
impl<'a> ConvertToFmt for &'a mut dyn std::io::Write {
type Writer = WriteAdapter<Self>;
fn convert(self) -> Self::Writer {
WriteAdapter { f: self }
}
}
impl<'a> ConvertToFmt for &'a mut File {
type Writer = WriteAdapter<Self>;
fn convert(self) -> Self::Writer {
WriteAdapter { f: self }
}
}
pub struct PlainTextOutput<W: ConvertToFmt> {
writer: W::Writer,
last_end: f64,
last_y: f64,
first_char: bool,
flip_ctm: Transform,
}
impl<W: ConvertToFmt> PlainTextOutput<W> {
pub fn new(writer: W) -> PlainTextOutput<W> {
PlainTextOutput{
writer: writer.convert(),
last_end: 100000.,
first_char: false,
last_y: 0.,
flip_ctm: Transform2D::identity(),
}
}
}
/* There are some structural hints that PDFs can use to signal word and line endings:
* however relying on these is not likely to be sufficient. */
impl<W: ConvertToFmt> OutputDev for PlainTextOutput<W> {
fn begin_page(&mut self, _page_num: u32, media_box: &MediaBox, _: Option<ArtBox>) -> Result<(), OutputError> {
self.flip_ctm = Transform2D::row_major(1., 0., 0., -1., 0., media_box.ury - media_box.lly);
Ok(())
}
fn end_page(&mut self) -> Result<(), OutputError> {
Ok(())
}
fn output_character(&mut self, trm: &Transform, width: f64, _spacing: f64, font_size: f64, char: &str) -> Result<(), OutputError> {
let position = trm.post_transform(&self.flip_ctm);
let transformed_font_size_vec = trm.transform_vector(vec2(font_size, font_size));
// get the length of one sized of the square with the same area with a rectangle of size (x, y)
let transformed_font_size = (transformed_font_size_vec.x*transformed_font_size_vec.y).sqrt();
let (x, y) = (position.m31, position.m32);
use std::fmt::Write;
//dlog!("last_end: {} x: {}, width: {}", self.last_end, x, width);
if self.first_char {
if (y - self.last_y).abs() > transformed_font_size * 1.5 {
write!(self.writer, "\n")?;
}
// we've moved to the left and down
if x < self.last_end && (y - self.last_y).abs() > transformed_font_size * 0.5 {
write!(self.writer, "\n")?;
}
if x > self.last_end + transformed_font_size * 0.1 {
dlog!("width: {}, space: {}, thresh: {}", width, x - self.last_end, transformed_font_size * 0.1);
write!(self.writer, " ")?;
}
}
//let norm = unicode_normalization::UnicodeNormalization::nfkc(char);
write!(self.writer, "{}", char)?;
self.first_char = false;
self.last_y = y;
self.last_end = x + width * transformed_font_size;
Ok(())
}
fn begin_word(&mut self) -> Result<(), OutputError> {
self.first_char = true;
Ok(())
}
fn end_word(&mut self) -> Result<(), OutputError> {Ok(())}
fn end_line(&mut self) -> Result<(), OutputError>{
//write!(self.file, "\n");
Ok(())
}
}
pub fn print_metadata(doc: &Document) {
dlog!("Version: {}", doc.version);
if let Some(ref info) = get_info(&doc) {
for (k, v) in *info {
match v {
&Object::String(ref s, StringFormat::Literal) => { dlog!("{}: {}", pdf_to_utf8(k), pdf_to_utf8(s)); }
_ => {}
}
}
}
dlog!("Page count: {}", get::<i64>(&doc, &get_pages(&doc), b"Count"));
dlog!("Pages: {:?}", get_pages(&doc));
dlog!("Type: {:?}", get_pages(&doc).get(b"Type").and_then(|x| x.as_name()).unwrap());
}
/// Extract the text from a pdf at `path` and return a `String` with the results
pub fn extract_text<P: std::convert::AsRef<std::path::Path>>(path: P) -> Result<String, OutputError> {
let mut s = String::new();
{
let mut output = PlainTextOutput::new(&mut s);
let mut doc = Document::load(path)?;
maybe_decrypt(&mut doc)?;
output_doc(&doc, &mut output)?;
}
Ok(s)
}
fn maybe_decrypt(doc: &mut Document) -> Result<(), OutputError> {
if ! doc.is_encrypted() {
return Ok(());
}
if let Err(e) = doc.decrypt("") {
if let Error::Decryption(DecryptionError::IncorrectPassword) = e {
error!("Encrypted documents must be decrypted with a password using {{extract_text|extract_text_from_mem|output_doc}}_encrypted")
}
return Err(OutputError::PdfError(e));
}
Ok(())
}
pub fn extract_text_encrypted<P: std::convert::AsRef<std::path::Path>>(
path: P,
password: &str,
) -> Result<String, OutputError> {
let mut s = String::new();
{
let mut output = PlainTextOutput::new(&mut s);
let mut doc = Document::load(path)?;
output_doc_encrypted(&mut doc, &mut output, password)?;
}
Ok(s)
}
pub fn extract_text_from_mem(buffer: &[u8]) -> Result<String, OutputError> {
let mut s = String::new();
{
let mut output = PlainTextOutput::new(&mut s);
let mut doc = Document::load_mem(buffer)?;
maybe_decrypt(&mut doc)?;
output_doc(&doc, &mut output)?;
}
Ok(s)
}
pub fn extract_text_from_mem_encrypted(
buffer: &[u8],
password: &str,
) -> Result<String, OutputError> {
let mut s = String::new();
{
let mut output = PlainTextOutput::new(&mut s);
let mut doc = Document::load_mem(buffer)?;
output_doc_encrypted(&mut doc, &mut output, password)?;
}
Ok(s)
}
fn extract_text_by_page(doc: &Document, page_num: u32) -> Result<String, OutputError> {
let mut s = String::new();
{
let mut output = PlainTextOutput::new(&mut s);
output_doc_page(doc, &mut output, page_num)?;
}
Ok(s)
}
/// Extract the text from a pdf at `path` and return a `Vec<String>` with the results separately by page
pub fn extract_text_by_pages<P: std::convert::AsRef<std::path::Path>>(path: P) -> Result<Vec<String>, OutputError> {
let mut v = Vec::new();
{
let mut doc = Document::load(path)?;
maybe_decrypt(&mut doc)?;
let mut page_num = 1;
while let Ok(content) = extract_text_by_page(&doc, page_num) {
v.push(content);
page_num += 1;
}
}
Ok(v)
}
pub fn extract_text_by_pages_encrypted<P: std::convert::AsRef<std::path::Path>>(path: P, password: &str) -> Result<Vec<String>, OutputError> {
let mut v = Vec::new();
{
let mut doc = Document::load(path)?;
doc.decrypt(password)?;
let mut page_num = 1;
while let Ok(content) = extract_text_by_page(&mut doc, page_num) {
v.push(content);
page_num += 1;
}
}
Ok(v)
}
pub fn extract_text_from_mem_by_pages(buffer: &[u8]) -> Result<Vec<String>, OutputError> {
let mut v = Vec::new();
{
let mut doc = Document::load_mem(buffer)?;
maybe_decrypt(&mut doc)?;
let mut page_num = 1;
while let Ok(content) = extract_text_by_page(&doc, page_num) {
v.push(content);
page_num += 1;
}
}
Ok(v)
}
pub fn extract_text_from_mem_by_pages_encrypted(buffer: &[u8], password: &str) -> Result<Vec<String>, OutputError> {
let mut v = Vec::new();
{
let mut doc = Document::load_mem(buffer)?;
doc.decrypt(password)?;
let mut page_num = 1;
while let Ok(content) = extract_text_by_page(&doc, page_num) {
v.push(content);
page_num += 1;
}
}
Ok(v)
}
/// Depth bound for the two recursions in this file.
///
/// LOCAL PATCH (QuickSearch): a `/Parent` chain is a tree in a well-formed
/// document and can be a cycle in a crafted one, and a Form XObject may draw
/// itself. Both recursions were unbounded, so a few hundred bytes of PDF
/// could exhaust the stack — which is not a catchable panic: Rust's handler
/// aborts the process, which under the sandbox is the PDF child and without
/// it the indexer and the GUI. Real documents nest nothing here more than a
/// handful deep. The depth bounds the stack, not the work: a form drawing the
/// next level twice costs 2^depth calls, which [`MAX_PDF_WORK`] bounds.
///
/// That this depth fits the worker stack is tested, over the deepest shape
/// these recursions reach: `extract::pdf`'s
/// `pdf_recursion_bounds_fit_the_worker_stack`. It was 64, which fit a
/// release build but overflowed an unoptimized one, where a
/// `process_stream` frame measures about 7 KB and lopdf's content tokenizer
/// adds its own nesting inside each. lopdf's own recursions (an indirect
/// `/Length` chain, a `/Length` inside its own object stream) are not
/// bounded at all, which is why PDFs are parsed in a child process.
pub const MAX_PDF_RECURSION_DEPTH: u32 = 32;
/// LOCAL PATCH (QuickSearch): the work one document may spend rendering,
/// counted in bytes; past it, rendering stops with [`OutputError::Budget`].
/// Each content stream inflated is charged its encoded length (a page's
/// `/Contents` entries, a form each time it is drawn), each content stream
/// entered its decoded length plus [`MIN_STREAM_WORK`], and each font built
/// what [`font_work`] says it reads plus the decoded length of the font
/// streams it was first to parse. The 2^depth branching shape is the reason:
/// a text-free form never fills the caller's text sink, so nothing else stops
/// it. Here it stops after about half a million streams, measured at 0.3 to
/// 0.5 s in an optimized build (2026-10-10), or 0.8 s when the form is a tiny
/// FlateDecode stream, inflated afresh for each draw; an indexer thread
/// holding a write transaction can afford that. It also bounds what a page's
/// `/Contents` concatenates, however often one stream repeats.
pub const MAX_PDF_WORK: u64 = 256 << 20;
/// LOCAL PATCH (QuickSearch): the least a content stream entered costs
/// against [`MAX_PDF_WORK`], however small, so the budget bounds how many
/// there are as well as how many bytes are decoded.
const MIN_STREAM_WORK: u64 = 512;
/// LOCAL PATCH (QuickSearch): the least a font built costs against
/// [`MAX_PDF_WORK`]. Building a base-14 font's tables takes about 93 µs in an
/// optimized build, so the budget allows 16,384 fonts, about 1.5 s of them.
const FONT_WORK: u64 = 16 << 10;
/// LOCAL PATCH (QuickSearch): what a font built costs for each element of the
/// width and encoding arrays it reads; see [`font_work`].
const FONT_ELEMENT_WORK: u64 = 8;
/// LOCAL PATCH (QuickSearch): what building `font` costs against
/// [`MAX_PDF_WORK`], charged before it is built: [`FONT_WORK`], plus
/// [`FONT_ELEMENT_WORK`] for each element of the `/Widths`, `/W` (nested
/// arrays included) and `/Differences` arrays it reads. Any number of font
/// dictionaries may share one such array, and each reads it afresh. A lookup
/// that fails costs nothing, and does not panic: the build reports it.
fn font_work(doc: &Document, font: &Dictionary) -> u64 {
fn deref<'a>(doc: &'a Document, o: &'a Object) -> Option<&'a Object> {
doc.dereference(o).ok().map(|(_, o)| o)
}
fn array<'a>(doc: &'a Document, dict: &'a Dictionary, key: &[u8]) -> &'a [Object] {
let array = dict.get(key).ok().and_then(|o| deref(doc, o)?.as_array().ok());
array.map_or(&[][..], Vec::as_slice)
}
let dict = |o| deref(doc, o).and_then(|o| o.as_dict().ok());
let mut elements = array(doc, font, b"Widths").len();
if let Some(encoding) = font.get(b"Encoding").ok().and_then(dict) {
elements += array(doc, encoding, b"Differences").len();
}
if let Some(cid) = array(doc, font, b"DescendantFonts").first().and_then(dict) {
let w = array(doc, cid, b"W");
elements += w.len();
elements += w.iter().filter_map(|o| deref(doc, o)?.as_array().ok()).map(Vec::len).sum::<usize>();
}
FONT_WORK + FONT_ELEMENT_WORK * elements as u64
}
fn get_inherited<'a, T: FromObj<'a>>(doc: &'a Document, dict: &'a Dictionary, key: &[u8], depth: u32) -> Option<T> {
let o: Option<T> = get(doc, dict, key);
if let Some(o) = o {
Some(o)
} else {
// LOCAL PATCH (QuickSearch): stop before the guard page. Treating a
// too-deep chain as "absent" matches what this function already does
// for a chain that simply ends.
if depth >= MAX_PDF_RECURSION_DEPTH {
return None;
}
let parent = dict.get(b"Parent")
.and_then(|parent| parent.as_reference())
.and_then(|id| doc.get_dictionary(id)).ok()?;
get_inherited(doc, parent, key, depth + 1)
}
}
pub fn output_doc_encrypted(
doc: &mut Document,
output: &mut dyn OutputDev,
password: &str,
) -> Result<(), OutputError> {
doc.decrypt(password)?;
output_doc(doc, output)
}
/// Parse a given document and output it to `output`
pub fn output_doc(doc: &Document, output: &mut dyn OutputDev) -> Result<(), OutputError> {
if doc.is_encrypted() {
error!("Encrypted documents must be decrypted with a password using {{extract_text|extract_text_from_mem|output_doc}}_encrypted");
}
let empty_resources = Dictionary::new();
let pages = doc.get_pages();
let mut p = Processor::new();
for dict in pages {
let page_num = dict.0;
let object_id = dict.1;
output_doc_inner(page_num, object_id, doc, &mut p, output, &empty_resources)?;
}
Ok(())
}
pub fn output_doc_page(doc: &Document, output: &mut dyn OutputDev, page_num: u32) -> Result<(), OutputError> {
if doc.is_encrypted() {
error!("Encrypted documents must be decrypted with a password using {{extract_text|extract_text_from_mem|output_doc}}_encrypted");
}
let empty_resources = Dictionary::new();
let pages = doc.get_pages();
let object_id = pages.get(&page_num).ok_or(lopdf::Error::PageNumberNotFound(page_num))?;
let mut p = Processor::new();
output_doc_inner(page_num, *object_id, doc, &mut p, output, &empty_resources)?;
Ok(())
}
fn output_doc_inner<'a>(page_num: u32, object_id: ObjectId, doc: &'a Document, p: & mut Processor<'a>, output: &mut dyn OutputDev, empty_resources: &'a Dictionary) -> Result<(), OutputError> {
let page_dict = doc.get_object(object_id).unwrap().as_dict().unwrap();
dlog!("page {} {:?}", page_num, page_dict);
// XXX: Some pdfs lack a Resources directory
let resources = get_inherited(doc, page_dict, b"Resources", 0).unwrap_or(empty_resources);
dlog!("resources {:?}", resources);
// pdfium searches up the page tree for MediaBoxes as needed
let media_box: Vec<f64> = get_inherited(doc, page_dict, b"MediaBox", 0).expect("MediaBox");
let media_box = MediaBox { llx: media_box[0], lly: media_box[1], urx: media_box[2], ury: media_box[3] };
let art_box = get::<Option<Vec<f64>>>(&doc, page_dict, b"ArtBox")
.map(|x| (x[0], x[1], x[2], x[3]));
output.begin_page(page_num, &media_box, art_box)?;
let content = p.page_content(doc, object_id)?;
p.process_stream(&doc, content, resources, &media_box, output, page_num)?;
output.end_page()?;
Ok(())
}