How to Extract Pages from PDF
  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. """
  25.  
  26. from datetime import datetime, timezone
  27. import hashlib
  28. from importlib.metadata import version
  29. import json
  30. import os
  31. from pathlib import Path
  32. import re
  33. import sys
  34. from tempfile import TemporaryDirectory
  35. from uuid import uuid4
  36.  
  37. import aspose.pdf as apdf
  38.  
  39.  
  40. # Configuration ---------------------------------------------------------------
  41. BASE_DIR = Path(__file__).resolve().parent
  42. INPUT_FILE = BASE_DIR / "data" / "input" / "report.pdf"
  43. OUTPUT_ROOT = BASE_DIR / "data" / "output"
  44. PAGE_SELECTION = "2,5,9-11"
  45. OUTPUT_FILENAME = "evidence-packet.pdf"
  46. PASSWORD = os.environ.get("PDF_PASSWORD")
  47. LICENSE_FILE = os.environ.get("ASPOSE_PDF_LICENSE")
  48.  
  49.  
  50. def parse_selection(selection: str, page_count: int) -> list[int]:
  51. """Expand inclusive ranges, retaining order and rejecting duplicates.
  52.  
  53. Invalid examples: "", "2,,5", "two", "5-2", "0", "2,2".
  54. Validate range bounds before expansion to avoid allocating huge ranges.
  55. """
  56. if page_count < 1:
  57. raise ValueError("Source PDF has no pages")
  58. pages = []
  59. seen = set()
  60. for token in selection.split(","):
  61. match = re.fullmatch(r"\s*([0-9]+)\s*(?:-\s*([0-9]+)\s*)?", token)
  62. if not match:
  63. raise ValueError(f"Invalid page selection item: {token!r}")
  64. start = int(match[1])
  65. end = int(match[2]) if match[2] is not None else start
  66. if not 1 <= start <= end <= page_count:
  67. raise ValueError(
  68. f"Invalid range {start}-{end}; source has {page_count} pages"
  69. )
  70. for page in range(start, end + 1):
  71. if page in seen:
  72. raise ValueError(f"Repeated source page: {page}")
  73. seen.add(page)
  74. pages.append(page)
  75. return pages
  76.  
  77.  
  78. def source_digest(path: Path) -> str:
  79. """Identify source bytes without loading the entire PDF into memory."""
  80. with path.open("rb") as stream:
  81. return hashlib.file_digest(stream, "sha256").hexdigest()
  82.  
  83.  
  84. def page_text(document: apdf.Document, page_number: int) -> str:
  85. absorber = apdf.text.TextAbsorber()
  86. absorber.visit(document.pages[page_number])
  87. return absorber.text
  88.  
  89.  
  90. def copy_pages(source: apdf.Document, pages: list[int], output_file: Path) -> None:
  91. """Copy requested pages using Aspose's one-based page collection."""
  92. with apdf.Document() as output:
  93. output.ignore_corrupted_objects = False
  94. for position, page_number in enumerate(pages, start=1):
  95. try:
  96. output.pages.add(source.pages[page_number])
  97. except Exception as exc:
  98. raise RuntimeError(
  99. f"Copying source page {page_number} to output page "
  100. f"{position} failed: {exc}"
  101. ) from exc
  102. try:
  103. output.save(str(output_file))
  104. except Exception as exc:
  105. raise RuntimeError(f"Saving {output_file.name} failed: {exc}") from exc
  106.  
  107.  
  108. def verify_packet(
  109. source: apdf.Document, pages: list[int], output_file: Path
  110. ) -> None:
  111. """Reopen the staged PDF and compare page count and text per position."""
  112. with apdf.Document(str(output_file)) as output:
  113. output.ignore_corrupted_objects = False
  114. if len(output.pages) != len(pages):
  115. raise ValueError(
  116. f"{output_file.name}: expected {len(pages)} pages, "
  117. f"found {len(output.pages)}"
  118. )
  119. for position, source_page in enumerate(pages, start=1):
  120. if page_text(source, source_page) != page_text(output, position):
  121. raise ValueError(
  122. f"Text verification failed for output page {position} "
  123. f"(source page {source_page})"
  124. )
  125.  
  126.  
  127. def extract_packet(
  128. input_file: Path,
  129. output_root: Path,
  130. selection: str,
  131. password: str | None = None,
  132. ) -> Path:
  133. """Validate, stage, verify, and publish a PDF and its manifest together."""
  134. input_file = input_file.resolve(strict=True)
  135. if not input_file.is_file():
  136. raise ValueError(f"Input is not a file: {input_file}")
  137. digest = source_digest(input_file)
  138. try:
  139. source = (
  140. apdf.Document(str(input_file))
  141. if password is None
  142. else apdf.Document(str(input_file), password)
  143. )
  144. except Exception as exc:
  145. raise RuntimeError(
  146. f"Cannot open {input_file}; check PDF contents, permissions, "
  147. f"and PDF_PASSWORD. Library error: {exc}"
  148. ) from exc
  149.  
  150. with source:
  151. source.ignore_corrupted_objects = False
  152. page_count = len(source.pages)
  153. pages = parse_selection(selection, page_count)
  154. output_root.mkdir(parents=True, exist_ok=True)
  155. destination = output_root / f"packet-{uuid4().hex}"
  156. if os.path.lexists(destination):
  157. raise FileExistsError(f"Refusing to overwrite {destination}")
  158. with TemporaryDirectory(prefix=".pdf-extract-", dir=output_root) as temp:
  159. staging_dir = Path(temp) / "results"
  160. staging_dir.mkdir()
  161. output_file = staging_dir / OUTPUT_FILENAME
  162. copy_pages(source, pages, output_file)
  163. verify_packet(source, pages, output_file)
  164. if source_digest(input_file) != digest:
  165. raise RuntimeError(f"Source changed during extraction: {input_file}")
  166. manifest = {
  167. "created_utc": datetime.now(timezone.utc).isoformat(),
  168. "aspose_pdf_version": version("aspose-pdf"),
  169. "source": {
  170. "path": str(input_file),
  171. "sha256": digest,
  172. "page_count": page_count,
  173. },
  174. "selection": selection,
  175. "requested_pages": pages,
  176. "output_filename": OUTPUT_FILENAME,
  177. "output_sha256": source_digest(output_file),
  178. "page_mapping": [
  179. {"output_page": position, "source_page": page}
  180. for position, page in enumerate(pages, start=1)
  181. ],
  182. "verification": "page count and per-page extracted text matched",
  183. }
  184. manifest_file = staging_dir / "manifest.json"
  185. manifest_file.write_text(
  186. json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
  187. )
  188. if json.loads(manifest_file.read_text(encoding="utf-8")) != manifest:
  189. raise ValueError("Staged manifest failed read-back verification")
  190. if os.path.lexists(destination):
  191. raise FileExistsError(f"Refusing to overwrite {destination}")
  192. staging_dir.rename(destination)
  193. return destination
  194.  
  195.  
  196. def main() -> int:
  197. try:
  198. if LICENSE_FILE:
  199. pdf_license = apdf.License()
  200. pdf_license.set_license(LICENSE_FILE)
  201.  
  202. destination = extract_packet(INPUT_FILE, OUTPUT_ROOT, PAGE_SELECTION, PASSWORD)
  203. print(f"Extraction completed: {destination / OUTPUT_FILENAME}")
  204. print(f"Manifest: {destination / 'manifest.json'}")
  205. print("Verified page count and text; review layout and document features.")
  206. return 0
  207. except Exception as exc:
  208. print(f"Extraction failed: {exc}", file=sys.stderr)
  209. return 1
  210.  
  211. if __name__ == "__main__":
  212. sys.exit(main())
  213.  
Pasted 2026-10-29 02:03:39

Short link: