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; #[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 for OutputError { fn from(e: std::fmt::Error) -> Self { OutputError::FormatError(e) } } impl From for OutputError { fn from(e: std::io::Error) -> Self { OutputError::IoError(e) } } impl From 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 = 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 = 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; } impl<'a, T: FromObj<'a>> FromOptObj<'a> for Option { 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 { fn from_obj(doc: &'a Document, obj: &'a Object) -> Option { 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 { 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 { 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 { 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 { 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> { 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>, unicode_map: Option, widths: HashMap, // 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>>, cff: Option>>, differences: HashMap, } 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>, unicode_map: Option>>, widths: HashMap, // should probably just use i32 here } fn make_font<'a>(doc: &'a Document, font: &'a Dictionary, parts: &mut FontParts) -> Rc { 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 { 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::>(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 = 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::(doc, font, b"FirstChar"), maybe_get::(doc, font, b"LastChar"), maybe_get::>(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::>(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> { 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 { maybe_get_name_string(self.doc, self.font, b"Name") } #[allow(dead_code)] fn get_descriptor(&self) -> Option { 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 = 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) -> 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::>(); 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) -> 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) -> 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, to_unicode: Option>>, widths: HashMap, // should probably just use i32 here default_width: Option, // 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, /// Sorted by `src_code_lo`, so a code's range is found by binary search. cid: Vec, } 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> { 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> { 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, 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> { 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>>>, encodings: HashMap>, /// A Type1 font file's built-in encoding, as the table it gives. type1: HashMap>, /// A Type1C font file's glyph names, as unicode. cff: HashMap>>, 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(cache: &mut HashMap, parsed: &mut u64, stream: &Stream, parse: impl FnOnce(&[u8]) -> T) -> T { cache.entry(std::ptr::from_ref::(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>> { 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::>(doc, ciddict, b"DW").unwrap_or(1000); let w: Option> = 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> ` /// around a nested `<0020> <007E>`): the first whose `reach` gets there /// does, since `reach` only grows. fn cid(&self, code: u32) -> Option { 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) -> 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>, 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 { 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 } 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>, /// 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, 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, OutputError> { let key = std::ptr::from_ref::(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, 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 += " "; } 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, "
{}
\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) -> Result<(), OutputError> { write!(self.file, " ")?; write!(self.file, "", page_num)?; write!(self.file, "
", 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, "
")?; 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, "
{}
", 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, "\n")?; if ver == 1.1 { write!(self.file, r#""#)?; } else { write!(self.file, r#""#)?; } 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, "", 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, "", 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, "\n", ctm.m11, ctm.m12, ctm.m21, ctm.m22, ctm.m31, ctm.m32, )?; Ok(()) } fn end_page(&mut self) -> Result<(), OutputError>{ write!(self.file, "\n")?; write!(self.file, "")?; 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, "", 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, "\n", x, y, width, height); write!(self.file, ""); 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, "", d.join(" "))?; write!(self.file, "")?; 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 { f: W, } impl std::fmt::Write for WriteAdapter { 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; fn convert(self) -> Self::Writer { WriteAdapter { f: self } } } impl<'a> ConvertToFmt for &'a mut File { type Writer = WriteAdapter; fn convert(self) -> Self::Writer { WriteAdapter { f: self } } } pub struct PlainTextOutput { writer: W::Writer, last_end: f64, last_y: f64, first_char: bool, flip_ctm: Transform, } impl PlainTextOutput { pub fn new(writer: W) -> PlainTextOutput { 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 OutputDev for PlainTextOutput { fn begin_page(&mut self, _page_num: u32, media_box: &MediaBox, _: Option) -> 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::(&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>(path: P) -> Result { 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>( path: P, password: &str, ) -> Result { 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 { 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 { 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 { 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` with the results separately by page pub fn extract_text_by_pages>(path: P) -> Result, 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>(path: P, password: &str) -> Result, 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, 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, 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::(); } 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 { let o: Option = 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 = 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::>>(&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(()) }