1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
| """按指定图片压缩质量压缩 PDF,尽量保留文字、矢量内容、页面尺寸和链接。
图片会重新编码以减小体积,因此不是像素级无损;文字和版式不会被栅格化。
示例: python compress_pdf.py input.pdf 92 python compress_pdf.py input.pdf 85 output.pdf """
from __future__ import annotations
import argparse import hashlib import os import sys import tempfile from pathlib import Path
try: import fitz except ImportError as exc: raise SystemExit( "缺少 PyMuPDF。请使用当前 Python 环境安装:\n" "/Users/pepper/.venv/codex/bin/pip install PyMuPDF" ) from exc
def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="按指定压缩质量压缩 PDF 内嵌图片,不设置目标文件体积。" ) parser.add_argument("input", type=Path, help="输入 PDF 文件") parser.add_argument( "quality", type=int, help="压缩质量/比例,1-100;数值越高,画质越好、文件通常越大", ) parser.add_argument( "output", type=Path, nargs="?", help="输出 PDF 文件;省略时自动生成 *_compressed.pdf", ) return parser.parse_args()
def text_fingerprint( pdf_path: Path, ) -> tuple[int, str, tuple[tuple[float, float, int], ...]]: """返回页数、文本指纹和页面几何信息。""" with fitz.open(pdf_path) as document: page_text = [page.get_text("text") for page in document] geometry = tuple( ( round(page.rect.width, 3), round(page.rect.height, 3), page.rotation, ) for page in document ) digest = hashlib.sha256( "\n\f\n".join(page_text).encode("utf-8") ).hexdigest() return len(page_text), digest, geometry
def compress_once(input_path: Path, temporary_path: Path, quality: int) -> None: """使用 PyMuPDF 重编码图片并写入临时 PDF。""" with fitz.open(input_path) as document: document.rewrite_images( dpi_threshold=0, dpi_target=0, quality=quality, lossy=True, lossless=True, bitonal=True, color=True, gray=True, ) document.save( temporary_path, garbage=4, clean=True, deflate=True, deflate_images=False, deflate_fonts=True, use_objstms=True, compression_effort=100, )
def compress_pdf(input_path: Path, output_path: Path, quality: int) -> int: input_path = input_path.expanduser().resolve() output_path = output_path.expanduser().resolve()
if not input_path.is_file(): raise FileNotFoundError(f"找不到输入文件:{input_path}") if input_path.suffix.lower() != ".pdf": raise ValueError("输入文件必须是 PDF") if output_path == input_path: raise ValueError("输出路径不能与输入路径相同,以免覆盖原文件") if not 1 <= quality <= 100: raise ValueError("压缩质量/比例必须是 1-100 的整数")
output_path.parent.mkdir(parents=True, exist_ok=True) source_fingerprint = text_fingerprint(input_path) temporary_path: Path | None = None
try: with tempfile.NamedTemporaryFile( prefix=f".{output_path.stem}.", suffix=".tmp.pdf", dir=output_path.parent, delete=False, ) as temporary_file: temporary_path = Path(temporary_file.name)
temporary_path.unlink() compress_once(input_path, temporary_path, quality) size = temporary_path.stat().st_size os.replace(temporary_path, output_path) temporary_path = None
output_fingerprint = text_fingerprint(output_path) if output_fingerprint != source_fingerprint: try: output_path.unlink() except FileNotFoundError: pass raise RuntimeError("压缩后文本或页面几何信息与原文件不一致,已删除异常输出。")
return size finally: if temporary_path is not None: try: temporary_path.unlink() except FileNotFoundError: pass
def main() -> int: args = parse_args() input_path = args.input output_path = args.output if output_path is None: output_path = input_path.with_name(f"{input_path.stem}_compressed.pdf")
try: size = compress_pdf( input_path=input_path, output_path=output_path, quality=args.quality, ) except (FileNotFoundError, ValueError, RuntimeError) as exc: print(f"错误:{exc}", file=sys.stderr) return 1
print(f"已生成:{output_path.expanduser().resolve()}") print(f"文件大小:{size:,} 字节({size / 1_000_000:.2f} MB)") print(f"压缩质量/比例:{args.quality}") return 0
if __name__ == "__main__": raise SystemExit(main())
|