Technical Article

JBIG2 Random-Access Files: Decoding Them in Delphi

Here is a mistake that a hand-written JBIG2 reader makes on the first unusual file it meets. It parses the file header, parses the first segment header, reads the four-byte data length, and takes that many bytes as the segment body. On most .jb2 files that is exactly right. On a file whose flags byte has bit 0 cleared, those "body" bytes are really the second, third and fourth segment headers, and the arithmetic decoder turns them into a bitmap of noise or runs off the end of the buffer. The file is not broken. It uses the random-access organization from ITU-T T.88 Annex D.2, where every segment header comes first and all segment data follows afterwards, in the same order

Random-access files are rare. They come out of archival and document-imaging systems that want a reader to see every page and dictionary dependency before it touches a compressed byte. If a conversion job mixes files from such a source with ordinary scans, one random-access file can sit in the middle of a batch that is otherwise all sequential. The decoder that reads them is the same native Pascal decoder that handles JBIG2 custom Huffman tables and canonical prefix codes, so the coding side is unchanged; only the order in which segments are found differs

Three file layouts behind one flags byte

PDF Library for Delphi reads standalone JBIG2 files through TPLJBIG2Decoder in the PDFlibJBIG2 unit, and the decoder works out the layout from the file header on its own. T.88 Annex D allows the same segments to be arranged three ways: sequential (D.1) puts each header directly in front of its data, random-access (D.2) puts all headers first and all data after them, and embedded (D.3) is the headerless form used inside other containers. A standalone file starts with the eight-byte identifier 97 4A 42 32 0D 0A 1A 0A, then one flags byte, then a four-byte page count unless the flags say the count is unknown

Bit 0 of that flags byte picks the organization: 1 means sequential, 0 means random-access. Bit 1 set means the number of pages is unknown and the four-byte count is absent, so the first segment header starts at byte 9 instead of byte 13. Bits 2 to 7 are reserved; the decoder accepts files that set them instead of rejecting them, because a stray reserved bit says nothing about whether the segments themselves are readable

JBIG2 file organizations in PDFlibPas: the flags byte that setFileHeaderFlags reads picks sequential D.1 with interleaved headers, random-access D.2 with every header before the data block, or embedded D.3, the headerless form a JBIG2Decode stream uses with dictionaries in JBIG2Globals
The three layouts carry the same segments, but only random access makes a reader see every page and dictionary dependency before touching a compressed byte, which is why archival pipelines asked for it

PDF itself never carries the random-access layout. A JBIG2Decode image stream, described in ISO 32000-1 §7.4.7, holds only page segments in embedded form, with shared symbol dictionaries moved into a separate JBIG2Globals stream and no file header, end-of-page or end-of-file segments. When the decoder does not find the eight-byte identifier, it treats the input as exactly that kind of stream: sequential, one page. Going the other way, when PDF Library for Delphi exports a JBIG2Decode image as a native .jb2 file, it writes the flags byte $03 (sequential, page count unknown) and appends an end-of-file segment header. So random access only ever arrives from outside, as a standalone file handed to TPLJBIG2Decoder directly, usually on its way to being converted or recompressed for PDF with the JBIG2 encoder backends in PDFlibPas

Why can't you read a random-access file in file order?

Because nothing in the byte stream marks where the header block ends and the data block begins, except the end-of-file segment header (type 51) itself. JBIG2 segment headers have no fixed size. The referred-to segment count is either a three-bit short form or a long form followed by a retention bitmap; each referred-to segment number takes one, two or four bytes depending on how large the current segment's own number is; the page association field is one byte or four. A reader has to parse every field of a header just to learn where the next one starts

A sequential reader that walks the file in order gets the first header right and everything after it wrong. Worse, it may not notice for a while: a generic region decoder fed header bytes produces pixels, not an error, until some later length or reference check finally trips. Our view is that a decoder which does not index headers first should refuse random-access files outright. A clear rejection costs one file in a batch; a plausible-looking but wrong bitmap can end up in an archive and stay there

Indexing every header before the first body

When the flags byte says random-access, PDF Library for Delphi makes one pass over the header block before decoding anything. Each header is parsed in full and then thrown away; the only thing kept is its byte offset in the file. The compressed data is never copied into a reordered second buffer, which matters on large archival scans where the data block can be most of the file. The pass stops at the first end-of-file header, and at that point the read position sits on the first byte of the first segment's data

