1
0

6 Ревизии ae0a19dc4d ... dd92babb27

Автор SHA1 Съобщение Дата
  zhch158_admin dd92babb27 feat: 改进文档处理流程,支持从 PDF 提取文本并与 OCR 结果对比,添加调试模式 преди 1 седмица
  zhch158_admin dc9a615776 fix: 修复加载和分类文档时的参数传递,添加 renderer_used 参数 преди 1 седмица
  zhch158_admin 85f5dfa1f4 feat: 更新 process_text_element 方法,改进预匹配 spans 的处理逻辑,支持 OCR 和 PDF 文本提取来源 преди 1 седмица
  zhch158_admin 43d0e1c5d3 feat: 在 bank_statement_wired_unet 配置中启用调试模式 преди 1 седмица
  zhch158_admin db1a81a141 feat: 添加 PDF 文档类型检测功能,支持 pypdfium2 和 fitz 渲染引擎,优化文本提取过程 преди 1 седмица
  zhch158_admin 939c825128 feat: Add `.gitignore` to exclude common development artifacts and specific project paths, and update `main_v2.py`. преди 1 седмица

+ 12 - 22
docs/mineru/mineru_文字PDF_处理流程.md

@@ -1,4 +1,4 @@
-正在收集工作区信息正在筛选到最相关的信息根据代码分析,我来详细说明文字PDF的判断和处理流程:
+根据代码分析,我来详细说明文字PDF的判断和处理流程:
 
 ## 文字PDF判断和处理详细说明
 
@@ -49,13 +49,10 @@ graph TB
     ProcessTable --> CropTable[裁剪表格区域图像]
     CropTable --> TableDetect[表格结构检测<br/>RapidTable/StructEqTable]
     TableDetect --> TableCells[识别单元格位置]
-    TableCells --> CellTextExtract{单元格文本提取}
+    TableCells --> TableOCR[OCR识别<br/>PaddleOCR]
     
-    CellTextExtract -->|文字PDF模式| ExtractCellPDFText[提取PDF文本<br/>根据单元格bbox]
-    CellTextExtract -->|需要OCR| TableOCR[OCR识别<br/>PaddleOCR]
-    
-    ExtractCellPDFText --> TableHTML[生成HTML表格<br/>type: table<br/>content: HTML]
-    TableOCR --> TableHTML
+    TableOCR --> TableOCRResult[获取单元格文本]
+    TableOCRResult --> TableHTML[生成HTML表格<br/>type: table<br/>content: HTML]
     
     %% 公式处理分支
     ProcessFormula --> FormulaClassify{公式类型检测}
@@ -130,26 +127,19 @@ for bbox in text_bboxes:
     text = extract_text_from_bbox(pdf_page, bbox)
 ```
 
-### **步骤4: 表格处理(文字PDF)**
+### **步骤4: 表格处理**
 ```python
 # 1. 裁剪表格区域图像
 table_image = crop_image(page_image, table_bbox)
 
-# 2. 表格结构检测
-table_structure = rapidtable_model.predict(table_image)
-# 返回: cells的bbox和行列信息
-
-# 3. 提取单元格文本
-for cell_bbox in table_structure.cells:
-    # 文字PDF: 提取PDF文本
-    cell_text = extract_text_from_bbox(pdf_page, cell_bbox)
-    
-    # 如果提取失败,使用OCR
-    if not cell_text:
-        cell_text = ocr_model.recognize(cell_image)
+# 2. 表格结构检测与内容识别
+# 使用端到端的表格识别模型,或组合使用表格结构识别 + OCR
+# 即使是文字PDF,表格部分目前也主要依赖视觉OCR方案,以保证复杂表格结构的准确恢复
+table_result = table_model.predict(table_image)
+# 返回: HTML格式的表格内容
 
 # 4. 生成HTML
