Skip to content

Fix #1582: fixes to reading PDF files larger than 2 GB#1591

Open
andreasrosdalw wants to merge 3 commits into
LibrePDF:masterfrom
andreasrosdalw:fix-1582-large-file-support
Open

Fix #1582: fixes to reading PDF files larger than 2 GB#1591
andreasrosdalw wants to merge 3 commits into
LibrePDF:masterfrom
andreasrosdalw:fix-1582-large-file-support

Conversation

@andreasrosdalw

@andreasrosdalw andreasrosdalw commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1582.

Reading a PDF larger than ~2 GB failed with InvalidPdfException: Rebuild failed: Position out of bounds; Original message: PDF startxref not found (after potentially scanning the file for a very long time). File positions were tracked as int in RandomAccessFileOrArray, PRTokeniser, PRStream and the PdfReader xref table, so any offset beyond Integer.MAX_VALUE overflowed.

Changes

The read path now uses 64-bit offsets end to end (the same migration iText 5 did for large-file support):

  • RandomAccessFileOrArray: length(), getFilePointer(), getStartOffset()/setStartOffset() and skip() are long-based; seek(long) is the real implementation (seek(int) delegates). The backing MappedRandomAccessFile was already long-based via LongMappedByteBuffer.
  • PRTokeniser: seek/getFilePointer/length/getStartxref use long; new longValue() parses 10-digit offsets.
  • PdfReader: the xref table is long[], lastXref/eofPos/fileLength are long, xref-stream type-1 offsets are read as long, objStmToOffset is now Map<Integer, Long>, rebuildXref() collects long positions. getLastXref(), getEofPos(), getFileLength() return long.
  • PRStream: the stream offset is long.
  • PdfNumber: new longValue() accessor (used for startxref, /Prev, /XRefStm).
  • PdfWriter: prevxref is long, so incremental updates (PdfStamper in append mode) of large files write a correct /Prev.
  • Font-parsing call sites (TrueTypeFont, Type1Font, EnumerateTTC, CFFFont) updated for the new signatures; font files themselves remain int-sized.

API note

Some protected fields and public getters changed from int to long (PdfReader.xref, getLastXref(), getEofPos(), getFileLength(), RandomAccessFileOrArray.length()/getFilePointer(), PRStream.getOffset()). Callers passing int positions still compile via widening; only code storing these values into int needs a cast. This mirrors iText 5's large-file migration.

Tests

LargeFilePdfReaderTest builds a sparse PDF whose objects live above the 2 GB boundary — only a few KB of real data are written, so the tests run in ~0.5 s:

  • filePointerAndLengthBeyond2GB: length(), seek(), getFilePointer() and read() at offsets beyond 2^31.
  • readPdfLargerThan2GB: full PdfReader open of a >2 GB PDF (partial-read path with plain random access, as in the issue report), asserting page count, page size and getLastXref().

The class carries a 30 s @Timeout because a regression here manifests as a long file scan rather than a fast failure. A skip-guard (assumeTrue) avoids running on filesystems without sparse-file support / insufficient disk. Full openpdf-core suite passes (2087 tests, 0 failures), and all modules compile.

File positions were tracked as int in RandomAccessFileOrArray,
PRTokeniser, PRStream and the PdfReader xref table, so offsets beyond
Integer.MAX_VALUE overflowed and reading a >2 GB PDF failed with
'Rebuild failed: Position out of bounds; Original message: PDF startxref
not found' (or scanned the file for a very long time before failing).

Switch the read path to 64-bit offsets:
- RandomAccessFileOrArray: length(), getFilePointer(), startOffset and
  skip() are now long-based; seek(long) is the real implementation.
- PRTokeniser: seek/getFilePointer/length/getStartxref use long; new
  longValue() for parsing 10-digit offsets.
- PdfReader: xref table is long[], lastXref/eofPos/fileLength are long,
  xref-stream type-1 offsets read as long, objStmToOffset maps to Long.
- PRStream: stream offset is long.
- PdfNumber: new longValue() accessor.
- PdfWriter: prevxref is long so incremental updates of large files
  write a correct /Prev.
- MappedRandomAccessFile was already long-based (LongMappedByteBuffer).

Font-parsing call sites (TrueTypeFont, Type1Font, EnumerateTTC, CFFFont)
updated for the new signatures; font files remain int-sized.

The regression test builds a sparse PDF whose objects live above the
2 GB boundary (a few KB of real data), so it runs in under a second; a
30s timeout guards against regressions, which would manifest as
long scans rather than fast failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andreasrosdalw andreasrosdalw changed the title Fix #1582: support reading PDF files larger than 2 GB Fix #1582: fixes to reading PDF files larger than 2 GB Jul 21, 2026
@codacy-production

codacy-production Bot commented Jul 21, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 11 complexity · 0 duplication

Metric Results
Complexity 11
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

andreasrosdalw and others added 2 commits July 21, 2026 10:21
- RandomAccessFileOrArray.read()/read(byte[],int,int): call insureOpen()
  before accessing the backing file, fixing a potential NPE when reading
  after close() (Sonar blocker; getFilePointer/length/seek already did
  this).
- PdfReader.readXRefStream: extract readXRefStreamEntry() and
  markObjStmEntry() helpers to reduce cognitive complexity.
- PdfReader.checkPRStreamLength: rename local that shadowed the
  fileLength field.
- LargeFilePdfReaderTest: build xref entries without String.format; the
  PDF xref format mandates LF, not the platform line separator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- RandomAccessFileOrArray: introduce openPlainFile()/openMappedFile()
  accessors that reopen and null-check the backing file, throwing a
  descriptive IOException instead of NPE when the object is closed and
  cannot be reopened. All backing-file accesses (read, length, seek,
  getFilePointer, getNioByteBuffer) go through them.
- PdfReader.readXRefStream: extract readXRefStreamObject() so the
  method's cognitive complexity is within the allowed limit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to read PDF files larger than ~2 GB due to integer overflow in RandomAccessFileOrArray

1 participant