2026-08-20 00:13:10 -04:00
extern crate lopdf ;
use encoding_rs ::UTF_16BE ;
2026-10-11 20:32:00 -04:00
use lopdf ::content ::{ Content , Operation } ;
2026-08-20 00:13:10 -04:00
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 ) ,
2026-10-11 20:32:00 -04:00
PdfError ( lopdf ::Error ) ,
/// LOCAL PATCH (QuickSearch): the document spent [`MAX_PDF_WORK`] and
/// rendering stopped. Whatever was output before the stop stands.
Budget ,
2026-08-20 00:13:10 -04:00
}
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 ) ,
2026-10-11 20:32:00 -04:00
OutputError ::PdfError ( e ) = > write! ( f , " PDF error: {} " , e ) ,
OutputError ::Budget = > write! ( f , " work budget of {} bytes spent " , MAX_PDF_WORK ) ,
2026-08-20 00:13:10 -04:00
}
}
}
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 > > ,
2026-10-11 20:32:00 -04:00
unicode_map : Option < UnicodeMap > ,
2026-08-20 00:13:10 -04:00
widths : HashMap < CharCode , f64 > , // should probably just use i32 here
missing_width : f64 ,
}
2026-10-11 20:32:00 -04:00
/// 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 ) )
}
}
2026-08-20 00:13:10 -04:00
#[ derive(Clone) ]
struct PdfType3Font < ' a > {
font : & ' a Dictionary ,
doc : & ' a Document ,
encoding : Option < Vec < u16 > > ,
2026-10-11 20:32:00 -04:00
unicode_map : Option < Rc < HashMap < CharCode , String > > > ,
2026-08-20 00:13:10 -04:00
widths : HashMap < CharCode , f64 > , // should probably just use i32 here
}
2026-10-11 20:32:00 -04:00
fn make_font < ' a > ( doc : & ' a Document , font : & ' a Dictionary , parts : & mut FontParts ) -> Rc < dyn PdfFont + ' a > {
2026-08-20 00:13:10 -04:00
let subtype = get_name_string ( doc , font , b " Subtype " ) ;
dlog! ( " MakeFont({}) " , subtype ) ;
if subtype = = " Type0 " {
2026-10-11 20:32:00 -04:00
Rc ::new ( PdfCIDFont ::new ( doc , font , parts ) )
2026-08-20 00:13:10 -04:00
} else if subtype = = " Type3 " {
2026-10-11 20:32:00 -04:00
Rc ::new ( PdfType3Font ::new ( doc , font , parts ) )
2026-08-20 00:13:10 -04:00
} else {
2026-10-11 20:32:00 -04:00
Rc ::new ( PdfSimpleFont ::new ( doc , font , parts ) )
2026-08-20 00:13:10 -04:00
}
}
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 > {
2026-10-11 20:32:00 -04:00
fn new ( doc : & ' a Document , font : & ' a Dictionary , parts : & mut FontParts ) -> PdfSimpleFont < ' a > {
2026-08-20 00:13:10 -04:00
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 " ) ;
2026-10-11 20:32:00 -04:00
let mut type1_file = None ;
let mut cff = None ;
2026-08-20 00:13:10 -04:00
if let Some ( descriptor ) = descriptor {
dlog! ( " descriptor {:?} " , descriptor ) ;
if subtype = = " Type1 " {
let file = maybe_get_obj ( doc , descriptor , b " FontFile " ) ;
match file {
2026-10-11 20:32:00 -04:00
// 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 ) ; }
2026-08-20 00:13:10 -04:00
_ = > { dlog! ( " font file {:?} " , file ) }
}
}
2026-10-11 20:32:00 -04:00
// LOCAL PATCH (QuickSearch): a TrueType `FontFile2` was inflated
// here and dropped unread.
2026-08-20 00:13:10 -04:00
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 ) ;
2026-10-11 20:32:00 -04:00
// LOCAL PATCH (QuickSearch): parsed once per font file
// (see FontParts); a file of another subtype was inflated
// here and dropped unread.
2026-08-20 00:13:10 -04:00
if subtype = = " Type1C " {
2026-10-11 20:32:00 -04:00
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 )
} ) ) ;
2026-08-20 00:13:10 -04:00
}
}
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);
}
2026-10-11 20:32:00 -04:00
// 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
2026-08-20 00:13:10 -04:00
} ;
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 ] ;
2026-10-11 20:32:00 -04:00
match unicode_map . get ( & ( code as u32 ) ) {
2026-08-20 00:13:10 -04:00
// If there's a unicode table entry missing use one based on the name
2026-10-11 20:32:00 -04:00
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 ( ) ) ;
2026-08-20 00:13:10 -04:00
if ! normal_match {
2026-10-11 20:32:00 -04:00
warn! ( " Unicode mismatch {} {} {:?} {:?} {:?} " , normal_match , name , e , String ::from_utf16 ( & be ) , be ) ;
2026-08-20 00:13:10 -04:00
}
}
}
}
}
} 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
2026-10-11 20:32:00 -04:00
match unicode_map . get ( & ( code as u32 ) ) {
None = > { unicode_map . differences . insert ( code as u32 , " " . to_owned ( ) ) ; }
Some ( _ ) = > {
2026-08-20 00:13:10 -04:00
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 = > {
2026-10-11 20:32:00 -04:00
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 ) ) ;
}
2026-08-20 00:13:10 -04:00
}
2026-10-11 20:32:00 -04:00
table
} ) )
2026-08-20 00:13:10 -04:00
} 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 > {
2026-10-11 20:32:00 -04:00
fn new ( doc : & ' a Document , font : & ' a Dictionary , parts : & mut FontParts ) -> PdfType3Font < ' a > {
2026-08-20 00:13:10 -04:00
2026-10-11 20:32:00 -04:00
let unicode_map = get_unicode_map ( doc , font , parts ) ;
2026-08-20 00:13:10 -04:00
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 {
2026-10-11 20:32:00 -04:00
// 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 )
2026-08-20 00:13:10 -04:00
}
/* 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) ]
2026-10-11 20:32:00 -04:00
encoding : Rc < ByteMapping > ,
to_unicode : Option < Rc < HashMap < u32 , String > > > ,
2026-08-20 00:13:10 -04:00
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
}
2026-10-11 20:32:00 -04:00
/// 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 ;
}
}
2026-08-20 00:13:10 -04:00
}
2026-10-11 20:32:00 -04:00
}
" 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 {
2026-08-20 00:13:10 -04:00
continue ;
}
2026-10-11 20:32:00 -04:00
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 ;
}
}
}
}
_ = > { }
}
2026-08-20 00:13:10 -04:00
}
}
2026-10-11 20:32:00 -04:00
_ = > { }
}
}
Some ( map )
}
2026-08-20 00:13:10 -04:00
2026-10-11 20:32:00 -04:00
/// 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 } ) ;
}
}
}
_ = > { }
2026-08-20 00:13:10 -04:00
}
2026-10-11 20:32:00 -04:00
}
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 ,
2026-08-20 00:13:10 -04:00
Some ( & Object ::Name ( ref name ) ) = > {
let name = pdf_to_utf8 ( name ) ;
if name ! = " Identity-H " {
todo! ( " unsupported ToUnicode name: {:?} " , name ) ;
}
2026-10-11 20:32:00 -04:00
None
2026-08-20 00:13:10 -04:00
}
_ = > { panic! ( " unsupported cmap {:?} " , to_unicode ) }
}
}
impl < ' a > PdfCIDFont < ' a > {
2026-10-11 20:32:00 -04:00
fn new ( doc : & ' a Document , font : & ' a Dictionary , parts : & mut FontParts ) -> PdfCIDFont < ' a > {
2026-08-20 00:13:10 -04:00
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 " {
2026-10-11 20:32:00 -04:00
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 } ] } )
2026-08-20 00:13:10 -04:00
} else {
panic! ( " unsupported encoding {} " , name ) ;
}
}
& Object ::Stream ( ref stream ) = > {
2026-10-11 20:32:00 -04:00
parse_once ( & mut parts . encodings , & mut parts . parsed , stream , | s | Rc ::new ( byte_mapping ( s ) ) )
2026-08-20 00:13:10 -04:00
}
_ = > { 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
2026-10-11 20:32:00 -04:00
let unicode_map = get_unicode_map ( doc , font , parts ) ;
2026-08-20 00:13:10 -04:00
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 ) }
}
}
2026-10-11 20:32:00 -04:00
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 )
}
}
2026-08-20 00:13:10 -04:00
impl < ' a > PdfFont for PdfCIDFont < ' a > {
2026-10-11 20:32:00 -04:00
/// LOCAL PATCH (QuickSearch): `id` is the character code (see
/// next_char); `/W` is keyed by CID.
2026-08-20 00:13:10 -04:00
fn get_width ( & self , id : CharCode ) -> f64 {
2026-10-11 20:32:00 -04:00
let width = self . cid ( id ) . and_then ( | cid | self . widths . get ( & cid ) ) ;
2026-08-20 00:13:10 -04:00
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 ? ;
2026-10-11 20:32:00 -04:00
// 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 ) )
2026-08-20 00:13:10 -04:00
}
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 ( )
}
}
2026-10-11 20:32:00 -04:00
/// 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.
2026-08-20 00:13:10 -04:00
#[ 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! ( ) }
}
}
}
2026-10-11 20:32:00 -04:00
/// 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 ;
2026-08-20 00:13:10 -04:00
struct Processor < ' a > {
2026-10-11 20:32:00 -04:00
/// 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 ,
2026-08-20 00:13:10 -04:00
/// LOCAL PATCH (QuickSearch): nesting of `Do` (Form XObject) recursion.
xobject_depth : u32 ,
2026-10-11 20:32:00 -04:00
/// LOCAL PATCH (QuickSearch): the work this document has spent; see
/// [`MAX_PDF_WORK`].
work : u64 ,
2026-08-20 00:13:10 -04:00
_none : PhantomData < & ' a ( ) > ,
}
impl < ' a > Processor < ' a > {
fn new ( ) -> Processor < ' a > {
2026-10-11 20:32:00 -04:00
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 )
2026-08-20 00:13:10 -04:00
}
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 > {
2026-10-11 20:32:00 -04:00
// LOCAL PATCH (QuickSearch): charge the stream before decoding it.
self . charge ( content . len ( ) as u64 + MIN_STREAM_WORK ) ? ;
2026-08-20 00:13:10 -04:00
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 ( ) ;
2026-10-11 20:32:00 -04:00
// LOCAL PATCH (QuickSearch): `q`s past MAX_GS_STACK not yet matched.
let mut gs_overflow = 0 usize ;
2026-08-20 00:13:10 -04:00
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 ) ;
}
2026-10-11 20:32:00 -04:00
// LOCAL PATCH (QuickSearch): colour is not tracked; see GraphicsState.
" CS " | " cs " | " SC " | " SCN " | " sc " | " scn " | " G " | " g " | " RG " | " rg " | " K " | " k " = > {
2026-08-20 00:13:10 -04:00
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 ( ) ;
2026-10-11 20:32:00 -04:00
let font_dict : & Dictionary = get ( doc , fonts , name ) ;
let font = self . font ( doc , font_dict ) ? ;
2026-08-20 00:13:10 -04:00
{
/* 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 ( ) ? ;
}
2026-10-11 20:32:00 -04:00
" q " = > {
if gs_stack . len ( ) < MAX_GS_STACK {
gs_stack . push ( gs . clone ( ) ) ;
} else {
gs_overflow + = 1 ;
}
}
2026-08-20 00:13:10 -04:00
" Q " = > {
2026-10-11 20:32:00 -04:00
if gs_overflow > 0 {
gs_overflow - = 1 ;
} else if let Some ( s ) = gs_stack . pop ( ) {
2026-08-20 00:13:10 -04:00
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 ] ) ; }
2026-10-11 20:32:00 -04:00
" J " | " j " | " M " | " d " | " ri " | " gs " = > { dlog! ( " unknown graphics state operator {:?} " , operation ) ; }
2026-08-20 00:13:10 -04:00
" 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 " = > {
2026-10-11 20:32:00 -04:00
output . stroke ( & gs . ctm , & path ) ? ;
2026-08-20 00:13:10 -04:00
path . ops . clear ( ) ;
}
" F " | " f " = > {
2026-10-11 20:32:00 -04:00
output . fill ( & gs . ctm , & path ) ? ;
2026-08-20 00:13:10 -04:00
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 ) ;
2026-10-11 20:32:00 -04:00
// 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 {
2026-08-20 00:13:10 -04:00
dlog! ( " skipping Do: XObject nesting past {} " , MAX_PDF_RECURSION_DEPTH ) ;
} else {
2026-10-11 20:32:00 -04:00
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 ) ;
2026-08-20 00:13:10 -04:00
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 > ;
2026-10-11 20:32:00 -04:00
// 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 ( ( ) ) }
2026-08-20 00:13:10 -04:00
}
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 , " <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 ( ( ) ) }
2026-10-11 20:32:00 -04:00
fn fill ( & mut self , ctm : & Transform , path : & Path ) -> Result < ( ) , OutputError > {
2026-08-20 00:13:10 -04:00
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
2026-10-11 20:32:00 -04:00
/// 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
}
2026-08-20 00:13:10 -04:00
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 ) ? ;
2026-10-11 20:32:00 -04:00
let content = p . page_content ( doc , object_id ) ? ;
p . process_stream ( & doc , content , resources , & media_box , output , page_num ) ? ;
2026-08-20 00:13:10 -04:00
output . end_page ( ) ? ;
Ok ( ( ) )
}