A random-access file has less redundancy than a sequential one, so the pre-scan is strict. It rejects the file when:

  • a header is cut off by the end of the buffer, reported as JBIG2 truncated random-access header at byte N with the offset where that header began
  • segment numbers do not strictly increase (compared as unsigned values), because two headers claiming the same number make a later region's referred-to list ambiguous
  • a data length has its top bit set, which includes the 0xFFFFFFFF "unknown length" marker; in this layout there is no other way to find where the next body starts, so the decoder refuses it immediately instead of scanning ahead for an end marker
  • the end-of-file header declares any data, or never appears at all (JBIG2 random-access file is missing its end-of-file header)
  • the declared lengths add up to more than the bytes that remain, or to fewer
IndexRandomHeaders pre-scan in PDFlibPas: every segment header is parsed and discarded while only its byte offset is kept, segment numbers must strictly increase, the 0xFFFFFFFF unknown length is refused, the scan stops at the type 51 end-of-file header, and declared lengths must equal the remaining bytes exactly
The strictness is deliberate: in a layout where headers give the only map of the data, one stray byte means every later body may be shifted, so a decoder that tolerates it cannot tell padding from misalignment

The last rule is the one people trip over. A single extra byte after the final body fails with JBIG2 trailing random-access data. That looks harsh next to the sequential path, where decoding simply stops at the end-of-file segment and anything after it is ignored. The asymmetry has a reason. In sequential layout each header sits right next to its own data, so junk at the end cannot have shifted anything. In random-access layout the headers are the only map of the data block, and if the lengths do not add up, the decoder cannot tell harmless padding from a byte lost or added much earlier that has shifted every body after it

Why does a byte-counted loop drop the last end-of-page segment?

A loop that runs "while there are bytes left" ends too early on a random-access file, because the data runs out before the header index does. End-of-page (type 49) and end-of-file segments carry zero bytes of data and normally come last. Once the final region body has been consumed, the read position is exactly at the end of the buffer; a byte-counted loop exits there, and the zero-length end-of-page segment never gets dispatched, which leaves the page unfinished

Random-access decode loop in PDFlibPas: each iteration seeks to HeaderOffsets of the current header, resets bitPointer to 7 to undo mid-byte tails, jumps to NextBodyOffset for the body, and counts headers instead of bytes so zero-length end-of-page segments are dispatched before the loop ends
Because end-of-page and end-of-file segments carry zero bytes of data, the data stream runs out before the header index does, and only a loop that counts headers can give those final segments their turn

So for random-access files the decoder counts headers, not bytes. Each step jumps back to the next indexed header, realigns to a byte boundary (an arithmetic-coded body can end in the middle of a byte), parses the header again, then jumps forward to where the next body starts and advances past it by the declared length. A body whose decoder finishes early is skipped to its declared end; one that reads past it is rejected. From there the segment goes through the same handlers and reference checks as in a sequential file. Errors raised while decoding a body carry the header's original position, in the form JBIG2 segment N (type T) at byte B: ..., so the offset in LastError points at something you can find in a hex editor

Loading a random-access .jb2 file in Delphi

Application code does not change with the organization. TPLJBIG2Decoder.LoadFromByteArray detects the file header and layout, returns False on any rejected input with the reason in LastError, and exposes the decoded page through Width, Height and GetScanline, which fills one byte per pixel with 1 for black

uses
  SysUtils, Classes, PDFlibJBIG2;

function ReadJb2(const FileName: string): TJBIG2ByteArray;
var
  FS: TFileStream;
begin
  FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Result, FS.Size);
    if Length(Result) > 0 then
      FS.ReadBuffer(Result[0], Length(Result));
  finally
    FS.Free;
  end;
end;

function CountBlackPixels(const FileName: string): Integer;
var
  Decoder: TPLJBIG2Decoder;
  Row: TJBIG2ByteArray;
  X, Y: Integer;
