PDFlibPas version 3.539.23 avkodar fristående JBIG2-filer som använder random-access-organisationen ur ITU-T T.88 Annex D.2, där alla segmentheaders kommer först och segmentdatan följer i samma ordning. Den inbyggda Pascal-avkodaren i PDFlibJBIG2.pas indexerar headeroffsets fram till obligatoriska end-of-file-headern, kontrollerar att segmentnummer ökar och att de deklarerade datalängderna summerar till exakt de byte som återstår, och avkodar sedan varje kropp i headerordning utan att kopiera eller ordna om den komprimerade datan. Före den här releasen kastade samma fil ett platt "random-access organisation is not supported"-fel i samma ögonblick som headerflaggorna lästes
Random-access-JBIG2-filer är ovanliga, vilket är precis varför de gör ont när de väl dyker upp. De kommer från arkiveringspipelines och dokumentavbildningssystem som vill att en läsare ska se varje segmentheader, och därmed varje sid- och dictionary-beroende, innan den rör en enda komprimerad byte. En Delphi-applikation som batchkonverterar skannade arkiv till PDF möter vanligen en av dessa mitt i ett jobb, efter att hundratals sekventiella filer gått igenom rent, och en avkodare som stannar helt på en välbildad fil är bara marginellt bättre än en som renderar skräp. Samma releaseserie hade nyss avslutat att lära avkodaren JBIG2 custom Huffman-tabeller och kanoniska prefixkoder, så random access var den sista organisationsluckan kvar i avkodarens dokumenterade kapacitetsgränser
Vad är JBIG2 random-access-organisationen?
Random-access-organisationen är ett av tre sätt T.88 Annex D tillåter samma segment att läggas ut på: sekventiell (D.1) flätar varje header med sin data, random-access (D.2) lägger alla headers först och all data efteråt, och embedded (D.3) är den headerlösa form som används inuti andra behållare som PDF. En fristående .jb2-fil börjar med den åtta byte långa identifieraren 97 4A 42 32 0D 0A 1A 0A, följt av en flags-byte och, när sidantalet är känt, ett fyra byte långt sidantal. Bit 0 i flags-byten väljer organisationen, där 1 betyder sekventiell och 0 betyder random-access; bit 1 satt betyder att antalet sidor är okänt och att det fyra byte långa antalet saknas. PDFlibPas läser dessa i checkHeader och setFileHeaderFlags, och de reserverade bitarna 2 till 7 tolereras i stället för att avvisas
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access-organisation (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = sidantal utelämnat
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF-ström: ingen filheader, embedded-organisation, en sida
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF själv bär aldrig denna layout. En JBIG2Decode-bildström, beskriven i ISO 32000-1 §7.4.7, innehåller bara sidsegmenten i embedded-organisation, med delade symbol-dictionaries flyttade till en separat JBIG2Globals-ström och ingen filheader, end-of-page- eller end-of-file-segment. När decodeJBIG2 inte hittar den åtta byte långa identifieraren antar den exakt det och tvingar fram sekventiell, ensidig avkodning. Den inbyggda JBIG2-bildexporten går åt andra hållet och lindar in PDF-segmenten i en fristående fil vars flags-byte är $03, sekventiell med okänt sidantal, följt av en tillagd end-of-file-header. Random-access-arbetet rör alltså bara en väg: fristående filer som lämnas till TPLJBIG2Decoder direkt, typiskt innan de konverteras eller komprimeras om för PDF, jobbet som JBIG2-encoder-backenderna i PDFlibPas sköter på utdatasidan
Varför kan en random-access-fil inte läsas i filordning?
En random-access-fil kan inte läsas i filordning eftersom ingenting i byteströmmen markerar var headerblocket slutar och datablocket börjar, utom själva end-of-file-segmentheadern. JBIG2-segmentheaders har variabel längd: antalet referred-to-segment kan vara en trebitars kortform eller en lång form med en retention-bitmap, referred-to-segmentnummer tar en, två eller fyra byte beroende på segmentets eget nummer, och sidassociationsfältet är en eller fyra byte. En naiv sekventiell läsare tolkar första headern, läser dess datalängd och behandlar sedan de första bytena i den andra headern som det segmentets data. Avkodaren kan inte se att den gått fel förrän mycket senare, vilket är varför den gamla koden vägrade organisationen rakt av i stället för att försöka
Hur indexerar PDFlibPas random-access-segmentheaders?
PDFlibPas indexerar random-access-headers i en enda förskanning, IndexRandomHeaders, som tolkar varje header, registrerar bara dess byteoffset och stannar vid första end-of-file-headern (segmenttyp 51). Varje header tolkas fullständigt och kasseras, så indexet är en array av heltal i stället för en lista av objekt, och förskanningen ackumulerar de deklarerade datalängderna allt eftersom. När skanningen är klar står läsaren på första byten av första segmentets data, och den positionen blir NextBodyOffset
// IndexRandomHeaders, lokal i 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-ackumulator
HeaderOffsets[HeaderCount] := Offset; // växer i omgångar
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;
Varje kontroll i den loopen finns för att en random-access-fil har mindre redundans än en sekventiell. Segmentnummer måste strikt öka, jämförda som teckenlösa värden, för att två headers som påstår samma nummer gör det tvetydigt vilken kropp en senare regions referred-to-lista menar. Datalängdfältet läses av handleSegmentDataLength, som avbildar varje värde med toppbiten satt, inklusive markören 0xFFFFFFFF för "unknown length", till -1; i random-access-layout finns det inget annat sätt att hitta var nästa kropp börjar, så PDFlibPas avvisar den längden omedelbart i stället för att söka efter en slutmarkör. Totalen måste matcha de återstående bytena exakt i båda riktningarna, och en enda extra byte efter sista kroppen misslyckas med "trailing random-access data". Striktheten är medveten: i den layouten betyder ett längdfel att varje kropp efter felspunkten är förskjuten, och en avkodare som rycker på axlarna åt en ensam vilsekommen byte har inget sätt att veta om den är harmlös padding eller det första symptomet på förskjuten data
Varför försvann sista end-of-page-segmentet?
Sista end-of-page-segmentet försvann för att den första versionen av avkodningsloopen behöll det sekventiella avslutstestet, while not reader.isFinished, och i random-access-layout tar dataströmmen slut innan headerindexet gör det. End-of-page-segment (typ 49) och end-of-file-segment bär noll byte data, och de är normalt de sista headers i filen. Efter att sista regionkroppen konsumerats står läsaren exakt vid slutet av bufferten, så loopen avslutas och de nollängd-segmenten skickas aldrig vidare, vilket lämnar sidan ofullständig. Fixen låter random-access-loopen räkna headers i stället för byte. Varje iteration hoppar läsaren till nästa indexerade header, nollställer bitPointer till 7 eftersom föregående kropp kan ha slutat mitt i en byte, tolkar den headern igen, flyttar sedan bytePointer till NextBodyOffset och för den förbi kroppen. De befintliga segmenthanterarna, referred-to-kontrollerna och Context-diagnostiken kör oförändrade, och ett felmeddelande rapporterar fortfarande headerns ursprungliga byteoffset, inte kroppspositionen
// TJBIG2StreamDecoder.readSegments, huvudloop
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; // rikta om efter en partiell byte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // används i felkontext
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // hoppa till detta segments data
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... skicka vidare till befintlig segmenthanterare, sök sedan till DataEnd
end;
Vad bevisar random-access-valideringen egentligen?
Valideringen bevisar att de omorganiserade bytena avkodar till samma pixlar som sina sekventiella original, och den bevisar att felformaterad random-access-indata misslyckas rent; den bevisar inte täckning av random-access-filer från godtyckliga encoders. Den gemensamma Pascal-regressionssviten använder en 235 byte stor syntetisk fil byggd på en custom-table-fixtur som måste avkoda till en rad av 7 gånger 1 svarta pixlar, både med känt sidantal och med antalsfältet borttaget, och matar sedan avkodaren varje avkortat prefix av filen, ett duplicerat segmentnummer, en avslutande extra byte och en okänd datalängd, och hävdar varje gång att LoadFromByteArray returnerar False och lämnar Width och Height på noll. Fallet med riktig bild är en refinement-bild på 500 gånger 473 med custom-table vars segment omorganiserades till random-access-layout med varje originalheader och komprimerad byte bevarad; dess SHA-256 matchar den granskade sekventiella baslinjen exakt. Den filen är en derivata skapad av en organisationstransform, inte ett naturligt random-access-dokument hittat i det vilda, och inget sådant naturligt exemplar var tillgängligt. Sviterna gick igenom vid 1 598 tester för Delphi Win32, 42 för Delphi Win64-bildsviten, 48 för FPC Win32 och 46 för FPC Win64, jämte de tre befintliga sekventiella pixelfallen
Läsa in en random-access-.jb2-fil och dess begränsningar
Applikationskoden ändras inte: TPLJBIG2Decoder.LoadFromByteArray upptäcker filheadern och organisationen på egen hand, returnerar False för varje avvisad indata med skälet i LastError, och exponerar den avkodade sidan genom Width, Height och GetScanline, som returnerar en byte per pixel
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
// Sekventiella och random-access-fristående filer tar samma anrop
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;
Gränserna är värda att säga rakt ut. Random-access-stödet är en filorganisationsfunktion, inte ett godtyckligt sid-API: TPLJBIG2Decoder returnerar fortfarande första sidans bitmap, och det finns inget anrop för att plocka sida 7 ur en 40-sidig fil eller för att avkoda sidor lättjädigt. Segment med okänd datalängd avvisas i random-access-filer, och de befintliga gränserna för custom Huffman-prefixlängder och tabellposter är oförändrade. De gränserna är snäva nog att en Delphi-applikation kan dirigera de avvisade fallen någon annanstans via LastError, och resten av bildpipelinen, från PDF-bildextraktion till JBIG2-kodning, täcks på produktsidan för PDFlibPas, PDF-biblioteket för Delphi