Artikel Teknis

File Random-Access JBIG2: Mendekodenya di Delphi

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

Organization file JBIG2 di PDFlibPas: byte flags yang dibaca setFileHeaderFlags memilih sequential D.1 dengan header berselang-seling, random-access D.2 dengan setiap header sebelum blok data, atau embedded D.3, bentuk tanpa header yang dipakai stream JBIG2Decode dengan dictionary di JBIG2Globals
Ketiga tata letak membawa segmen yang sama, tapi hanya random access yang membuat reader melihat setiap dependensi halaman dan dictionary sebelum menyentuh satu byte terkompresi, dan itulah kenapa pipeline arsip memintanya
// 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

Pre-scan IndexRandomHeaders di PDFlibPas: setiap header segmen di-parse lalu dibuang sementara hanya offset byte-nya yang disimpan, nomor segmen harus benar-benar meningkat, panjang tak diketahui 0xFFFFFFFF ditolak, pemindaian berhenti di header end-of-file tipe 51, dan panjang yang dideklarasikan harus sama persis dengan byte yang tersisa
Ketatnya memang disengaja: dalam tata letak yang header-nya memberi satu-satunya peta data, satu byte nyasar berarti setiap body setelahnya bisa bergeser, jadi decoder yang menoleransinya tak bisa membedakan padding dari misalignment
// 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

Loop dekode random-access di PDFlibPas: setiap iterasi melompat ke HeaderOffsets header berjalan, mereset bitPointer ke 7 untuk membatalkan ekor tengah-byte, melompat ke NextBodyOffset untuk body-nya, dan menghitung header alih-alih byte sehingga segmen end-of-page tanpa panjang sempat di-dispatch sebelum loop berakhir
Karena segmen end-of-page dan end-of-file membawa nol byte data, data stream habis sebelum indeks headernya, dan hanya loop yang menghitung header yang bisa memberi segmen penutup itu gilirannya
// 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