HotPDF, the native Delphi PDF component, decodes CCITTFaxDecode images with full support for the T.4/T.6 uncompressed-mode extension and for damaged-row recovery driven by DamagedRowsBeforeError. Uncompressed mode is entered only by the extension code ending in 111, and damaged rows are repaired only for Group 3 data with EndOfLine set. Everything else fails cleanly instead of producing a skewed page
Fax-encoded images are the long tail of PDF image decoding. Most CCITT streams you meet came out of a scanner driver or a document capture pipeline that only ever emits plain Group 4, and a decoder that handles the common case passes every test you throw at it. Then an archive from a fifteen-year-old fax server arrives, a page renders as diagonal noise from row 400 down, and you discover that the two parts of the ITU-T recommendations nobody implements are exactly the two parts that file uses. This article walks through those two parts as HotPDF handles them, and through the places where a decoder can go wrong while still looking correct
Which CCITTFaxDecode parameters actually change the bit stream?
Four entries in the DecodeParms dictionary change how the decoder frames the bit stream: K, EndOfLine, EncodedByteAlign and DamagedRowsBeforeError. ISO 32000-1 §7.4.6 (Table 11) defines them, and getting any one wrong shifts every row after the first mismatch. BlackIs1, Columns, Rows and EndOfBlock matter too, but they affect polarity, geometry and where decoding stops rather than how a row boundary is found
Kpicks the coding scheme: negative means pure two-dimensional Group 4 (T.6), zero means one-dimensional Modified Huffman (Group 3 1D), and positive means mixed Group 3 2D, where a tag bit after each EOL says whether the next row is 1D or 2DEndOfLinesays every Group 3 row is preceded by an EOL code, eleven zero bits followed by a oneEncodedByteAlignsays each encoded row starts on a byte boundary, so the decoder must discard the padding bits before reading itDamagedRowsBeforeErroris how many corrupt rows the decoder may tolerate before giving up, with a default of 0
HotPDF reads all of these from the image dictionary when you call THotPDF.ExtractLoadedImage or THotPDF.DecodeLoadedImageStream. One deliberate deviation is worth knowing about: when Columns is missing, HotPDF falls back to the image Width rather than the spec default of 1728, and when Rows is missing it uses Height. A producer that omits Columns on a 2480-pixel A4 scan is wrong, but it is common enough that trusting the image geometry gives a usable page where the literal default would give garbage
Why can 000000 not be treated as the end of the block?
In two-dimensional rows, six leading zero bits are ambiguous, and a decoder that reads them as the start of EOFB will misread a valid uncompressed-mode entry as the end of the image. Group 4 has no EOLs between rows; the only zero-heavy codes are the EOFB, which is two EOLs back to back, and the extension codes from ITU-T T.4 and T.6. Both begin with 000000, and the seventh bit is where they diverge
The HotPDF CCITT decoder resolves the prefix bit by bit. A seventh bit of 1 means the next three bits are an extension code, the full pattern being 0000001xxx. Only xxx = 111 enters uncompressed mode; any other extension value is treated as malformed, since a PDF fax filter has no use for the others. A seventh bit of 0 can only continue an EOL, so the decoder then requires four more zero bits and a closing 1, eleven zeros and a one in total, before it reports EOFB. Anything shorter or longer is an error, not a lenient end-of-image
The lenient version is tempting because it makes a whole class of broken files look fine: decoding just stops early and you get a short image with no exception. It is also exactly the bug that truncates legitimate faxes at the first stretch of uncompressed data, which on a scanned form is usually a dense signature or a stamp. HotPDF chooses the strict reading, so a short image means the encoder really ended there
How does uncompressed mode encode pixels?
Uncompressed mode replaces run-length coding with literal pixel patterns, each code word carrying at most five pixels, until an exit code switches back. One-dimensional rows enter it with the 12-bit code 000000001111; two-dimensional rows use the 10-bit extension 0000001111 described above. Encoders switch to it for halftone-like regions where run lengths of one or two pixels would cost more than the pixels themselves
Inside the mode, the code words are counted by their leading zeros. Zero to four zeros followed by a 1 mean that many white pixels and then one black pixel. Five zeros followed by a 1 mean five white pixels and nothing else. Six to ten zeros followed by a 1 are exit codes: the zeros beyond six emit up to four more white pixels, and the single bit after the exit code, the tag bit, sets the colour of the next run when ordinary run coding resumes
{ Uncompressed-mode code words (ITU-T T.4 / T.6)
W = white pixel, B = black pixel, T = tag bit
1 B
01 W B
001 W W B
0001 W W W B
00001 W W W W B
000001 W W W W W
0000001T exit, then resume runs with colour T
00000001T W, exit
000000001T W W, exit
0000000001T W W W, exit
00000000001T W W W W, exit
more than ten zeros: malformed }
The tag bit is the subtle part. Run coding is stateful: a 1D row alternates white and black runs, and a 2D row tracks the colour of the current changing element a0. After the exit code, the decoder has to resume with whatever colour the tag names, which may or may not match the last literal pixel. The HotPDF decoder records a colour change at the current position when the tag differs from the colour it was tracking, so the next run starts from the right colour. Ignore the tag and the rest of the row comes out colour-inverted, and in 2D mode every following row inherits the damage through its reference line
What does DamagedRowsBeforeError recover, and what does it not?
DamagedRowsBeforeError lets HotPDF replace a corrupt row and continue, but only when K is 0 or positive and EndOfLine is true. Those two conditions are not arbitrary. Recovery needs a way to find where the next row starts after the bits stop making sense, and the only reliable landmark in a CCITT stream is the EOL code. Group 4 data has no EOLs, and Group 3 data without mandatory EOLs gives no guarantee one exists, so in both cases a damaged row ends decoding regardless of the parameter value
When a row fails, the HotPDF decoder scans forward to the next EOL, a run of at least eleven zero bits followed by a one, and substitutes a replacement row. The first damaged row after a good one is replaced by a copy of the previous row, which on a scanned text page is usually close enough to be invisible. A damaged row that directly follows another damaged row is replaced with an all-white row instead, because copying a copy would smear one line of text into a tall black bar. The substitute then becomes the reference line for the next 2D row, which is what keeps a mixed-mode stream decodable after the repair
Two boundaries are easy to miss. The budget is cumulative across the whole image, not per run of bad rows, so DamagedRowsBeforeError 3 tolerates three damaged rows in total and the fourth fails the decode. And a failed decode in HotPDF is all or nothing: THotPDF.ExtractLoadedImage returns nil rather than a bitmap with a ragged bottom edge. THPDFLoadedImageInfo.Decodable reports that the filter is supported, not that this particular stream is intact, so always check the bitmap for nil
Why the scan must stop at the start of the EOL
The resynchronization scan leaves the reader at the first zero bit of the EOL, not after it. The decoder has one framing loop that consumes EOLs, checks for the double EOL that ends the block, and reads the mixed-mode tag bit. If the recovery scan swallows the EOL itself, that loop sees a row with no EOL, treats it as damaged too, and burns a second unit of the budget while dropping a good row. Worse, a skipped tag bit means a 2D row gets decoded as 1D or the reverse. The same rule applies when a 1D run decoder hits an unexpected EOL mid-row: it rewinds to where the run code started, so the marker is still there for the recovery scan
Inventorying CCITT images in a loaded PDF
Before trusting a batch of archived faxes, list which images use CCITTFaxDecode and which ones fail to decode. THotPDF.GetLoadedImageInfo gives you geometry and the filter name without decoding anything, and THotPDF.ExtractLoadedImage tells you whether the stream survived. For the wider picture of how the image dispatcher routes each filter, see the guide to extracting loaded PDF images and their decode filters
var
Pdf: THotPDF;
Info: THPDFLoadedImageInfo;
Bmp: TBitmap;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('fax-archive.pdf', '') <= 0 then
Exit;
for I := 0 to Pdf.GetLoadedImageCount - 1 do
begin
if not Pdf.GetLoadedImageInfo(I, Info) then
Continue;
if Pos('CCITT', UpperCase(String(Info.Filter))) = 0 then
Continue;
Bmp := Pdf.ExtractLoadedImage(I); // nil when the stream is too damaged
try
if Bmp = nil then
Writeln(Format('obj %d: %dx%d CCITT failed to decode',
[Info.ObjectNumber, Info.Width, Info.Height]))
else
Bmp.SaveToFile(Format('fax_%d.bmp', [I]));
finally
Bmp.Free; // caller owns the bitmap
end;
end;
finally
Pdf.Free;
end;
end;
For the failures, keep the evidence. THotPDF.ExtractLoadedImageToStream with the default lismEncoded mode writes the stored stream bytes untouched, which is what you want to hand to a second decoder, wrap in a TIFF container for inspection, or attach to a bug report. Re-encoding a damaged fax only hides where the damage was
procedure SaveRawFax(Pdf: THotPDF; ImageIndex: Integer; const FileName: string);
var
Fs: TFileStream;
begin
Fs := TFileStream.Create(FileName, fmCreate);
try
if not Pdf.ExtractLoadedImageToStream(ImageIndex, Fs, lismEncoded) then
raise Exception.CreateFmt('Image %d has no stream data', [ImageIndex]);
finally
Fs.Free;
end;
end;
If you walk image streams yourself through THotPDF.CollectLoadedImageStreams, pass each THPDFStreamObject to THotPDF.DecodeLoadedImageStream and you get the same CCITT path, parameters and recovery rules. Free the returned list but not the stream objects in it, which stay owned by the document
Should you raise DamagedRowsBeforeError on files you do not control?
No, not by editing the PDF, and usually not at all. The parameter belongs to the producer, which is the only party that knows whether its transmission channel drops bits. A file that declares DamagedRowsBeforeError 0 and then contains a damaged row is telling you the archive copy is corrupt, and the honest result is a decode failure you can log. Silently patching rows on a contract or a medical record turns a detectable defect into a plausible-looking image with a missing line, which is a worse outcome for anyone who later relies on it
The safer pattern for untrusted archives is to keep the decode strict, record which object numbers failed, and route those to a human or to a separate decoder. If the archive is large and comes from many sources, running image decoding out of process limits what a hostile stream can do to the host application; the article on isolating image codecs when processing untrusted PDFs covers that setup, and the JBIG2 bilevel compression guide compares the other bilevel filter you will find in the same scanned archives
FAQ
Does HotPDF support CCITT uncompressed mode in Group 4 images?
Yes. HotPDF recognises the 10-bit 0000001111 extension in two-dimensional rows and the 12-bit 000000001111 entrance code in one-dimensional rows, decodes the literal code words, and honours the exit tag bit when run coding resumes
Why does DamagedRowsBeforeError have no effect on my Group 4 fax?
Recovery requires EOL markers to resynchronize. HotPDF applies DamagedRowsBeforeError only when K is 0 or positive and EndOfLine is true; Group 4 data (K negative) has no EOLs, so any damaged row ends the decode
What does ExtractLoadedImage return for a damaged CCITT image?
THotPDF.ExtractLoadedImage returns nil when the damage exceeds what DamagedRowsBeforeError allows or cannot be recovered at all. It never returns a partial bitmap, so test for nil and use ExtractLoadedImageToStream to keep the raw bytes
HotPDF handles CCITT, JBIG2, JPEG, JPEG 2000 and Flate images in loaded PDFs from plain Delphi and C++Builder code, with no external DLL for the fax path. Details and editions are on the HotPDF Delphi PDF component page