andruhovski

How to Extract Pages from PDF

Oct 29th, 2026
197
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.76 KB | Source Code | 0 0
  1. """Extract selected physical pages into a PDF and an audit manifest.
  2.  
  3. Install: pip install aspose-pdf
  4. Run: python "src/06-pdf-extract pages/pdf-page-extractor.py"
  5.  
  6. Edit configuration below. "2,5,9-11" produces five pages; "5,2" preserves
  7. that order. Repeated pages are rejected. Numbers are one-based physical page
  8. positions, not printed labels. Set ASPOSE_PDF_LICENSE for the full example:
  9. evaluation mode adds watermarks and limits processing to the first four pages.
  10.  
  11. Each successful run creates a new directory containing evidence-packet.pdf
  12. and manifest.json. Files are staged beside that directory and published by
  13. directory rename after verification. No existing destination is overwritten;
  14. this is not a guarantee of atomic publication on every filesystem.
  15.  
  16. Verification compares page counts and extracted text in requested order.
  17. For a synthetic 20-page report, put a unique "Source page N" marker on each
  18. page and inspect the five resulting markers: 2, 5, 9, 10, 11. Scanned pages
  19. need visual review; matching empty text does not prove correct page content.
  20. Forms, annotations, bookmarks, links to omitted pages, and signatures need
  21. separate review. This script does not repair those document features.
  22.  
  23. API references:
  24. https://docs.aspose.com/pdf/python-net/extract-pages/
  25. https://reference.aspose.com/pdf/python-net/aspose.pdf.text/textabsorber/
  26. https://docs.aspose.com/pdf/python-net/licensing/
  27. https://docs.aspose.com/pdf/python-net/system-requirements/
  28. """
  29.  
  30. from datetime import datetime, timezone
  31. import hashlib
  32. from importlib.metadata import version
  33. import json
  34. import os
  35. from pathlib import Path
  36. import re
  37. import sys
  38. from tempfile import TemporaryDirectory
  39. from uuid import uuid4
  40.  
  41. import aspose.pdf as apdf
  42.  
  43.  
  44. # Configuration ---------------------------------------------------------------
  45. BASE_DIR = Path(__file__).resolve().parent
  46. INPUT_FILE = BASE_DIR / "data" / "input" / "report.pdf"
  47. OUTPUT_ROOT = BASE_DIR / "data" / "output"
  48. PAGE_SELECTION = "2,5,9-11"
  49. OUTPUT_FILENAME = "evidence-packet.pdf"
  50. PASSWORD = os.environ.get("PDF_PASSWORD")
  51. LICENSE_FILE = os.environ.get("ASPOSE_PDF_LICENSE")
  52.  
  53.  
  54. def parse_selection(selection: str, page_count: int) -> list[int]:
  55.     """Expand inclusive ranges, retaining order and rejecting duplicates.
  56.  
  57.    Invalid examples: "", "2,,5", "two", "5-2", "0", "2,2".
  58.    Validate range bounds before expansion to avoid allocating huge ranges.
  59.    """
  60.     if page_count < 1:
  61.         raise ValueError("Source PDF has no pages")
  62.     pages = []
  63.     seen = set()
  64.     for token in selection.split(","):
  65.         match = re.fullmatch(r"\s*([0-9]+)\s*(?:-\s*([0-9]+)\s*)?", token)
  66.         if not match:
  67.             raise ValueError(f"Invalid page selection item: {token!r}")
  68.         start = int(match[1])
  69.         end = int(match[2]) if match[2] is not None else start
  70.         if not 1 <= start <= end <= page_count:
  71.             raise ValueError(
  72.                 f"Invalid range {start}-{end}; source has {page_count} pages"
  73.             )
  74.         for page in range(start, end + 1):
  75.             if page in seen:
  76.                 raise ValueError(f"Repeated source page: {page}")
  77.             seen.add(page)
  78.             pages.append(page)
  79.     return pages
  80.  
  81.  
  82. def source_digest(path: Path) -> str:
  83.     """Identify source bytes without loading the entire PDF into memory."""
  84.     with path.open("rb") as stream:
  85.         return hashlib.file_digest(stream, "sha256").hexdigest()
  86.  
  87.  
  88. def page_text(document: apdf.Document, page_number: int) -> str:
  89.     absorber = apdf.text.TextAbsorber()
  90.     absorber.visit(document.pages[page_number])
  91.     return absorber.text
  92.  
  93.  
  94. def copy_pages(source: apdf.Document, pages: list[int], output_file: Path) -> None:
  95.     """Copy requested pages using Aspose's one-based page collection."""
  96.     with apdf.Document() as output:
  97.         output.ignore_corrupted_objects = False
  98.         for position, page_number in enumerate(pages, start=1):
  99.             try:
  100.                 output.pages.add(source.pages[page_number])
  101.             except Exception as exc:
  102.                 raise RuntimeError(
  103.                     f"Copying source page {page_number} to output page "
  104.                     f"{position} failed: {exc}"
  105.                 ) from exc
  106.         try:
  107.             output.save(str(output_file))
  108.         except Exception as exc:
  109.             raise RuntimeError(f"Saving {output_file.name} failed: {exc}") from exc
  110.  
  111.  
  112. def verify_packet(
  113.     source: apdf.Document, pages: list[int], output_file: Path
  114. ) -> None:
  115.     """Reopen the staged PDF and compare page count and text per position."""
  116.     with apdf.Document(str(output_file)) as output:
  117.         output.ignore_corrupted_objects = False
  118.         if len(output.pages) != len(pages):
  119.             raise ValueError(
  120.                 f"{output_file.name}: expected {len(pages)} pages, "
  121.                 f"found {len(output.pages)}"
  122.             )
  123.         for position, source_page in enumerate(pages, start=1):
  124.             if page_text(source, source_page) != page_text(output, position):
  125.                 raise ValueError(
  126.                     f"Text verification failed for output page {position} "
  127.                     f"(source page {source_page})"
  128.                 )
  129.  
  130.  
  131. def extract_packet(
  132.     input_file: Path,
  133.     output_root: Path,
  134.     selection: str,
  135.     password: str | None = None,
  136. ) -> Path:
  137.     """Validate, stage, verify, and publish a PDF and its manifest together."""
  138.     input_file = input_file.resolve(strict=True)
  139.     if not input_file.is_file():
  140.         raise ValueError(f"Input is not a file: {input_file}")
  141.     digest = source_digest(input_file)
  142.     try:
  143.         source = (
  144.             apdf.Document(str(input_file))
  145.             if password is None
  146.             else apdf.Document(str(input_file), password)
  147.         )
  148.     except Exception as exc:
  149.         raise RuntimeError(
  150.             f"Cannot open {input_file}; check PDF contents, permissions, "
  151.             f"and PDF_PASSWORD. Library error: {exc}"
  152.         ) from exc
  153.  
  154.     with source:
  155.         source.ignore_corrupted_objects = False
  156.         page_count = len(source.pages)
  157.         pages = parse_selection(selection, page_count)
  158.         output_root.mkdir(parents=True, exist_ok=True)
  159.         destination = output_root / f"packet-{uuid4().hex}"
  160.         if os.path.lexists(destination):
  161.             raise FileExistsError(f"Refusing to overwrite {destination}")
  162.         with TemporaryDirectory(prefix=".pdf-extract-", dir=output_root) as temp:
  163.             staging_dir = Path(temp) / "results"
  164.             staging_dir.mkdir()
  165.             output_file = staging_dir / OUTPUT_FILENAME
  166.             copy_pages(source, pages, output_file)
  167.             verify_packet(source, pages, output_file)
  168.             if source_digest(input_file) != digest:
  169.                 raise RuntimeError(f"Source changed during extraction: {input_file}")
  170.             manifest = {
  171.                 "created_utc": datetime.now(timezone.utc).isoformat(),
  172.                 "aspose_pdf_version": version("aspose-pdf"),
  173.                 "source": {
  174.                     "path": str(input_file),
  175.                     "sha256": digest,
  176.                     "page_count": page_count,
  177.                 },
  178.                 "selection": selection,
  179.                 "requested_pages": pages,
  180.                 "output_filename": OUTPUT_FILENAME,
  181.                 "output_sha256": source_digest(output_file),
  182.                 "page_mapping": [
  183.                     {"output_page": position, "source_page": page}
  184.                     for position, page in enumerate(pages, start=1)
  185.                 ],
  186.                 "verification": "page count and per-page extracted text matched",
  187.             }
  188.             manifest_file = staging_dir / "manifest.json"
  189.             manifest_file.write_text(
  190.                 json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
  191.             )
  192.             if json.loads(manifest_file.read_text(encoding="utf-8")) != manifest:
  193.                 raise ValueError("Staged manifest failed read-back verification")
  194.             if os.path.lexists(destination):
  195.                 raise FileExistsError(f"Refusing to overwrite {destination}")
  196.             staging_dir.rename(destination)
  197.     return destination
  198.  
  199.  
  200. def main() -> int:
  201.     try:
  202.         if LICENSE_FILE:
  203.             pdf_license = apdf.License()
  204.             pdf_license.set_license(LICENSE_FILE)
  205.  
  206.         destination = extract_packet(INPUT_FILE, OUTPUT_ROOT, PAGE_SELECTION, PASSWORD)
  207.         print(f"Extraction completed: {destination / OUTPUT_FILENAME}")
  208.         print(f"Manifest: {destination / 'manifest.json'}")
  209.         print("Verified page count and text; review layout and document features.")
  210.         return 0
  211.     except Exception as exc:
  212.         print(f"Extraction failed: {exc}", file=sys.stderr)
  213.         return 1
  214.  
  215. if __name__ == "__main__":
  216.     sys.exit(main())
  217.  
Advertisement