Browse Source

refactor: remove MinerUOutputFormatter and related classes, streamline output formatting in OutputFormatterV2, and enhance JSON and Markdown generation capabilities

zhch158_admin 1 day ago
parent
commit
d86fb0815b

+ 2 - 9
zhch/universal_doc_parser/utils/__init__.py

@@ -1,17 +1,10 @@
 """工具模块"""
 """工具模块"""
 
 
 from .output_formatter import OutputFormatter
 from .output_formatter import OutputFormatter
-from .output_formatter_v2 import OutputFormatterV2
-from .mineru_output_formatter import (
-    MinerUOutputFormatter,
-    MinerUFormatConverter,
-    save_mineru_format
-)
+from .output_formatter_v2 import OutputFormatterV2, save_mineru_format
 
 
 __all__ = [
 __all__ = [
     'OutputFormatter', 
     'OutputFormatter', 
     'OutputFormatterV2',
     'OutputFormatterV2',
-    'MinerUOutputFormatter',
-    'MinerUFormatConverter',
     'save_mineru_format'
     'save_mineru_format'
-]
+]

+ 0 - 632
zhch/universal_doc_parser/utils/mineru_output_formatter.py

@@ -1,632 +0,0 @@
-"""
-MinerU 标准输出格式化器
-
-严格遵循 MinerU 的输出格式:
-1. JSON: 与 mineru_vllm_results_cell_bbox 格式完全一致
-2. Markdown: 带 bbox 注释的 Markdown
-3. 表格单元格坐标: table_cells 数组
-
-封装 MinerU 的 vlm_middle_json_mkcontent 和 pipeline_middle_json_mkcontent 实现
-"""
-import json
-import os
-import sys
-from pathlib import Path
-from typing import Dict, Any, List, Optional, Tuple
-from loguru import logger
-import numpy as np
-from PIL import Image, ImageDraw
-import cv2
-import hashlib
-
-# 添加 MinerU 路径
-mineru_path = Path(__file__).parents[3]
-if str(mineru_path) not in sys.path:
-    sys.path.insert(0, str(mineru_path))
-
-# 导入 MinerU 组件
-try:
-    from mineru.utils.enum_class import MakeMode, BlockType, ContentType
-    MINERU_AVAILABLE = True
-except ImportError as e:
-    logger.warning(f"MinerU components not available: {e}")
-    MINERU_AVAILABLE = False
-    # 定义占位符
-    class MakeMode:
-        MM_MD = 'mm_md'
-        NLP_MD = 'nlp_md'
-        CONTENT_LIST = 'content_list'
-    
-    class BlockType:
-        TEXT = 'text'
-        TABLE = 'table'
-        IMAGE = 'image'
-        TITLE = 'title'
-        LIST = 'list'
-        HEADER = 'header'
-        FOOTER = 'footer'
-        TABLE_BODY = 'table_body'
-        TABLE_CAPTION = 'table_caption'
-        TABLE_FOOTNOTE = 'table_footnote'
-    
-    class ContentType:
-        TEXT = 'text'
-        TABLE = 'table'
-        IMAGE = 'image'
-
-
-class MinerUOutputFormatter:
-    """
-    MinerU 标准输出格式化器
-    
-    输出格式严格遵循 MinerU 的 mineru_vllm_results_cell_bbox 格式:
-    - JSON: 每页一个 JSON 文件,包含元素列表
-    - Markdown: 每页一个 MD 文件,带 bbox 注释
-    - 表格: HTML 格式,带 data-bbox 属性
-    - 单元格坐标: table_cells 数组
-    """
-    
-    def __init__(self, output_dir: str, save_images: bool = True):
-        """
-        初始化格式化器
-        
-        Args:
-            output_dir: 输出目录
-            save_images: 是否保存图片
-        """
-        self.output_dir = Path(output_dir)
-        self.output_dir.mkdir(parents=True, exist_ok=True)
-        self.save_images = save_images
-        
-        # 创建 images 子目录
-        self.images_dir = self.output_dir / "images"
-        self.images_dir.mkdir(parents=True, exist_ok=True)
-    
-    def format_and_save(
-        self,
-        results: Dict[str, Any],
-        doc_name: str = None
-    ) -> Dict[str, List[str]]:
-        """
-        格式化并保存结果
-        
-        Args:
-            results: pipeline 处理结果
-            doc_name: 文档名称(可选)
-            
-        Returns:
-            输出文件路径字典
-        """
-        if doc_name is None:
-            doc_name = Path(results.get('document_path', 'unknown')).stem
-        
-        output_paths = {
-            'json': [],
-            'markdown': [],
-            'images': []
-        }
-        
-        for page in results.get('pages', []):
-            page_idx = page.get('page_idx', 0)
-            page_name = f"{doc_name}_page_{page_idx + 1:03d}"
-            
-            # 1. 转换为 MinerU 格式
-            mineru_elements = self._convert_page_to_mineru_format(
-                page, 
-                results.get('metadata', {})
-            )
-            
-            # 2. 保存 JSON
-            json_path = self.output_dir / f"{page_name}.json"
-            with open(json_path, 'w', encoding='utf-8') as f:
-                json.dump(mineru_elements, f, ensure_ascii=False, indent=2)
-            output_paths['json'].append(str(json_path))
-            
-            # 3. 生成并保存 Markdown
-            md_content = self._generate_markdown(mineru_elements)
-            md_path = self.output_dir / f"{page_name}.md"
-            with open(md_path, 'w', encoding='utf-8') as f:
-                f.write(md_content)
-            output_paths['markdown'].append(str(md_path))
-            
-            # 4. 保存原始图片
-            if self.save_images and 'original_image' in page:
-                img_path = self._save_page_image(
-                    page['original_image'], 
-                    page_name
-                )
-                if img_path:
-                    output_paths['images'].append(img_path)
-        
-        logger.info(f"✅ MinerU format output saved to: {self.output_dir}")
-        return output_paths
-    
-    def _convert_page_to_mineru_format(
-        self,
-        page: Dict[str, Any],
-        metadata: Dict[str, Any]
-    ) -> List[Dict[str, Any]]:
-        """
-        将页面转换为 MinerU 标准格式
-        
-        Args:
-            page: 页面数据
-            metadata: 文档元数据
-            
-        Returns:
-            MinerU 格式的元素列表
-        """
-        mineru_elements = []
-        page_idx = page.get('page_idx', 0)
-        
-        for element in page.get('elements', []):
-            mineru_elem = self._convert_element_to_mineru(element, page_idx)
-            if mineru_elem:
-                mineru_elements.append(mineru_elem)
-        
-        return mineru_elements
-    
-    def _convert_element_to_mineru(
-        self,
-        element: Dict[str, Any],
-        page_idx: int
-    ) -> Optional[Dict[str, Any]]:
-        """
-        将单个元素转换为 MinerU 格式
-        
-        Args:
-            element: 元素数据
-            page_idx: 页面索引
-            
-        Returns:
-            MinerU 格式的元素
-        """
-        elem_type = element.get('type', '')
-        bbox = element.get('bbox', [0, 0, 0, 0])
-        content = element.get('content', {})
-        
-        # 确保 bbox 是整数列表
-        bbox = [int(x) for x in bbox[:4]] if bbox else [0, 0, 0, 0]
-        
-        result = {
-            'type': self._map_element_type(elem_type),
-            'bbox': bbox,
-            'page_idx': page_idx
-        }
-        
-        # 处理不同类型的元素
-        if elem_type in ['text', 'title', 'ref_text', 'ocr_text']:
-            text = content.get('text', '') if isinstance(content, dict) else str(content)
-            result['text'] = text
-            
-        elif elem_type in ['header', 'footer']:
-            text = content.get('text', '') if isinstance(content, dict) else str(content)
-            result['text'] = text
-            
-        elif elem_type == 'list':
-            result['sub_type'] = 'text'
-            list_items = content.get('list_items', []) if isinstance(content, dict) else []
-            if not list_items and isinstance(content, dict):
-                text = content.get('text', '')
-                if text:
-                    list_items = [text]
-            result['list_items'] = list_items
-            
-        elif elem_type in ['table', 'table_body']:
-            result = self._convert_table_element(element, page_idx)
-            
-        elif elem_type in ['image', 'image_body', 'figure']:
-            result = self._convert_image_element(element, page_idx)
-            
-        elif elem_type in ['interline_equation', 'inline_equation', 'equation']:
-            latex = content.get('latex', '') if isinstance(content, dict) else ''
-            result['text'] = latex
-            result['text_format'] = 'latex'
-        
-        return result
-    
-    def _convert_table_element(
-        self,
-        element: Dict[str, Any],
-        page_idx: int
-    ) -> Dict[str, Any]:
-        """
-        转换表格元素为 MinerU 格式
-        
-        关键:
-        1. table_body: 带 data-bbox 属性的 HTML
-        2. table_cells: 单元格坐标数组
-        3. image_rotation_angle: 旋转角度
-        4. skew_angle: 倾斜角度
-        """
-        bbox = element.get('bbox', [0, 0, 0, 0])
-        bbox = [int(x) for x in bbox[:4]] if bbox else [0, 0, 0, 0]
-        content = element.get('content', {})
-        
-        # 获取表格 HTML
-        table_html = content.get('html', '') or content.get('original_html', '')
-        
-        # 获取表格标题和脚注
-        table_caption = content.get('table_caption', [])
-        if isinstance(table_caption, str):
-            table_caption = [table_caption] if table_caption else []
-        
-        table_footnote = content.get('table_footnote', [])
-        if isinstance(table_footnote, str):
-            table_footnote = [table_footnote] if table_footnote else []
-        
-        # 保存表格图片
-        img_path = ''
-        if self.save_images and 'table_image' in content:
-            img_path = self._save_table_image(
-                content['table_image'],
-                f"table_{page_idx}_{bbox[0]}_{bbox[1]}"
-            )
-        
-        result = {
-            'type': 'table',
-            'img_path': img_path,
-            'table_caption': table_caption,
-            'table_footnote': table_footnote,
-            'table_body': table_html,
-            'bbox': bbox,
-            'page_idx': page_idx
-        }
-        
-        # 添加单元格坐标信息
-        cells = content.get('cells', [])
-        if cells:
-            result['table_cells'] = self._format_table_cells(cells)
-        
-        # 添加旋转和倾斜信息
-        if 'table_angle' in content:
-            result['image_rotation_angle'] = float(content['table_angle'])
-        
-        if 'skew_angle' in content:
-            result['skew_angle'] = float(content['skew_angle'])
-        
-        return result
-    
-    def _format_table_cells(self, cells: List[Dict]) -> List[Dict[str, Any]]:
-        """
-        格式化表格单元格为 MinerU 格式
-        
-        MinerU 格式:
-        {
-            "type": "table_cell",
-            "text": "单元格内容",
-            "matched_text": "OCR匹配文本",
-            "bbox": [y1, x1, y2, x2],  # 注意坐标顺序
-            "row": 2,
-            "col": 1,
-            "score": 100.0,
-            "paddle_bbox_indices": [11, 12]
-        }
-        """
-        formatted_cells = []
-        
-        for cell in cells:
-            formatted_cell = {
-                'type': 'table_cell',
-                'text': cell.get('text', ''),
-                'matched_text': cell.get('matched_text', cell.get('text', '')),
-                'bbox': cell.get('bbox', [0, 0, 0, 0]),
-                'row': cell.get('row', 0),
-                'col': cell.get('col', 0),
-                'score': cell.get('score', 100.0),
-                'paddle_bbox_indices': cell.get('paddle_bbox_indices', 
-                                                cell.get('paddle_indices', []))
-            }
-            
-            # 确保 bbox 是整数列表
-            if formatted_cell['bbox']:
-                formatted_cell['bbox'] = [int(x) for x in formatted_cell['bbox'][:4]]
-            
-            formatted_cells.append(formatted_cell)
-        
-        return formatted_cells
-    
-    def _convert_image_element(
-        self,
-        element: Dict[str, Any],
-        page_idx: int
-    ) -> Dict[str, Any]:
-        """转换图片元素为 MinerU 格式"""
-        bbox = element.get('bbox', [0, 0, 0, 0])
-        bbox = [int(x) for x in bbox[:4]] if bbox else [0, 0, 0, 0]
-        content = element.get('content', {})
-        
-        # 保存图片
-        img_path = ''
-        if self.save_images and 'image_data' in content:
-            img_path = self._save_content_image(
-                content['image_data'],
-                f"image_{page_idx}_{bbox[0]}_{bbox[1]}"
-            )
-        
-        return {
-            'type': 'image',
-            'img_path': img_path,
-            'image_caption': content.get('caption', []),
-            'image_footnote': content.get('footnote', []),
-            'bbox': bbox,
-            'page_idx': page_idx
-        }
-    
-    def _map_element_type(self, elem_type: str) -> str:
-        """映射元素类型到 MinerU 标准类型"""
-        type_mapping = {
-            'text': 'text',
-            'title': 'title',
-            'ref_text': 'ref_text',
-            'ocr_text': 'text',
-            'header': 'header',
-            'footer': 'footer',
-            'table': 'table',
-            'table_body': 'table',
-            'image': 'image',
-            'image_body': 'image',
-            'figure': 'image',
-            'list': 'list',
-            'interline_equation': 'equation',
-            'inline_equation': 'equation',
-            'equation': 'equation',
-        }
-        return type_mapping.get(elem_type, elem_type)
-    
-    def _generate_markdown(self, elements: List[Dict[str, Any]]) -> str:
-        """
-        生成 MinerU 格式的 Markdown
-        
-        格式:
-        <!-- bbox: [x1, y1, x2, y2] -->
-        内容
-        """
-        md_lines = []
-        
-        for elem in elements:
-            elem_type = elem.get('type', '')
-            bbox = elem.get('bbox', [])
-            
-            # 添加 bbox 注释
-            bbox_comment = f"<!-- bbox: {bbox} -->"
-            
-            if elem_type == 'header':
-                md_lines.append(bbox_comment)
-                md_lines.append(f"<!-- 页眉: {elem.get('text', '')} -->")
-                md_lines.append("")
-                
-            elif elem_type == 'footer':
-                md_lines.append(bbox_comment)
-                md_lines.append(f"<!-- 页脚: {elem.get('text', '')} -->")
-                md_lines.append("")
-                
-            elif elem_type == 'title':
-                md_lines.append(bbox_comment)
-                level = elem.get('text_level', 1)
-                md_lines.append(f"{'#' * min(level, 6)} {elem.get('text', '')}")
-                md_lines.append("")
-                
-            elif elem_type == 'text':
-                md_lines.append(bbox_comment)
-                md_lines.append(elem.get('text', ''))
-                md_lines.append("")
-                
-            elif elem_type == 'list':
-                md_lines.append(bbox_comment)
-                for item in elem.get('list_items', []):
-                    md_lines.append(item)
-                md_lines.append("")
-                
-            elif elem_type == 'table':
-                # 表格标题
-                for caption in elem.get('table_caption', []):
-                    md_lines.append(f"**{caption}**")
-                md_lines.append("")
-                
-                md_lines.append(bbox_comment)
-                
-                # 表格 HTML
-                table_body = elem.get('table_body', '')
-                if table_body:
-                    md_lines.append(table_body)
-                else:
-                    # 如果没有 HTML,显示图片
-                    img_path = elem.get('img_path', '')
-                    if img_path:
-                        md_lines.append(f"![Image]({img_path})")
-                md_lines.append("")
-                
-            elif elem_type == 'image':
-                md_lines.append(bbox_comment)
-                img_path = elem.get('img_path', '')
-                if img_path:
-                    md_lines.append(f"![Image]({img_path})")
-                md_lines.append("")
-                
-            elif elem_type == 'equation':
-                md_lines.append(bbox_comment)
-                latex = elem.get('text', '')
-                if latex:
-                    md_lines.append(f"$$\n{latex}\n$$")
-                md_lines.append("")
-        
-        return '\n'.join(md_lines)
-    
-    def _save_page_image(
-        self,
-        image: np.ndarray,
-        name: str
-    ) -> Optional[str]:
-        """保存页面图片"""
-        try:
-            # 生成唯一文件名
-            img_hash = hashlib.md5(image.tobytes()[:1000]).hexdigest()[:16]
-            filename = f"{name}_{img_hash}.png"
-            filepath = self.images_dir / filename
-            
-            if isinstance(image, np.ndarray):
-                if len(image.shape) == 3 and image.shape[2] == 3:
-                    # BGR to RGB
-                    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
-                Image.fromarray(image).save(filepath)
-            
-            return f"images/{filename}"
-        except Exception as e:
-            logger.warning(f"Failed to save page image: {e}")
-            return None
-    
-    def _save_table_image(
-        self,
-        image: np.ndarray,
-        name: str
-    ) -> str:
-        """保存表格图片"""
-        try:
-            img_hash = hashlib.md5(image.tobytes()[:1000]).hexdigest()[:32]
-            filename = f"{img_hash}.jpg"
-            filepath = self.images_dir / filename
-            
-            if isinstance(image, np.ndarray):
-                if len(image.shape) == 3 and image.shape[2] == 3:
-                    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
-                Image.fromarray(image).save(filepath, quality=95)
-            
-            return f"images/{filename}"
-        except Exception as e:
-            logger.warning(f"Failed to save table image: {e}")
-            return ""
-    
-    def _save_content_image(
-        self,
-        image: np.ndarray,
-        name: str
-    ) -> str:
-        """保存内容图片"""
-        return self._save_table_image(image, name)
-
-
-class MinerUFormatConverter:
-    """
-    MinerU 格式转换器
-    
-    用于将 pipeline 的内部格式转换为 MinerU 标准输出格式
-    """
-    
-    @staticmethod
-    def convert_pipeline_result_to_mineru(
-        results: Dict[str, Any]
-    ) -> List[Dict[str, Any]]:
-        """
-        将 pipeline 结果转换为 MinerU content_list 格式
-        
-        Args:
-            results: pipeline 处理结果
-            
-        Returns:
-            MinerU content_list 格式的结果
-        """
-        content_list = []
-        
-        for page in results.get('pages', []):
-            page_idx = page.get('page_idx', 0)
-            page_size = page.get('image_shape', [0, 0])[:2]
-            
-            for element in page.get('elements', []):
-                content = MinerUFormatConverter._element_to_content(
-                    element, page_idx, page_size
-                )
-                if content:
-                    content_list.append(content)
-        
-        return content_list
-    
-    @staticmethod
-    def _element_to_content(
-        element: Dict[str, Any],
-        page_idx: int,
-        page_size: Tuple[int, int]
-    ) -> Optional[Dict[str, Any]]:
-        """将元素转换为 MinerU content 格式"""
-        elem_type = element.get('type', '')
-        bbox = element.get('bbox', [0, 0, 0, 0])
-        content = element.get('content', {})
-        
-        # 转换 bbox 为相对坐标(千分比)
-        page_height, page_width = page_size if len(page_size) >= 2 else (1, 1)
-        if page_width > 0 and page_height > 0 and bbox:
-            relative_bbox = [
-                int(bbox[0] * 1000 / page_width),
-                int(bbox[1] * 1000 / page_height),
-                int(bbox[2] * 1000 / page_width),
-                int(bbox[3] * 1000 / page_height)
-            ]
-        else:
-            relative_bbox = bbox
-        
-        result = {
-            'bbox': relative_bbox,
-            'page_idx': page_idx
-        }
-        
-        if elem_type in ['text', 'title', 'ref_text', 'header', 'footer']:
-            text = content.get('text', '') if isinstance(content, dict) else str(content)
-            result['type'] = elem_type
-            result['text'] = text
-            
-        elif elem_type in ['table', 'table_body']:
-            result['type'] = 'table'
-            result['img_path'] = content.get('img_path', '')
-            result['table_caption'] = content.get('table_caption', [])
-            result['table_footnote'] = content.get('table_footnote', [])
-            result['table_body'] = content.get('html', '')
-            
-            # 添加单元格信息
-            cells = content.get('cells', [])
-            if cells:
-                result['table_cells'] = cells
-            
-            # 添加旋转信息
-            if 'table_angle' in content:
-                result['image_rotation_angle'] = content['table_angle']
-            if 'skew_angle' in content:
-                result['skew_angle'] = content['skew_angle']
-                
-        elif elem_type in ['image', 'figure']:
-            result['type'] = 'image'
-            result['img_path'] = content.get('img_path', '')
-            result['image_caption'] = content.get('caption', [])
-            result['image_footnote'] = content.get('footnote', [])
-            
-        elif elem_type == 'list':
-            result['type'] = 'list'
-            result['sub_type'] = 'text'
-            result['list_items'] = content.get('list_items', [])
-            
-        else:
-            return None
-        
-        return result
-
-
-def save_mineru_format(
-    results: Dict[str, Any],
-    output_dir: str,
-    doc_name: str = None,
-    save_images: bool = True
-) -> Dict[str, List[str]]:
-    """
-    便捷函数:保存为 MinerU 格式
-    
-    Args:
-        results: pipeline 处理结果
-        output_dir: 输出目录
-        doc_name: 文档名称
-        save_images: 是否保存图片
-        
-    Returns:
-        输出文件路径
-    """
-    formatter = MinerUOutputFormatter(output_dir, save_images)
-    return formatter.format_and_save(results, doc_name)
-

