文章目录
gc.py 功能介绍:PDF 对象流还原工具(用于 pdfium 测试)
核心定位
gc.py 的核心目的是:将经过对象流压缩的 PDF 文件还原为标准交叉引用表(XRef Table)格式 ,为测试 pdfium 的对象流压缩功能提供纯净的测试材料。
工作原理
对象流压缩 vs 交叉引用表格式
| 格式 | 特点 | 用途 |
|---|---|---|
| 对象流压缩 | 对象数据存储在流中,通过流索引访问 | 减小文件体积,适合分发存储 |
| 交叉引用表 | 每个对象有独立的偏移量记录 | 易于解析和编辑,适合测试场景 |
转换流程
┌──────────────────────────────────────────────────────────────────┐
│ Step 1: 输入 - 已压缩的 PDF 文件 │
│ 特征: 使用对象流(Object Streams)存储对象 │
│ 包含 /ObjStm 类型的流对象 │
└───────────────────────────┬──────────────────────────────────────┘
│
▼ podofogc -- 还原对象流
┌──────────────────────────────────────────────────────────────────┐
│ Step 2: 输出 - 还原后的 PDF 文件 │
│ 特征: 使用标准交叉引用表(XRef Table) │
│ 每个对象有独立的 offset 记录 │
│ 无 /ObjStm 流对象 │
└───────────────────────────┬──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Step 3: 测试材料 - 用于 pdfium 对象流压缩功能测试 │
│ 输入: 还原后的标准 PDF(交叉引用表格式) │
│ 输出: 使用 pdfium 压缩后的 PDF(对象流格式) │
│ 验证: 对比压缩率、文件完整性、兼容性 │
└──────────────────────────────────────────────────────────────────┘
使用方法
1. 配置参数
python
# gc.py 配置部分
PODOFO_GC_PATH = r"D:/PDF_SOURCE/podofo_exe/podofogc.exe" # podofogc路径
INPUT_DIR = r"D:/Compress_output/F2" # 已压缩的PDF目录
OUTPUT_DIR = r"D:/Compress_output/P2" # 还原后的测试材料目录
RECURSIVE = True # 递归扫描
PRESERVE_STRUCTURE = True # 保留目录结构
OVERWRITE = False # 不覆盖已存在文件
2. 运行脚本
bash
python gc.py
3. 验证还原效果
使用 qpdf 验证还原是否成功:
bash
# 检查还原后的PDF是否使用交叉引用表
qpdf --show-xref restored_file.pdf
# 检查是否存在对象流
qpdf --show-object-streams restored_file.pdf
预期结果:
--show-xref应显示标准的交叉引用表(每行一个对象的 offset)--show-object-streams应显示 "no object streams" 或为空
测试工作流
完整测试流程
python
# 伪代码示例:pdfium 对象流压缩测试流程
def test_pdfium_object_stream_compression():
# 1. 准备测试材料(已通过 gc.py 还原的 PDF)
test_files = get_restored_pdfs("D:/Compress_output/P2")
for pdf in test_files:
# 2. 使用 pdfium 进行对象流压缩
compressed_pdf = pdfium.compress_with_object_stream(pdf)
# 3. 验证压缩效果
original_size = get_file_size(pdf)
compressed_size = get_file_size(compressed_pdf)
compression_ratio = (1 - compressed_size / original_size) * 100
# 4. 验证文件完整性
is_valid = pdfium.validate(compressed_pdf)
# 5. 验证对象流生成
has_object_stream = check_object_stream(compressed_pdf)
# 6. 记录测试结果
log_result({
'file': pdf.name,
'original_size': original_size,
'compressed_size': compressed_size,
'compression_ratio': compression_ratio,
'is_valid': is_valid,
'has_object_stream': has_object_stream
})
测试指标
| 指标 | 说明 | 验证方法 |
|---|---|---|
| 压缩率 | 压缩前后体积变化百分比 | 文件大小对比 |
| 文件完整性 | 压缩后文件是否可正常打开 | pdfium 验证 |
| 对象流生成 | 是否成功生成对象流 | qpdf --show-object-streams |
| 兼容性 | 是否兼容其他阅读器 | 使用多种阅读器打开测试 |
为什么选择 podofogc
podofogc 的优势
┌─────────────────────────────────────────────────────────────┐
│ podofogc 还原特性 │
├─────────────────────────────────────────────────────────────┤
│ ✓ 完全展开对象流为独立对象 │
│ ✓ 重建标准交叉引用表 │
│ ✓ 清理未引用的垃圾对象 │
│ ✓ 保持页面内容和结构完整 │
│ ✓ 生成干净的测试材料 │
└─────────────────────────────────────────────────────────────┘
与其他工具对比
| 工具 | 对象流还原 | 垃圾清理 | 输出质量 | 推荐度 |
|---|---|---|---|---|
| podofogc | ✓ | ✓ | 高 | ★★★★★ |
| qpdf --linearize | ✗ | ✓ | 中 | ★★★☆☆ |
| pdftk | ✗ | ✗ | 中 | ★★☆☆☆ |
注意事项
1. 测试材料一致性
确保所有测试文件都经过相同的还原流程:
python
# 建议:在测试前清理输出目录,确保每次测试使用相同的输入
import shutil
shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
2. 文件命名冲突
当 PRESERVE_STRUCTURE=False 时,同名文件会产生冲突:
python
# 如果需要平坦输出,建议启用覆盖或添加序号
OVERWRITE = True # 或在代码中添加自动重命名逻辑
3. 超时处理
对于超大 PDF 文件,建议增加超时时间:
python
# gc.py 第130行
timeout=600 # 从5分钟增加到10分钟
总结
gc.py 在您的 pdfium 对象流压缩测试流程中扮演关键的准备角色:
- 还原格式:将已压缩的 PDF 还原为标准交叉引用表格式
- 提供纯净材料:确保测试输入的一致性和可追溯性
- 支持批量处理:高效处理大量测试文件
- 辅助验证:通过 podofogc 的垃圾清理确保测试材料质量
通过 gc.py 准备的测试材料,可以更准确地评估您的 pdfium 对象流压缩功能的压缩率、正确性和兼容性。
完整代码
podofo_gc.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批量使用podofogc处理PDF文件 - 简化版
功能:遍历顶级目录及子目录中的所有PDF,调用podofogc进行垃圾清理(GC),保存到另一个目录
"""
import subprocess
import time
from pathlib import Path
from typing import Dict, List
# ==================== 配置参数(直接修改这里) ====================PODOFO_GC_PATH = r"D:/PDF_SOURCE/podofo_exe/podofogc.exe" # podofogc.exe的路径
INPUT_DIR = r"D:/Compress_output/F2" # 输入顶级目录
OUTPUT_DIR = r"D:/Compress_output/P2" # 输出顶级目录
RECURSIVE = True # 是否递归子目录
PRESERVE_STRUCTURE = True # 是否保留目录结构(True:保留, False:所有文件直接输出到根目录)
OVERWRITE = False # 是否覆盖已存在的文件(False:跳过已存在的文件)
# =================================================================
class PodofoGCProcessor:
"""Podofo GC批量处理器"""
def __init__(self):
"""初始化处理器"""
self.podofo_gc_path = Path(PODOFO_GC_PATH)
self.input_dir = Path(INPUT_DIR)
self.output_dir = Path(OUTPUT_DIR)
self.recursive = RECURSIVE
self.preserve_structure = PRESERVE_STRUCTURE
self.overwrite = OVERWRITE
# 验证podofogc是否存在
if not self.podofo_gc_path.exists():
raise FileNotFoundError(f"找不到podofogc程序: {self.podofo_gc_path}")
# 验证输入目录
if not self.input_dir.exists():
raise FileNotFoundError(f"输入目录不存在: {self.input_dir}")
# 创建输出目录
self.output_dir.mkdir(parents=True, exist_ok=True)
# 统计信息
self.stats = {
'total': 0,
'success': 0,
'failed': 0,
'skipped': 0,
'input_size_total': 0,
'output_size_total': 0
}
def get_pdf_files(self) -> List[Path]:
"""获取所有需要处理的PDF文件"""
if self.recursive:
# 递归查找所有PDF文件
pdf_files = list(self.input_dir.glob("**/*.pdf"))
else: # 只在当前目录查找
pdf_files = list(self.input_dir.glob("*.pdf"))
# 按路径排序,保持一致性
pdf_files.sort()
return pdf_files
def get_output_path(self, input_path: Path) -> Path:
"""根据输入路径计算输出路径"""
if self.preserve_structure:
# 保留目录结构
relative_path = input_path.relative_to(self.input_dir)
output_path = self.output_dir / relative_path
else:
# 平坦结构,所有文件直接放在输出目录
output_path = self.output_dir / input_path.name
return output_path
def process_single_file(self, input_path: Path) -> Dict:
"""处理单个PDF文件"""
result = {
'input': input_path,
'output': None,
'success': False,
'error': None,
'input_size': 0,
'output_size': 0
}
try: # 获取输出路径
output_path = self.get_output_path(input_path)
result['output'] = output_path
# 创建输出目录
output_path.parent.mkdir(parents=True, exist_ok=True)
# 检查输出文件是否已存在
if output_path.exists() and not self.overwrite:
result['error'] = "输出文件已存在,跳过"
return result
# 获取输入文件大小
input_size = input_path.stat().st_size
result['input_size'] = input_size
print(f"处理: {input_path.name}")
print(f" 源文件: {input_path}")
print(f" 目标文件: {output_path}")
# 构建命令: podofogc.exe input.pdf output.pdf
cmd = [
str(self.podofo_gc_path),
str(input_path),
str(output_path)
]
# 执行命令
start_time = time.time()
process = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding='utf-8',
errors='ignore',
timeout=300 # 5分钟超时
)
elapsed_time = time.time() - start_time
if process.returncode == 0:
# 成功
output_size = output_path.stat().st_size if output_path.exists() else 0
result['output_size'] = output_size
result['success'] = True
# 计算压缩率
if input_size > 0:
compression_ratio = (1 - output_size / input_size) * 100
else:
compression_ratio = 0
print(f" ✓ 成功! 耗时: {elapsed_time:.1f}s")
print(
f" 大小: {input_size / 1024:.1f}KB -> {output_size / 1024:.1f}KB (压缩率: {compression_ratio:.1f}%)")
else: # 失败
error_msg = process.stderr if process.stderr else process.stdout
result['error'] = error_msg or f"返回码: {process.returncode}"
print(f" ✗ 失败: {result['error'][:100]}")
except subprocess.TimeoutExpired:
result['error'] = "处理超时(超过5分钟)"
print(f" ✗ 超时: {input_path.name}")
except Exception as e:
result['error'] = str(e)
print(f" ✗ 异常: {e}")
return result
def process_all(self) -> Dict:
"""批量处理所有PDF文件"""
# 获取所有PDF文件
print(f"\n{'=' * 60}")
print(f"扫描目录: {self.input_dir}")
pdf_files = self.get_pdf_files()
if not pdf_files:
print(f"未找到任何PDF文件!")
return self.stats
self.stats['total'] = len(pdf_files)
print(f"找到 {len(pdf_files)} 个PDF文件")
print(f"输出目录: {self.output_dir}")
print(f"保留目录结构: {self.preserve_structure}")
print(f"覆盖已存在文件: {self.overwrite}")
print(f"{'=' * 60}\n")
# 开始处理
start_time = time.time()
for i, pdf_file in enumerate(pdf_files, 1):
print(f"\n[{i}/{len(pdf_files)}] 处理中...")
result = self.process_single_file(pdf_file)
if result['success']:
self.stats['success'] += 1
self.stats['input_size_total'] += result['input_size']
self.stats['output_size_total'] += result['output_size']
elif result['error'] and "跳过" in result['error']:
self.stats['skipped'] += 1
print(f" ⊙ 跳过: {result['error']}")
else: self.stats['failed'] += 1
elapsed_time = time.time() - start_time
self._print_summary(elapsed_time)
return self.stats
def _print_summary(self, elapsed_time: float):
"""打印处理总结"""
print(f"\n{'=' * 60}")
print("处理完成!统计信息:")
print(f" 总文件数: {self.stats['total']}")
print(f" 成功: {self.stats['success']}")
print(f" 失败: {self.stats['failed']}")
print(f" 跳过: {self.stats['skipped']}")
if self.stats['success'] > 0:
input_mb = self.stats['input_size_total'] / (1024 * 1024)
output_mb = self.stats['output_size_total'] / (1024 * 1024)
saved_mb = input_mb - output_mb
compression_ratio = (saved_mb / input_mb) * 100 if input_mb > 0 else 0
print(f" 总输入大小: {input_mb:.2f} MB")
print(f" 总输出大小: {output_mb:.2f} MB")
print(f" 节省空间: {saved_mb:.2f} MB ({compression_ratio:.1f}%)")
print(f" 总耗时: {elapsed_time:.1f} 秒")
print(f"{'=' * 60}")
def main():
"""主函数"""
try:
# 创建处理器并执行
processor = PodofoGCProcessor()
stats = processor.process_all()
# 根据结果返回退出码
return 0 if stats['failed'] == 0 else 1
except Exception as e:
print(f"程序执行失败: {e}")
return 1
if __name__ == "__main__":
exit(main())