@@ -18,16 +18,85 @@ class AdvancedPDFReaderProvider(LlamaIndexReaderProviderBase, S3FileMixin):
1818 def __init__ (self , config : PDFReaderConfig ):
1919
2020 try :
21- import pymupdf
21+ import pymupdf
2222 except ImportError as e :
2323 raise ImportError (
2424 "pymupdf package not found, install with 'pip install pymupdf'"
2525 ) from e
2626
27-
27+ # Hold the module on the instance: the import above is local to __init__,
28+ # so read() cannot reach the bare `pymupdf` name without this.
29+ self ._pymupdf = pymupdf
2830 self .config = config
31+ self .extract_tables = config .extract_tables
2932 self .metadata_fn = config .metadata_fn
30- logger .debug ("Initialized AdvancedPDFReaderProvider" )
33+ logger .debug (f"Initialized AdvancedPDFReaderProvider with extract_tables={ config .extract_tables } " )
34+
35+ def _find_tables (self , page , page_number ):
36+ """Return the tables detected on a page, or an empty list on failure."""
37+ try :
38+ return list (page .find_tables ().tables )
39+ except Exception as e :
40+ logger .warning (f"Failed to detect tables on page { page_number } : { e } " )
41+ return []
42+
43+ @staticmethod
44+ def _block_in_a_table (x0 , y0 , x1 , y1 , table_bboxes , threshold = 0.5 ):
45+ """True if more than `threshold` of a block's area lies inside any table
46+ bbox (default: more than half), i.e. the block belongs to the table."""
47+ block_area = max (0.0 , x1 - x0 ) * max (0.0 , y1 - y0 )
48+ if block_area == 0 :
49+ return False
50+ for bx0 , by0 , bx1 , by1 in table_bboxes :
51+ overlap_w = max (0.0 , min (x1 , bx1 ) - max (x0 , bx0 ))
52+ overlap_h = max (0.0 , min (y1 , by1 ) - max (y0 , by0 ))
53+ if overlap_w * overlap_h > threshold * block_area :
54+ return True
55+ return False
56+
57+ def _page_text (self , page , tables ):
58+ """Page text with table-region text removed.
59+
60+ get_text() returns each table's cell text inline, and the table is also
61+ appended as markdown, so without this the cell content would land in the
62+ document twice and inflate downstream extraction (which dedups only on
63+ exact-string output). Drop any text block sitting mostly inside a table's
64+ bounding box, leaving the markdown as the table's single representation.
65+ """
66+ if not tables :
67+ return page .get_text ()
68+ # Skip tables with a missing/empty bbox: a None would raise in tuple()
69+ # and a partial bbox would unpack short in _block_in_a_table.
70+ table_bboxes = [tuple (table .bbox ) for table in tables if table .bbox ]
71+ kept = []
72+ # "blocks" tuple layout: (x0, y0, x1, y1, text, block_no, block_type).
73+ # block_type 1 is an image block (placeholder text); images are handled
74+ # separately below, so keep only text blocks (type 0).
75+ for block in page .get_text ("blocks" ):
76+ x0 , y0 , x1 , y1 , block_text = block [:5 ]
77+ block_type = block [6 ] if len (block ) > 6 else 0
78+ if block_type != 0 :
79+ continue
80+ if not self ._block_in_a_table (x0 , y0 , x1 , y1 , table_bboxes ):
81+ kept .append (block_text )
82+ return "" .join (kept )
83+
84+ def _append_tables (self , page_number , tables , text ):
85+ """Append each table to the text as a markdown block.
86+
87+ Returns the augmented text and the number of tables rendered. Row/column
88+ relationships are preserved via the pipe-delimited markdown that pymupdf's
89+ TableFinder produces. The count reflects tables actually written, not just
90+ detected, so table_count never overstates what a consumer can find.
91+ """
92+ rendered = 0
93+ for tbl_index , table in enumerate (tables ):
94+ try :
95+ text += f"\n [TABLE_{ page_number } _{ tbl_index } ]\n { table .to_markdown ()} "
96+ rendered += 1
97+ except Exception as e :
98+ logger .warning (f"Failed to render table { tbl_index } on page { page_number } : { e } " )
99+ return text , rendered
31100
32101 def read (self , input_source ) -> List [Document ]:
33102 """Read PDF with text, images, and tables."""
@@ -41,32 +110,41 @@ def read(self, input_source) -> List[Document]:
41110 try :
42111 pdf_path = processed_paths [0 ]
43112 logger .debug (f"Opening PDF file: { pdf_path } " )
44- doc = pymupdf .open (pdf_path )
113+ doc = self . _pymupdf .open (pdf_path )
45114 documents = []
46-
115+
47116 for page_num in range (len (doc )):
48117 page = doc [page_num ]
49- text = page .get_text ()
50-
118+ # 1-indexed page number used consistently across markers, log
119+ # messages, and metadata (page_num itself stays 0-based for the
120+ # doc[page_num] lookup).
121+ page_number = page_num + 1
122+
123+ tables = self ._find_tables (page , page_number ) if self .extract_tables else []
124+ text = self ._page_text (page , tables )
125+
51126 image_list = page .get_images ()
52127 for img_index , img in enumerate (image_list ):
53128 try :
54129 xref = img [0 ]
55- pix = pymupdf .Pixmap (doc , xref )
130+ pix = self . _pymupdf .Pixmap (doc , xref )
56131 if pix .n - pix .alpha < 4 :
57132 img_data = pix .tobytes ("png" )
58133 img_b64 = base64 .b64encode (img_data ).decode ()
59- text += f"\n [IMAGE_{ page_num } _{ img_index } : base64_data={ img_b64 [:100 ]} ...]"
134+ text += f"\n [IMAGE_{ page_number } _{ img_index } : base64_data={ img_b64 [:100 ]} ...]"
60135 pix = None
61136 except Exception as e :
62- logger .warning (f"Failed to extract image { img_index } from page { page_num } : { e } " )
63-
137+ logger .warning (f"Failed to extract image { img_index } from page { page_number } : { e } " )
138+
139+ text , table_count = self ._append_tables (page_number , tables , text )
140+
64141 page_doc = Document (
65142 text = text ,
66143 metadata = {
67- 'page_number' : page_num + 1 ,
144+ 'page_number' : page_number ,
68145 'source' : 'advanced_pdf' ,
69- 'file_path' : original_paths [0 ]
146+ 'file_path' : original_paths [0 ],
147+ 'table_count' : table_count
70148 }
71149 )
72150
@@ -84,4 +162,4 @@ def read(self, input_source) -> List[Document]:
84162 logger .error (f"Failed to read advanced PDF from { input_source } : { e } " , exc_info = True )
85163 raise RuntimeError (f"Failed to read advanced PDF: { e } " ) from e
86164 finally :
87- self ._cleanup_temp_files (temp_files )
165+ self ._cleanup_temp_files (temp_files )
0 commit comments