+ 459 - 124
zhch/universal_doc_parser/utils/output_formatter_v2.py

@@ -1,16 +1,20 @@
 """
 """
-增强版输出格式化器 v2
+统一输出格式化器
+严格遵循 MinerU mineru_vllm_results_cell_bbox 格式
+
 支持:
 支持:
-1. MinerU 标准 middle.json 格式
-2. Markdown 输出
-3. Debug 模式:layout 图片、OCR 图片、单元格坐标图片
-4. 表格 HTML 输出(带坐标信息)
+1. MinerU 标准 middle.json 格式(用于 union_make 生成 Markdown)
+2. mineru_vllm_results_cell_bbox 格式(每页独立 JSON)
+3. Markdown 输出(复用 MinerU union_make)
+4. Debug 模式:layout 图片、OCR 图片
+5. 表格 HTML 输出(带坐标信息)
 """
 """
 import json
 import json
 import os
 import os
 import sys
 import sys
+import hashlib
 from pathlib import Path
 from pathlib import Path
-from typing import Dict, Any, List, Optional, Union
+from typing import Dict, Any, List, Optional, Union, Tuple
 from loguru import logger
 from loguru import logger
 import numpy as np
 import numpy as np
 from PIL import Image, ImageDraw, ImageFont
 from PIL import Image, ImageDraw, ImageFont
