Skip to content

feat: Add link_format to PyPDFToDocument and PDFMinerToDocument (#10677) - #12273

Merged
julian-risch merged 8 commits into
deepset-ai:mainfrom
DhanushPillay:feature/issue-10677-pdf-links
Aug 10, 2026
Merged

feat: Add link_format to PyPDFToDocument and PDFMinerToDocument (#10677)#12273
julian-risch merged 8 commits into
deepset-ai:mainfrom
DhanushPillay:feature/issue-10677-pdf-links

Conversation

@DhanushPillay

Copy link
Copy Markdown
Contributor

Related Issues

Proposed Changes:

This PR adds a link_format parameter to both PyPDFToDocument and PDFMinerToDocument components, matching the existing functionality in DOCXToDocument.

  • Extracted DOCXLinkFormat to a reusable LinkFormat Enum in haystack/components/converters/utils.py.
  • Updated PyPDFToDocument to parse page /Annots and append formatted links to the end of the extracted page text.
  • Refactored PDFMinerToDocument to natively iterate over PDFPage objects (using PDFPageAggregator and PDFPageInterpreter) instead of using extract_pages. This allows access to the page annotations, from which hyperlink URIs are extracted and safely appended to the text.
  • Both components now correctly serialize and deserialize the link_format Enum.

How did you test it?

  • Added comprehensive unit tests for PyPDFToDocument to verify initialization, serialization, and mock extraction of link annotations.
  • Added comprehensive unit tests for PDFMinerToDocument to verify initialization, serialization, and mock extraction of link annotations.
  • Tested serialization edge cases (string to Enum).

Notes for the reviewer

  • DOCXLinkFormat has been aliased to LinkFormat to maintain backward compatibility for users currently importing it directly from the docx module.
  • We opted not to use PyMuPDF to extract links due to its AGPL license, adhering to the discussion in the original issue.
  • Links are appended to the bottom of the page content as a text list, rather than injected inline, to avoid brittle layout heuristics with PDF formats.

@DhanushPillay
DhanushPillay requested a review from a team as a code owner August 8, 2026 14:17
@DhanushPillay
DhanushPillay requested review from julian-risch and removed request for a team August 8, 2026 14:17
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

@DhanushPillay is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added topic:tests type:documentation Improvements on the docs labels Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/components/converters
  docx.py
  pdfminer.py
  pypdf.py 192, 209-211
  utils.py
Project Total  

This report was generated by python-coverage-comment-action

@julian-risch julian-risch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for opening this PR @DhanushPillay ! I spotted two issues that we need to fixe before merging:

  1. One bad annotation drops the whole document. Link extraction runs inside the try/except that guards the entire conversion, and both implementations raise on shapes that occur in real PDFs. Please wrap per-annotation extraction in its own try/except so that one bad annotation only affects that link and not the whole document.

  2. PDFMiner refactor significantly increases peak memory usage because the new code materialises every PDFPage and LTPage before converting. On react_paper.pdf mrmoty usage is 8.4 MB on main and 60 MB here, scaling with page count. Yielding (LTPage, PDFPage) pairs from a generator keeps the annotations available. That could also remove the zip(..., strict=False).

@DhanushPillay
DhanushPillay force-pushed the feature/issue-10677-pdf-links branch 4 times, most recently from b958463 to 7f209a0 Compare August 10, 2026 14:32
@DhanushPillay

Copy link
Copy Markdown
Contributor Author

Thanks for the review @julian-risch, really appreciate the catches!

I've pushed a fix for both issues:

  1. Annotation Error Handling: Wrapped the per-annotation extraction in its own try/except block for both pypdf and pdfminer. If a single weird shape or bad link fails, it'll just log a debug message and keep going instead of taking down the whole document. Also added a couple test cases to cover this.
  2. PDFMiner Memory Usage: Moved the logic to an _iter_pages() generator that lazily yields (LTPage, PDFPage) pairs. The conversion loop now processes them one at a time, which gets rid of the memory spike and lets us drop the zip(..., strict=False) entirely.

Let me know if this looks good to merge or if there's anything else!

Follow-up to the review of deepset-ai#12273.

- `PyPDFToDocument` resolved and iterated `/Annots` outside the `try`, so a
  page whose `/Annots` is an unresolvable reference lost the whole document
  with `TypeError: 'NoneType' object is not iterable` - the same file
  converts fine with `link_format="none"`. Link extraction now lives in
  `_extract_links`, which guards the resolution of the annotation array as
  well as each individual annotation. The page is indexed rather than using
  `get`, because only indexing resolves an indirect reference to the array.
- Move `resolve1` to the `LazyImport` block of `pdfminer.py` instead of
  importing it inside `_convert_page` on every page, and repoint the tests
  that patched `pdfminer.pdftypes.resolve1` at the module under test.
- Type `_iter_pages` as `Iterator[tuple[Any, Any]]` and document it.
- The `link_format` docstrings promised `[text](address)`, which no PDF
  converter produces: link annotations carry no anchor text, so the address
  is used as the link text and the links are appended at the end of the page.
- Add a `fixes:` release note for the new `PDFMinerToDocument.to_dict`.
  Without it, `component_to_dict` falls back to reading same-named
  attributes, and since the layout parameters live in `self.layout_params`
  they were silently serialized with their defaults, so `char_margin=0.5`
  round-tripped as `char_margin=2.0`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@julian-risch julian-risch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me now. Thanks again!

@julian-risch
julian-risch enabled auto-merge (squash) August 10, 2026 15:14
@julian-risch
julian-risch disabled auto-merge August 10, 2026 15:14
@julian-risch julian-risch changed the title Add link_format to PyPDFToDocument and PDFMinerToDocument (#10677) feat: Add link_format to PyPDFToDocument and PDFMinerToDocument (#10677) Aug 10, 2026
@julian-risch
julian-risch enabled auto-merge (squash) August 10, 2026 15:14
@julian-risch
julian-risch merged commit 0c3a485 into deepset-ai:main Aug 10, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:tests type:documentation Improvements on the docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Add link_format parameter to PDFMinerToDocument and PyPDFToDocument

2 participants