begin
  Result := 0;
  Decoder := TPLJBIG2Decoder.Create;
  try
    // Sequential and random-access standalone files take the same call
    if not Decoder.LoadFromByteArray(ReadJb2(FileName)) then
      raise Exception.Create('JBIG2 rejected: ' + Decoder.LastError);
    for Y := 0 to Decoder.Height - 1 do
      if Decoder.GetScanline(Y, Row) then
        for X := 0 to Decoder.Width - 1 do
          if Row[X] = 1 then
            Inc(Result);
  finally
    Decoder.Free;
  end;
end;

If you want to know which layout a file uses, for logging or for sorting a corpus before a conversion run, read the flags byte yourself. The library does not expose the organization it detected, and nine bytes are all you need. The helper below is something to add to your own code; it simply restates the T.88 header rules:

type
  TJb2Layout = (jlNoFileHeader, jlSequential, jlRandomAccess);

// Your own helper, not part of PDFlibJBIG2: classifies a buffer by its T.88 file header
function DetectJb2Layout(const Data: TJBIG2ByteArray;
  out PageCountKnown: Boolean): TJb2Layout;
const
  Signature: array[0..7] of Byte = ($97, $4A, $42, $32, $0D, $0A, $1A, $0A);
var
  I: Integer;
begin
  Result := jlNoFileHeader;          // e.g. an embedded stream lifted out of a PDF
  PageCountKnown := False;
  if Length(Data) < 9 then
    Exit;
  for I := 0 to 7 do
    if Data[I] <> Signature[I] then
      Exit;
  PageCountKnown := (Data[8] and 2) = 0;   // bit 1 set: no four-byte page count
  if (Data[8] and 1) = 0 then
    Result := jlRandomAccess
  else
    Result := jlSequential;
end;

Batch jobs: padded buffers and rejected files

The trailing-data rule is where batch code tends to go wrong, and when a file that opens elsewhere is rejected this way, look at the buffer before the file. A .jb2 pulled from a database BLOB column, a fixed-size network read or a container format with alignment padding can arrive with a few zero bytes after the real end. A sequential file shrugs that off. A random-access file with the same padding fails with JBIG2 trailing random-access data, and so does a buffer cut a few bytes short, which fails as truncated. Pass exactly the file's bytes, sized from the source's real length, never from a chunk size or a capacity

A rejected file also leaves nothing behind. LoadFromByteArray resets Width, Height and the scanlines before it starts decoding, so a failed call reports zero dimensions rather than the previous file's page, and the same decoder instance can go straight on to the next file:

procedure DecodeBatch(const Files, Log, Quarantine: TStrings);
var
  Decoder: TPLJBIG2Decoder;
  I: Integer;
begin
  Decoder := TPLJBIG2Decoder.Create;
  try
    for I := 0 to Files.Count - 1 do
      if Decoder.LoadFromByteArray(ReadJb2(Files[I])) then
        Log.Add(Format('%s: %d x %d', [Files[I], Decoder.Width, Decoder.Height]))
      else
      begin
        // Width and Height are 0 here; nothing from the previous file survives
        Log.Add(Files[I] + ': ' + Decoder.LastError);
        if Pos('trailing random-access data', Decoder.LastError) > 0 then
          Quarantine.Add(Files[I]);   // check how the bytes were stored before blaming the file
      end;
  finally
    Decoder.Free;
  end;
end;

Checking this in your own pipeline does not need rare sample files. Reorganize a sequential .jb2 into random-access layout, keeping every header and every compressed byte, move the end-of-page and end-of-file headers to the end of the header block, and the decoded pixels must match the sequential original exactly, both with and without the page count field. Then feed it every truncated prefix, a duplicated segment number, one extra trailing byte and a 0xFFFFFFFF length; each should return False with Width and Height at zero

What the decoder returns, and what it expects

Random access here is a property of the file layout. TPLJBIG2Decoder returns the first page's bitmap whichever organization the file uses, and Width and Height describe that page. Every segment must declare its data length; T.88 lets an immediate generic region use the 0xFFFFFFFF marker, and the decoder rejects that marker in either layout with unknown or oversized segment length is not supported. Custom Huffman prefix lengths and table-entry counts are capped the same way they are in sequential files

Decoding JBIG2 is one stage of a longer image path that also covers PDF image extraction, recompression and JBIG2 encoding; the PDF Library for Delphi product page covers the rest of that pipeline and the supported image formats