Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions pageindex/flash/parser_pdfium_charlevel/pdf_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,16 @@ def get_fonts(self, full: bool = True):
class _PdfDoc:
"""PyPDF2-backed adapter for raw object and stream access PDFium cannot expose."""

__slots__ = ("_reader", "_virtual")
__slots__ = ("_reader", "_virtual", "_page_count")

def __init__(self, reader):
self._reader = reader
# PyPDF2 parses lazily, so a document it cannot read (AES without the
# crypto backend, damaged xref) raises on first object access rather
# than at PdfReader(). Resolving the page tree here keeps that failure
# inside the callers' guarded open, which is what lets them fall back
# to the PDFium-only channel instead of propagating.
self._page_count = len(reader.pages)
# Negative pseudo-xrefs for DIRECT (inline) dicts that have no object
# number -- text extraction reference resolution treats direct and indirect values alike,
# so inline font dicts must be addressable by the same integer-keyed
Expand All @@ -122,7 +128,7 @@ def register_virtual(self, obj) -> int:

@property
def page_count(self) -> int:
return len(self._reader.pages)
return self._page_count

def __getitem__(self, idx):
return _PdfPage(self._reader.pages[idx])
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ openai = ">=1.70.0"
openai-agents = ">=0.18.1"
litellm = ">=1.97.0"
PyPDF2 = ">=3.0.0"
pycryptodome = ">=3.15.0"
pypdfium2 = ">=5"
sortedcontainers = ">=2.4.0"
regex = ">=2024.0.0"
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ openai>=1.70.0
requests>=2.28.0
openai-agents>=0.18.1
# pymupdf # optional
pycryptodome==3.23.0
PyPDF2==3.0.1
pypdfium2==5.13.0
python-dotenv==1.2.2
Expand Down
53 changes: 53 additions & 0 deletions tests/test_issue_426.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""PyPDF2's lazy parse must fail inside the callers' guarded open (#426).

An AES-encrypted PDF raises DependencyError on first object access, not at
PdfReader(), so the guard around the PyPDF2 open used to miss it and the
error surfaced from _page_pass1's page_count instead — an unhandled crash on
a document PDFium reads fine on its own.
"""
import pytest

from PyPDF2.errors import DependencyError


class _LazyBombReader:
"""A reader that parses lazily and fails on first object access, the way
an AES-encrypted document does without the crypto backend installed."""

@property
def pages(self):
raise DependencyError("PyCryptodome is required for AES algorithm")


def test_pdf_doc_open_fails_at_construction():
"""_PdfDoc resolves the page tree eagerly, so an unreadable document
raises where both callers already guard the open (the sequential pipeline
and the parallel worker init) rather than at first page_count."""
from pageindex.flash.parser_pdfium_charlevel.pdf_objects import _PdfDoc

with pytest.raises(DependencyError):
_PdfDoc(_LazyBombReader())


def test_flash_falls_back_to_pdfium_when_pypdf2_channel_is_unreadable(
sample_pdf, monkeypatch):
"""The PyPDF2 channel only refines what PDFium already extracts, so losing
it degrades the parse instead of failing the run."""
from pageindex.flash import page_index_flash
from pageindex.flash.parser_pdfium_charlevel import pipeline

baseline = page_index_flash(sample_pdf, summary=False, optimize=False)

opened = []

class _BombingPyPDF2:
@staticmethod
def PdfReader(*args, **kwargs):
opened.append(args)
return _LazyBombReader()

monkeypatch.setattr(pipeline, "_pypdf2", _BombingPyPDF2)
degraded = page_index_flash(sample_pdf, summary=False, optimize=False)

assert opened, "the PyPDF2 channel never opened; the fallback went untested"
assert degraded == baseline