Преглед на файлове

refactor: 移除未使用的保存结构函数,优化结构数据处理逻辑

zhch158_admin преди 1 ден
родител
ревизия
3716bf591e

+ 0 - 2
table_line_generator/editor/__init__.py

@@ -61,7 +61,6 @@ from .adjustments import create_adjustment_section
 # 配置加载
 from .config_loader import (
     load_structure_from_config,
-    save_structure_to_config,
     load_table_editor_config,
     parse_table_editor_cli_args,
     build_data_source_catalog,
@@ -117,7 +116,6 @@ __all__ = [
     
     # 配置加载
     'load_structure_from_config',
-    'save_structure_to_config',
     'load_table_editor_config',
     'parse_table_editor_cli_args',
     'build_data_source_catalog',

+ 15 - 7
table_line_generator/editor/adjustments.py

@@ -47,7 +47,9 @@ def create_adjustment_section(structure):
             if new_y != horizontal_lines[line_index]:
                 save_state_for_undo(structure)
                 structure['horizontal_lines'][line_index] = new_y
-                structure.setdefault('modified_h_lines', set()).add(line_index)
+                structure.setdefault('modified_h_lines', [])
+                if line_index not in structure['modified_h_lines']:
+                    structure['modified_h_lines'].append(line_index)
                 _update_row_intervals(structure)
                 clear_table_image_cache()
                 adjusted = True
@@ -65,7 +67,9 @@ def create_adjustment_section(structure):
             structure['horizontal_lines'].append(new_h_y)
             structure['horizontal_lines'].sort()
             idx = structure['horizontal_lines'].index(new_h_y)
-            structure.setdefault('modified_h_lines', set()).add(idx)
+            structure.setdefault('modified_h_lines', [])
+            if idx not in structure['modified_h_lines']:
+                structure['modified_h_lines'].append(idx)
             _update_row_intervals(structure)
             clear_table_image_cache()
             adjusted = True
@@ -81,7 +85,7 @@ def create_adjustment_section(structure):
             save_state_for_undo(structure)
             for idx in sorted(to_delete, reverse=True):
                 del structure['horizontal_lines'][idx]
-            structure['modified_h_lines'] = set()
+            structure['modified_h_lines'] = []
             _update_row_intervals(structure)
             clear_table_image_cache()
             adjusted = True
@@ -105,7 +109,9 @@ def create_adjustment_section(structure):
             if new_x != vertical_lines[line_index]:
                 save_state_for_undo(structure)
                 structure['vertical_lines'][line_index] = new_x
-                structure.setdefault('modified_v_lines', set()).add(line_index)
+                structure.setdefault('modified_v_lines', [])
+                if line_index not in structure['modified_v_lines']:
+                    structure['modified_v_lines'].append(line_index)
                 _update_column_intervals(structure)
                 clear_table_image_cache()
                 adjusted = True
@@ -119,11 +125,13 @@ def create_adjustment_section(structure):
             key="add_v_value"
         )
         if st.button("➕ 确认添加竖线"):
-            save_state_for_undo(structure)
             structure['vertical_lines'].append(new_v_x)
             structure['vertical_lines'].sort()
             idx = structure['vertical_lines'].index(new_v_x)
-            structure.setdefault('modified_v_lines', set()).add(idx)
+            structure.setdefault('modified_v_lines', [])
+            if idx not in structure['modified_v_lines']:
+                structure['modified_v_lines'].append(idx)
+            _update_column_intervals(structure)
             _update_column_intervals(structure)
             clear_table_image_cache()
             adjusted = True
@@ -139,7 +147,7 @@ def create_adjustment_section(structure):
             save_state_for_undo(structure)
             for idx in sorted(to_delete, reverse=True):
                 del structure['vertical_lines'][idx]
-            structure['modified_v_lines'] = set()
+            structure['modified_v_lines'] = []
             _update_column_intervals(structure)
             clear_table_image_cache()
             adjusted = True

+ 42 - 49
table_line_generator/editor/batch_template_controls.py

@@ -67,7 +67,15 @@ def create_batch_template_section(current_line_width: int, current_line_color: s
         st.error("❌ 未找到当前页的结构文件,请先保存")
         st.info(f"期望文件: {current_structure_file}")
         return
-    
+
+    # 🔑 检测当前结构文件的模式
+    try:
+        with open(current_structure_file, 'r', encoding='utf-8') as f:
+            template_structure = json.load(f)
+        template_mode = template_structure.get('mode', 'fixed')
+    except:
+        template_mode = 'fixed'    
+
     unlabeled_pages = []
     for entry in catalog:
         if entry["index"] == current_page:
@@ -113,17 +121,19 @@ def create_batch_template_section(current_line_width: int, current_line_color: s
             unlabeled_pages,
             output_dir,
             structure_suffix,
+            template_mode,  # 🔑 默认使用固定行高模式
             current_line_width,
             line_color
         )
 
 
 def _apply_template_batch(
-    template_file: Path,  # 🔑 改为直接传入模板文件路径
+    template_file: Path,
     template_entry: Dict,
     target_entries: List[Dict],
     output_dir: Path,
     structure_suffix: str,
+    template_mode: str,
     line_width: int,
     line_color: tuple
 ):
@@ -136,6 +146,7 @@ def _apply_template_batch(
         target_entries: 目标页面列表
         output_dir: 输出目录
         structure_suffix: 结构文件后缀
+        template_mode: 模板模式 ('fixed' / 'hybrid')
         line_width: 线条宽度
         line_color: 线条颜色 (r, g, b)
     """
@@ -144,6 +155,7 @@ def _apply_template_batch(
         applier = TableTemplateApplier(str(template_file))
         
         st.info(f"📋 使用模板: {template_file.name}")
+        st.info(f"🔧 模式: {'混合模式 (MinerU)' if template_mode == 'hybrid' else '固定行高模式'}")
         
         # 进度条
         progress_bar = st.progress(0)
@@ -160,59 +172,39 @@ def _apply_template_batch(
             status_text.text(f"处理中: {entry['display']} ({idx + 1}/{len(target_entries)})")
             
             try:
-                # 加载 OCR 数据
-                with open(entry["json"], "r", encoding="utf-8") as fp:
-                    raw = json.load(fp)
+                # ✅ 直接调用统一的处理函数
+                from table_template_applier import apply_template_to_single_file
                 
-                # 解析 OCR 数据
-                if 'parsing_res_list' in raw and 'overall_ocr_res' in raw:
-                    table_bbox, ocr_data = TableLineGenerator.parse_ppstructure_result(raw)
-                else:
-                    raise ValueError("不支持的 OCR 格式")
+                # 确定是否使用混合模式
+                use_hybrid = (template_mode == 'hybrid')
+                
+                success = apply_template_to_single_file(
+                    applier=applier,
+                    image_file=entry["image"],
+                    json_file=entry["json"],
+                    output_dir=output_dir,
+                    structure_suffix=structure_suffix,
+                    use_hybrid_mode=use_hybrid,
+                    line_width=line_width,
+                    line_color=line_color
+                )
                 
-                # 加载图片
-                if entry["image"] and entry["image"].exists():
-                    image = Image.open(entry["image"])
+                if success:
+                    success_count += 1
+                    base_name = entry["json"].stem
+                    results.append({
+                        'page': entry['index'],
+                        'status': 'success',
+                        'image': str(output_dir / f"{base_name}.png"),
+                        'structure': str(output_dir / f"{base_name}{structure_suffix}")
+                    })
                 else:
-                    st.warning(f"⚠️ 跳过 {entry['display']}: 未找到图片")
                     failed_count += 1
                     results.append({
                         'page': entry['index'],
-                        'status': 'skipped',
-                        'reason': 'no_image'
+                        'status': 'error',
+                        'error': 'Processing failed'
                     })
-                    continue
-                
-                # 应用模板生成图片
-                img_with_lines = applier.apply_to_image(
-                    image,
-                    ocr_data,
-                    line_width=line_width,
-                    line_color=line_color
-                )
-                
-                # 生成结构配置
-                structure = applier.generate_structure_for_image(ocr_data)
-                
-                # 保存图片
-                base_name = entry["json"].stem
-                image_suffix = st.session_state.current_output_config.get("image_suffix", ".png")
-                output_image_path = output_dir / f"{base_name}{image_suffix}"
-                img_with_lines.save(output_image_path)
-                
-                # 🔑 保存结构(确保 set 转为 list)
-                structure_path = output_dir / f"{base_name}{structure_suffix}"
-                
-                with open(structure_path, 'w', encoding='utf-8') as f:
-                    json.dump(structure, f, indent=2, ensure_ascii=False)
-                
-                success_count += 1
-                results.append({
-                    'page': entry['index'],
-                    'status': 'success',
-                    'image': str(output_image_path),
-                    'structure': str(structure_path)
-                })
                 
             except Exception as e:
                 failed_count += 1
@@ -233,6 +225,7 @@ def _apply_template_batch(
             json.dump({
                 'template': template_entry['display'],
                 'template_file': str(template_file),
+                'template_mode': template_mode,
                 'total': len(target_entries),
                 'success': success_count,
                 'failed': failed_count,
@@ -249,7 +242,7 @@ def _apply_template_batch(
                 f"失败: {failed_count} 页"
             )
             
-            # 🔑 提供下载批处理结果
+            # 🔑 提供下载批处理报告
             with open(batch_result_path, 'r', encoding='utf-8') as f:
                 st.download_button(
                     "📥 下载批处理报告",

+ 20 - 20
table_line_generator/editor/config_loader.py

@@ -275,25 +275,25 @@ def load_structure_from_config(config_path: Path) -> dict:
     return structure
 
 
-def save_structure_to_config(structure: dict, output_path: Path):
-    """
-    保存表格结构到配置文件
+# def save_structure_to_config(structure: dict, output_path: Path):
+#     """
+#     保存表格结构到配置文件
     
-    Args:
-        structure: 表格结构字典
-        output_path: 输出文件路径
-    """
-    save_data = {
-        'rows': structure['rows'],
-        'columns': structure['columns'],
-        'horizontal_lines': structure.get('horizontal_lines', []),
-        'vertical_lines': structure.get('vertical_lines', []),
-        'row_height': structure['row_height'],
-        'col_widths': structure['col_widths'],
-        'table_bbox': structure['table_bbox'],
-        'modified_h_lines': list(structure.get('modified_h_lines', set())),
-        'modified_v_lines': list(structure.get('modified_v_lines', set()))
-    }
+#     Args:
+#         structure: 表格结构字典
+#         output_path: 输出文件路径
+#     """
+#     save_data = {
+#         'rows': structure['rows'],
+#         'columns': structure['columns'],
+#         'horizontal_lines': structure.get('horizontal_lines', []),
+#         'vertical_lines': structure.get('vertical_lines', []),
+#         'row_height': structure['row_height'],
+#         'col_widths': structure['col_widths'],
+#         'table_bbox': structure['table_bbox'],
+#         'modified_h_lines': list(structure.get('modified_h_lines', set())),
+#         'modified_v_lines': list(structure.get('modified_v_lines', set()))
+#     }
     
-    with open(output_path, 'w', encoding='utf-8') as f:
-        json.dump(save_data, f, indent=2, ensure_ascii=False)
+#     with open(output_path, 'w', encoding='utf-8') as f:
+#         json.dump(save_data, f, indent=2, ensure_ascii=False)

+ 8 - 10
table_line_generator/editor/data_processor.py

@@ -28,22 +28,20 @@ def get_structure_from_ocr(
     Returns:
         (table_bbox, structure): 表格边界框和结构信息
     """
-    from PIL import Image
-    
     # 🎯 第一步:解析数据(统一接口)
     table_bbox, ocr_data = TableLineGenerator.parse_ocr_data(raw_data, tool)
     
-    # 🎯 第二步:创建生成器
-    dummy_image = Image.new('RGB', (2000, 3000), 'white')
-    generator = TableLineGenerator(dummy_image, ocr_data)
-    
-    # 🎯 第三步:分析结构(根据工具选择算法)
+    # 🎯 第二步:分析结构(根据工具选择算法)
     if tool.lower() == "mineru":
-        # MinerU 使用基于索引的算法
-        structure = generator.analyze_table_structure(method="mineru")
+        # ✅ 使用静态方法,无需图片
+        structure = TableLineGenerator.analyze_structure_only(
+            ocr_data,
+            method="mineru"
+        )
     else:
         # PPStructure 使用聚类算法
-        structure = generator.analyze_table_structure(
+        structure = TableLineGenerator.analyze_structure_only(
+            ocr_data,
             y_tolerance=5,
             x_tolerance=10,
             min_row_height=20,

+ 4 - 2
table_line_generator/editor/save_controls.py

@@ -3,10 +3,10 @@
 """
 import streamlit as st
 import io
+import json
 from pathlib import Path
 from typing import Dict
 
-from .config_loader import save_structure_to_config
 from .drawing import draw_clean_table_lines
 
 
@@ -142,7 +142,9 @@ def _save_structure_file(structure, output_dir, base_name, suffix, saved_files):
     """保存结构配置文件"""
     structure_filename = f"{base_name}{suffix}"
     structure_path = output_dir / structure_filename
-    save_structure_to_config(structure, structure_path)
+    # save_structure_to_config(structure, structure_path)
+    with open(structure_path, 'w', encoding='utf-8') as f:
+        json.dump(structure, f, indent=2, ensure_ascii=False)
     saved_files.append(("配置文件", structure_path))
     
     with open(structure_path, 'r') as f:

+ 0 - 634
table_line_generator/editor/ui_components_v1.py

@@ -1,634 +0,0 @@
-"""
-UI 组件
-"""
-
-import streamlit as st
-import json
-from pathlib import Path
-from PIL import Image
-import tempfile
-from typing import Dict, List
-
-try:
-    from ..table_line_generator import TableLineGenerator
-except ImportError:
-    from table_line_generator import TableLineGenerator
-
-from .config_loader import load_structure_from_config, build_data_source_catalog
-from .drawing import clear_table_image_cache
-
-def create_file_uploader_section(work_mode: str):
-    """
-    创建文件上传区域
-    
-    Args:
-        work_mode: 工作模式("🆕 新建标注" 或 "📂 加载已有标注")
-    """
-    if work_mode == "🆕 新建标注":
-        st.sidebar.subheader("上传文件")
-        uploaded_json = st.sidebar.file_uploader("上传OCR结果JSON", type=['json'], key="new_json")
-        uploaded_image = st.sidebar.file_uploader("上传对应图片", type=['jpg', 'png'], key="new_image")
-        
-        # 处理 JSON 上传
-        if uploaded_json is not None:
-            if st.session_state.loaded_json_name != uploaded_json.name:
-                try:
-                    raw_data = json.load(uploaded_json)
-                    
-                    with st.expander("🔍 原始数据结构"):
-                        if isinstance(raw_data, dict):
-                            st.json({k: f"<{type(v).__name__}>" if not isinstance(v, (str, int, float, bool, type(None))) else v 
-                                    for k, v in list(raw_data.items())[:5]})
-                        else:
-                            st.json(raw_data[:3] if len(raw_data) > 3 else raw_data)
-                    
-                    ocr_data = parse_ocr_data(raw_data)
-                    
-                    if not ocr_data:
-                        st.error("❌ 无法解析 OCR 数据,请检查 JSON 格式")
-                        st.stop()
-                    
-                    st.session_state.ocr_data = ocr_data
-                    st.session_state.loaded_json_name = uploaded_json.name
-                    st.session_state.loaded_config_name = None
-                    
-                    # 清除旧数据
-                    if 'structure' in st.session_state:
-                        del st.session_state.structure
-                    if 'generator' in st.session_state:
-                        del st.session_state.generator
-                    st.session_state.undo_stack = []
-                    st.session_state.redo_stack = []
-                    clear_table_image_cache()
-                    
-                    st.success(f"✅ 成功加载 {len(ocr_data)} 条 OCR 记录")
-                    
-                except Exception as e:
-                    st.error(f"❌ 加载数据失败: {e}")
-                    st.stop()
-        
-        # 处理图片上传
-        if uploaded_image is not None:
-            if st.session_state.loaded_image_name != uploaded_image.name:
-                try:
-                    image = Image.open(uploaded_image)
-                    st.session_state.image = image
-                    st.session_state.loaded_image_name = uploaded_image.name
-                    
-                    # 清除旧数据
-                    if 'structure' in st.session_state:
-                        del st.session_state.structure
-                    if 'generator' in st.session_state:
-                        del st.session_state.generator
-                    st.session_state.undo_stack = []
-                    st.session_state.redo_stack = []
-                    clear_table_image_cache()
-                    
-                    st.success(f"✅ 成功加载图片: {uploaded_image.name}")
-                    
-                except Exception as e:
-                    st.error(f"❌ 加载图片失败: {e}")
-                    st.stop()
-    
-    else:  # 加载已有标注
-        st.sidebar.subheader("加载已保存的标注")
-        
-        uploaded_config = st.sidebar.file_uploader(
-            "上传配置文件 (*_structure.json)",
-            type=['json'],
-            key="load_config"
-        )
-        
-        uploaded_image_for_config = st.sidebar.file_uploader(
-            "上传对应图片(可选)",
-            type=['jpg', 'png'],
-            key="load_image"
-        )
-        
-        # 处理配置文件加载
-        if uploaded_config is not None:
-            if st.session_state.loaded_config_name != uploaded_config.name:
-                try:
-                    # 创建临时文件
-                    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as tmp:
-                        tmp.write(uploaded_config.getvalue().decode('utf-8'))
-                        tmp_path = tmp.name
-                    
-                    # 加载结构
-                    structure = load_structure_from_config(Path(tmp_path))
-                    
-                    # 清理临时文件
-                    Path(tmp_path).unlink()
-                    
-                    st.session_state.structure = structure
-                    st.session_state.loaded_config_name = uploaded_config.name
-                    
-                    # 清除历史记录和缓存
-                    st.session_state.undo_stack = []
-                    st.session_state.redo_stack = []
-                    clear_table_image_cache()
-                    
-                    st.success(f"✅ 成功加载配置: {uploaded_config.name}")
-                    st.info(
-                        f"📊 表格结构: {len(structure['rows'])}行 x {len(structure['columns'])}列\n\n"
-                        f"📏 横线数: {len(structure.get('horizontal_lines', []))}\n\n"
-                        f"📏 竖线数: {len(structure.get('vertical_lines', []))}"
-                    )
-                    
-                    # 显示配置文件详情
-                    with st.expander("📋 配置详情"):
-                        st.json({
-                            "行数": len(structure['rows']),
-                            "列数": len(structure['columns']),
-                            "横线数": len(structure.get('horizontal_lines', [])),
-                            "竖线数": len(structure.get('vertical_lines', [])),
-                            "行高": structure.get('row_height'),
-                            "列宽": structure.get('col_widths'),
-                            "已修改的横线": list(structure.get('modified_h_lines', set())),
-                            "已修改的竖线": list(structure.get('modified_v_lines', set()))
-                        })
-                    
-                except Exception as e:
-                    st.error(f"❌ 加载配置失败: {e}")
-                    import traceback
-                    st.code(traceback.format_exc())
-                    st.stop()
-        
-        # 处理图片加载
-        if uploaded_image_for_config is not None:
-            if st.session_state.loaded_image_name != uploaded_image_for_config.name:
-                try:
-                    image = Image.open(uploaded_image_for_config)
-                    st.session_state.image = image
-                    st.session_state.loaded_image_name = uploaded_image_for_config.name
-                    
-                    clear_table_image_cache()
-                    
-                    st.success(f"✅ 成功加载图片: {uploaded_image_for_config.name}")
-                    
-                except Exception as e:
-                    st.error(f"❌ 加载图片失败: {e}")
-                    st.stop()
-        
-        # 提示信息
-        if 'structure' in st.session_state and st.session_state.image is None:
-            st.warning("⚠️ 已加载配置,但未加载对应图片。请上传图片以查看效果。")
-            st.info("💡 提示:配置文件已加载,您可以:\n1. 上传对应图片查看效果\n2. 直接编辑配置并保存")
-
-
-def create_display_settings_section(display_config: Dict):
-    """显示设置(由配置驱动)"""
-    st.sidebar.divider()
-    st.sidebar.subheader("🖼️ 显示设置")
-
-    line_width = st.sidebar.slider(
-        "线条宽度",
-        int(display_config.get("line_width_min", 1)),
-        int(display_config.get("line_width_max", 5)),
-        int(display_config.get("default_line_width", 2)),
-    )
-    display_mode = st.sidebar.radio(
-        "显示模式",
-        ["对比显示", "仅显示划线图", "仅显示原图"],
-        index=1,
-    )
-    zoom_level = st.sidebar.slider(
-        "图片缩放",
-        float(display_config.get("zoom_min", 0.25)),
-        float(display_config.get("zoom_max", 2.0)),
-        float(display_config.get("default_zoom", 1.0)),
-        float(display_config.get("zoom_step", 0.25)),
-    )
-    show_line_numbers = st.sidebar.checkbox(
-        "显示线条编号",
-        value=bool(display_config.get("show_line_numbers", True)),
-    )
-
-    return line_width, display_mode, zoom_level, show_line_numbers
-
-
-def create_undo_redo_section():
-    """创建撤销/重做区域"""
-    from .state_manager import undo_last_action, redo_last_action
-    from .drawing import clear_table_image_cache
-    
-    st.sidebar.divider()
-    st.sidebar.subheader("↩️ 撤销/重做")
-    
-    col1, col2 = st.sidebar.columns(2)
-    with col1:
-        if st.button("↩️ 撤销", disabled=len(st.session_state.undo_stack) == 0):
-            if undo_last_action():
-                clear_table_image_cache()
-                st.success("✅ 已撤销")
-                st.rerun()
-    
-    with col2:
-        if st.button("↪️ 重做", disabled=len(st.session_state.redo_stack) == 0):
-            if redo_last_action():
-                clear_table_image_cache()
-                st.success("✅ 已重做")
-                st.rerun()
-    
-    st.sidebar.info(f"📚 历史记录: {len(st.session_state.undo_stack)} 条")
-
-
-def create_analysis_section(y_tolerance, x_tolerance, min_row_height):
-    """
-    创建分析区域
-    
-    Args:
-        y_tolerance: Y轴聚类容差
-        x_tolerance: X轴聚类容差
-        min_row_height: 最小行高
-    """
-    if st.button("🔍 分析表格结构"):
-        with st.spinner("分析中..."):
-            try:
-                generator = st.session_state.generator
-                structure = generator.analyze_table_structure(
-                    y_tolerance=y_tolerance,
-                    x_tolerance=x_tolerance,
-                    min_row_height=min_row_height
-                )
-                
-                if not structure:
-                    st.warning("⚠️ 未检测到表格结构")
-                    st.stop()
-                
-                structure['modified_h_lines'] = set()
-                structure['modified_v_lines'] = set()
-                
-                st.session_state.structure = structure
-                st.session_state.undo_stack = []
-                st.session_state.redo_stack = []
-                clear_table_image_cache()
-                
-                st.success(
-                    f"✅ 检测到 {len(structure['rows'])} 行({len(structure['horizontal_lines'])} 条横线),"
-                    f"{len(structure['columns'])} 列({len(structure['vertical_lines'])} 条竖线)"
-                )
-                
-                col1, col2, col3, col4 = st.columns(4)
-                with col1:
-                    st.metric("行数", len(structure['rows']))
-                with col2:
-                    st.metric("横线数", len(structure['horizontal_lines']))
-                with col3:
-                    st.metric("列数", len(structure['columns']))
-                with col4:
-                    st.metric("竖线数", len(structure['vertical_lines']))
-            
-            except Exception as e:
-                st.error(f"❌ 分析失败: {e}")
-                import traceback
-                st.code(traceback.format_exc())
-                st.stop()
-
-
-def create_save_section(work_mode, structure, image, line_width, output_config: Dict):
-    """
-    保存设置(目录/命名来自配置)
-    """
-    from .config_loader import save_structure_to_config
-    from .drawing import draw_clean_table_lines
-    import io
-
-    st.divider()
-
-    defaults = output_config.get("defaults", {})
-    line_colors = output_config.get("line_colors") or [
-        {"name": "黑色", "rgb": [0, 0, 0]},
-        {"name": "蓝色", "rgb": [0, 0, 255]},
-        {"name": "红色", "rgb": [255, 0, 0]},
-    ]
-
-    save_col1, save_col2, save_col3 = st.columns(3)
-
-    with save_col1:
-        save_structure = st.checkbox(
-            "保存表格结构配置",
-            value=bool(defaults.get("save_structure", True)),
-        )
-
-    with save_col2:
-        save_image = st.checkbox(
-            "保存表格线图片",
-            value=bool(defaults.get("save_image", True)),
-        )
-
-    color_names = [c["name"] for c in line_colors]
-    default_color = defaults.get("line_color", color_names[0])
-    default_index = color_names.index(default_color) if default_color in color_names else 0
-
-    with save_col3:
-        line_color_option = st.selectbox(
-            "保存时线条颜色",
-            color_names,
-            label_visibility="collapsed",
-            index=default_index,
-        )
-
-    if st.button("💾 保存", type="primary"):
-        output_dir = Path(output_config.get("directory", "output/table_structures"))
-        output_dir.mkdir(parents=True, exist_ok=True)
-
-        structure_suffix = output_config.get("structure_suffix", "_structure.json")
-        image_suffix = output_config.get("image_suffix", "_with_lines.png")
-
-        # 确定文件名
-        if work_mode == "🆕 新建标注":
-            if st.session_state.loaded_json_name:
-                base_name = Path(st.session_state.loaded_json_name).stem
-            else:
-                base_name = "table_structure"
-        else:
-            if st.session_state.loaded_config_name:
-                base_name = Path(st.session_state.loaded_config_name).stem
-                if base_name.endswith('_structure'):
-                    base_name = base_name[:-10]
-            elif st.session_state.loaded_image_name:
-                base_name = Path(st.session_state.loaded_image_name).stem
-            else:
-                base_name = "table_structure"
-        
-        saved_files = []
-        
-        if save_structure:
-            structure_filename = f"{base_name}{structure_suffix}"
-            structure_path = output_dir / structure_filename
-            save_structure_to_config(structure, structure_path)
-            saved_files.append(("配置文件", structure_path))
-            
-            with open(structure_path, 'r') as f:
-                st.download_button(
-                    "📥 下载配置文件",
-                    f.read(),
-                    file_name=f"{base_name}_structure.json",
-                    mime="application/json"
-                )
-        
-        if save_image:
-            if st.session_state.image is None:
-                st.warning("⚠️ 无法保存图片:未加载图片文件")
-            else:
-                selected_color_rgb = next(
-                    (tuple(c["rgb"]) for c in line_colors if c["name"] == line_color_option),
-                    (0, 0, 0),
-                )
-                clean_img = draw_clean_table_lines(
-                    st.session_state.image,
-                    structure,
-                    line_width=line_width,
-                    line_color=selected_color_rgb,
-                )
-                image_filename = f"{base_name}{image_suffix}"
-                output_image_path = output_dir / image_filename
-                clean_img.save(output_image_path)
-                saved_files.append(("表格线图片", output_image_path))
-                
-                buf = io.BytesIO()
-                clean_img.save(buf, format='PNG')
-                buf.seek(0)
-                
-                st.download_button(
-                    "📥 下载表格线图片",
-                    buf,
-                    file_name=f"{base_name}_with_lines.png",
-                    mime="image/png"
-                )
-        
-        if saved_files:
-            st.success(f"✅ 已保存 {len(saved_files)} 个文件:")
-            for file_type, file_path in saved_files:
-                st.info(f"  • {file_type}: {file_path}")
-
-def setup_new_annotation_mode(ocr_data, image, config: Dict):
-    """
-    设置新建标注模式的通用逻辑
-    
-    Args:
-        ocr_data: OCR 数据
-        image: 图片对象
-        config: 显示配置
-    
-    Returns:
-        tuple: (y_tolerance, x_tolerance, min_row_height, line_width, display_mode, zoom_level, show_line_numbers)
-    """
-    # 参数调整
-    st.sidebar.header("🔧 参数调整")
-    y_tolerance = st.sidebar.slider("Y轴聚类容差(像素)", 1, 20, 5, key="new_y_tol")
-    x_tolerance = st.sidebar.slider("X轴聚类容差(像素)", 5, 50, 10, key="new_x_tol")
-    min_row_height = st.sidebar.slider("最小行高(像素)", 10, 100, 20, key="new_min_h")
-    
-    # 显示设置
-    line_width, display_mode, zoom_level, show_line_numbers = create_display_settings_section(config)
-    create_undo_redo_section()
-    
-    # 初始化生成器
-    if 'generator' not in st.session_state or st.session_state.generator is None:
-        try:
-            generator = TableLineGenerator(image, ocr_data)
-            st.session_state.generator = generator
-        except Exception as e:
-            st.error(f"❌ 初始化生成器失败: {e}")
-            st.stop()
-    
-    # 分析按钮
-    create_analysis_section(y_tolerance, x_tolerance, min_row_height)
-    
-    return y_tolerance, x_tolerance, min_row_height, line_width, display_mode, zoom_level, show_line_numbers
-
-
-def setup_edit_annotation_mode(structure, image, config: Dict):
-    """
-    设置编辑标注模式的通用逻辑
-    
-    Args:
-        structure: 表格结构
-        image: 图片对象(可为 None)
-        config: 显示配置
-    
-    Returns:
-        tuple: (image, line_width, display_mode, zoom_level, show_line_numbers)
-    """
-    # 如果没有图片,创建虚拟画布
-    if image is None:
-        if 'table_bbox' in structure:
-            bbox = structure['table_bbox']
-            dummy_width = bbox[2] + 100
-            dummy_height = bbox[3] + 100
-        else:
-            dummy_width = 2000
-            dummy_height = 2000
-        image = Image.new('RGB', (dummy_width, dummy_height), color='white')
-        st.info(f"💡 使用虚拟画布 ({dummy_width}x{dummy_height})")
-    
-    # 显示设置
-    line_width, display_mode, zoom_level, show_line_numbers = create_display_settings_section(config)
-    create_undo_redo_section()
-    
-    return image, line_width, display_mode, zoom_level, show_line_numbers
-
-
-def render_table_structure_view(structure, image, line_width, display_mode, zoom_level, show_line_numbers, 
-                                viewport_width, viewport_height):
-    """
-    渲染表格结构视图(统一三种模式的显示逻辑)
-    
-    Args:
-        structure: 表格结构
-        image: 图片对象
-        line_width: 线条宽度
-        display_mode: 显示模式
-        zoom_level: 缩放级别
-        show_line_numbers: 是否显示线条编号
-        viewport_width: 视口宽度
-        viewport_height: 视口高度
-    """
-    # 绘制表格线
-    img_with_lines = get_cached_table_lines_image(
-        image, structure, line_width=line_width, show_numbers=show_line_numbers
-    )
-    
-    # 根据显示模式显示图片
-    if display_mode == "对比显示":
-        col1, col2 = st.columns(2)
-        with col1:
-            show_image_with_scroll(image, "原图", viewport_width, viewport_height, zoom_level)
-        with col2:
-            show_image_with_scroll(img_with_lines, "表格线", viewport_width, viewport_height, zoom_level)
-    elif display_mode == "仅显示划线图":
-        show_image_with_scroll(
-            img_with_lines, 
-            f"表格线图 (缩放: {zoom_level:.0%})", 
-            viewport_width, 
-            viewport_height, 
-            zoom_level
-        )
-    else:
-        show_image_with_scroll(
-            image, 
-            f"原图 (缩放: {zoom_level:.0%})", 
-            viewport_width, 
-            viewport_height, 
-            zoom_level
-        )
-    
-    # 手动调整区域
-    create_adjustment_section(structure)
-    
-    # 显示详细信息
-    with st.expander("📊 表格结构详情"):
-        st.json({
-            "行数": len(structure['rows']),
-            "列数": len(structure['columns']),
-            "横线数": len(structure.get('horizontal_lines', [])),
-            "竖线数": len(structure.get('vertical_lines', [])),
-            "横线坐标": structure.get('horizontal_lines', []),
-            "竖线坐标": structure.get('vertical_lines', []),
-            "标准行高": structure.get('row_height'),
-            "列宽度": structure.get('col_widths'),
-            "修改的横线": list(structure.get('modified_h_lines', set())),
-            "修改的竖线": list(structure.get('modified_v_lines', set()))
-        })
-
-
-def create_directory_selector(data_sources: List[Dict], global_output_config: Dict):
-    """目录模式选择器(优化:避免重复加载)"""
-    st.sidebar.subheader("目录模式")
-    source_names = [src["name"] for src in data_sources]
-    selected_name = st.sidebar.selectbox("选择数据源", source_names, key="dir_mode_source")
-    source_cfg = next(src for src in data_sources if src["name"] == selected_name)
-    
-    output_cfg = source_cfg.get("output", global_output_config)
-    output_dir = Path(output_cfg.get("directory", "output/table_structures"))
-    structure_suffix = output_cfg.get("structure_suffix", "_structure.json")
-    
-    catalog_key = f"catalog::{selected_name}"
-    if catalog_key not in st.session_state:
-        st.session_state[catalog_key] = build_data_source_catalog(source_cfg)
-    catalog = st.session_state[catalog_key]
-
-    if not catalog:
-        st.sidebar.warning("目录中没有 JSON 文件")
-        return
-
-    if 'dir_selected_index' not in st.session_state:
-        st.session_state.dir_selected_index = 0
-
-    selected = st.sidebar.selectbox(
-        "选择文件",
-        range(len(catalog)),
-        format_func=lambda i: catalog[i]["display"],
-        index=st.session_state.dir_selected_index,
-        key="dir_select_box"
-    )
-
-    page_input = st.sidebar.number_input(
-        "页码跳转",
-        min_value=1,
-        max_value=len(catalog),
-        value=catalog[selected]["index"],
-        step=1,
-        key="dir_page_input"
-    )
-    
-    # 🔑 关键优化:只在切换文件时才重新加载
-    current_entry_key = f"{selected_name}::{catalog[selected]['json']}"
-    
-    if 'last_loaded_entry' not in st.session_state or st.session_state.last_loaded_entry != current_entry_key:
-        # 文件切换,重新加载
-        entry = catalog[selected]
-        base_name = entry["json"].stem
-        structure_file = output_dir / f"{base_name}{structure_suffix}"
-        has_structure = structure_file.exists()
-        
-        # 📂 加载 JSON
-        with open(entry["json"], "r", encoding="utf-8") as fp:
-            raw = json.load(fp)
-        st.session_state.ocr_data = parse_ocr_data(raw)
-        st.session_state.loaded_json_name = entry["json"].name
-
-        # 🖼️ 加载图片
-        if entry["image"] and entry["image"].exists():
-            st.session_state.image = Image.open(entry["image"])
-            st.session_state.loaded_image_name = entry["image"].name
-        else:
-            st.session_state.image = None
-
-        # 🎯 自动模式判断
-        if has_structure:
-            st.session_state.dir_auto_mode = "edit"
-            st.session_state.loaded_config_name = structure_file.name
-            
-            try:
-                structure = load_structure_from_config(structure_file)
-                st.session_state.structure = structure
-                st.session_state.undo_stack = []
-                st.session_state.redo_stack = []
-                clear_table_image_cache()
-                st.sidebar.success(f"✅ 编辑模式")
-            except Exception as e:
-                st.error(f"❌ 加载标注失败: {e}")
-                st.session_state.dir_auto_mode = "new"
-        else:
-            st.session_state.dir_auto_mode = "new"
-            if 'structure' in st.session_state:
-                del st.session_state.structure
-            if 'generator' in st.session_state:
-                del st.session_state.generator
-            st.sidebar.info(f"🆕 新建模式")
-        
-        # 标记已加载
-        st.session_state.last_loaded_entry = current_entry_key
-        st.info(f"📂 已加载: {entry['json'].name}")
-    
-    # 页码跳转处理
-    if page_input != catalog[selected]["index"]:
-        target = next((i for i, item in enumerate(catalog) if item["index"] == page_input), None)
-        if target is not None:
-            st.session_state.dir_selected_index = target
-            st.rerun()
-
-    return st.session_state.get('dir_auto_mode', 'new')