@@ -28,10 +32,23 @@ try:
 except ImportError as e:
 except ImportError as e:
     logger.warning(f"MinerU components not available: {e}")
     logger.warning(f"MinerU components not available: {e}")
     MINERU_AVAILABLE = False
     MINERU_AVAILABLE = False
+    
+    # 占位符定义
+    class MakeMode:
+        MM_MD = 'mm_md'
+        NLP_MD = 'nlp_md'
 
 
 
 
 class OutputFormatterV2:
 class OutputFormatterV2:
-    """增强版输出格式化器"""
+    """
+    统一输出格式化器
+    
+    严格遵循 MinerU mineru_vllm_results_cell_bbox 格式:
+    - middle.json: MinerU 标准格式,用于生成 Markdown
+    - page_xxx.json: 每页独立的 JSON,包含 table_cells
+    - Markdown: 带 bbox 注释
+    - 表格: HTML 格式,带 data-bbox 属性
+    """
     
     
     # 颜色映射(与 MinerU 保持一致)
     # 颜色映射(与 MinerU 保持一致)
     COLOR_MAP = {
     COLOR_MAP = {
@@ -74,7 +91,7 @@ class OutputFormatterV2:
         self,
         self,
         results: Dict[str, Any],
         results: Dict[str, Any],
         output_config: Dict[str, Any]
         output_config: Dict[str, Any]
-    ) -> Dict[str, str]:
+    ) -> Dict[str, Any]:
         """
         """
         保存处理结果
         保存处理结果
         
         
