PDFlibPas ab Version 3.539.23 dekodiert eigenständige JBIG2-Dateien, die die Random-Access-Organisation aus ITU-T T.88 Anhang D.2 verwenden, bei der jeder Segment-Header zuerst kommt und die Segmentdaten in derselben Reihenfolge folgen. Der native Pascal-Decoder in PDFlibJBIG2.pas indexiert die Header-Offsets bis zum zwingenden End-of-File-Header, prüft, dass die Segmentnummern steigen und dass die deklarierten Datenlängen exakt auf die verbleibenden Bytes aufgehen, und dekodiert dann jeden Body in Header-Reihenfolge, ohne die komprimierten Daten zu kopieren oder umzuordnen. Vor dieser Release warf dieselbe Datei den pauschalen Fehler „random-access organisation is not supported“, sobald die Header-Flags gelesen waren
Random-Access-JBIG2-Dateien sind selten – genau deshalb tun sie weh, wenn sie doch auftauchen. Sie kommen aus Archiv-Pipelines und Document-Imaging-Systemen, die wollen, dass ein Reader jeden Segment-Header und damit jede Seiten- und Dictionary-Abhängigkeit sieht, bevor er ein einziges komprimiertes Byte anfasst. Eine Delphi-Anwendung, die gescannte Archive stückweise nach PDF konvertiert, trifft meist mitten im Job auf so eine Datei, nachdem Hunderte sequenzielle Dateien sauber durchgelaufen sind, und ein Decoder, der bei einer wohlgeformten Datei stehen bleibt, ist nur wenig besser als einer, der Müll rendert. Dieselbe Release-Linie hatte dem Decoder gerade erst JBIG2 custom Huffman tables und kanonische Prefix-Codes beigebracht, Random Access war also die letzte Organisationslücke in den dokumentierten Fähigkeitsgrenzen des Decoders
Was ist die JBIG2-Random-Access-Organisation?
Die Random-Access-Organisation ist eine von drei Arten, wie T.88 Anhang D dieselben Segmente ablegen darf: sequential (D.1) verschränkt jeden Header mit seinen Daten, random-access (D.2) stellt alle Header voran und alle Daten dahinter, und embedded (D.3) ist die headerlose Form, die in anderen Containern wie PDF steckt. Eine eigenständige .jb2-Datei beginnt mit dem Acht-Byte-Bezeichner 97 4A 42 32 0D 0A 1A 0A, gefolgt von einem Flags-Byte und, wenn die Seitenzahl bekannt ist, einer Vier-Byte-Seitenzahl. Bit 0 des Flags-Bytes wählt die Organisation, 1 bedeutet sequential und 0 random-access; ist Bit 1 gesetzt, ist die Seitenzahl unbekannt und der Vier-Byte-Count fehlt. PDFlibPas liest das in checkHeader und setFileHeaderFlags, und die reservierten Bits 2 bis 7 werden toleriert statt zurückgewiesen
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = Seitenzahl weggelassen
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-Stream: kein File-Header, embedded-Organisation, eine Seite
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF selbst trägt dieses Layout nie. Ein JBIG2Decode-Bild-Stream, beschrieben in ISO 32000-1 §7.4.7, enthält nur die Seiten-Segmente in embedded-Organisation, mit gemeinsamen Symbol-Dictionaries in einem separaten JBIG2Globals-Stream und ohne File-Header, End-of-Page- oder End-of-File-Segmente. Findet decodeJBIG2 den Acht-Byte-Bezeichner nicht, nimmt es genau das an und erzwingt sequenzielles, einseitiges Dekodieren. Der native JBIG2-Bildexport geht den umgekehrten Weg und wickelt die PDF-Segmente in eine eigenständige Datei, deren Flags-Byte $03 ist – sequential mit unbekannter Seitenzahl –, gefolgt von einem angehängten End-of-File-Header. Die Random-Access-Arbeit betrifft also nur einen Pfad: eigenständige Dateien, die direkt an TPLJBIG2Decoder gehen, typischerweise bevor sie für PDF konvertiert oder neu komprimiert werden – der Job, den die JBIG2-Encoder-Backends in PDFlibPas auf der Ausgabeseite erledigen
Warum kann man eine Random-Access-Datei nicht in Dateireihenfolge lesen?
Weil nichts im Byte-Strom markiert, wo der Header-Block endet und der Datenblock beginnt – außer dem End-of-File-Segment-Header selbst. JBIG2-Segment-Header haben variable Länge: Die referred-to-Segment-Anzahl kann eine Drei-Bit-Kurzform oder eine Langform mit Retention-Bitmap sein, die referred-to-Segment-Nummern nehmen je nach eigener Nummer des Segments ein, zwei oder vier Bytes, und das Page-Association-Feld ist ein oder vier Bytes breit. Ein naiver sequenzieller Reader parst den ersten Header, liest seine Datenlänge und hält dann die ersten Bytes des zweiten Headers für die Daten dieses Segments. Der Decoder merkt den Fehler erst viel später – deshalb hat der alte Code die Organisation pauschal abgelehnt, statt es zu versuchen
Wie indexiert PDFlibPas Random-Access-Segment-Header?
PDFlibPas indexiert Random-Access-Header in einem einzigen Pre-Scan, IndexRandomHeaders, der jeden Header parst, nur sein Byte-Offset notiert und am ersten End-of-File-Header (Segmenttyp 51) anhält. Jeder Header wird vollständig geparst und wieder verworfen, der Index ist also ein Array von Integern statt einer Objektliste, und der Pre-Scan akkumuliert unterwegs die deklarierten Datenlängen. Ist der Scan fertig, sitzt der Reader auf dem ersten Byte der Daten des ersten Segments, und diese Position wird NextBodyOffset
// IndexRandomHeaders, lokal in TJBIG2StreamDecoder.readSegments
while not reader.isFinished do
begin
Offset := reader.bytePointer;
Header := TSegmentHeader.Create;
try
readSegmentHeader(Header);
if reader.BufferOverrun then
raise EJBIG2DecodeError.CreateFmt(
'JBIG2 truncated random-access header at byte %d', [Offset]);
if (HeaderCount > 0) and
(Cardinal(Header.getSegmentNumber) <= Cardinal(PreviousNumber)) then
raise EJBIG2DecodeError.Create('JBIG2 random-access segment numbers must increase');
PreviousNumber := Header.getSegmentNumber;
Count := Header.getSegmentDataLength;
if Count < 0 then
raise EJBIG2DecodeError.Create('JBIG2 unknown or oversized segment length is not supported');
Inc(TotalLength, Count); // Int64-Akkumulator
HeaderOffsets[HeaderCount] := Offset; // wächst stückweise
Inc(HeaderCount);
if Header.getSegmentType = JBIG2_END_OF_FILE then
begin
if Count <> 0 then
raise EJBIG2DecodeError.Create('JBIG2 invalid end segment length');
FoundEnd := True;
Break;
end;
finally
Header.Free;
end;
end;
if not FoundEnd then
raise EJBIG2DecodeError.Create('JBIG2 random-access file is missing its end-of-file header');
if TotalLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 truncated random-access segment data');
if TotalLength < Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 trailing random-access data');
NextBodyOffset := reader.bytePointer;
Jede Prüfung in dieser Schleife existiert, weil eine Random-Access-Datei weniger Redundanz hat als eine sequenzielle. Segmentnummern müssen streng steigen, verglichen als vorzeichenlose Werte, denn zwei Header mit derselben Nummer machen es mehrdeutig, welchen Body die referred-to-Liste einer späteren Region meint. Das Datenlängen-Feld liest handleSegmentDataLength, das jeden Wert mit gesetztem Top-Bit – auch den „unknown length“-Marker 0xFFFFFFFF – auf -1 abbildet; im Random-Access-Layout gibt es keine andere Möglichkeit zu finden, wo der nächste Body beginnt, also weist PDFlibPas diese Länge sofort zurück, statt nach einem Ende-Marker zu scannen. Die Summe muss in beide Richtungen exakt den verbleibenden Bytes entsprechen, und ein einziges zusätzliches Byte nach dem letzten Body scheitert an „trailing random-access data“. Diese Strenge ist Absicht: In diesem Layout bedeutet eine Längenabweichung, dass jeder Body hinter dem Fehlerpunkt verschoben ist, und ein Decoder, der über ein fremdes Byte hinwegsieht, hat keine Möglichkeit zu wissen, ob es harmloses Padding ist oder das erste Symptom verhaspelter Daten
Warum ging das letzte End-of-Page-Segment verloren?
Weil die erste Version der Decode-Schleife den sequenziellen Abbruchtest while not reader.isFinished beibehielt, und im Random-Access-Layout ist der Datenstrom zu Ende, bevor der Header-Index es ist. End-of-Page- (Typ 49) und End-of-File-Segmente tragen null Bytes Daten und sind normalerweise die letzten Header der Datei. Nachdem der letzte Region-Body verbraucht ist, sitzt der Reader exakt am Buffer-Ende, also bricht die Schleife ab und diese Segmente mit Länge null werden nie dispatched – die Seite bleibt unfertig. Der Fix lässt die Random-Access-Schleife Header statt Bytes zählen. Jede Iteration springt den Reader zum nächsten indexierten Header, setzt bitPointer auf 7 zurück, weil der vorherige Body mitten im Byte enden kann, parst diesen Header erneut, dann setzt sie bytePointer auf NextBodyOffset und schiebt ihn über den Body hinweg. Die bestehenden Segment-Handler, die referred-to-Prüfungen und die Context-Diagnostik laufen unverändert, und eine Fehlermeldung meldet weiterhin das ursprüngliche Byte-Offset des Headers, nicht die Body-Position
// TJBIG2StreamDecoder.readSegments, Hauptschleife
if randomAccessOrganisation then
IndexRandomHeaders;
while (randomAccessOrganisation and (HeaderIndex < HeaderCount)) or
((not randomAccessOrganisation) and (not reader.isFinished)) do
begin
if randomAccessOrganisation then
begin
reader.bytePointer := HeaderOffsets[HeaderIndex];
reader.bitPointer := 7; // neu ausrichten nach einem Teilbyte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // für den Fehlerkontext
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // Sprung zu den Daten dieses Segments
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... an den bestehenden Segment-Handler dispatchen, dann zu DataEnd springen
end;
Was beweist die Random-Access-Validierung wirklich?
Die Validierung beweist, dass die umgeordneten Bytes dieselben Pixel dekodieren wie ihre sequenziellen Originale, und dass fehlerhafte Random-Access-Eingaben sauber scheitern; sie beweist keine Abdeckung von Random-Access-Dateien beliebiger Encoder. Die gemeinsame Pascal-Regression nutzt eine 235-Byte-Synthetik-Datei auf einem Custom-Table-Fixture, die zu einer 7-mal-1-Reihe schwarzer Pixel dekodieren muss, einmal mit bekannter Seitenzahl und einmal mit entferntem Count-Feld, und füttert den Decoder dann mit jedem abgeschnittenen Präfix dieser Datei, einer doppelten Segmentnummer, einem einzelnen Trailing-Byte und einer unbekannten Datenlänge, wobei jedes Mal geprüft wird, dass LoadFromByteArray False zurückgibt und Width und Height auf null lässt. Der Real-Image-Fall ist ein 500-mal-473-Refinement-Image mit Custom Table, dessen Segmente in Random-Access-Layout umgeordnet wurden, mit jedem Original-Header und jedem komprimierten Byte erhalten; sein SHA-256 entspricht exakt dem geprüften sequenziellen Baseline. Diese Datei ist ein Derivat aus einem Organisations-Transform, kein natürliches Random-Access-Dokument aus freier Wildbahn, und ein solches natürliches Sample war nicht verfügbar. Die Suiten bestanden mit 1.598 Tests für Delphi Win32, 42 für die Delphi-Win64-Image-Suite, 48 für FPC Win32 und 46 für FPC Win64, neben den drei bestehenden sequenziellen Pixel-Fällen
Eine Random-Access-.jb2-Datei laden und ihre Grenzen
Anwendungscode ändert sich nicht: TPLJBIG2Decoder.LoadFromByteArray erkennt File-Header und Organisation selbst, gibt bei jeder abgelehnten Eingabe False zurück, mit dem Grund in LastError, und legt die dekodierte Seite über Width, Height und GetScanline offen, das ein Byte pro Pixel zurückgibt
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
// Sequenzielle und Random-Access-Einzeldateien nehmen denselben Aufruf
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;
Die Grenzen sollte man klar benennen. Random-Access-Support ist ein Feature zur Dateiorganisation, keine Random-Page-API: TPLJBIG2Decoder liefert weiterhin die Bitmap der ersten Seite, und es gibt keinen Aufruf, um Seite 7 aus einer 40-Seiten-Datei zu picken oder Seiten lazy zu dekodieren. Segmente mit unbekannter Datenlänge werden in Random-Access-Dateien abgelehnt, und die bestehenden Grenzen für Custom-Huffman-Prefix-Längen und Table-Entry-Counts bleiben unverändert. Diese Grenzen sind eng genug, dass eine Delphi-Anwendung die abgelehnten Fälle über LastError woanders hin umleiten kann, und der Rest der Bild-Pipeline, von der PDF-Bildextraktion bis zur JBIG2-Kodierung, ist auf der Produktseite der PDFlibPas-Delphi-PDF-Library abgedeckt