200 lines
5.9 KiB
Python
200 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify PDF text gates, blank pages, and optionally render page previews."""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import fitz
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(description="核验长文 PDF 产物")
|
|
parser.add_argument("pdf", help="待核验 PDF")
|
|
parser.add_argument("--forbid", nargs="*", default=[], help="禁用关键词")
|
|
parser.add_argument(
|
|
"--allow-blank-page",
|
|
action="append",
|
|
type=int,
|
|
default=[],
|
|
help="允许为空白的页码,可重复指定",
|
|
)
|
|
parser.add_argument(
|
|
"--min-text-chars",
|
|
type=int,
|
|
default=30,
|
|
help="无图片页面低于该文本长度时视为疑似空白",
|
|
)
|
|
parser.add_argument("--json", dest="json_path", help="JSON 报告输出路径")
|
|
parser.add_argument("--render-dir", help="逐页 PNG 输出目录")
|
|
parser.add_argument("--dpi", type=int, default=300, help="页面渲染 DPI")
|
|
parser.add_argument(
|
|
"--no-fail",
|
|
action="store_true",
|
|
help="发现乱码、禁用词或非豁免空白页时仍返回 0",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def inspect_document(
|
|
document,
|
|
path,
|
|
forbidden=(),
|
|
min_text_chars=30,
|
|
allowed_blank_pages=(),
|
|
):
|
|
allowed = set(allowed_blank_pages)
|
|
terms = [term for term in dict.fromkeys(forbidden) if term]
|
|
forbidden_hits = {term: {"count": 0, "pages": []} for term in terms}
|
|
pages = []
|
|
replacement_characters = 0
|
|
suspicious_blank_pages = []
|
|
for index, page in enumerate(document):
|
|
page_number = index + 1
|
|
text = page.get_text().strip()
|
|
image_count = len(page.get_images(full=True))
|
|
pages.append(
|
|
{
|
|
"page": page_number,
|
|
"text_chars": len(text),
|
|
"images": image_count,
|
|
}
|
|
)
|
|
replacement_characters += text.count("\ufffd")
|
|
if (
|
|
len(text) < min_text_chars
|
|
and image_count == 0
|
|
and page_number not in allowed
|
|
):
|
|
suspicious_blank_pages.append(page_number)
|
|
for term in terms:
|
|
count = text.count(term)
|
|
if count:
|
|
forbidden_hits[term]["count"] += count
|
|
forbidden_hits[term]["pages"].append(page_number)
|
|
|
|
return {
|
|
"file": str(path),
|
|
"page_count": len(document),
|
|
"replacement_characters": replacement_characters,
|
|
"forbidden": {
|
|
term: result
|
|
for term, result in forbidden_hits.items()
|
|
if result["count"]
|
|
},
|
|
"suspicious_blank_pages": suspicious_blank_pages,
|
|
"allowed_blank_pages": sorted(allowed),
|
|
"pages": pages,
|
|
}
|
|
|
|
|
|
def inspect_pdf(pdf_path, forbidden=(), min_text_chars=30, allowed_blank_pages=()):
|
|
path = Path(pdf_path).expanduser().resolve()
|
|
if not path.is_file():
|
|
raise FileNotFoundError(f"PDF 不存在:{path}")
|
|
document = fitz.open(path)
|
|
try:
|
|
return inspect_document(
|
|
document,
|
|
path,
|
|
forbidden=forbidden,
|
|
min_text_chars=min_text_chars,
|
|
allowed_blank_pages=allowed_blank_pages,
|
|
)
|
|
finally:
|
|
document.close()
|
|
|
|
|
|
def render_document(document, output_dir, dpi=300):
|
|
if dpi < 72:
|
|
raise ValueError("dpi 不能低于 72")
|
|
output = Path(output_dir).expanduser().resolve()
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
existing = sorted(output.glob("page-*.png"))
|
|
if existing:
|
|
raise FileExistsError(
|
|
f"渲染目录已有页面图,请改用空目录:{output}"
|
|
)
|
|
scale = dpi / 72
|
|
matrix = fitz.Matrix(scale, scale)
|
|
digits = max(3, len(str(len(document))))
|
|
rendered = []
|
|
for index, page in enumerate(document):
|
|
target = output / f"page-{index + 1:0{digits}d}.png"
|
|
page.get_pixmap(matrix=matrix, alpha=False).save(target)
|
|
rendered.append(str(target))
|
|
return rendered
|
|
|
|
|
|
def render_pages(pdf_path, output_dir, dpi=300):
|
|
document = fitz.open(Path(pdf_path).expanduser().resolve())
|
|
try:
|
|
return render_document(document, output_dir, dpi=dpi)
|
|
finally:
|
|
document.close()
|
|
|
|
|
|
def has_failures(report):
|
|
return bool(
|
|
report["replacement_characters"]
|
|
or report["forbidden"]
|
|
or report["suspicious_blank_pages"]
|
|
)
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
pdf_path = Path(args.pdf).expanduser().resolve()
|
|
if not pdf_path.is_file():
|
|
raise FileNotFoundError(f"PDF 不存在:{pdf_path}")
|
|
json_path = (
|
|
Path(args.json_path).expanduser().resolve()
|
|
if args.json_path
|
|
else None
|
|
)
|
|
render_dir = (
|
|
Path(args.render_dir).expanduser().resolve()
|
|
if args.render_dir
|
|
else None
|
|
)
|
|
if json_path == pdf_path:
|
|
raise ValueError("JSON 报告路径不能覆盖输入 PDF")
|
|
if render_dir == pdf_path:
|
|
raise ValueError("渲染目录不能与输入 PDF 同路径")
|
|
|
|
document = fitz.open(pdf_path)
|
|
try:
|
|
report = inspect_document(
|
|
document,
|
|
pdf_path,
|
|
forbidden=args.forbid,
|
|
min_text_chars=args.min_text_chars,
|
|
allowed_blank_pages=args.allow_blank_page,
|
|
)
|
|
if render_dir:
|
|
report["rendered_pages"] = render_document(
|
|
document,
|
|
render_dir,
|
|
args.dpi,
|
|
)
|
|
finally:
|
|
document.close()
|
|
|
|
output = json.dumps(report, ensure_ascii=False, indent=2)
|
|
if json_path:
|
|
json_path.parent.mkdir(parents=True, exist_ok=True)
|
|
json_path.write_text(output + "\n", encoding="utf-8")
|
|
print(f"report: {json_path}")
|
|
else:
|
|
print(output)
|
|
|
|
if has_failures(report) and not args.no_fail:
|
|
print("PDF 核验失败:存在乱码、禁用词或疑似空白页。", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|