@@ -85,17 +102,29 @@ class OutputFormatterV2:
         Returns:
         Returns:
             输出文件路径字典
             输出文件路径字典
         """
         """
-        output_paths = {}
+        output_paths = {
+            'images': [],
+            'json_pages': [],
+        }
         
         
         # 创建文档输出目录
         # 创建文档输出目录
         doc_name = Path(results['document_path']).stem
         doc_name = Path(results['document_path']).stem
         doc_output_dir = self.output_dir / doc_name
         doc_output_dir = self.output_dir / doc_name
         doc_output_dir.mkdir(parents=True, exist_ok=True)
         doc_output_dir.mkdir(parents=True, exist_ok=True)
         
         
-        # 1. 转换为 MinerU 标准 middle.json
+        # 创建 images 子目录
+        images_dir = doc_output_dir / 'images'
+        images_dir.mkdir(exist_ok=True)
+        
+        # 1. 首先保存图片元素(设置 image_path)
+        image_paths = self._save_image_elements(results, images_dir, doc_name)
+        if image_paths:
+            output_paths['images'] = image_paths
+        
+        # 2. 转换为 MinerU middle.json 格式(用于 union_make)
         middle_json = self._convert_to_middle_json(results)
         middle_json = self._convert_to_middle_json(results)
         
         
-        # 2. 保存 middle.json
+        # 3. 保存 middle.json
         if output_config.get('save_json', True):
         if output_config.get('save_json', True):
             json_path = doc_output_dir / f"{doc_name}_middle.json"
             json_path = doc_output_dir / f"{doc_name}_middle.json"
             with open(json_path, 'w', encoding='utf-8') as f:
             with open(json_path, 'w', encoding='utf-8') as f:
@@ -103,24 +132,22 @@ class OutputFormatterV2:
             output_paths['middle_json'] = str(json_path)
             output_paths['middle_json'] = str(json_path)
             logger.info(f"📄 Middle JSON saved: {json_path}")
             logger.info(f"📄 Middle JSON saved: {json_path}")
         
         
-        # 3. 保存增强版 JSON(包含单元格坐标)
-        enhanced_json_path = doc_output_dir / f"{doc_name}_enhanced.json"
-        with open(enhanced_json_path, 'w', encoding='utf-8') as f:
-            json.dump(results, f, ensure_ascii=False, indent=2, default=self._json_serializer)
-        output_paths['enhanced_json'] = str(enhanced_json_path)
-        logger.info(f"📄 Enhanced JSON saved: {enhanced_json_path}")
+        # 4. 保存每页独立的 mineru_vllm_results_cell_bbox 格式 JSON
+        if output_config.get('save_page_json', True):
+            page_json_paths = self._save_page_jsons(results, doc_output_dir, doc_name)
+            output_paths['json_pages'] = page_json_paths
         
         
-        # 4. 保存 Markdown
+        # 5. 保存 Markdown
         if output_config.get('save_markdown', True):
         if output_config.get('save_markdown', True):
-            md_path = self._save_markdown(results, doc_output_dir, doc_name)
+            md_path = self._save_markdown(results, middle_json, doc_output_dir, doc_name)
             output_paths['markdown'] = str(md_path)
             output_paths['markdown'] = str(md_path)
         
         
-        # 5. 保存表格 HTML
+        # 6. 保存表格 HTML
         if output_config.get('save_html', True):
         if output_config.get('save_html', True):
             html_dir = self._save_table_htmls(results, doc_output_dir, doc_name)
             html_dir = self._save_table_htmls(results, doc_output_dir, doc_name)
             output_paths['table_htmls'] = str(html_dir)
             output_paths['table_htmls'] = str(html_dir)
         
         
-        # 6. Debug 模式:保存可视化图片
+        # 7. Debug 模式:保存可视化图片
         if output_config.get('save_layout_image', False):
         if output_config.get('save_layout_image', False):
             layout_paths = self._save_layout_images(
             layout_paths = self._save_layout_images(
                 results, doc_output_dir, doc_name,
                 results, doc_output_dir, doc_name,
@@ -136,20 +163,14 @@ class OutputFormatterV2:
         logger.info(f"✅ All results saved to: {doc_output_dir}")
         logger.info(f"✅ All results saved to: {doc_output_dir}")
         return output_paths
         return output_paths
     
     
-    def _json_serializer(self, obj):
-        """JSON 序列化辅助函数"""
-        if isinstance(obj, np.ndarray):
-            return obj.tolist()
-        if isinstance(obj, np.integer):
-            return int(obj)
-        if isinstance(obj, np.floating):
-            return float(obj)
-        if isinstance(obj, Path):
-            return str(obj)
-        return str(obj)
+    # ==================== MinerU middle.json 格式 ====================
     
     
     def _convert_to_middle_json(self, results: Dict[str, Any]) -> Dict[str, Any]:
     def _convert_to_middle_json(self, results: Dict[str, Any]) -> Dict[str, Any]:
-        """转换为 MinerU 标准 middle.json 格式"""
+        """
+        转换为 MinerU 标准 middle.json 格式
+        
+        用于 vlm_union_make 生成 Markdown
+        """
         middle_json = {
         middle_json = {
             "pdf_info": [],
             "pdf_info": [],
             "_backend": "vlm",
             "_backend": "vlm",
@@ -167,10 +188,10 @@ class OutputFormatterV2:
             }
             }
             
             
             for element in page.get('elements', []):
             for element in page.get('elements', []):
-                block = self._element_to_block(element, page_info['page_size'])
+                block = self._element_to_middle_block(element)
                 if block:
                 if block:
                     elem_type = element.get('type', '')
                     elem_type = element.get('type', '')
-                    if elem_type in ['header', 'footer', 'page_number', 'aside_text']:
+                    if elem_type in ['header', 'footer', 'page_number', 'aside_text', 'abandon', 'discarded']:
                         page_info['discarded_blocks'].append(block)
                         page_info['discarded_blocks'].append(block)
                     else:
                     else:
                         page_info['para_blocks'].append(block)
                         page_info['para_blocks'].append(block)
@@ -179,12 +200,14 @@ class OutputFormatterV2:
         
         
         return middle_json
         return middle_json
     
     
-    def _element_to_block(
-        self,
-        element: Dict[str, Any],
-        page_size: List[int]
-    ) -> Dict[str, Any]:
-        """将元素转换为 MinerU block 格式"""
+    def _element_to_middle_block(self, element: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+        """
+        将元素转换为 MinerU middle.json block 格式
+        
+        MinerU 期望的嵌套结构:
+        - image 类型: { type: "image", blocks: [{ type: "image_body", lines: [...] }] }
+        - table 类型: { type: "table", blocks: [{ type: "table_body", lines: [...] }] }
+        """
         elem_type = element.get('type', '')
         elem_type = element.get('type', '')
         bbox = element.get('bbox', [0, 0, 0, 0])
         bbox = element.get('bbox', [0, 0, 0, 0])
         content = element.get('content', {})
         content = element.get('content', {})
@@ -209,11 +232,12 @@ class OutputFormatterV2:
                     }]
                     }]
                 }]
                 }]
         
         
