Repository navigation
Scrambled Content Stream Problem
This page documents a critical issue we discovered and solved: PDF content streams can contain text in any order.
Redaction was failing for certain PDFs. The user could select text correctly (text selection worked), but redaction didn't remove the selected text.
CharacterMatcher logging showed:
CharacterMatcher: Searching for 'FIRST' in 150 candidate letters.
Candidates text: 'R PLEEQAUSEES TPR FIONRT OLNAMFEI RLASMT IANDADMLREE S...'
CharacterMatcher: Operation text NOT FOUND in candidates.
The expected text "PLEASE PRINT FIRST..." was scrambled!
The PDF content stream contained text in storage order (order written to file), not visual order (left-to-right reading).
# Content stream (simplified)
(R) Tj at x=150 # 'R' in "FIRST"
( ) Tj at x=155 # space
(P) Tj at x=10 # 'P' in "PLEASE"
(L) Tj at x=20 # 'L' in "PLEASE"
...
Each letter has a transformation matrix that positions it correctly on the page, but the order in the file is arbitrary.
PDF generators may:
- Group by font: All Helvetica-Bold text first, then Helvetica regular
- Group by color: All black text, then blue text
- Group by layer: Background elements, then foreground
- Process order: Order text was added during document creation
The PDF specification allows any order. The viewer must use transformation matrices to position glyphs correctly.
PdfPig returns letters sorted by reading order (top-to-bottom, left-to-right), not content stream order.
Our content stream operations were in storage order, but PdfPig letters were in reading order.
// CharacterMatcher.cs - BEFORE (broken)
var candidateLetters = letters.Where(letter => /* spatial filter */).ToList();
// CharacterMatcher.cs - AFTER (working)
var candidateLetters = letters
.Where(letter => /* spatial filter */)
// CRITICAL: Sort by position to get reading order!
.OrderBy(l => pageHeight - l.GlyphRectangle.Top) // Top to bottom
.ThenBy(l => l.GlyphRectangle.Left) // Left to right
.ToList();Now candidates are in reading order, matching what we search for.
birth-certificate-request-scrambled.pdf - A public government form with scrambled content order.
[Fact]
public void Redact_ScrambledContentStream_FindsWord()
{
var pdfPath = "Resources/sample-pdfs/birth-certificate-request-scrambled.pdf";
// This PDF has scrambled content stream order
var textBefore = PdfTestHelpers.ExtractAllText(pdfPath);
textBefore.Should().Contain("FIRST");
// Redaction should still work
var redactedPath = Path.Combine(_tempDir, "redacted.pdf");
RedactionService.RedactText(pdfPath, redactedPath, "FIRST");
var textAfter = PdfTestHelpers.ExtractAllText(redactedPath);
textAfter.Should().NotContain("FIRST");
}Never assume text appears in reading order in the content stream. Always sort by position when reconstructing reading order.
Simple test PDFs generated by our code always have correct order. Real-world PDFs from various sources expose this issue.
PdfPig's text extraction works because it sorts letters by position. We should follow the same approach.
The debug logging in CharacterMatcher was crucial:
_logger.LogDebug("Candidates text: '{CandText}'", candidatesText);This immediately showed the scrambled order.
How to identify if a PDF has scrambled content:
// Compare stream order vs position order
var streamOrder = GetLettersInStreamOrder(page);
var positionOrder = page.Letters
.OrderBy(l => -l.GlyphRectangle.Top)
.ThenBy(l => l.GlyphRectangle.Left)
.ToList();
bool isScrambled = !streamOrder.SequenceEqual(positionOrder);- PDF 32000-1:2008, Section 9.4 - Text objects can appear in any order
- PdfPig source code - See how they sort letters
Issue discovered: December 2024 Fix committed: dbdc7b5