feat: improve skill import and runtime delivery
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user