Το PDFlibPas έκδοση 3.539.23 αποκωδικοποιεί standalone αρχεία JBIG2 που χρησιμοποιούν την random-access οργάνωση από το Παράρτημα D.2 του ITU-T T.88, όπου όλα τα segment headers έρχονται πρώτα και τα segment data ακολουθούν στην ίδια σειρά. Ο native Pascal decoder στο PDFlibJBIG2.pas ευρετηριάζει τα header offsets μέχρι το υποχρεωτικό end-of-file header, ελέγχει ότι οι αριθμοί segment αυξάνονται και ότι τα δηλωμένα μήκη δεδομένων αθροίζουν ακριβώς τα bytes που απομένουν, και μετά αποκωδικοποιεί κάθε body σε σειρά header χωρίς αντιγραφή ή αναδιάταξη των συμπιεσμένων δεδομένων. Πριν από αυτή την έκδοση το ίδιο αρχείο πετούσε ένα flat error «random-access organisation is not supported» τη στιγμή που διαβαζόταν τα header flags
Τα random-access αρχεία JBIG2 είναι σπάνια, που είναι ακριβώς ο λόγος που πονάνε όταν εμφανιστούν. Βγαίνουν από archival pipelines και document-imaging συστήματα που θέλουν ο reader να δει κάθε segment header, άρα κάθε εξάρτηση σελίδας και dictionary, πριν αγγίξει ένα συμπιεσμένο byte. Μια εφαρμογή Delphi που μετατρέπει μαζικά σκαναρισμένα αρχεία σε PDF συνήθως πέφτει σε ένα από αυτά στη μέση μιας δουλειάς, αφού εκατοντάδες sequential αρχεία πέρασαν καθαρά, και ένας decoder που σταματά νεκρός σε ένα καλοσχηματισμένο αρχείο είναι οριακά καλύτερος από έναν που αποδίδει σκουπίδια. Η ίδια σειρά εκδόσεων είχε μόλις τελειώσει να μαθαίνει στον decoder το custom Huffman tables και canonical prefix codes του JBIG2, οπότε το random access ήταν το τελευταίο κενό οργάνωσης που απέμενε στα τεκμηριωμένα όρια δυνατοτήτων του decoder
Τι είναι η random-access οργάνωση του JBIG2;
Η random-access οργάνωση είναι ένας από τους τρεις τρόπους που το Παράρτημα D του T.88 επιτρέπει να τοποθετούνται τα ίδια segments: το sequential (D.1) μπλέκει κάθε header με τα δεδομένα του, το random-access (D.2) βάζει όλα τα headers πρώτα και όλα τα δεδομένα μετά, και το embedded (D.3) είναι η χωρίς header μορφή που χρησιμοποιείται μέσα σε άλλα containers όπως το PDF. Ένα standalone αρχείο .jb2 ξεκινά με τον οκτά-byte αναγνωριστικό 97 4A 42 32 0D 0A 1A 0A, ακολουθούμενο από ένα byte flags και, όταν ο αριθμός σελίδων είναι γνωστός, ένα τεσσάρων-byte πλήθος σελίδων. Το bit 0 του byte flags επιλέγει την οργάνωση, με 1 να σημαίνει sequential και 0 random-access· το bit 1 σε 1 σημαίνει ότι ο αριθμός σελίδων είναι άγνωστος και το τεσσάρων-byte πλήθος απουσιάζει. Το PDFlibPas τα διαβάζει στο checkHeader και στο setFileHeaderFlags, και τα reserved bits 2 έως 7 γίνονται ανεκτά αντί να απορρίπτονται
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = παραλείπεται ο αριθμός σελίδων
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: κανένα file header, embedded οργάνωση, μία σελίδα
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
Το ίδιο το PDF δεν κουβαλά ποτέ αυτή τη διάταξη. Ένα image stream JBIG2Decode, όπως περιγράφεται στο ISO 32000-1 §7.4.7, κρατά μόνο τα page segments σε embedded οργάνωση, με τα κοινόχρηστα symbol dictionaries μετακομισμένα σε ξεχωριστό stream JBIG2Globals και κανένα file header, end-of-page ή end-of-file segment. Όταν το decodeJBIG2 δεν βρίσκει τον οκτά-byte αναγνωριστικό, υποθέτει ακριβώς αυτό και επιβάλλει sequential, single-page αποκωδικοποίηση. Το native JBIG2 image export πάει προς την άλλη κατεύθυνση και τυλίγει τα PDF segments σε standalone αρχείο με byte flags $03, sequential με άγνωστο πλήθος σελίδων, ακολουθούμενο από προσαρτημένο end-of-file header. Η δουλειά του random access λοιπόν αγγίζει ένα μόνο μονοπάτι: standalone αρχεία που παραδίδονται απευθείας στο TPLJBIG2Decoder, τυπικά πριν μετατραπούν ή ξανασυμπιεστούν για PDF, η δουλειά που αναλαμβάνουν στην έξοδο τα JBIG2 encoder backends στο PDFlibPas
Γιατί ένα random-access αρχείο δεν μπορεί να διαβαστεί σε σειρά αρχείου;
Ένα random-access αρχείο δεν μπορεί να διαβαστεί σε σειρά αρχείου επειδή τίποτα στη ροή bytes δεν σηματοδοτεί πού σταματά το block headers και πού αρχίζει το block δεδομένων, εκτός από το ίδιο το segment header end-of-file. Τα segment headers του JBIG2 έχουν μεταβλητό μήκος: το πλήθος referred-to segment μπορεί να είναι τρίbit σύντομη μορφή ή μεγάλη μορφή με retention bitmap, οι αριθμοί referred-to segment παίρνουν ένα, δύο ή τέσσερα bytes ανάλογα με τον αριθμό του ίδιου του segment, και το πεδίο page association είναι ένα ή τέσσερα bytes. Ένας αφελής sequential reader αναλύει το πρώτο header, διαβάζει το μήκος δεδομένων του, και μετά μεταχειρίζεται τα πρώτα bytes του δεύτερου header ως δεδομένα εκείνου του segment. Ο decoder δεν μπορεί να καταλάβει ότι πήγε στραβά μέχρι πολύ αργότερα, γι' αυτό ο παλιός κώδικας αρνιόταν την οργάνωση εξ ολοκλήρου αντί να την επιχειρήσει
Πώς ευρετηριάζει το PDFlibPas τα random-access segment headers;
Το PDFlibPas ευρετηριάζει τα random-access headers σε ένα pre-scan, το IndexRandomHeaders, που αναλύει κάθε header, καταγράφει μόνο το byte offset του, και σταματά στο πρώτο end-of-file header (segment type 51). Κάθε header αναλύεται πλήρως και πετιέται, οπότε το index είναι πίνακας ακέραιων αντί λίστα objects, και το pre-scan αθροίζει τα δηλωμένα μήκη δεδομένων καθώς προχωρά. Όταν το σκαν τελειώσει, ο reader κάθεται στο πρώτο byte των δεδομένων του πρώτου segment, και εκείνη η θέση γίνεται NextBodyOffset
// IndexRandomHeaders, τοπικό στο 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
HeaderOffsets[HeaderCount] := Offset; // μεγαλώνει σε κομμάτια
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;
Κάθε έλεγχος σε εκείνη τη λούπα υπάρχει επειδή ένα random-access αρχείο έχει λιγότερη πλεονάζουσα πληροφορία από ένα sequential. Οι αριθμοί segment πρέπει να αυξάνονται αυστηρά, συγκρινόμενοι ως unsigned τιμές, επειδή δύο headers που διεκδικούν τον ίδιο αριθμό κάνουν ασαφές ποιο body εννοεί η referred-to λίστα μιας μεταγενέστερης περιοχής. Το πεδίο μήκους δεδομένων διαβάζεται από το handleSegmentDataLength, που αντιστοιχίζει κάθε τιμή με set το πάνω bit, συμπεριλαμβανομένου του δείκτη «unknown length» 0xFFFFFFFF, στο -1· σε random-access διάταξη δεν υπάρχει άλλος τρόπος να βρεθεί πού αρχίζει το επόμενο body, οπότε το PDFlibPas απορρίπτει εκείνο το μήκος αμέσως αντί να σκανάρει για end marker. Το σύνολο πρέπει να ταιριάζει τα υπόλοιπα bytes ακριβώς και προς τις δύο κατευθύνσεις, και ένα μοναδικό παραπάνω byte μετά το τελευταίο body αποτυγχάνει με «trailing random-access data». Η αυστηρότητα είναι σκόπιμη: σε αυτή τη διάταξη ένα mismatch μήκους σημαίνει ότι κάθε body μετά το σημείο λάθους είναι μετατοπισμένο, και ένας decoder που το περνά ελαφρά από ένα παραπάνω byte δεν έχει τρόπο να ξέρει αν είναι αβλαβές padding ή το πρώτο σύμπτωμα παραστρατημένων δεδομένων
Γιατί χάθηκε το τελευταίο segment end-of-page;
Το τελευταίο segment end-of-page χάθηκε επειδή η πρώτη εκδοχή της λούπας αποκωδικοποίησης κράτησε το sequential τεστ τερματισμού, while not reader.isFinished, και σε random-access διάταξη η ροή δεδομένων τελειώνει πριν το header index. Τα segments end-of-page (type 49) και end-of-file κουβαλάνε μηδέν bytes δεδομένων, και κανονικά είναι τα τελευταία headers στο αρχείο. Αφού καταναλωθεί το body της τελικής περιοχής, ο reader κάθεται ακριβώς στο τέλος του buffer, οπότε η λούπα εξέρχεται και εκείνα τα segments μηδενικού μήκους δεν γίνονται ποτέ dispatch, αφήνοντας τη σελίδα ημιτελή. Το fix κάνει τη random-access λούπα να μετράει headers αντί για bytes. Κάθε επανάληψη πηδάει τον reader στο επόμενο ευρετηριασμένο header, μηδενίζει το bitPointer στο 7 επειδή το προηγούμενο body μπορεί να τελείωσε μέσα σε byte, ξανα-αναλύει εκείνο το header, μετά μετακινεί το bytePointer στο NextBodyOffset και το προχωρά πέρα από το body. Οι υπάρχοντες segment handlers, οι έλεγχοι referred-to segment και η διαγνωστική Context τρέχουν αμετάβλητοι, και ένα μήνυμα error αναφέρει ακόμα το πρωτότυπο byte offset του header, όχι τη θέση του body
// TJBIG2StreamDecoder.readSegments, κύρια λούπα
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; // ευθυγράμμιση μετά από μερικό byte
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // χρησιμοποιείται σε error context
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // άλμα στα δεδομένα αυτού του segment
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch στον υπάρχοντα segment handler, μετά seek στο DataEnd
end;
Τι αποδεικνύει στην πραγματικότητα η random-access επικύρωση;
Η επικύρωση αποδεικνύει ότι τα αναδιοργανωμένα bytes αποκωδικοποιούνται στα ίδια pixels με τα sequential πρωτότυπά τους, και αποδεικνύει ότι κακοσχηματισμένη random-access είσοδος αποτυγχάνει καθαρά· δεν αποδεικνύει κάλυψη random-access αρχείων από αυθαίρετους encoders. Το κοινό Pascal regression χρησιμοποιεί ένα συνθετικό αρχείο 235 bytes χτισμένο πάνω σε custom-table fixture που πρέπει να αποκωδικοποιηθεί σε σειρά 7 επί 1 μαύρα pixels, τόσο με γνωστό πλήθος σελίδων όσο και με αφαιρεμένο το πεδίο πλήθους, και μετά ταΐζει στον decoder κάθε κομμένο πρόθεμα εκείνου του αρχείου, έναν διπλό αριθμό segment, ένα byte ουράς και ένα άγνωστο μήκος δεδομένων, ισχυριζόμενο κάθε φορά ότι το LoadFromByteArray επιστρέφει False και αφήνει Width και Height στο μηδέν. Η περίπτωση πραγματικής εικόνας είναι ένα refinement image custom-table 500 επί 473 του οποίου τα segments αναδιοργανώθηκαν σε random-access διάταξη με κάθε πρωτότυπο header και συμπιεσμένο byte διατηρημένο· το SHA-256 του ταιριάζει ακριβώς το reviewed sequential baseline. Εκείνο το αρχείο είναι παράγωγο που παρήχθη από μετασχηματισμό οργάνωσης, όχι φυσικό random-access έγγραφο βρεμένο στην άγρια φύση, και κανένα τέτοιο φυσικό δείγμα δεν ήταν διαθέσιμο. Οι suites πέρασαν σε 1.598 tests για Delphi Win32, 42 για το image suite Delphi Win64, 48 για FPC Win32 και 46 για FPC Win64, μαζί με τις τρεις υπάρχουσες sequential pixel περιπτώσεις
Φόρτωση ενός random-access .jb2 αρχείου και τα όριά της
Ο κώδικας εφαρμογής δεν αλλάζει: το TPLJBIG2Decoder.LoadFromByteArray ανιχνεύει μόνο του το file header και την οργάνωση, επιστρέφει False σε κάθε απορριφθείσα είσοδο με την αιτία στο LastError, και εκθέτει την αποκωδικοποιημένη σελίδα μέσω Width, Height και GetScanline, που επιστρέφει ένα byte ανά 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
// Sequential και random-access standalone αρχεία παίρνουν την ίδια κλήση
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;
Τα όρια αξίζει να δηλωθούν απλά. Η υποστήριξη random-access είναι χαρακτηριστικό οργάνωσης αρχείου, όχι API τυχαίων σελίδων: το TPLJBIG2Decoder εξακολουθεί να επιστρέφει το bitmap της πρώτης σελίδας, και δεν υπάρχει κλήση να διαλέξετε τη σελίδα 7 από αρχείο 40 σελίδων ή να αποκωδικοποιήσετε σελίδες τεμπέλικα. Segments με άγνωστο μήκος δεδομένων απορρίπτονται σε random-access αρχεία, και τα υπάρχοντα όρια στα μήκη προθέματος custom Huffman και στα πλήθη εγγραφών πινάκων μένουν αμετάβλητα. Εκείνα τα όρια είναι αρκετά στενά ώστε μια εφαρμογή Delphi να δρομολογεί τις απορριφθείσες περιπτώσεις αλλού μέσω LastError, και το υπόλοιπο του image pipeline, από εξαγωγή PDF εικόνων ως κωδικοποίηση JBIG2, καλύπτεται στη σελίδα προϊόντος PDFlibPas Delphi PDF library