-        # 表格类型
+        # 表格类型 - 嵌套结构
         elif elem_type in ['table', 'table_body']:
         elif elem_type in ['table', 'table_body']:
             table_html = content.get('html', '')
             table_html = content.get('html', '')
             cells = content.get('cells', [])
             cells = content.get('cells', [])
             
             
+            block['type'] = 'table'
             block['blocks'] = [{
             block['blocks'] = [{
                 'type': 'table_body',
                 'type': 'table_body',
                 'bbox': bbox,
                 'bbox': bbox,
@@ -224,7 +248,26 @@ class OutputFormatterV2:
                         'bbox': bbox,
                         'bbox': bbox,
                         'type': 'table',
                         'type': 'table',
                         'html': table_html,
                         'html': table_html,
-                        'cells': cells  # 增强:包含单元格坐标
+                        'cells': cells
+                    }]
+                }]
+            }]
+        
+        # 图片类型 - 嵌套结构
+        # 注意:MinerU vlm_union_make 期望字段名是 'image_path',不是 'img_path'
+        elif elem_type in ['image', 'image_body', 'figure']:
+            block['type'] = 'image'
+            block['blocks'] = [{
+                'type': 'image_body',
+                'bbox': bbox,
+                'angle': element.get('angle', 0),
+                'lines': [{
+                    'bbox': bbox,
+                    'spans': [{
+                        'bbox': bbox,
+                        'type': 'image',
+                        'image_path': content.get('image_path', ''),  # MinerU 期望 'image_path'
+                        'description': content.get('description', '')
                     }]
                     }]
                 }]
                 }]
             }]
             }]
@@ -241,17 +284,273 @@ class OutputFormatterV2:
                 }]
                 }]
             }]
             }]
         
         
+        # 表格/图片附属文本
+        elif elem_type in ['table_caption', 'table_footnote', 'image_caption', 'image_footnote']:
+            text = content.get('text', '') if isinstance(content, dict) else str(content)
+            if text:
+                block['lines'] = [{
+                    'bbox': bbox,
+                    'spans': [{
+                        'bbox': bbox,
+                        'type': 'text',
+                        'content': text
+                    }]
+                }]
+        
+        # 丢弃类型
+        elif elem_type in ['abandon', 'discarded']:
+            block['type'] = 'abandon'
+        
         return block
         return block
     
     
+    # ==================== mineru_vllm_results_cell_bbox 格式 ====================
+    
+    def _save_page_jsons(
+        self,
+        results: Dict[str, Any],
+        output_dir: Path,
+        doc_name: str
+    ) -> List[str]:
+        """
+        保存每页独立的 JSON(mineru_vllm_results_cell_bbox 格式)
+        
+        格式示例:
+        [
+            {
+                "type": "table",
+                "img_path": "images/xxx.jpg",
+                "table_caption": [],
+                "table_footnote": [],
+                "table_body": "<table>...</table>",
+                "bbox": [x1, y1, x2, y2],
+                "page_idx": 0,
+                "table_cells": [
+                    {
+                        "type": "table_cell",
+                        "text": "单元格内容",
+                        "matched_text": "OCR匹配文本",
+                        "bbox": [x1, y1, x2, y2],
+                        "row": 1,
+                        "col": 1,
+                        "score": 100.0,
+                        "paddle_bbox_indices": [0, 1]
+                    }
+                ],
+                "image_rotation_angle": 0,
+                "skew_angle": 0.0
+            }
+        ]
+        """
+        saved_paths = []
+        
+        for page in results.get('pages', []):
+            page_idx = page.get('page_idx', 0)
+            page_name = f"{doc_name}_page_{page_idx + 1:03d}"
+            
+            # 转换为 mineru_vllm_results_cell_bbox 格式
+            page_elements = []
+            for element in page.get('elements', []):
+                converted = self._element_to_cell_bbox_format(element, page_idx)
+                if converted:
+                    page_elements.append(converted)
+            
+            # 保存 JSON
+            json_path = output_dir / f"{page_name}.json"
+            with open(json_path, 'w', encoding='utf-8') as f:
+                json.dump(page_elements, f, ensure_ascii=False, indent=2)
+            
+            saved_paths.append(str(json_path))
+            logger.debug(f"📄 Page JSON saved: {json_path}")
+        
+        if saved_paths:
+            logger.info(f"📄 {len(saved_paths)} page JSONs saved")
+        
+        return saved_paths
+    
+    def _element_to_cell_bbox_format(
+        self,
+        element: Dict[str, Any],
+        page_idx: int
+    ) -> Optional[Dict[str, Any]]:
+        """
+        将元素转换为 mineru_vllm_results_cell_bbox 格式
+        """
+        elem_type = element.get('type', '')
+        bbox = element.get('bbox', [0, 0, 0, 0])
+        content = element.get('content', {})
+        
+        # 确保 bbox 是整数列表
+        bbox = [int(x) for x in bbox[:4]] if bbox else [0, 0, 0, 0]
+        
+        result = {
+            'bbox': bbox,
+            'page_idx': page_idx
+        }
+        
+        # 文本类型
+        if elem_type in ['text', 'title', 'ref_text', 'ocr_text']:
+            text = content.get('text', '') if isinstance(content, dict) else str(content)
+            result['type'] = 'text' if elem_type != 'title' else 'title'
+            result['text'] = text
+            if elem_type == 'title':
+                result['text_level'] = element.get('level', 1)
+        
+        # 表格类型
+        elif elem_type in ['table', 'table_body']:
+            result['type'] = 'table'
+            result['img_path'] = content.get('table_image_path', '')
+            result['table_caption'] = self._ensure_list(content.get('table_caption', []))
+            result['table_footnote'] = self._ensure_list(content.get('table_footnote', []))
+            result['table_body'] = content.get('html', '')
+            
+            # 关键:table_cells 数组
+            cells = content.get('cells', [])
+            if cells:
+                result['table_cells'] = self._format_table_cells(cells)
+            
+            # 旋转和倾斜信息
+            if 'table_angle' in content:
+                result['image_rotation_angle'] = float(content['table_angle'])
+            if 'skew_angle' in content:
+                result['skew_angle'] = float(content['skew_angle'])
+        
+        # 图片类型
+        elif elem_type in ['image', 'image_body', 'figure']:
+            result['type'] = 'image'
+            # page JSON 需要完整的相对路径
+            image_filename = content.get('image_path', '')
+            result['img_path'] = f"images/{image_filename}" if image_filename else ''
+            result['image_caption'] = self._ensure_list(content.get('caption', []))
+            result['image_footnote'] = self._ensure_list(content.get('footnote', []))
+        
+        # 公式类型
+        elif elem_type in ['interline_equation', 'inline_equation', 'equation']:
+            result['type'] = 'equation'
+            result['text'] = content.get('latex', '') if isinstance(content, dict) else ''
+            result['text_format'] = 'latex'
+        
+        # 列表类型
+        elif elem_type == 'list':
+            result['type'] = 'list'
+            result['sub_type'] = 'text'
+            result['list_items'] = content.get('list_items', []) if isinstance(content, dict) else []
+        
+        # 页眉页脚
+        elif elem_type in ['header', 'footer']:
+            result['type'] = elem_type
+            result['text'] = content.get('text', '') if isinstance(content, dict) else str(content)
+        
+        else:
+            return None
+        
+        return result
+    
+    def _format_table_cells(self, cells: List[Dict]) -> List[Dict[str, Any]]:
+        """
+        格式化表格单元格为 mineru_vllm_results_cell_bbox 格式
+        
+        输出格式:
+        {
+            "type": "table_cell",
+            "text": "单元格内容",
+            "matched_text": "OCR匹配文本",
+            "bbox": [x1, y1, x2, y2],
+            "row": 1,
+            "col": 1,
+            "score": 100.0,
+            "paddle_bbox_indices": [0, 1]
+        }
+        """
+        formatted_cells = []
+        
+        for cell in cells:
+            formatted_cell = {
+                'type': 'table_cell',
+                'text': cell.get('text', ''),
+                'matched_text': cell.get('matched_text', cell.get('text', '')),
+                'bbox': [float(x) for x in cell.get('bbox', [0, 0, 0, 0])[:4]],
+                'row': cell.get('row', 0),
+                'col': cell.get('col', 0),
+                'score': float(cell.get('score', 100.0)),
+                'paddle_bbox_indices': cell.get('paddle_bbox_indices', 
+                                                cell.get('paddle_indices', []))
+            }
+            formatted_cells.append(formatted_cell)
+        
+        return formatted_cells
+    
+    def _ensure_list(self, value) -> List:
+        """确保值是列表"""
+        if value is None:
+            return []
+        if isinstance(value, str):
+            return [value] if value else []
+        if isinstance(value, list):
+            return value
+        return [str(value)]
+    
+    # ==================== Markdown 生成 ====================
+    
     def _save_markdown(
     def _save_markdown(
         self,
         self,
         results: Dict[str, Any],
         results: Dict[str, Any],
+        middle_json: Dict[str, Any],
         output_dir: Path,
         output_dir: Path,
         doc_name: str
         doc_name: str
     ) -> Path:
     ) -> Path:
