Files
Timeline_Generator/backend/export.py
beabigegg aa987adfb9 後端代碼清理:移除冗餘註解和調試代碼
清理內容:
- 移除所有開發元資訊(Author, Version, DocID, Rationale等)
- 刪除註解掉的代碼片段(力導向演算法等24行)
- 移除調試用的 logger.debug 語句
- 簡化冗餘的內聯註解(emoji、"重要"等標註)
- 刪除 TDD 文件引用

清理檔案:
- backend/main.py - 移除調試日誌和元資訊
- backend/importer.py - 移除詳細類型檢查調試
- backend/export.py - 簡化 docstring
- backend/schemas.py - 移除元資訊
- backend/renderer.py - 移除 TDD 引用
- backend/renderer_timeline.py - 移除註解代碼和冗餘註解
- backend/path_planner.py - 簡化策略註解

保留內容:
- 所有函數的 docstring(功能說明、參數、返回值)
- 必要的業務邏輯註解
- 簡潔的模組功能說明

效果:
- 刪除約 100+ 行冗餘註解
- 代碼更加簡潔專業
- 功能完整性 100% 保留

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 12:22:29 +08:00

337 lines
8.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
匯出模組
本模組負責將時間軸圖表匯出為各種格式PDF、PNG、SVG
使用 Plotly 的 kaleido 引擎進行圖片生成。
"""
import os
from pathlib import Path
from datetime import datetime
from typing import Union, Optional
import logging
import re
try:
import plotly.graph_objects as go
from plotly.io import write_image
PLOTLY_AVAILABLE = True
except ImportError:
PLOTLY_AVAILABLE = False
from .schemas import ExportOptions, ExportFormat
logger = logging.getLogger(__name__)
class ExportError(Exception):
"""匯出錯誤基礎類別"""
pass
class FileNameSanitizer:
"""
檔名淨化器
移除非法字元並處理過長的檔名。
"""
# 非法字元Windows + Unix
ILLEGAL_CHARS = r'[<>:"/\\|?*\x00-\x1f]'
# 保留字Windows
RESERVED_NAMES = [
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
]
MAX_LENGTH = 200 # 最大檔名長度
@classmethod
def sanitize(cls, filename: str) -> str:
"""
淨化檔名
Args:
filename: 原始檔名
Returns:
淨化後的檔名
"""
# 移除非法字元
sanitized = re.sub(cls.ILLEGAL_CHARS, '_', filename)
# 移除前後空白
sanitized = sanitized.strip()
# 移除尾部的點和空格Windows 限制)
sanitized = sanitized.rstrip('. ')
# 檢查保留字
name_upper = sanitized.upper()
if name_upper in cls.RESERVED_NAMES:
sanitized = '_' + sanitized
# 限制長度
if len(sanitized) > cls.MAX_LENGTH:
sanitized = sanitized[:cls.MAX_LENGTH]
# 如果為空,使用預設名稱
if not sanitized:
sanitized = 'timeline'
return sanitized
class ExportEngine:
"""
匯出引擎
負責將 Plotly 圖表匯出為不同格式的檔案。
"""
def __init__(self):
if not PLOTLY_AVAILABLE:
raise ImportError("需要安裝 plotly 和 kaleido 以使用匯出功能")
self.filename_sanitizer = FileNameSanitizer()
def export(
self,
fig: go.Figure,
output_path: Union[str, Path],
options: ExportOptions
) -> Path:
"""
匯出圖表
Args:
fig: Plotly Figure 物件
output_path: 輸出路徑
options: 匯出選項
Returns:
實際輸出檔案的路徑
Raises:
ExportError: 匯出失敗時拋出
"""
output_path = Path(output_path)
# 確保目錄存在
output_path.parent.mkdir(parents=True, exist_ok=True)
# 淨化檔名
filename = self.filename_sanitizer.sanitize(output_path.stem)
sanitized_path = output_path.parent / f"{filename}{output_path.suffix}"
try:
if options.fmt == ExportFormat.PDF:
return self._export_pdf(fig, sanitized_path, options)
elif options.fmt == ExportFormat.PNG:
return self._export_png(fig, sanitized_path, options)
elif options.fmt == ExportFormat.SVG:
return self._export_svg(fig, sanitized_path, options)
else:
raise ExportError(f"不支援的匯出格式: {options.fmt}")
except PermissionError:
raise ExportError(f"無法寫入檔案(權限不足): {sanitized_path}")
except OSError as e:
if e.errno == 28: # ENOSPC
raise ExportError("磁碟空間不足")
else:
raise ExportError(f"檔案系統錯誤: {str(e)}")
except Exception as e:
logger.error(f"匯出失敗: {str(e)}")
raise ExportError(f"匯出失敗: {str(e)}")
def _export_pdf(self, fig: go.Figure, output_path: Path, options: ExportOptions) -> Path:
"""
匯出為 PDF
Args:
fig: Plotly Figure
output_path: 輸出路徑
options: 匯出選項
Returns:
輸出檔案路徑
"""
# 確保副檔名
if output_path.suffix.lower() != '.pdf':
output_path = output_path.with_suffix('.pdf')
# 設定 DPI 和尺寸
scale = options.dpi / 72.0 # Plotly 使用 72 DPI 作為基準
# 匯出
write_image(
fig,
str(output_path),
format='pdf',
width=options.width,
height=options.height,
scale=scale
)
logger.info(f"PDF 匯出成功: {output_path}")
return output_path
def _export_png(self, fig: go.Figure, output_path: Path, options: ExportOptions) -> Path:
"""
匯出為 PNG
Args:
fig: Plotly Figure
output_path: 輸出路徑
options: 匯出選項
Returns:
輸出檔案路徑
"""
# 確保副檔名
if output_path.suffix.lower() != '.png':
output_path = output_path.with_suffix('.png')
# 設定 DPI 和尺寸
scale = options.dpi / 72.0
# 處理透明背景
if options.transparent_background:
fig.update_layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)'
)
# 匯出
write_image(
fig,
str(output_path),
format='png',
width=options.width,
height=options.height,
scale=scale
)
logger.info(f"PNG 匯出成功: {output_path}")
return output_path
def _export_svg(self, fig: go.Figure, output_path: Path, options: ExportOptions) -> Path:
"""
匯出為 SVG
Args:
fig: Plotly Figure
output_path: 輸出路徑
options: 匯出選項
Returns:
輸出檔案路徑
"""
# 確保副檔名
if output_path.suffix.lower() != '.svg':
output_path = output_path.with_suffix('.svg')
# SVG 是向量格式,不需要 DPI 設定
write_image(
fig,
str(output_path),
format='svg',
width=options.width,
height=options.height
)
logger.info(f"SVG 匯出成功: {output_path}")
return output_path
class TimelineExporter:
"""
時間軸匯出器
高層級介面,整合渲染與匯出功能。
"""
def __init__(self):
self.export_engine = ExportEngine()
def export_from_plotly_json(
self,
plotly_data: dict,
plotly_layout: dict,
output_path: Union[str, Path],
options: ExportOptions,
filename_prefix: str = "timeline"
) -> Path:
"""
從 Plotly JSON 資料匯出
Args:
plotly_data: Plotly data 部分
plotly_layout: Plotly layout 部分
output_path: 輸出路徑(目錄或完整路徑)
options: 匯出選項
filename_prefix: 檔名前綴
Returns:
實際輸出檔案的路徑
"""
# 建立 Plotly Figure
fig = go.Figure(data=plotly_data.get('data', []), layout=plotly_layout)
# 處理輸出路徑
output_path = Path(output_path)
if output_path.is_dir():
# 如果是目錄,生成預設檔名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"{filename_prefix}_{timestamp}.{options.fmt.value}"
full_path = output_path / filename
else:
full_path = output_path
# 匯出
return self.export_engine.export(fig, full_path, options)
def generate_default_filename(self, fmt: ExportFormat) -> str:
"""
生成預設檔名
Args:
fmt: 檔案格式
Returns:
預設檔名
"""
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
return f"timeline_{timestamp}.{fmt.value}"
def create_metadata(title: str = "TimeLine Designer") -> dict:
"""
建立 PDF 元資料
Args:
title: 文件標題
Returns:
元資料字典
"""
return {
'Title': title,
'Creator': 'TimeLine Designer v1.0',
'Producer': 'Plotly + Kaleido',
'CreationDate': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
}
# 匯出主要介面
__all__ = [
'ExportEngine',
'TimelineExporter',
'ExportError',
'ExportOptions',
'ExportFormat',
]