kgrag/doc2pdf.py
2026-06-30 13:35:52 +08:00

264 lines
7.7 KiB
Python
Raw Permalink 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.

#!/usr/bin/env python3
"""
文档转 PDF 工具
使用 LibreOffice 将各种文档格式转换为 PDF
支持格式:
- Word: .doc, .docx, .rtf
- Excel: .xls, .xlsx
- PowerPoint: .ppt, .pptx
- OpenDocument: .odt, .ods, .odp
- 文本: .txt, .csv
- 图片: .jpg, .png (会嵌入PDF)
"""
import subprocess
import os
import sys
from pathlib import Path
from typing import Optional, List
import concurrent.futures
class Doc2PDF:
"""文档转 PDF 转换器"""
SUPPORTED_FORMATS = {
# 文档
'.doc', '.docx', '.rtf', '.odt', '.txt',
# 表格
'.xls', '.xlsx', '.ods', '.csv',
# 演示文稿
'.ppt', '.pptx', '.odp',
# 图片
'.jpg', '.jpeg', '.png', '.bmp', '.gif',
# 其他
'.html', '.htm', '.xml'
}
def __init__(self, libreoffice_path: str = 'libreoffice'):
self.libreoffice_path = libreoffice_path
self._verify_libreoffice()
def _verify_libreoffice(self):
"""验证 LibreOffice 是否可用"""
try:
result = subprocess.run(
[self.libreoffice_path, '--version'],
capture_output=True, text=True
)
if result.returncode != 0:
raise RuntimeError("LibreOffice 不可用")
print(f"✓ 检测到 {result.stdout.strip().split(chr(10))[0]}")
except FileNotFoundError:
raise RuntimeError("未找到 LibreOffice请先安装")
def convert(
self,
input_file: str,
output_dir: Optional[str] = None,
timeout: int = 120
) -> str:
"""
转换单个文件为 PDF
Args:
input_file: 输入文件路径
output_dir: 输出目录(默认为输入文件所在目录)
timeout: 超时时间(秒)
Returns:
生成的 PDF 文件路径
"""
input_path = Path(input_file).resolve()
if not input_path.exists():
raise FileNotFoundError(f"文件不存在: {input_file}")
suffix = input_path.suffix.lower()
if suffix not in self.SUPPORTED_FORMATS:
raise ValueError(f"不支持的格式: {suffix}")
if output_dir is None:
output_dir = str(input_path.parent)
else:
output_dir = str(Path(output_dir).resolve())
os.makedirs(output_dir, exist_ok=True)
cmd = [
self.libreoffice_path,
'--headless',
'--invisible',
'--nologo',
'--nofirststartwizard',
'--convert-to', 'pdf',
'--outdir', output_dir,
str(input_path)
]
print(f" 转换中: {input_path.name} ...", end=' ', flush=True)
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout
)
if result.returncode != 0:
print("")
raise RuntimeError(f"转换失败: {result.stderr}")
output_file = os.path.join(output_dir, f"{input_path.stem}.pdf")
if os.path.exists(output_file):
print("")
return output_file
else:
print("")
raise RuntimeError("转换完成但未找到输出文件")
except subprocess.TimeoutExpired:
print("✗ (超时)")
raise RuntimeError(f"转换超时 ({timeout}秒)")
def convert_batch(
self,
input_files: List[str],
output_dir: Optional[str] = None,
max_workers: int = 1 # LibreOffice 并发可能有问题建议设为1
) -> List[dict]:
"""
批量转换文件
Args:
input_files: 输入文件列表
output_dir: 输出目录
max_workers: 并发数
Returns:
转换结果列表
"""
results = []
print(f"\n开始批量转换 {len(input_files)} 个文件...\n")
for i, input_file in enumerate(input_files, 1):
print(f"[{i}/{len(input_files)}]", end='')
try:
output_file = self.convert(input_file, output_dir)
results.append({
'input': input_file,
'output': output_file,
'success': True,
'error': None
})
except Exception as e:
results.append({
'input': input_file,
'output': None,
'success': False,
'error': str(e)
})
# 打印统计
success_count = sum(1 for r in results if r['success'])
print(f"\n转换完成: {success_count}/{len(results)} 成功")
return results
def convert_directory(
self,
input_dir: str,
output_dir: Optional[str] = None,
recursive: bool = False
) -> List[dict]:
"""
转换目录中的所有支持文件
Args:
input_dir: 输入目录
output_dir: 输出目录
recursive: 是否递归处理子目录
"""
input_path = Path(input_dir)
if not input_path.is_dir():
raise NotADirectoryError(f"不是目录: {input_dir}")
# 收集所有支持的文件
files = []
pattern = '**/*' if recursive else '*'
for file_path in input_path.glob(pattern):
if file_path.is_file() and file_path.suffix.lower() in self.SUPPORTED_FORMATS:
files.append(str(file_path))
if not files:
print("未找到支持的文件")
return []
return self.convert_batch(files, output_dir)
def main():
"""命令行入口"""
import argparse
parser = argparse.ArgumentParser(
description='使用 LibreOffice 将文档转换为 PDF',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
python doc2pdf.py document.docx
python doc2pdf.py document.xlsx -o ./output
python doc2pdf.py ./documents/ -o ./pdfs -r
'''
)
parser.add_argument('input', help='输入文件或目录')
parser.add_argument('-o', '--output', help='输出目录')
parser.add_argument('-r', '--recursive', action='store_true',
help='递归处理子目录')
parser.add_argument('-t', '--timeout', type=int, default=120,
help='单个文件转换超时时间(秒)')
args = parser.parse_args()
converter = Doc2PDF()
input_path = Path(args.input)
if input_path.is_file():
# 单文件转换
try:
output = converter.convert(args.input, args.output, args.timeout)
print(f"\n输出文件: {output}")
except Exception as e:
print(f"\n错误: {e}")
sys.exit(1)
elif input_path.is_dir():
# 目录转换
results = converter.convert_directory(
args.input,
args.output,
args.recursive
)
# 显示失败的文件
failures = [r for r in results if not r['success']]
if failures:
print("\n失败的文件:")
for f in failures:
print(f" - {f['input']}: {f['error']}")
else:
print(f"错误: 路径不存在 - {args.input}")
sys.exit(1)
if __name__ == '__main__':
main()