PDFlibPas versi 3.539.23 mendekode file JBIG2 standalone yang memakai random-access organization dari ITU-T T.88 Annex D.2, di mana setiap header segmen datang lebih dulu dan data segmennya menyusul dalam urutan yang sama. Decoder Pascal native di PDFlibJBIG2.pas mengindeks offset header sampai header end-of-file yang wajib ada, memeriksa bahwa nomor segmen meningkat dan panjang data yang dideklarasikan berjumlah tepat byte yang tersisa, lalu mendekode setiap body dalam urutan header tanpa menyalin atau mengurutkan ulang data terkompresinya. Sebelum rilis ini, file yang sama hanya kebagian error datar "random-access organisation is not supported" begitu flag header-nya terbaca
File JBIG2 random-access itu langka, dan justru itu sebabnya ia menyakitkan saat muncul. File-file itu keluar dari pipeline arsip dan sistem document-imaging yang ingin reader melihat setiap header segmen, dan dengan itu setiap dependensi halaman dan dictionary, sebelum menyentuh satu byte terkompresi pun. Aplikasi Delphi yang mengonversi arsip hasil pindai ke PDF secara batch biasanya bertemu salah satunya di tengah pekerjaan, setelah ratusan file sequential lolos mulus, dan decoder yang berhenti total di file yang well-formed hanya sedikit lebih baik daripada yang merender sampah. Rilis segaris itu baru saja selesai mengajari decoder custom Huffman tables dan canonical prefix codes JBIG2, jadi random access adalah celah organization terakhir yang tersisa di batas kemampuan terdokumentasi decoder
Apa itu random-access organization JBIG2?
Random-access organization adalah satu dari tiga cara T.88 Annex D mengizinkan segmen-segmen yang sama ditata: sequential (D.1) menyisipkan setiap header bersama datanya, random-access (D.2) meletakkan semua header lebih dulu dan semua data sesudahnya, dan embedded (D.3) adalah bentuk tanpa header yang dipakai di dalam container lain seperti PDF. File .jb2 standalone dimulai dengan identifier delapan byte 97 4A 42 32 0D 0A 1A 0A, disusul satu byte flags dan, ketika jumlah halaman diketahui, jumlah halaman empat byte. Bit 0 dari byte flags memilih organization-nya, dengan 1 berarti sequential dan 0 berarti random-access; bit 1 yang terisi berarti jumlah halaman tidak diketahui dan hitungan empat byte itu absen. PDFlibPas membaca semuanya di checkHeader dan setFileHeaderFlags, dan bit cadangan 2 sampai 7 ditoleransi alih-alih ditolak
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = random-access (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = jumlah halaman dihilangkan
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// PDF stream: tanpa file header, embedded organisation, satu halaman
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF sendiri tidak pernah membawa tata letak ini. Stream gambar JBIG2Decode, yang dijelaskan di ISO 32000-1 §7.4.7, hanya memuat segmen halaman dalam embedded organization, dengan dictionary simbol bersama dipindahkan ke stream JBIG2Globals terpisah serta tanpa file header, segmen end-of-page, atau end-of-file. Saat decodeJBIG2 gagal menemukan identifier delapan byte itu, ia mengasumsikan tepat kondisi itu dan memaksa dekode sequential satu halaman. Ekspor gambar JBIG2 native berjalan ke arah sebaliknya dan membungkus segmen PDF dalam file standalone yang byte flags-nya $03, sequential dengan jumlah halaman tak diketahui, disusul header end-of-file yang ditambahkan di belakang. Jadi pekerjaan random-access ini hanya menyentuh satu jalur: file standalone yang diserahkan langsung ke TPLJBIG2Decoder, biasanya sebelum dikonversi atau dikompres ulang untuk PDF, pekerjaan yang di sisi output ditangani encoder backend JBIG2 di PDFlibPas
Kenapa file random-access tidak bisa dibaca dalam urutan file?
File random-access tidak bisa dibaca dalam urutan file karena tidak ada apa pun di byte stream yang menandai di mana blok header berhenti dan blok data dimulai, kecuali header segmen end-of-file itu sendiri. Header segmen JBIG2 panjangnya variabel: jumlah referred-to segment bisa berupa bentuk pendek tiga bit atau bentuk panjang dengan retention bitmap, nomor referred-to segment memakan satu, dua, atau empat byte tergantung nomor segmennya sendiri, dan field page association berukuran satu atau empat byte. Reader sequential yang naif mem-parse header pertama, membaca panjang datanya, lalu memperlakukan byte pertama dari header kedua sebagai data segmen itu. Decoder baru tahu bahwa ia sudah keliru jauh kemudian, dan itulah kenapa kode lama menolak organization ini bulat-bulat alih-alih mencobanya
Bagaimana PDFlibPas mengindeks header segmen random-access?
PDFlibPas mengindeks header random-access dalam satu pre-scan, IndexRandomHeaders, yang mem-parse setiap header, hanya mencatat offset byte-nya, dan berhenti di header end-of-file pertama (tipe segmen 51). Setiap header di-parse penuh lalu dibuang, jadi indeksnya adalah array integer alih-alih daftar objek, dan pre-scan mengakumulasi panjang data yang dideklarasikan selagi berjalan. Ketika pemindaian selesai, reader duduk di byte pertama dari data segmen pertama, dan posisi itu menjadi NextBodyOffset
// IndexRandomHeaders, lokal di 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); // akumulator Int64
HeaderOffsets[HeaderCount] := Offset; // tumbuh per chunk
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;
Setiap pemeriksaan di loop itu ada karena file random-access punya redundansi lebih sedikit daripada yang sequential. Nomor segmen harus benar-benar meningkat, dibandingkan sebagai nilai unsigned, karena dua header yang mengklaim nomor yang sama membuat region berikutnya ambigu soal body mana yang dimaksud daftar referred-to-nya. Field panjang data dibaca oleh handleSegmentDataLength, yang memetakan setiap nilai dengan bit tertinggi terisi, termasuk penanda "unknown length" 0xFFFFFFFF, ke -1; dalam tata letak random-access tidak ada cara lain menemukan di mana body berikutnya dimulai, jadi PDFlibPas menolak panjang itu seketika alih-alih memindai penanda akhir. Totalnya harus cocok dengan byte yang tersisa persis di kedua arah, dan satu byte ekstra setelah body terakhir gagal dengan "trailing random-access data". Ketatnya memang disengaja: dalam tata letak ini ketidakcocokan panjang berarti setiap body setelah titik error bergeser, dan decoder yang mengabaikan satu byte nyasar tak punya cara mengetahui apakah itu padding yang tak berbahaya atau gejala pertama data yang tak sejajar
Kenapa segmen end-of-page terakhir hilang?
Segmen end-of-page terakhir hilang karena versi pertama loop dekode mempertahankan tes penghentian sequential, while not reader.isFinished, dan dalam tata letak random-access data stream habis sebelum indeks headernya. Segmen end-of-page (tipe 49) dan end-of-file membawa nol byte data, dan mereka biasanya header-header terakhir di file. Setelah body region terakhir dikonsumsi, reader duduk tepat di ujung buffer, jadi loop keluar dan segmen-segmen tanpa panjang itu tak pernah di-dispatch, menyisakan halaman yang tak selesai. Perbaikannya membuat loop random-access menghitung header alih-alih byte. Setiap iterasi melompatkan reader ke header terindeks berikutnya, mereset bitPointer ke 7 karena body sebelumnya bisa berakhir di tengah byte, mem-parse ulang header itu, lalu memindahkan bytePointer ke NextBodyOffset dan memajukannya melewati body. Handler segmen yang ada, pemeriksaan referred-to segment, dan diagnostik Context berjalan tanpa berubah, dan pesan error tetap melaporkan offset byte asli header-nya, bukan posisi body-nya
// TJBIG2StreamDecoder.readSegments, loop utama
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; // sejajarkan ulang setelah byte parsial
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // dipakai dalam konteks error
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // lompat ke data segmen ini
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... dispatch ke handler segmen yang ada, lalu seek ke DataEnd
end;
Apa yang sebenarnya dibuktikan validasi random-access ini?
Validasinya membuktikan bahwa byte yang ditata ulang terdekode ke piksel yang sama seperti aslinya yang sequential, dan membuktikan bahwa input random-access yang malformed gagal dengan bersih; ia tidak membuktikan cakupan atas file random-access dari encoder sembarangan. Regresi Pascal bersama memakai file sintetis 235 byte yang dibangun di atas fixture custom-table yang harus terdekode menjadi satu baris piksel hitam 7 kali 1, baik dengan jumlah halaman yang diketahui maupun dengan field hitungannya dihilangkan, lalu menyodorkan ke decoder setiap prefiks terpotong dari file itu, nomor segmen duplikat, satu byte ekor, dan panjang data tak diketahui, menegaskan setiap kali bahwa LoadFromByteArray mengembalikan False dan membiarkan Width serta Height di nol. Kasus gambar nyatanya adalah gambar refinement custom-table 500 kali 473 yang segmennya ditata ulang ke tata letak random-access dengan setiap header asli dan byte terkompresi dipertahankan; SHA-256-nya cocok persis dengan baseline sequential yang sudah ditinjau. File itu adalah turunan yang dihasilkan transformasi organization, bukan dokumen random-access alami yang ditemukan di alam liar, dan tak ada sampel alami semacam itu yang tersedia. Suite-nya lolos pada 1.598 tes untuk Delphi Win32, 42 untuk suite gambar Delphi Win64, 48 untuk FPC Win32, dan 46 untuk FPC Win64, bersama tiga kasus piksel sequential yang sudah ada
Memuat file .jb2 random-access dan batas-batasnya
Kode aplikasi tidak berubah: TPLJBIG2Decoder.LoadFromByteArray mendeteksi file header dan organization-nya sendiri, mengembalikan False pada setiap input yang ditolak dengan alasannya di LastError, dan mengekspos halaman hasil dekode lewat Width, Height, dan GetScanline, yang mengembalikan satu byte per piksel
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
// File standalone sequential maupun random-access lewat panggilan yang sama
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;
Batas-batasnya layak dinyatakan apa adanya. Dukungan random-access adalah fitur file organization, bukan API halaman acak: TPLJBIG2Decoder tetap mengembalikan bitmap halaman pertama, dan tak ada panggilan untuk mengambil halaman 7 dari file 40 halaman atau mendekode halaman secara lazy. Segmen dengan panjang data tak diketahui ditolak di file random-access, dan batas yang sudah ada pada panjang prefiks custom Huffman serta jumlah entri tabel tidak berubah. Batas-batas itu cukup sempit sehingga aplikasi Delphi bisa mengarahkan kasus yang ditolak ke tempat lain lewat LastError, dan sisa pipeline gambarnya, dari ekstraksi gambar PDF sampai encoding JBIG2, tercakup di halaman produk library PDF PDFlibPas untuk Delphi