-        """保存 Markdown 文件"""
+        """
+        保存 Markdown 文件
+        
+        优先使用 MinerU union_make,降级使用自定义实现
+        """
         md_path = output_dir / f"{doc_name}.md"
         md_path = output_dir / f"{doc_name}.md"
         
         
+        if MINERU_AVAILABLE:
+            try:
+                # image_path 只保存文件名,vlm_union_make 会添加此前缀
+                img_bucket_path = "images"
+                markdown_content = vlm_union_make(
+                    middle_json['pdf_info'],
+                    MakeMode.MM_MD,
+                    img_bucket_path
+                )
+                
+                if markdown_content:
+                    if isinstance(markdown_content, list):
+                        markdown_content = '\n\n'.join(markdown_content)
+                    
+                    header = self._generate_markdown_header(results)
+                    markdown_content = header + str(markdown_content)
+                    
+                    with open(md_path, 'w', encoding='utf-8') as f:
+                        f.write(markdown_content)
+                    
+                    logger.info(f"📝 Markdown saved (MinerU format): {md_path}")
+                    return md_path
+                    
+            except Exception as e:
+                logger.warning(f"MinerU union_make failed: {e}, falling back to custom implementation")
+        
+        # 降级方案
+        markdown_content = self._generate_markdown_fallback(results)
+        with open(md_path, 'w', encoding='utf-8') as f:
+            f.write(markdown_content)
+        
+        logger.info(f"📝 Markdown saved (fallback): {md_path}")
+        return md_path
+    
+    def _generate_markdown_header(self, results: Dict[str, Any]) -> str:
+        """生成 Markdown 文件头"""
+        return f"""---
+scene: {results.get('scene', 'unknown')}
+document: {results.get('document_path', '')}
+pages: {len(results.get('pages', []))}
+---
+
+"""
+    
+    def _generate_markdown_fallback(self, results: Dict[str, Any]) -> str:
+        """降级方案:自定义 Markdown 生成"""
         md_lines = [
         md_lines = [
             f"---",
             f"---",
             f"scene: {results.get('scene', 'unknown')}",
             f"scene: {results.get('scene', 'unknown')}",
@@ -263,7 +562,6 @@ class OutputFormatterV2:
         
         
         for page in results.get('pages', []):
         for page in results.get('pages', []):
             page_idx = page.get('page_idx', 0)
             page_idx = page.get('page_idx', 0)
-            md_lines.append(f"\n## Page {page_idx + 1}\n")
             
             
             for element in page.get('elements', []):
             for element in page.get('elements', []):
                 elem_type = element.get('type', '')
                 elem_type = element.get('type', '')
@@ -271,83 +569,92 @@ class OutputFormatterV2:
                 bbox = element.get('bbox', [])
                 bbox = element.get('bbox', [])
                 
                 
                 # 添加 bbox 注释
                 # 添加 bbox 注释
-                bbox_comment = f"<!-- bbox: {bbox} -->"
+                if bbox:
+                    md_lines.append(f"<!-- bbox: {bbox} -->")
                 
                 
                 if elem_type == 'title':
                 if elem_type == 'title':
                     text = content.get('text', '') if isinstance(content, dict) else str(content)
                     text = content.get('text', '') if isinstance(content, dict) else str(content)
                     level = element.get('level', 1)
                     level = element.get('level', 1)
                     md_lines.append(f"{'#' * min(level, 6)} {text}")
                     md_lines.append(f"{'#' * min(level, 6)} {text}")
-                    md_lines.append(bbox_comment)
                     md_lines.append("")
                     md_lines.append("")
                 
                 
                 elif elem_type in ['text', 'ocr_text', 'ref_text']:
                 elif elem_type in ['text', 'ocr_text', 'ref_text']:
                     text = content.get('text', '') if isinstance(content, dict) else str(content)
                     text = content.get('text', '') if isinstance(content, dict) else str(content)
                     if text:
                     if text:
                         md_lines.append(text)
                         md_lines.append(text)
-                        md_lines.append(bbox_comment)
                         md_lines.append("")
                         md_lines.append("")
                 
                 
                 elif elem_type in ['table', 'table_body']:
                 elif elem_type in ['table', 'table_body']:
+                    # 表格标题
+                    for caption in self._ensure_list(content.get('table_caption', [])):
+                        md_lines.append(f"**{caption}**")
+                    
                     html = content.get('html', '')
                     html = content.get('html', '')
                     if html:
                     if html:
-                        # 转换 HTML 表格为 Markdown
-                        md_table = self._html_to_markdown_table(html)
-                        md_lines.append(md_table)
-                        md_lines.append(bbox_comment)
-                        
-                        # 添加单元格坐标信息
-                        cells = content.get('cells', [])
-                        if cells:
-                            md_lines.append("")
-                            md_lines.append("<details>")
-                            md_lines.append("<summary>单元格坐标信息</summary>")
-                            md_lines.append("")
-                            md_lines.append("```json")
-                            md_lines.append(json.dumps(cells, ensure_ascii=False, indent=2))
-                            md_lines.append("```")
-                            md_lines.append("</details>")
+                        md_lines.append(f"\n{html}\n")
+                    md_lines.append("")
+                
+                elif elem_type in ['image', 'image_body', 'figure']:
+                    img_filename = content.get('image_path', '')
+                    if img_filename:
+                        md_lines.append(f"![](images/{img_filename})")
                         md_lines.append("")
                         md_lines.append("")
                 
                 
                 elif elem_type in ['interline_equation', 'inline_equation', 'equation']:
                 elif elem_type in ['interline_equation', 'inline_equation', 'equation']:
                     latex = content.get('latex', '')
                     latex = content.get('latex', '')
                     if latex:
                     if latex:
                         md_lines.append(f"$$\n{latex}\n$$")
                         md_lines.append(f"$$\n{latex}\n$$")
-                        md_lines.append(bbox_comment)
                         md_lines.append("")
                         md_lines.append("")
         
         
-        with open(md_path, 'w', encoding='utf-8') as f:
-            f.write('\n'.join(md_lines))
-        
-        logger.info(f"📝 Markdown saved: {md_path}")
-        return md_path
+        return '\n'.join(md_lines)
     
     
-    def _html_to_markdown_table(self, html: str) -> str:
-        """将 HTML 表格转换为 Markdown 格式"""
-        try:
-            from bs4 import BeautifulSoup
-            soup = BeautifulSoup(html, 'html.parser')
-            
-            rows = soup.find_all('tr')
-            if not rows:
-                return html
-            
-            md_rows = []
-            for row_idx, row in enumerate(rows):
-                cells = row.find_all(['td', 'th'])
-                cell_texts = [cell.get_text(strip=True) for cell in cells]
-                md_row = '| ' + ' | '.join(cell_texts) + ' |'
-                md_rows.append(md_row)
-                
-                # 添加表头分隔符
-                if row_idx == 0:
-                    separator = '| ' + ' | '.join(['---'] * len(cells)) + ' |'
-                    md_rows.append(separator)
-            
-            return '\n'.join(md_rows)
+    # ==================== 图片保存 ====================
+    
+    def _save_image_elements(
+        self,
+        results: Dict[str, Any],
+        images_dir: Path,
+        doc_name: str
+    ) -> List[str]:
+        """保存图片元素"""
+        saved_paths = []
+        image_count = 0
+        
+        for page in results.get('pages', []):
+            page_idx = page.get('page_idx', 0)
             
             
-        except Exception as e:
-            logger.warning(f"HTML to Markdown conversion failed: {e}")
-            return html
+            for element in page.get('elements', []):
+                if element.get('type') in ['image', 'image_body', 'figure']:
+                    content = element.get('content', {})
+                    image_data = content.get('image_data')
+                    
+                    if image_data is not None:
+                        image_count += 1
+                        image_filename = f"{doc_name}_page_{page_idx + 1}_image_{image_count}.png"
+                        image_path = images_dir / image_filename
+                        
+                        try:
+                            if isinstance(image_data, np.ndarray):
+                                cv2.imwrite(str(image_path), image_data)
+                            else:
+                                Image.fromarray(image_data).save(image_path)
+                            
+                            # 更新路径(只保存文件名,不含 images/ 前缀)
+                            # vlm_union_make 会自动添加 img_bucket_path 前缀
+                            content['image_path'] = image_filename
+                            content.pop('image_data', None)
+                            
+                            saved_paths.append(str(image_path))
+                            logger.debug(f"🖼️ Image saved: {image_path}")
+                        except Exception as e:
+                            logger.warning(f"Failed to save image: {e}")
+        
+        if image_count > 0:
+            logger.info(f"🖼️ {image_count} images saved to: {images_dir}")
+        
+        return saved_paths
+    
+    # ==================== 表格 HTML ====================
     
     
     def _save_table_htmls(
     def _save_table_htmls(
         self,
         self,
@@ -355,7 +662,7 @@ class OutputFormatterV2:
         output_dir: Path,
         output_dir: Path,
         doc_name: str
         doc_name: str
     ) -> Path:
     ) -> Path:
-        """保存表格 HTML 文件(带坐标信息)"""
+        """保存表格 HTML 文件"""
         tables_dir = output_dir / 'tables'
         tables_dir = output_dir / 'tables'
         tables_dir.mkdir(exist_ok=True)
         tables_dir.mkdir(exist_ok=True)
         
         
@@ -372,7 +679,6 @@ class OutputFormatterV2:
                     cells = content.get('cells', [])
                     cells = content.get('cells', [])
                     
                     
                     if html:
                     if html:
-                        # 生成带样式的 HTML
                         full_html = self._generate_table_html_with_styles(
                         full_html = self._generate_table_html_with_styles(
                             html, cells, doc_name, page_idx, table_count
                             html, cells, doc_name, page_idx, table_count
                         )
                         )
@@ -394,7 +700,7 @@ class OutputFormatterV2:
         page_idx: int,
         page_idx: int,
         table_idx: int
         table_idx: int
     ) -> str:
     ) -> str:
