264 lines
7.7 KiB
Python
264 lines
7.7 KiB
Python
#!/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()
|