andruhovski

How to Reorder Pages

Oct 29th, 2026
261
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.77 KB | Source Code | 0 0
  1. """Rebuild a static PDF using original page identities; publish after verification."""
  2.  
  3. import argparse
  4. from contextlib import ExitStack
  5. from importlib.metadata import version
  6. import json
  7. import os
  8. from pathlib import Path
  9. import re
  10. import sys
  11. from tempfile import TemporaryDirectory
  12.  
  13. import aspose.pdf as pdf
  14.  
  15. BASE = Path(__file__).resolve().parent
  16. MARKER = re.compile(r"\b(?:cover|original-[1-9][0-9]*)\b")
  17.  
  18.  
  19. def validate_plan(page_count, deleted, sequence):
  20.     """Require every retained original exactly once, plus exactly one cover."""
  21.     if any(type(n) is not int or not 1 <= n <= page_count for n in deleted):
  22.         raise ValueError(f"Deleted pages must be integers in 1..{page_count}")
  23.     if len(set(deleted)) != len(deleted):
  24.         raise ValueError("Repeated deletion")
  25.     expected = {f"original-{n}" for n in range(1, page_count + 1) if n not in deleted}
  26.     expected.add("cover")
  27.     if len(sequence) != len(set(sequence)):
  28.         raise ValueError("Repeated identity in final sequence")
  29.     if set(sequence) != expected:
  30.         raise ValueError(f"Sequence mismatch: missing={sorted(expected - set(sequence))}, "
  31.                          f"unexpected={sorted(set(sequence) - expected)}")
  32.  
  33.  
  34. def delete_original_pages(document, deleted):
  35.     """Call before inserting or moving anything; descending indices stay stable."""
  36.     validate_plan(len(document.pages), deleted,
  37.                   ["cover"] + [f"original-{n}" for n in range(1, len(document.pages) + 1)
  38.                                if n not in deleted])
  39.     for number in sorted(deleted, reverse=True):
  40.         document.pages.delete(number)
  41.  
  42.  
  43. def insert_cover(document, cover, position=1):
  44.     """One-based insertion position, including count + 1 for appending."""
  45.     if type(position) is not int or not 1 <= position <= len(document.pages) + 1:
  46.         raise ValueError("Cover position must be in 1..page_count + 1")
  47.     if len(cover.pages) != 1:
  48.         raise ValueError("Cover PDF must contain exactly one page")
  49.     document.pages.insert(position, cover.pages[1])
  50.  
  51.  
  52. def add_blank_page(document, width=595, height=842):
  53.     """Explicit dimensions in PDF points (72 points per inch)."""
  54.     if width <= 0 or height <= 0:
  55.         raise ValueError("Blank page dimensions must be positive")
  56.     page = document.pages.add()
  57.     page.set_page_size(width, height)
  58.     return page
  59.  
  60.  
  61. def inspect_static(document, name):
  62.     """Conservative scope gate, not a comprehensive PDF security audit."""
  63.     if document.is_encrypted:
  64.         raise ValueError(f"{name}: encrypted PDFs require a separate workflow")
  65.     if len(document.outlines) or len(document.form.fields):
  66.         raise ValueError(f"{name}: bookmarks or form/signature fields need separate handling")
  67.     for n in range(1, len(document.pages) + 1):
  68.         if len(document.pages[n].annotations):
  69.             raise ValueError(f"{name}, page {n}: annotations/links require separate handling")
  70.  
  71.  
  72. def signature(page, allow_evaluation_watermark=False):
  73.     absorber = pdf.text.TextAbsorber()
  74.     absorber.visit(page)
  75.     text = " ".join(absorber.text.split())
  76.     if allow_evaluation_watermark:
  77.         text = re.sub(r"Evaluation Only\. Created with Aspose\.PDF\. Copyright 2002-2026 Aspose Pty Ltd\.", "", text).strip()
  78.     rect = page.rect
  79.     return text, (rect.width, rect.height), str(page.rotate)
  80.  
  81.  
  82. def rebuild(report_file, cover_file, destination, deleted, sequence, allow_evaluation_watermark=False):
  83.     """Inspect -> validate -> process -> stage -> verify -> publish.
  84.  
  85.    Inputs stay open until copied pages are saved. Context manager exits
  86.    release .NET resources, including on errors. Existing output is refused.
  87.    """
  88.     report_file = Path(report_file).resolve(strict=True)
  89.     cover_file = Path(cover_file).resolve(strict=True)
  90.     destination = Path(destination).resolve()
  91.     if destination in (report_file, cover_file) or os.path.lexists(destination):
  92.         raise FileExistsError(f"Destination must be new and separate: {destination}")
  93.     with ExitStack() as stack:
  94.         def opened(path=None):
  95.             doc = pdf.Document(str(path)) if path else pdf.Document()
  96.             stack.enter_context(doc)
  97.             return doc
  98.  
  99.         report, cover = opened(report_file), opened(cover_file)
  100.         validate_plan(len(report.pages), deleted, sequence)
  101.         if len(cover.pages) != 1:
  102.             raise ValueError("Cover PDF must contain exactly one page")
  103.         inspect_static(report, report_file.name)
  104.         inspect_static(cover, cover_file.name)
  105.         output = opened()
  106.         expected = []
  107.         for position, identity in enumerate(sequence, 1):
  108.             source = cover.pages[1] if identity == "cover" else report.pages[int(identity[9:])]
  109.             item = signature(source)
  110.             if MARKER.findall(item[0]) != [identity]:
  111.                 raise ValueError(f"{identity}: expected one unique fixture marker; scans need OCR/review")
  112.             expected.append(item)
  113.             try:
  114.                 output.pages.add(source)
  115.             except Exception as exc:
  116.                 raise RuntimeError(f"Copy {identity} to output page {position}: {exc}") from exc
  117.         destination.parent.mkdir(parents=True, exist_ok=True)
  118.         with TemporaryDirectory(prefix=".reorder-", dir=destination.parent) as temporary:
  119.             staged = Path(temporary) / "report.pdf"
  120.             output.save(str(staged))
  121.             check = opened(staged)
  122.             if len(check.pages) != len(sequence):
  123.                 raise ValueError("Saved page count differs from requested sequence")
  124.             for position, identity in enumerate(sequence, 1):
  125.                 actual = signature(check.pages[position], allow_evaluation_watermark)
  126.                 text, size, rotation = expected[position - 1]
  127.                 if (actual[0] != text or actual[2] != rotation
  128.                         or any(abs(a - b) > 0.01 for a, b in zip(actual[1], size))):
  129.                     raise ValueError(f"Verification failed at output page {position} ({identity})")
  130.             inspect_static(check, staged.name)
  131.             # check remains open through publication; ExitStack releases it.
  132.             # Hard-link publication refuses an existing target, including a race.
  133.             # Same-filesystem hard links are required; failure leaves no final PDF.
  134.             os.link(staged, destination)
  135.     return destination
  136.  
  137.  
  138. def load_license():
  139.     path = os.environ.get("ASPOSE_PDF_LICENSE")
  140.     if path:
  141.         license = pdf.License()
  142.         license.set_license(str(Path(path).resolve(strict=True)))
  143.         return license
  144.     return None
  145.  
  146.  
  147. def main():
  148.     parser = argparse.ArgumentParser(description=__doc__)
  149.     parser.add_argument("--report", type=Path, default=BASE / "input/report.pdf")
  150.     parser.add_argument("--cover", type=Path, default=BASE / "input/cover.pdf")
  151.     parser.add_argument("--output", type=Path, default=BASE / "output/repaired-report.pdf")
  152.     parser.add_argument("--plan", type=Path, default=BASE / "plans/repair.json")
  153.     parser.add_argument("--allow-evaluation-watermark", action="store_true")
  154.     args = parser.parse_args()
  155.     try:
  156.         license = load_license()  # Keep the license alive throughout processing.
  157.         plan = json.loads(args.plan.read_text(encoding="utf-8"))
  158.         result = rebuild(args.report, args.cover, args.output, plan["delete"], plan["sequence"], args.allow_evaluation_watermark)
  159.         print(f"Verified {len(plan['sequence'])} pages: {', '.join(plan['sequence'])}")
  160.         print(f"Published: {result}")
  161.         print(f"Aspose.PDF {version('aspose-pdf')}; licensed={license is not None}")
  162.     except Exception as exc:
  163.         print(f"FAILED ({args.report} -> {args.output}): {exc}", file=sys.stderr)
  164.         return 1
  165.     return 0
  166.  
  167.  
  168. if __name__ == "__main__":
  169.     raise SystemExit(main())
  170.  
  171.  
  172.  
Advertisement