-table_html = generate_html_table(table_structure, cell_texts)
+table_html = table_result['html']
 ```
 
 ### **步骤5: 公式处理**
@@ -189,7 +179,7 @@ middle_json = {
 | 特性 | 文字PDF (txt模式) | 图片PDF (ocr模式) |
 |------|------------------|------------------|
 | **文本提取** | 直接读取PDF文本层 | OCR识别 |
-| **表格单元格** | 优先提取PDF文本,失败才OCR | 全部OCR |
+| **表格单元格** | OCR识别(暂未利用PDF文本层) | OCR识别 |
 | **公式识别** | UniMERNet(图像→LaTeX) | 同左 |
 | **处理速度** | 快(跳过大部分OCR) | 慢(全页OCR) |
 | **准确性** | 文本100%准确 | 依赖OCR质量 |

+ 1 - 1
docs/mineru/文字PDF-表格处理.md

@@ -1,4 +1,4 @@
-正在收集工作区信息正在筛选到最相关的信息根据代码分析,表格处理的代码主要在以下几个文件中:
+根据代码分析,表格处理的代码主要在以下几个文件中:
 
 ## 表格处理流程代码位置
 

+ 1 - 0
ocr_tools/universal_doc_parser/config/bank_statement_wired_unet.yaml

@@ -70,3 +70,4 @@ output:
   save_enhanced_json: true
   coordinate_precision: 2
   normalize_numbers: true
+  debug_mode: true

+ 14 - 9
ocr_tools/universal_doc_parser/core/element_processors.py

@@ -116,7 +116,7 @@ class ElementProcessors:
             pdf_doc: PDF文档对象
             page_idx: 页码索引
             scale: 缩放比例
-            pre_matched_spans: 预匹配的 OCR spans(来自整页 OCR)
+            pre_matched_spans: 预匹配的 OCR/PDF-TXT spans
             
         Returns:
             处理后的元素字典
@@ -129,22 +129,27 @@ class ElementProcessors:
         
         # 优先级1:使用预匹配的 spans(整页 OCR 结果)
         if pre_matched_spans and len(pre_matched_spans) > 0:
+            # 检查 spans 来源
+            span_source = pre_matched_spans[0].get('source', 'ocr')
+            
             text_content, sorted_spans = SpanMatcher.merge_spans_to_text(
                 pre_matched_spans, bbox
             )
+            
             if text_content.strip():
-                # spans 的坐标已经是绝对坐标,直接使用
                 ocr_details = sorted_spans
-                extraction_method = "fullpage_ocr"
-                logger.debug(f"📝 Text from full-page OCR: '{text_content[:30]}...'")
-        
-        # 优先级2:数字 PDF 字符提取
-        if not text_content.strip() and pdf_type == 'txt' and pdf_doc is not None:
+                extraction_method = f"fullpage_{span_source}"  # 'fullpage_pdf' 或 'fullpage_ocr'
+                logger.debug(f"📝 Text from {span_source}: '{text_content[:30]}...'")        
+
+        # 优先级2:PDF 字符提取 如果没有预匹配的 spans)
+        # 注意: 如果 pdf_type='txt' 且没有 pre_matched_spans,说明 pipeline 跳过了整页识别 ,必须走这里
+        elif pdf_type == 'txt' and pdf_doc is not None:
             try:
-                text_content, extraction_success = PDFUtils.extract_text_from_pdf(
+                extraction_text, extraction_success = PDFUtils.extract_text_from_pdf(
                     pdf_doc, page_idx, bbox, scale
                 )
-                if extraction_success and text_content.strip():
+                if extraction_success and extraction_text.strip():
+                    text_content = extraction_text
                     extraction_method = "pdf_extract"
                     logger.debug(f"📝 Text extracted from PDF: '{text_content[:30]}...'")
             except Exception as e:

+ 165 - 14
ocr_tools/universal_doc_parser/core/pipeline_manager_v2.py

@@ -217,7 +217,7 @@ class EnhancedDocPipeline:
         try:
             # 1. 加载文档并分类
             dpi = self.config.get('input', {}).get('dpi', 200)
-            images, pdf_type, pdf_doc = PDFUtils.load_and_classify_document(
+            images, pdf_type, pdf_doc, renderer_used = PDFUtils.load_and_classify_document(
                 doc_path, dpi=dpi, page_range=page_range
             )
             results['metadata']['pdf_type'] = pdf_type
@@ -359,21 +359,58 @@ class EnhancedDocPipeline:
         page_result['layout_raw'] = layout_results
         
         # 3. 整页 OCR 获取所有 text spans(关键改进)
-        all_ocr_spans = []
-        try:
-            all_ocr_spans = self.ocr_recognizer.recognize_text(detection_image)
-            # 去除重复 spans
-            all_ocr_spans = SpanMatcher.remove_duplicate_spans(all_ocr_spans)
-            # 按坐标排序(从上到下,从左到右),方便人工检查缺失字符
-            all_ocr_spans = self._sort_spans_by_position(all_ocr_spans)
-            logger.info(f"📝 Page {page_idx}: OCR detected {len(all_ocr_spans)} text spans")
-        except Exception as e:
-            logger.warning(f"⚠️ Full-page OCR failed: {e}")
+        all_text_spans = []
+        should_run_ocr = True
+        text_source = 'ocr'
+        
+        if pdf_type == 'txt' and pdf_doc is not None:
+            # 文字 PDF:直接从 PDF 提取文本块
+            try:
+                pdf_text_blocks = PDFUtils.extract_all_text_blocks(
+                    pdf_doc, page_idx, scale=scale
+                )
+                # 将 PDF 文本块转换为 span 格式
+                all_text_spans = self._convert_pdf_blocks_to_spans(
+                    pdf_text_blocks, detection_image.shape
+                )
+                text_source = 'pdf'
+                logger.info(f"📝 Page {page_idx}: PDF extracted {len(all_text_spans)} text blocks")
+            except Exception as e:
+                logger.warning(f"⚠️ PDF text extraction failed, fallback to OCR: {e}")
+                pdf_type = 'ocr'  # Fallback to OCR
+
+        # OCR PDF 或 PDF 提取失败时使用 OCR
+        elif pdf_type == 'ocr':
+            should_run_ocr = True
+            if self.debug_mode and text_source == 'pdf':
+                # 调试模式:同时运行 OCR 对比
+                should_run_ocr = True
+                
+            if should_run_ocr:
+                try:
+                    all_text_spans = self.ocr_recognizer.recognize_text(detection_image)
+                    all_text_spans = SpanMatcher.remove_duplicate_spans(all_text_spans)
+                    all_text_spans = self._sort_spans_by_position(all_text_spans)
+                    text_source = 'ocr'
+                    logger.info(f"📝 Page {page_idx}: OCR detected {len(all_text_spans)} text spans")
+                except Exception as e:
+                    logger.warning(f"⚠️ Full-page OCR failed: {e}")                
+                # 3.1 调试模式:对比 OCR 和 PDF 提取结果
+                if self.debug_mode and pdf_type == 'txt' and pdf_doc is not None:
+                    self._compare_ocr_and_pdf_text(
+                        page_idx, pdf_doc, all_ocr_spans, detection_image, output_dir, page_name, scale
+                    )
+        else:
+            raise ValueError(f"Unknown pdf_type: {pdf_type}")
         
         # 4. 将 OCR spans 匹配到 layout blocks
-        matched_spans = SpanMatcher.match_spans_to_blocks(
-            all_ocr_spans, layout_results, overlap_threshold=0.5
-        )
+        matched_spans = {}
+        if all_text_spans:
+            matched_spans = SpanMatcher.match_spans_to_blocks(
+                all_text_spans, layout_results, overlap_threshold=0.5
+            )
+        # 记录文本来源
+        page_result['text_source'] = text_source  # 'ocr' 或 'pdf'
         
         # 5. 分类元素
         classified_elements = self._classify_elements(layout_results, page_idx)
@@ -413,6 +450,120 @@ class EnhancedDocPipeline:
         page_result['elements'] = sorted_elements
         page_result['discarded_blocks'] = sorted_discarded
         return page_result
+
+    @staticmethod
+    def _convert_pdf_blocks_to_spans(
+        pdf_text_blocks: List[Dict[str, Any]],
+        image_shape: tuple
+    ) -> List[Dict[str, Any]]:
+        """
+        将 PDF 文本块转换为 OCR span 格式
+        
+        Args:
+            pdf_text_blocks: PDF 提取的文本块 [{'text': str, 'bbox': [x1,y1,x2,y2]}, ...]
+            image_shape: 图像尺寸 (height, width, channels)
+            
+        Returns:
+            OCR span 格式的列表
+        """
+        spans = []
+        
+        for block in pdf_text_blocks:
+            text = block.get('text', '').strip()
+            bbox = block.get('bbox')
+            
+            if not text or not bbox or len(bbox) < 4:
+                continue
+            
+            # 确保 bbox 在图像范围内
+            x1, y1, x2, y2 = bbox
+            h, w = image_shape[:2]
+            
+            x1 = max(0, min(x1, w))
+            y1 = max(0, min(y1, h))
+            x2 = max(0, min(x2, w))
+            y2 = max(0, min(y2, h))
+            
+            if x2 <= x1 or y2 <= y1:
+                continue
+            
+            # 转换为 OCR span 格式
+            span = {
+                'text': text,
+                'bbox': [x1, y1, x2, y2],  # 或者转为 poly 格式
+                'score': 1.0,  # PDF 提取的置信度设为 1.0
+                'source': 'pdf'  # 标记来源
+            }
+            
+            spans.append(span)
+        
+        return spans
+        
+    def _compare_ocr_and_pdf_text(
+        self, 
+        page_idx: int, 
+        pdf_doc: Any, 
+        ocr_spans: List[Dict[str, Any]], 
+        image: np.ndarray,
+        output_dir: Optional[str],
+        page_name: str,
+        scale: float
+    ):
+        """
+        对比 OCR 和 PDF 提取结果,并输出调试信息
+        """
+        if not output_dir:
+            return
+
+        try:
+            import cv2
+            import json
+            
+            # 获取 PDF 文本
+            pdf_text_blocks = PDFUtils.extract_all_text_blocks(pdf_doc, page_idx, scale=scale)
+            
+            # 准备可视化图像
+            vis_image = image.copy()
+            
+            # 绘制 PDF 文本框 (蓝色)
+            for block in pdf_text_blocks:
+                bbox = [int(x) for x in block['bbox']]
+                cv2.rectangle(vis_image, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (255, 0, 0), 2)
+                
+            # 绘制 OCR 文本框 (红色)
+            for span in ocr_spans:
+                bbox = span.get('bbox')
+                if bbox:
+                    if isinstance(bbox[0], list): # poly
+                        pts = np.array(bbox, np.int32)
+                        pts = pts.reshape((-1, 1, 2))
+                        cv2.polylines(vis_image, [pts], True, (0, 0, 255), 2)
+                    else: # bbox
+                        cv2.rectangle(vis_image, (int(bbox[0]), int(bbox[1])), (int(bbox[2]), int(bbox[3])), (0, 0, 255), 2)
+            
+            # 保存对比图像
+            debug_dir = Path(output_dir) / "debug_comparison"
+            debug_dir.mkdir(parents=True, exist_ok=True)
+            output_path = debug_dir / f"{page_name}_comparison.jpg"
+            cv2.imwrite(str(output_path), vis_image)
+            
+            # 保存对比 JSON
+            comparison_data = {
+                'page_idx': page_idx,
+                'ocr_count': len(ocr_spans),
+                'pdf_text_count': len(pdf_text_blocks),
+                'ocr_spans': [{'text': s['text'], 'bbox': s.get('bbox')} for s in ocr_spans],
+                'pdf_blocks': pdf_text_blocks
+            }
+            
+            json_path = debug_dir / f"{page_name}_comparison.json"
+            with open(json_path, 'w', encoding='utf-8') as f:
+                json.dump(comparison_data, f, ensure_ascii=False, indent=2)
+                
+            logger.info(f"📝 Saved debug comparison to {debug_dir}")
+            
+        except Exception as e:
+            logger.warning(f"⚠️ Debug comparison failed: {e}")
     
     # ==================== OCR Spans 排序 ====================
     

+ 1 - 1
ocr_tools/universal_doc_parser/core/pipeline_manager_v2_streaming.py

@@ -124,7 +124,7 @@ class StreamingDocPipeline(EnhancedDocPipeline):
         try:
             # 1. 加载文档并分类
             dpi = self.config.get('input', {}).get('dpi', 200)
-            images, pdf_type, pdf_doc = PDFUtils.load_and_classify_document(
+            images, pdf_type, pdf_doc, renderer_used = PDFUtils.load_and_classify_document(
                 doc_path, dpi=dpi, page_range=page_range
             )
             

+ 232 - 11
ocr_utils/pdf_utils.py

@@ -52,13 +52,41 @@ class PDFUtils:
             页面索引集合(0-based)
         """
         return parse_page_range(page_range, total_pages)
+
+    @staticmethod
+    def _detect_pdf_doc_type(pdf_doc: Any) -> str:
+        """
+        检测 PDF 文档对象类型
+        
+        Args:
+            pdf_doc: PDF 文档对象
+            
+        Returns:
+            'pypdfium2' 或 'fitz'
+        """
+        doc_type_name = type(pdf_doc).__name__
+        doc_module = type(pdf_doc).__module__
+        
+        if 'pdfium' in doc_module.lower() or 'PdfDocument' in doc_type_name:
+            return 'pypdfium2'
+        elif 'fitz' in doc_module.lower() or 'Document' in doc_type_name:
+            return 'fitz'
+        else:
+            # 尝试通过属性判断
+            if hasattr(pdf_doc, 'get_page') or hasattr(pdf_doc, 'page_count'):
+                # fitz.Document 有 page_count 属性
+                return 'fitz'
+            else:
+                # pypdfium2 通过索引访问
+                return 'pypdfium2'
     
     @staticmethod
     def load_and_classify_document(
         document_path: Path,
         dpi: int = 200,
-        page_range: Optional[str] = None
-    ) -> Tuple[List[Dict], str, Optional[Any]]:
+        page_range: Optional[str] = None,
+        renderer: str = "fitz"  # 新增参数,默认 fitz
+    ) -> Tuple[List[Dict], str, Optional[Any], str]:
         """
         加载文档并分类,支持页面范围过滤
         
@@ -68,12 +96,14 @@ class PDFUtils:
             page_range: 页面范围字符串,如 "1-5,7,9-12"
                        - PDF:按页码(从1开始)
                        - 图片目录:按文件名排序后的位置(从1开始)
+            renderer: PDF渲染引擎,"fitz" 或 "pypdfium2"
             
         Returns:
             (images_list, pdf_type, pdf_doc)
             - images_list: 图像列表,每个元素包含 {'img_pil': PIL.Image, 'scale': float, 'page_idx': int}
             - pdf_type: 'ocr' 或 'txt'
-            - pdf_doc: PDF文档对象(如果是PDF)
+            - pdf_doc: PDF文档对象(如果PDF)
+            - renderer_used: 实际使用的渲染器类型
         """
         pdf_doc = None
         pdf_type = 'ocr'  # 默认使用OCR模式
@@ -128,7 +158,7 @@ class PDFUtils:
                 pdf_bytes, 
                 dpi=dpi,
                 image_type=ImageType.PIL,
-                renderer='fitz'
+                renderer=renderer   # 使用指定的渲染引擎
             )
             
             # 解析页面范围
@@ -167,7 +197,7 @@ class PDFUtils:
         else:
             raise ValueError(f"Unsupported file format: {document_path.suffix}")
         
-        return all_images, pdf_type, pdf_doc
+        return all_images, pdf_type, pdf_doc, renderer
     
     @staticmethod
     def extract_text_from_pdf(
@@ -177,10 +207,10 @@ class PDFUtils:
         scale: float
     ) -> Tuple[str, bool]:
         """
-        从PDF直接提取文本(使用 MinerU 的 pypdfium2 方式
+        从PDF直接提取文本(支持 pypdfium2 和 fitz
         
         Args:
-            pdf_doc: pypdfium2 的 PdfDocument 对象
+            pdf_doc: PDF文档对象 (pypdfium2.PdfDocument 或 fitz.Document)
             page_idx: 页码索引
             bbox: 目标区域的bbox(图像坐标)
             scale: 图像与PDF的缩放比例
@@ -188,8 +218,24 @@ class PDFUtils:
         Returns:
             (text, success)
         """
+        # 检测 PDF 文档类型
+        doc_type = PDFUtils._detect_pdf_doc_type(pdf_doc)
+        
+        if doc_type == 'fitz':
+            return PDFUtils._extract_text_from_pdf_fitz(pdf_doc, page_idx, bbox, scale)
+        else:  # pypdfium2
+            return PDFUtils._extract_text_from_pdf_pypdfium2(pdf_doc, page_idx, bbox, scale)
+    
+    @staticmethod
+    def _extract_text_from_pdf_pypdfium2(
+        pdf_doc: Any,
+        page_idx: int,
+        bbox: List[float],
+        scale: float
+    ) -> Tuple[str, bool]:
+        """使用 pypdfium2 提取文本(原有实现)"""
         if not MINERU_AVAILABLE or pdf_get_page_text is None:
-            logger.debug("MinerU pdf_text_tool not available")
+            logger.error("MinerU pdf_text_tool not available")
             return "", False
             
         try:
@@ -212,13 +258,12 @@ class PDFUtils:
                 for line in block.get('lines', []):
                     line_bbox = line.get('bbox')
                     if line_bbox and hasattr(line_bbox, 'bbox'):
-                        line_bbox = line_bbox.bbox  # pdftext 的 BBox 对象
+                        line_bbox = line_bbox.bbox
                     elif isinstance(line_bbox, (list, tuple)) and len(line_bbox) >= 4:
                         line_bbox = list(line_bbox)
                     else:
                         continue
                     
-                    # 检查 line 是否与目标 bbox 重叠
                     if PDFUtils._bbox_overlap(pdf_bbox, line_bbox):
                         for span in line.get('spans', []):
                             span_text = span.get('text', '')
@@ -230,11 +275,187 @@ class PDFUtils:
             
         except Exception as e:
             import traceback
-            logger.debug(f"PDF text extraction error: {e}")
+            logger.debug(f"pypdfium2 text extraction error: {e}")
             logger.debug(traceback.format_exc())
             return "", False
     
     @staticmethod
+    def _extract_text_from_pdf_fitz(
+        pdf_doc: Any,
+        page_idx: int,
+        bbox: List[float],
+        scale: float
+    ) -> Tuple[str, bool]:
+        """使用 fitz 提取文本"""
+        try:
+            import fitz
+        except ImportError:
+            logger.error("PyMuPDF (fitz) not available")
+            return "", False
+        
+        try:
+            page = pdf_doc[page_idx]
+            
+            # 将图像坐标转换为PDF坐标
+            pdf_bbox = fitz.Rect(
+                bbox[0] / scale,
+                bbox[1] / scale,
+                bbox[2] / scale,
+                bbox[3] / scale
+            )
+            
+            # 提取区域内的文本
+            text = page.get_text("text", clip=pdf_bbox)
+            
+            return text.strip(), bool(text.strip())
+            
+        except Exception as e:
+            import traceback
+            logger.debug(f"fitz text extraction error: {e}")
+            logger.debug(traceback.format_exc())
+            return "", False
+    
+    @staticmethod
+    def extract_all_text_blocks(
+        pdf_doc: Any,
+        page_idx: int,
+        scale: float
+    ) -> List[Dict[str, Any]]:
+        """
+        提取页面所有文本块(支持 pypdfium2 和 fitz)
+        
+        Args:
+            pdf_doc: PDF文档对象
+            page_idx: 页码
+            scale: 缩放比例
+            
+        Returns:
+            文本块列表 [{'text': str, 'bbox': [x1, y1, x2, y2]}, ...]
+        """
+        # 检测 PDF 文档类型
+        doc_type = PDFUtils._detect_pdf_doc_type(pdf_doc)
+        
+        if doc_type == 'fitz':
+            return PDFUtils._extract_all_text_blocks_fitz(pdf_doc, page_idx, scale)
+        else:  # pypdfium2
+            return PDFUtils._extract_all_text_blocks_pypdfium2(pdf_doc, page_idx, scale)
+    
+    @staticmethod
+    def _extract_all_text_blocks_pypdfium2(
+        pdf_doc: Any,
+        page_idx: int,
+        scale: float
+    ) -> List[Dict[str, Any]]:
+        """使用 pypdfium2 提取所有文本块(原有实现)"""
+        if not MINERU_AVAILABLE or pdf_get_page_text is None:
+            return []
+            
+        try:
+            page = pdf_doc[page_idx]
+            page_dict = pdf_get_page_text(page)
+            
+            extracted_blocks = []
+            
+            for block in page_dict.get('blocks', []):
+                for line in block.get('lines', []):
+                    line_text = ""
+                    for span in line.get('spans', []):
+                        line_text += span.get('text', "")
+                    
+                    if not line_text.strip():
+                        continue
+                        
+                    line_bbox = line.get('bbox')
+                    if line_bbox and hasattr(line_bbox, 'bbox'):
+                        line_bbox = line_bbox.bbox
+                    elif isinstance(line_bbox, (list, tuple)) and len(line_bbox) >= 4:
+                        line_bbox = list(line_bbox)
+                    else:
+                        continue
+                        
+                    img_bbox = [
+                        line_bbox[0] * scale,
+                        line_bbox[1] * scale,
+                        line_bbox[2] * scale,
+                        line_bbox[3] * scale
+                    ]
+                    
+                    extracted_blocks.append({
+                        'text': line_text,
+                        'bbox': img_bbox,
+                        'origin_bbox': line_bbox
+                    })
+            
+            return extracted_blocks
+            
+        except Exception as e:
+            logger.warning(f"pypdfium2 extract_all_text_blocks failed: {e}")
+            import traceback
+            logger.debug(traceback.format_exc())
+            return []
+    
+    @staticmethod
+    def _extract_all_text_blocks_fitz(
+        pdf_doc: Any,
+        page_idx: int,
+        scale: float
+    ) -> List[Dict[str, Any]]:
+        """使用 fitz 提取所有文本块"""
+        try:
+            import fitz
+        except ImportError:
+            logger.warning("PyMuPDF (fitz) not available")
+            return []
+        
+        try:
+            page = pdf_doc[page_idx]
+            
+            # 使用 get_text("dict") 获取详细的文本信息
+            text_dict = page.get_text("dict")
+            
+            extracted_blocks = []
+            
+            # 遍历所有 blocks
+            for block in text_dict.get("blocks", []):
+                # 只处理文本块(type=0)
+                if block.get("type") != 0:
+                    continue
+                
+                # 遍历所有 lines
+                for line in block.get("lines", []):
+                    line_text = ""
+                    line_bbox = line.get("bbox")
+                    
+                    # 提取 line 中的所有 span 文本
+                    for span in line.get("spans", []):
+                        line_text += span.get("text", "")
+                    
+                    if not line_text.strip() or not line_bbox:
+                        continue
+                    
+                    # PDF 坐标转换为图像坐标
+                    img_bbox = [
+                        line_bbox[0] * scale,
+                        line_bbox[1] * scale,
+                        line_bbox[2] * scale,
+                        line_bbox[3] * scale
+                    ]
+                    
+                    extracted_blocks.append({
+                        'text': line_text,
+                        'bbox': img_bbox,
+                        'origin_bbox': list(line_bbox)
+                    })
+            
+            return extracted_blocks
+            
+        except Exception as e:
+            logger.warning(f"fitz extract_all_text_blocks failed: {e}")
+            import traceback
+            logger.debug(traceback.format_exc())
+            return []    
+
+    @staticmethod
     def _bbox_overlap(bbox1: List[float], bbox2: List[float]) -> bool:
         """检查两个 bbox 是否重叠"""
         if len(bbox1) < 4 or len(bbox2) < 4: