22//!
33//! This module extracts text with position information for structure detection.
44
5+ use crate :: tounicode:: FontCMaps ;
56use crate :: PdfError ;
67use lopdf:: { Document , Object , ObjectId } ;
78use std:: path:: Path ;
@@ -143,23 +144,33 @@ fn extract_text_from_doc(doc: &Document) -> Result<String, PdfError> {
143144
144145/// Extract text with position information from PDF file
145146pub fn extract_text_with_positions < P : AsRef < Path > > ( path : P ) -> Result < Vec < TextItem > , PdfError > {
146- let doc = Document :: load ( path) ?;
147- extract_positioned_text_from_doc ( & doc)
147+ // Read the raw PDF bytes for ToUnicode extraction
148+ let pdf_bytes = std:: fs:: read ( path. as_ref ( ) ) ?;
149+ let font_cmaps = FontCMaps :: from_pdf_bytes ( & pdf_bytes) ;
150+
151+ let doc = Document :: load_mem ( & pdf_bytes) ?;
152+ extract_positioned_text_from_doc ( & doc, & font_cmaps)
148153}
149154
150155/// Extract text with positions from memory buffer
151156pub fn extract_text_with_positions_mem ( buffer : & [ u8 ] ) -> Result < Vec < TextItem > , PdfError > {
157+ // Extract ToUnicode CMaps from raw PDF bytes
158+ let font_cmaps = FontCMaps :: from_pdf_bytes ( buffer) ;
159+
152160 let doc = Document :: load_mem ( buffer) ?;
153- extract_positioned_text_from_doc ( & doc)
161+ extract_positioned_text_from_doc ( & doc, & font_cmaps )
154162}
155163
156164/// Extract positioned text from loaded document
157- fn extract_positioned_text_from_doc ( doc : & Document ) -> Result < Vec < TextItem > , PdfError > {
165+ fn extract_positioned_text_from_doc (
166+ doc : & Document ,
167+ font_cmaps : & FontCMaps ,
168+ ) -> Result < Vec < TextItem > , PdfError > {
158169 let pages = doc. get_pages ( ) ;
159170 let mut all_items = Vec :: new ( ) ;
160171
161172 for ( page_num, & page_id) in pages. iter ( ) {
162- let items = extract_page_text_items ( doc, page_id, * page_num) ?;
173+ let items = extract_page_text_items ( doc, page_id, * page_num, font_cmaps ) ?;
163174 all_items. extend ( items) ;
164175 }
165176
@@ -187,6 +198,7 @@ fn extract_page_text_items(
187198 doc : & Document ,
188199 page_id : ObjectId ,
189200 page_num : u32 ,
201+ font_cmaps : & FontCMaps ,
190202) -> Result < Vec < TextItem > , PdfError > {
191203 use lopdf:: content:: Content ;
192204
@@ -195,6 +207,19 @@ fn extract_page_text_items(
195207 // Get fonts for encoding
196208 let fonts = doc. get_page_fonts ( page_id) . unwrap_or_default ( ) ;
197209
210+ // Build a map of font resource names to their base font names (for CMap lookup)
211+ let mut font_base_names: std:: collections:: HashMap < String , String > =
212+ std:: collections:: HashMap :: new ( ) ;
213+ for ( font_name, font_dict) in & fonts {
214+ let resource_name = String :: from_utf8_lossy ( font_name) . to_string ( ) ;
215+ if let Ok ( base_font) = font_dict. get ( b"BaseFont" ) {
216+ if let Ok ( name) = base_font. as_name ( ) {
217+ let base_name = String :: from_utf8_lossy ( name) . to_string ( ) ;
218+ font_base_names. insert ( resource_name, base_name) ;
219+ }
220+ }
221+ }
222+
198223 // Get content
199224 let content_data = doc
200225 . get_page_content ( page_id)
@@ -290,9 +315,14 @@ fn extract_page_text_items(
290315 "Tj" => {
291316 // Show text string
292317 if in_text_block && !op. operands . is_empty ( ) {
293- if let Some ( text) =
294- extract_text_from_operand ( & op. operands [ 0 ] , doc, & fonts, & current_font)
295- {
318+ if let Some ( text) = extract_text_from_operand (
319+ & op. operands [ 0 ] ,
320+ doc,
321+ & fonts,
322+ & current_font,
323+ font_cmaps,
324+ & font_base_names,
325+ ) {
296326 if !text. trim ( ) . is_empty ( ) {
297327 let rendered_size =
298328 effective_font_size ( current_font_size, & text_matrix) ;
@@ -319,9 +349,14 @@ fn extract_page_text_items(
319349 if let Ok ( array) = op. operands [ 0 ] . as_array ( ) {
320350 let mut combined_text = String :: new ( ) ;
321351 for item in array {
322- if let Some ( text) =
323- extract_text_from_operand ( item, doc, & fonts, & current_font)
324- {
352+ if let Some ( text) = extract_text_from_operand (
353+ item,
354+ doc,
355+ & fonts,
356+ & current_font,
357+ font_cmaps,
358+ & font_base_names,
359+ ) {
325360 combined_text. push_str ( & text) ;
326361 }
327362 }
@@ -350,9 +385,14 @@ fn extract_page_text_items(
350385 line_matrix[ 5 ] -= current_font_size * 1.2 ;
351386 text_matrix = line_matrix;
352387 if !op. operands . is_empty ( ) {
353- if let Some ( text) =
354- extract_text_from_operand ( & op. operands [ 0 ] , doc, & fonts, & current_font)
355- {
388+ if let Some ( text) = extract_text_from_operand (
389+ & op. operands [ 0 ] ,
390+ doc,
391+ & fonts,
392+ & current_font,
393+ font_cmaps,
394+ & font_base_names,
395+ ) {
356396 if !text. trim ( ) . is_empty ( ) {
357397 let rendered_size =
358398 effective_font_size ( current_font_size, & text_matrix) ;
@@ -408,9 +448,31 @@ fn extract_text_from_operand(
408448 doc : & Document ,
409449 fonts : & std:: collections:: BTreeMap < Vec < u8 > , & lopdf:: Dictionary > ,
410450 current_font : & str ,
451+ font_cmaps : & FontCMaps ,
452+ font_base_names : & std:: collections:: HashMap < String , String > ,
411453) -> Option < String > {
412454 if let Object :: String ( bytes, _) = obj {
413- // Try to decode using font encoding
455+ // First, check if this font has a ToUnicode CMap we can use
456+ // This is especially important for Identity-H encoded fonts (Type0/CIDFont)
457+ if let Some ( base_name) = font_base_names. get ( current_font) {
458+ if let Some ( cmap) = font_cmaps. get ( base_name) {
459+ // Use the ToUnicode CMap to decode CID bytes
460+ let decoded = cmap. decode_cids ( bytes) ;
461+ if !decoded. is_empty ( ) {
462+ return Some ( decoded) ;
463+ }
464+ }
465+ }
466+
467+ // Also try looking up by resource name directly
468+ if let Some ( cmap) = font_cmaps. get ( current_font) {
469+ let decoded = cmap. decode_cids ( bytes) ;
470+ if !decoded. is_empty ( ) {
471+ return Some ( decoded) ;
472+ }
473+ }
474+
475+ // Try to decode using font encoding from lopdf
414476 if let Some ( font_dict) = fonts. get ( current_font. as_bytes ( ) ) {
415477 if let Ok ( encoding) = font_dict. get_font_encoding ( doc) {
416478 if let Ok ( text) = Document :: decode_text ( & encoding, bytes) {
0 commit comments