Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """Rebuild a static PDF using original page identities; publish after verification."""
- import argparse
- from contextlib import ExitStack
- from importlib.metadata import version
- import json
- import os
- from pathlib import Path
- import re
- import sys
- from tempfile import TemporaryDirectory
- import aspose.pdf as pdf
- BASE = Path(__file__).resolve().parent
- MARKER = re.compile(r"\b(?:cover|original-[1-9][0-9]*)\b")
- def validate_plan(page_count, deleted, sequence):
- """Require every retained original exactly once, plus exactly one cover."""
- if any(type(n) is not int or not 1 <= n <= page_count for n in deleted):
- raise ValueError(f"Deleted pages must be integers in 1..{page_count}")
- if len(set(deleted)) != len(deleted):
- raise ValueError("Repeated deletion")
- expected = {f"original-{n}" for n in range(1, page_count + 1) if n not in deleted}
- expected.add("cover")
- if len(sequence) != len(set(sequence)):
- raise ValueError("Repeated identity in final sequence")
- if set(sequence) != expected:
- raise ValueError(f"Sequence mismatch: missing={sorted(expected - set(sequence))}, "
- f"unexpected={sorted(set(sequence) - expected)}")
- def delete_original_pages(document, deleted):
- """Call before inserting or moving anything; descending indices stay stable."""
- validate_plan(len(document.pages), deleted,
- ["cover"] + [f"original-{n}" for n in range(1, len(document.pages) + 1)
- if n not in deleted])
- for number in sorted(deleted, reverse=True):
- document.pages.delete(number)
- def insert_cover(document, cover, position=1):
- """One-based insertion position, including count + 1 for appending."""
- if type(position) is not int or not 1 <= position <= len(document.pages) + 1:
- raise ValueError("Cover position must be in 1..page_count + 1")
- if len(cover.pages) != 1:
- raise ValueError("Cover PDF must contain exactly one page")
- document.pages.insert(position, cover.pages[1])
- def add_blank_page(document, width=595, height=842):
- """Explicit dimensions in PDF points (72 points per inch)."""
- if width <= 0 or height <= 0:
- raise ValueError("Blank page dimensions must be positive")
- page = document.pages.add()
- page.set_page_size(width, height)
- return page
- def inspect_static(document, name):
- """Conservative scope gate, not a comprehensive PDF security audit."""
- if document.is_encrypted:
- raise ValueError(f"{name}: encrypted PDFs require a separate workflow")
- if len(document.outlines) or len(document.form.fields):
- raise ValueError(f"{name}: bookmarks or form/signature fields need separate handling")
- for n in range(1, len(document.pages) + 1):
- if len(document.pages[n].annotations):
- raise ValueError(f"{name}, page {n}: annotations/links require separate handling")
- def signature(page, allow_evaluation_watermark=False):
- absorber = pdf.text.TextAbsorber()
- absorber.visit(page)
- text = " ".join(absorber.text.split())
- if allow_evaluation_watermark:
- text = re.sub(r"Evaluation Only\. Created with Aspose\.PDF\. Copyright 2002-2026 Aspose Pty Ltd\.", "", text).strip()
- rect = page.rect
- return text, (rect.width, rect.height), str(page.rotate)
- def rebuild(report_file, cover_file, destination, deleted, sequence, allow_evaluation_watermark=False):
- """Inspect -> validate -> process -> stage -> verify -> publish.
- Inputs stay open until copied pages are saved. Context manager exits
- release .NET resources, including on errors. Existing output is refused.
- """
- report_file = Path(report_file).resolve(strict=True)
- cover_file = Path(cover_file).resolve(strict=True)
- destination = Path(destination).resolve()
- if destination in (report_file, cover_file) or os.path.lexists(destination):
- raise FileExistsError(f"Destination must be new and separate: {destination}")
- with ExitStack() as stack:
- def opened(path=None):
- doc = pdf.Document(str(path)) if path else pdf.Document()
- stack.enter_context(doc)
- return doc
- report, cover = opened(report_file), opened(cover_file)
- validate_plan(len(report.pages), deleted, sequence)
- if len(cover.pages) != 1:
- raise ValueError("Cover PDF must contain exactly one page")
- inspect_static(report, report_file.name)
- inspect_static(cover, cover_file.name)
- output = opened()
- expected = []
- for position, identity in enumerate(sequence, 1):
- source = cover.pages[1] if identity == "cover" else report.pages[int(identity[9:])]
- item = signature(source)
- if MARKER.findall(item[0]) != [identity]:
- raise ValueError(f"{identity}: expected one unique fixture marker; scans need OCR/review")
- expected.append(item)
- try:
- output.pages.add(source)
- except Exception as exc:
- raise RuntimeError(f"Copy {identity} to output page {position}: {exc}") from exc
- destination.parent.mkdir(parents=True, exist_ok=True)
- with TemporaryDirectory(prefix=".reorder-", dir=destination.parent) as temporary:
- staged = Path(temporary) / "report.pdf"
- output.save(str(staged))
- check = opened(staged)
- if len(check.pages) != len(sequence):
- raise ValueError("Saved page count differs from requested sequence")
- for position, identity in enumerate(sequence, 1):
- actual = signature(check.pages[position], allow_evaluation_watermark)
- text, size, rotation = expected[position - 1]
- if (actual[0] != text or actual[2] != rotation
- or any(abs(a - b) > 0.01 for a, b in zip(actual[1], size))):
- raise ValueError(f"Verification failed at output page {position} ({identity})")
- inspect_static(check, staged.name)
- # check remains open through publication; ExitStack releases it.
- # Hard-link publication refuses an existing target, including a race.
- # Same-filesystem hard links are required; failure leaves no final PDF.
- os.link(staged, destination)
- return destination
- def load_license():
- path = os.environ.get("ASPOSE_PDF_LICENSE")
- if path:
- license = pdf.License()
- license.set_license(str(Path(path).resolve(strict=True)))
- return license
- return None
- def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("--report", type=Path, default=BASE / "input/report.pdf")
- parser.add_argument("--cover", type=Path, default=BASE / "input/cover.pdf")
- parser.add_argument("--output", type=Path, default=BASE / "output/repaired-report.pdf")
- parser.add_argument("--plan", type=Path, default=BASE / "plans/repair.json")
- parser.add_argument("--allow-evaluation-watermark", action="store_true")
- args = parser.parse_args()
- try:
- license = load_license() # Keep the license alive throughout processing.
- plan = json.loads(args.plan.read_text(encoding="utf-8"))
- result = rebuild(args.report, args.cover, args.output, plan["delete"], plan["sequence"], args.allow_evaluation_watermark)
- print(f"Verified {len(plan['sequence'])} pages: {', '.join(plan['sequence'])}")
- print(f"Published: {result}")
- print(f"Aspose.PDF {version('aspose-pdf')}; licensed={license is not None}")
- except Exception as exc:
- print(f"FAILED ({args.report} -> {args.output}): {exc}", file=sys.stderr)
- return 1
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
Advertisement