-        """生成带样式和坐标信息的完整 HTML"""
+        """生成带样式的完整 HTML"""
         cells_json = json.dumps(cells, ensure_ascii=False, indent=2) if cells else "[]"
         cells_json = json.dumps(cells, ensure_ascii=False, indent=2) if cells else "[]"
         
         
         return f"""<!DOCTYPE html>
         return f"""<!DOCTYPE html>
@@ -499,6 +805,8 @@ class OutputFormatterV2:
 </body>
 </body>
 </html>"""
 </html>"""
     
     
+    # ==================== Debug 可视化 ====================
+    
     def _save_layout_images(
     def _save_layout_images(
         self,
         self,
         results: Dict[str, Any],
         results: Dict[str, Any],
@@ -512,12 +820,14 @@ class OutputFormatterV2:
         
         
         for page in results.get('pages', []):
         for page in results.get('pages', []):
             page_idx = page.get('page_idx', 0)
             page_idx = page.get('page_idx', 0)
-            processed_image = page.get('processed_image')
+            processed_image = page.get('original_image')
+            if processed_image is None:
+                processed_image = page.get('processed_image')
             
             
             if processed_image is None:
             if processed_image is None:
+                logger.warning(f"Page {page_idx}: No image data found for layout visualization")
                 continue
                 continue
             
             
-            # 转换为 PIL Image
             if isinstance(processed_image, np.ndarray):
             if isinstance(processed_image, np.ndarray):
                 image = Image.fromarray(processed_image).convert('RGB')
                 image = Image.fromarray(processed_image).convert('RGB')
             elif isinstance(processed_image, Image.Image):
             elif isinstance(processed_image, Image.Image):
@@ -525,13 +835,9 @@ class OutputFormatterV2:
             else:
             else:
                 continue
                 continue
             
             
-            # 创建绘图对象
             draw = ImageDraw.Draw(image, 'RGBA')
             draw = ImageDraw.Draw(image, 'RGBA')
-            
-            # 加载字体
             font = self._get_font(14)
             font = self._get_font(14)
             
             
-            # 绘制每个元素
             for idx, element in enumerate(page.get('elements', []), 1):
             for idx, element in enumerate(page.get('elements', []), 1):
                 elem_type = element.get('type', '')
                 elem_type = element.get('type', '')
                 bbox = element.get('bbox', [0, 0, 0, 0])
                 bbox = element.get('bbox', [0, 0, 0, 0])
@@ -542,31 +848,30 @@ class OutputFormatterV2:
                 x0, y0, x1, y1 = map(int, bbox[:4])
                 x0, y0, x1, y1 = map(int, bbox[:4])
                 color = self.COLOR_MAP.get(elem_type, (255, 0, 0))
                 color = self.COLOR_MAP.get(elem_type, (255, 0, 0))
                 
                 
-                # 绘制半透明填充
+                # 半透明填充
                 overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
                 overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
                 overlay_draw = ImageDraw.Draw(overlay)
                 overlay_draw = ImageDraw.Draw(overlay)
                 overlay_draw.rectangle([x0, y0, x1, y1], fill=(*color, 50))
                 overlay_draw.rectangle([x0, y0, x1, y1], fill=(*color, 50))
                 image = Image.alpha_composite(image.convert('RGBA'), overlay).convert('RGB')
                 image = Image.alpha_composite(image.convert('RGBA'), overlay).convert('RGB')
                 draw = ImageDraw.Draw(image)
                 draw = ImageDraw.Draw(image)
                 
                 
-                # 绘制边框
+                # 边框
                 draw.rectangle([x0, y0, x1, y1], outline=color, width=2)
                 draw.rectangle([x0, y0, x1, y1], outline=color, width=2)
                 
                 
-                # 标注类型
+                # 类型标签
                 if draw_type_label:
                 if draw_type_label:
                     label = elem_type.replace('_', ' ').title()
                     label = elem_type.replace('_', ' ').title()
                     bbox_label = draw.textbbox((x0 + 2, y0 + 2), label, font=font)
                     bbox_label = draw.textbbox((x0 + 2, y0 + 2), label, font=font)
                     draw.rectangle(bbox_label, fill=color)
                     draw.rectangle(bbox_label, fill=color)
                     draw.text((x0 + 2, y0 + 2), label, fill='white', font=font)
                     draw.text((x0 + 2, y0 + 2), label, fill='white', font=font)
                 
                 
-                # 标注序号
+                # 序号
                 if draw_bbox_number:
                 if draw_bbox_number:
                     number_text = str(idx)
                     number_text = str(idx)
                     bbox_number = draw.textbbox((x1 - 25, y0 + 2), number_text, font=font)
                     bbox_number = draw.textbbox((x1 - 25, y0 + 2), number_text, font=font)
                     draw.rectangle(bbox_number, fill=(255, 0, 0))
                     draw.rectangle(bbox_number, fill=(255, 0, 0))
                     draw.text((x1 - 25, y0 + 2), number_text, fill='white', font=font)
                     draw.text((x1 - 25, y0 + 2), number_text, fill='white', font=font)
             
             
-            # 保存图片
             layout_path = output_dir / f"{doc_name}_page_{page_idx + 1}_layout.png"
             layout_path = output_dir / f"{doc_name}_page_{page_idx + 1}_layout.png"
             image.save(layout_path)
             image.save(layout_path)
             layout_paths.append(str(layout_path))
             layout_paths.append(str(layout_path))
@@ -580,17 +885,19 @@ class OutputFormatterV2:
         output_dir: Path,
         output_dir: Path,
         doc_name: str
         doc_name: str
     ) -> List[str]:
     ) -> List[str]:
