Coverage for src/local_deep_research/utilities/pdf_page_walk.py: 92%
22 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""Walk a pdfplumber document's pages lazily.
3pdfplumber's ``PDF.pages`` builds a ``Page`` for every page of the
4document on first access, and ``PDF.close()`` -- which leaving a
5``with pdfplumber.open(...)`` block calls -- accesses ``PDF.pages`` too.
6A small upload whose page tree declares 100,000 pages therefore costs
7O(declared pages) time and memory before any page ceiling is consulted
8(#6771). The upload validator (``security/file_upload_validator``)
9avoids that by walking pdfminer's page tree with
10``PDFPage.create_pages``, which yields one page at a time; this module
11does the same for the text-extraction walks, so a walk that stops at
12``MAX_PDF_EXTRACTION_PAGES`` builds at most that many pages (plus the
13one that tells it there are more).
15Callers that walk this way must not use ``with pdfplumber.open(...)``
16nor call ``PDF.close()``, which would build every page anyway: they
17open the stream themselves, close it themselves, and close each page
18they were given.
20A walk stopped early knows only a lower bound on the page count. The
21page tree's declared ``/Count`` is deliberately not read to fill the
22gap: it is normally an indirect reference, and resolving one can loop
23inside pdfminer with no limit a caller can impose (a self-reference,
24an object-stream entry whose stream is a self-reference, a stream whose
25``/Length`` is one), outside the extraction's CPU budget.
26"""
28from __future__ import annotations
30from typing import TYPE_CHECKING, Any, Iterator, Optional
32if TYPE_CHECKING:
33 from .pdf_stream_release import PdfplumberWalkReleaser
36def iter_pdfplumber_pages(
37 pdf: Any, releaser: Optional[PdfplumberWalkReleaser] = None
38) -> Iterator[Any]:
39 """Yield *pdf*'s pages as pdfplumber ``Page`` objects, one at a time.
41 The page tree is walked only as far as the caller iterates; each
42 ``Page`` is built exactly as ``PDF.pages`` builds it (same page
43 number and running ``initial_doctop``). An error the page-tree walk
44 raises is re-raised as ``PdfminerException``, as ``PDF.pages`` does.
46 Building a page resolves its ``/Resources``, ``/Contents`` and the
47 page-tree nodes above it, which caches those objects on the
48 document. When a *releaser* is given, its ``before_page()`` is
49 called here, just before each page is built, so what building the
50 page caches is inside its window and counted by the ``after_page()``
51 the caller makes once done with the page. The caller then must not
52 call ``before_page()`` itself: that would move the window's start
53 past the page's construction.
54 """
55 from pdfminer.pdfpage import PDFPage
56 from pdfplumber.page import Page
57 from pdfplumber.utils.exceptions import PdfminerException
59 generator = PDFPage.create_pages(pdf.doc)
60 doctop = 0
61 page_number = 0
62 while True:
63 if releaser is not None:
64 releaser.before_page()
65 try:
66 page_obj = next(generator)
67 except StopIteration:
68 return
69 except Exception as exc:
70 raise PdfminerException(exc) from exc
71 page_number += 1
72 page = Page(
73 pdf, page_obj, page_number=page_number, initial_doctop=doctop
74 )
75 doctop += page.height
76 yield page