Skip to content

Fall back to merge sort for PDF text positions - #6988

Open
zxuhan wants to merge 1 commit into
spring-projects:mainfrom
zxuhan:pdf-layout-merge-sort-fallback
Open

zxuhan wants to merge 1 commit into
spring-projects:mainfrom
zxuhan:pdf-layout-merge-sort-fallback

Conversation

@zxuhan

@zxuhan zxuhan commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #2479

Problem

TextPositionComparator is not transitive (PDFBOX-5308) and TimSort rejects such comparators, so ForkPDFLayoutTextStripper caught the failure and logged it:

try {
	this.sortTextPositionList(textList);
}
catch (IllegalArgumentException e) {
	logger.error("Error sorting text positions", e);
}

ArrayList.sort reorders the backing array in place before giving up, so the page was written from a half sorted list. The extracted text was silently in the wrong order, with a stack trace logged for every affected page.

Fix

Fall back to a merge sort, which does not check the comparator contract, the way PDFTextStripper already does:

try {
	textList.sort(comparator);
}
catch (IllegalArgumentException ex) {
	logger.debug("Comparison contract violated while sorting text positions, falling back to merge sort", ex);
	IterativeMergeSort.sort(textList, comparator);
}

IterativeMergeSort is public API of the pdfbox artifact this module already depends on. The try/catch in writePage is gone and the ERROR log becomes a DEBUG one, since the page is no longer degraded. PDFLayoutTextStripperByArea inherits the fix through super.writePage().

Test

ForkPDFLayoutTextStripperTests lays out a staircase of 32 glyphs that makes the comparator intransitive, 32 being the smallest size for which TimSort checks the contract.

  • Fails against the previous implementation with IllegalArgumentException: Comparison method violates its general contract!
  • Sorted output keeps every element and is in comparator order
  • Ordinary multi line input is still sorted top to bottom, left to right
  • ./mvnw -pl document-readers/spring-ai-pdf-document-reader clean package: 43 tests, 0 checkstyle violations

Notes

#2479 suggests two other options.

  • Always use IterativeMergeSort. It is a non-adaptive bottom-up merge sort: toArray() plus clone(), then ⌈log₂n⌉ full passes with no early exit. TimSort instead detects pre-existing runs, and text positions arrive in content-stream order, which is usually close to reading order.
  • Write a transitive comparator. This changes the extracted text order for every user and makes the fork order text differently from PDFTextStripper. PDFBOX-5308 has been open since 2021, which suggests it is not a bugfix-sized change.

Sorting a 2000-glyph page 1000 times on JDK 17.0.19, a rough loop benchmark rather than JMH:

page order List.sort IterativeMergeSort ratio
fully ordered 15.4 ms 151.7 ms 9.9x
5% disordered 46.5 ms 167.1 ms 3.6x
25% disordered 63.5 ms 167.1 ms 2.6x
fully shuffled 220.4 ms 335.2 ms 1.5x

Always merge sorting pays that on every page, to avoid a second sort on the rare page that actually trips the comparator. The fallback instead leaves the fast path untouched and keeps the fork behaving exactly like upstream PDFTextStripper, which matters since the same PDF can be read through either.

ForkPDFLayoutTextStripper sorts each article's text positions with
TextPositionComparator, which is not transitive (PDFBOX-5308). TimSort
rejects such comparators, so List.sort threw IllegalArgumentException
after having already reordered part of the list, and writePage only
logged it, leaving the page in an arbitrary order.

Fall back to IterativeMergeSort, which does not check the comparator
contract, the way PDFTextStripper already does. The ERROR log becomes
a DEBUG one, since the page is no longer degraded.

Fixes spring-projects#2479

Signed-off-by: zxuhan7 <zxuhan7@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pdfDocumentReader error: java.lang.IllegalArgumentException: Comparison method violates its general contract!

2 participants