-        """保存 OCR 可视化图片(显示文本框和单元格坐标)"""
+        """保存 OCR 可视化图片"""
         ocr_paths = []
         ocr_paths = []
         
         
         for page in results.get('pages', []):
         for page in results.get('pages', []):
             page_idx = page.get('page_idx', 0)
             page_idx = page.get('page_idx', 0)
-            processed_image = page.get('processed_image')
+            processed_image = page.get('original_image')
+            if processed_image is None:
+                processed_image = page.get('processed_image')
             
             
             if processed_image is None:
             if processed_image is None:
+                logger.warning(f"Page {page_idx}: No image data found for OCR visualization")
                 continue
                 continue
             
             
-            # 转换为 PIL Image
             if isinstance(processed_image, np.ndarray):
             if isinstance(processed_image, np.ndarray):
                 image = Image.fromarray(processed_image).convert('RGB')
                 image = Image.fromarray(processed_image).convert('RGB')
             elif isinstance(processed_image, Image.Image):
             elif isinstance(processed_image, Image.Image):
@@ -601,18 +908,17 @@ class OutputFormatterV2:
             draw = ImageDraw.Draw(image)
             draw = ImageDraw.Draw(image)
             font = self._get_font(10)
             font = self._get_font(10)
             
             
-            # 遍历所有元素
             for element in page.get('elements', []):
             for element in page.get('elements', []):
                 content = element.get('content', {})
                 content = element.get('content', {})
                 
                 
-                # 绘制 OCR 文本框
+                # OCR 文本框
                 ocr_details = content.get('ocr_details', [])
                 ocr_details = content.get('ocr_details', [])
                 for ocr_item in ocr_details:
                 for ocr_item in ocr_details:
                     ocr_bbox = ocr_item.get('bbox', [])
                     ocr_bbox = ocr_item.get('bbox', [])
                     if ocr_bbox:
                     if ocr_bbox:
                         self._draw_polygon(draw, ocr_bbox, self.OCR_BOX_COLOR, width=1)
                         self._draw_polygon(draw, ocr_bbox, self.OCR_BOX_COLOR, width=1)
                 
                 
-                # 绘制表格单元格坐标
+                # 表格单元格
                 cells = content.get('cells', [])
                 cells = content.get('cells', [])
                 for cell in cells:
                 for cell in cells:
                     cell_bbox = cell.get('bbox', [])
                     cell_bbox = cell.get('bbox', [])
@@ -620,19 +926,17 @@ class OutputFormatterV2:
                         x0, y0, x1, y1 = map(int, cell_bbox[:4])
                         x0, y0, x1, y1 = map(int, cell_bbox[:4])
                         draw.rectangle([x0, y0, x1, y1], outline=self.CELL_BOX_COLOR, width=2)
                         draw.rectangle([x0, y0, x1, y1], outline=self.CELL_BOX_COLOR, width=2)
                         
                         
-                        # 标注单元格文本
                         cell_text = cell.get('text', '')[:10]
                         cell_text = cell.get('text', '')[:10]
                         if cell_text:
                         if cell_text:
                             draw.text((x0 + 2, y0 + 2), cell_text, fill=self.CELL_BOX_COLOR, font=font)
                             draw.text((x0 + 2, y0 + 2), cell_text, fill=self.CELL_BOX_COLOR, font=font)
                 
                 
-                # 绘制表格 OCR 框
+                # OCR 框
                 ocr_boxes = content.get('ocr_boxes', [])
                 ocr_boxes = content.get('ocr_boxes', [])
                 for ocr_box in ocr_boxes:
                 for ocr_box in ocr_boxes:
                     bbox = ocr_box.get('bbox', [])
                     bbox = ocr_box.get('bbox', [])
                     if bbox:
                     if bbox:
                         self._draw_polygon(draw, bbox, self.OCR_BOX_COLOR, width=1)
                         self._draw_polygon(draw, bbox, self.OCR_BOX_COLOR, width=1)
             
             
-            # 保存图片
             ocr_path = output_dir / f"{doc_name}_page_{page_idx + 1}_ocr.png"
             ocr_path = output_dir / f"{doc_name}_page_{page_idx + 1}_ocr.png"
             image.save(ocr_path)
             image.save(ocr_path)
             ocr_paths.append(str(ocr_path))
             ocr_paths.append(str(ocr_path))
@@ -648,12 +952,10 @@ class OutputFormatterV2:
         width: int = 1
         width: int = 1
     ):
     ):
         """绘制多边形或矩形"""
         """绘制多边形或矩形"""
-        # 4点坐标格式 [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
         if isinstance(bbox[0], (list, tuple)):
         if isinstance(bbox[0], (list, tuple)):
             points = [(int(p[0]), int(p[1])) for p in bbox]
             points = [(int(p[0]), int(p[1])) for p in bbox]
-            points.append(points[0])  # 闭合多边形
+            points.append(points[0])
             draw.line(points, fill=color, width=width)
             draw.line(points, fill=color, width=width)
-        # 4值坐标格式 [x1, y1, x2, y2]
         elif len(bbox) >= 4:
         elif len(bbox) >= 4:
             x0, y0, x1, y1 = map(int, bbox[:4])
             x0, y0, x1, y1 = map(int, bbox[:4])
             draw.rectangle([x0, y0, x1, y1], outline=color, width=width)
             draw.rectangle([x0, y0, x1, y1], outline=color, width=width)
@@ -675,6 +977,38 @@ class OutputFormatterV2:
         return ImageFont.load_default()
         return ImageFont.load_default()
 
 
 
 
+# ==================== 便捷函数 ====================
+
+def save_mineru_format(
+    results: Dict[str, Any],
+    output_dir: str,
+    output_config: Dict[str, Any] = None
+) -> Dict[str, Any]:
+    """
+    便捷函数:保存为 MinerU 格式
+    
+    Args:
+        results: pipeline 处理结果
+        output_dir: 输出目录
+        output_config: 输出配置
+        
+    Returns:
+        输出文件路径字典
+    """
+    if output_config is None:
+        output_config = {
+            'save_json': True,
+            'save_page_json': True,
+            'save_markdown': True,
+            'save_html': True,
+            'save_layout_image': False,
+            'save_ocr_image': False,
+        }
+    
+    formatter = OutputFormatterV2(output_dir)
+    return formatter.save_results(results, output_config)
+
+
 if __name__ == "__main__":
 if __name__ == "__main__":
     # 测试代码
     # 测试代码
     sample_results = {
     sample_results = {
@@ -707,11 +1041,12 @@ if __name__ == "__main__":
         ]
         ]
     }
     }
     
     
-    formatter = OutputFormatterV2("./test_output_v2")
-    output_files = formatter.save_results(
+    output_files = save_mineru_format(
         sample_results,
         sample_results,
+        "./test_output_v2",
         {
         {
             "save_json": True,
             "save_json": True,
+            "save_page_json": True,
             "save_markdown": True,
             "save_markdown": True,
             "save_html": True,
             "save_html": True,
             "save_layout_image": False,
             "save_layout_image": False,