Artikel Teknis

JBIG2 Halftone: Mask HSKIP dan Grid Negatif di PDFlibPas

PDFlibPas memperbaiki dua fault independen di decoder JBIG2 halftone region natifnya: di v3.539.37 mask skip HSKIP diindeks sebagai HSKIP[ng, mg] sesuai definisi ITU-T T.88 §6.6.5.1, dan di v3.539.38 grid yang mencapai koordinat negatif, lewat HGX atau HGY negatif atau lewat rotasi, ditempatkan dengan floor shift yang benar. Sebelum release-release itu, halftone region yang terdampak keluar garbled atau bergeser, tanpa error yang di-raise. Kedua bug itu sembunyi di balik data test yang kebetulan simetris atau non-negatif, dan yang kedua bergantung pada satu sifat Delphi dan Free Pascal yang menggigit jauh di luar JBIG2: shr pada integer signed adalah logical shift, bukan >> aritmetis yang diasumsikan standar

Halftone region adalah tipe region JBIG2 yang paling jarang, jadi sebuah decoder bisa memproses ribuan dokumen hasil scan sebelum bertemu foto terscreen yang di-encode sebagai satu. Begitu bertemu, kegagalannya menjijikkan: file-nya ter-parse, panjang segment berjumlah pas, halaman berukuran benar, dan region-nya sampah

Sebenarnya apa yang di-decode sebuah JBIG2 halftone region?

JBIG2 halftone region adalah grid bitmap kecil yang diambil dari sebuah pattern dictionary, dan kerja sesungguhnya decoder adalah menghitung indeks untuk setiap sel grid serta posisi piksel tempat sel itu mendarat. Pattern dictionary menyimpan HNUMPATS pattern berukuran HPW × HPH piksel. Segment halftone region lalu mendeskripsikan grid HGW kolom kali HGH baris dan sebuah gray-scale image berukuran sama, di-code sebagai bitplane Gray-coded. Tiap bitplane di-decode dengan prosedur generic region di atas bitmap HGW × HGH, plane paling signifikan lebih dulu, dan plane-plane itu bersama-sama memberi tiap sel indeks pattern-nya

Penempatan sel memakai aritmetika fixed-point dengan pecahan 8-bit. Origin grid HGX, HGY adalah sepasang nilai 32-bit, dan vektor grid HRX, HRY mendeskripsikan langkah antar sel tetangga, yang memungkinkan grid berotasi. Untuk baris grid mg dan kolom grid ng, T.88 §6.6.5 menghitung posisi piksel sebagai:

  • x = (HGX + mg × HRY + ng × HRX) >> 8
  • y = (HGY + mg × HRX − ng × HRY) >> 8

Mask skip masuk lewat flag opsional HENABLESKIP. Ketika flag diset, §6.6.5.1 membangun bitmap HGW × HGH bernama HSKIP dan men-set HSKIP[ng, mg] ke 1 untuk setiap sel yang pattern-nya berada sepenuhnya di luar region: x + HPW <= 0, x >= HBW, y + HPH <= 0, atau y >= HBH. Bitplane gray-scale lalu di-decode dengan mask itu sebagai skip bitmap generic region, sehingga arithmetic decoder tidak membaca dan tidak memperbarui context untuk sel yang di-skip. Decoder dan encoder harus sepakat pada setiap bit HSKIP, atau kedua arithmetic coder kehilangan irama

Kenapa mask HSKIP yang tertransposisikan hanya merusak grid non-persegi?

Mask skip ditulis dengan koordinatnya tertukar, dan hanya grid non-persegi yang membukanya, karena grid persegi menjaga setiap koordinat tertukar tetap di dalam mask. PDFlibPas menyimpan bitmap dengan accessor piksel (column, row), dan code yang membangun mask meneruskan (mg, ng), baris dulu. Decoder bitplane gray-scale membaca mask itu dengan benar sebagai (ng, mg). Loop penempatan pattern membacanya kembali dalam urutan tertukar milik builder, jadi keduanya sepakat, dan review atas logika penempatan saja akan lolos. Jebakan penamaan memperparahnya: di loop penempatan, variabel bernama col mengiterasi baris grid dan Row mengiterasi kolom grid

Ambil grid 5 × 3 berisi pattern 4 × 4 di region 16 × 8 yang dipakai v3.539.37 sebagai kasus regresinya. Dengan HRX = 1024 dan HRY = 0, kolom grid 4 mendarat di x = 16 dan baris grid 2 di y = 8, keduanya di luar region. Mask yang benar menandai tujuh sel: seluruh kolom 4 dan seluruh baris 2. Penulisan yang tertukar mencoba men-set piksel di indeks baris 3 dan 4 pada mask yang tingginya cuma tiga baris, dan setter bitmap diam-diam mengabaikan penulisan out-of-range itu. Yang tersisa adalah kolom 2, baris 0 sampai 2. Decoder karenanya melewati dua sel yang sudah di-code encoder, dan men-decode enam sel yang dilewati encoder

Mask skip halftone JBIG2 PDFlibPas untuk grid 5 kali 3 di mana HSKIP[ng, mg] yang benar menandai kolom 4 dan baris 2 sebagai skipped, sedangkan penulisan tertransposisi yang menyasar baris 3 dan 4 dari mask tiga baris dibuang diam-diam dan hanya kolom 2 yang tersisa, membuat arithmetic coder kehilangan sinkron
Hanya grid non-persegi yang membuka mask tertransposisi, dan desync coder yang dihasilkannya menggarble region alih-alih me-raise error

Arithmetic decoder tak gagal ketika itu terjadi. Ia men-decode piksel ekstra dari bit milik sel-sel berikutnya, context-nya membaca tetangga yang salah, dan setiap indeks pattern setelah ketidaksepakatan pertama adalah derau, itulah kenapa gejalanya region yang tergarble alih-alih beberapa sel yang salah tempat. Di grid persegi bug yang sama sering tak terlihat: tak ada koordinat tertukar yang keluar dari mask, dan ketika sel di luar region simetris terhadap diagonal, misalnya grid yang menjorok ke tepi kanan dan bawah sebanyak jumlah sel yang sama, mask tertransposisinya adalah mask yang benar bit demi bit. HENABLESKIP juga opsional, harus 0 ketika gray-scale image di-code MMR, dan jarang diset encoder, sehingga bug itu nyaris tak punya celah untuk muncul. Sejak v3.539.37 builder menulis HSKIP[ng, mg] dan loop penempatan membaca urutan yang sama

Kenapa offset grid halftone negatif gagal di tiga lapis?

Grid halftone yang mulai di kiri atau di atas region-nya merusak PDFlibPas di tiga tempat terpisah, dan tiap fault menyembunyikan fault berikutnya. T.88 membolehkan geometri ini dengan sengaja. Encoder yang menyelaraskan screen-nya ke halaman alih-alih ke region, atau memakai grid berotasi, alami menghasilkan sudut sel negatif yang di-crop region. v3.539.38 memperbaiki ketiga lapis sekaligus, karena memperbaiki satu saja hanya mengubah gejalanya

Lapis 1: field signed dibaca sebagai unsigned

T.88 §7.4.5.1.2 mendefinisikan HGX dan HGY sebagai nilai 32-bit signed, tapi decoder membacanya dengan helper 32-bit yang sama yang ia pakai untuk field unsigned, dan helper itu men-clamp setiap hasil negatif ke 0. Grid yang seharusnya mulai di HGX = -900 diam-diam dipindahkan ke origin region. Di kasus regresi v3.539.38 seluruh gambarnya keluar dua baris terlalu rendah. Clamp itu juga menjelaskan kenapa dua fault lainnya bertahan begitu lama: dengan origin yang dipaksa non-negatif, koordinat negatif hanya bisa muncul lewat grid berotasi dengan HRY > 0, tempat y = HGY + mg × HRX − ng × HRY jatuh di bawah nol untuk kolom grid berikutnya

Lapis 2: shr bukan >> 8

T.88 menulis >> 8 dan maksudnya arithmetic shift yang membulatkan ke minus infinity. Decoder menerjemahkannya sebagai shr 8. Di Delphi dan Free Pascal, shr pada integer signed adalah logical shift: bit tanda digeser masuk sebagai nol. Untuk Integer yang bernilai -512, shr 8 menghasilkan 16777214 alih-alih -2. Pattern yang seharusnya digambar di y = -2 dan di-crop ke paruh bawahnya terkirim 16 juta baris ke bawah dan dibuang sebagai off-region. Tak ada yang crash; baris atas halftone begitu saja lenyap

Lapis 3: membandingkan fixed point alih-alih piksel

Test skip membandingkan nilai fixed-point, bukan posisi piksel, dan keduanya tak setara begitu pecahannya non-nol. Code aslinya menghindari logical shift dengan menguji xx + HPW × 256 <= 0 pada nilai yang belum digeser, padanan yang diduga setara dengan test T.88. Dengan HGX = -900 dan pattern 4 piksel, hasilnya -900 + 1024 = 124, positif, jadi sel itu tak di-skip. Standar menggesernya lebih dulu: floor(-900 / 256) = -4, dan -4 + 4 = 0 memenuhi x + HPW <= 0, jadi sel itu berada sepenuhnya di luar dan harus di-skip. Encoder melewatinya, decoder men-decode-nya, dan gray-scale image bergeser persis seperti di kasus mask tertransposisi

Fault halftone JBIG2 PDFlibPas untuk grid di HGX negatif: field signed yang dibaca lewat helper unsigned ter-clamp ke nol, shift right T.88 yang diterjemahkan sebagai shr logical yang mengirim pattern 16 juta baris ke bawah, dan test skip pada nilai fixed-point yang mempertahankan sel yang dilewati encoder
Tiap fault menyembunyikan fault berikutnya, itulah kenapa v3.539.38 memperbaiki ketiga lapis sekaligus dalam satu helper bersama HalftoneGridPixel yang dipakai builder mask dan loop penempatan

Kasus regresi dari v3.539.38 memakai grid 4 × 3 berisi pattern 4 × 4 di HGX = -900, HGY = -512, HRX = 1024 pada region 12 × 10. Kolom grid mendarat di x = -4, 0, 4, dan 8, jadi kolom 0 sepenuhnya di luar dan pantas masuk HSKIP; baris grid mendarat di y = -2, 2, dan 6, jadi baris 0 harus di-crop ke dua baris piksel bawahnya alih-alih dibuang. Memperbaiki lapis satu per satu mereproduksi tumpukannya:

Fault yang diperbaikiRegion hasil decode
Tak ada (sebelum v3.539.38)Grid tertarik ke origin, seluruh gambar dua baris terlalu rendah
Hanya pembacaan HGX / HGY signedBaris grid pertama hilang, sisanya tergarble oleh drift test skip
Pembacaan signed, floor shift, dan test skip di ruang pikselIdentik, piksel demi piksel, dengan halaman yang dihitung dari T.88 §6.6.5 dan dengan dua reference decoder independen

Perbaikannya satu helper, HalftoneGridPixel, dipakai bersama oleh builder mask skip dan loop penempatan. Ia mengakumulasi koordinat di Int64 agar hasil kali mg × HRX yang besar tak bisa wrap, membagi dengan 256 dengan pembulatan ke minus infinity, dan men-clamp ke ±MaxInt div 2 agar grid korup tak bisa meng-overflow aritmetika bitmap belakangan. Test skip kini membandingkan nilai piksel itu terhadap HPW, HPH, HBW, dan HBH, persis seperti yang dinyatakan §6.6.5.1

Bagaimana menulis arithmetic right shift di Delphi?

Delphi tak punya operator shift aritmetis, jadi right shift signed yang benar harus ditulis sebagai floor division, dan div polos bukan pembagian itu. div memotong ke arah nol. Untuk nilai non-negatif, truncation dan floor sepakat, dan keduanya juga sepakat untuk nilai negatif yang merupakan kelipatan eksak pembaginya, itulah kenapa -512 div 256 = -2 terlihat benar di test singkat. Keduanya berselisih di tempat lain: -900 div 256 adalah -3, sementara floor-nya -4, dan -1 div 256 adalah 0, sementara floor-nya -1. Koordinat JBIG2 dengan pecahan non-nol adalah persis kasus di mana div memberi piksel yang salah

Di compiler Delphi Win32 dan Win64, variabel Integer yang bernilai -512 digeser kanan 8 menghasilkan 16777214, dan Int64 yang bernilai -512 menghasilkan 72057594037927934. Free Pascal juga mendefinisikan shr sebagai logical shift dan menyertakan SarLongint serta SarInt64 di unit System-nya untuk versi aritmetis, tapi fungsi-fungsi itu tak ada di Delphi, jadi code yang dipakai bersama kedua compiler butuh helper-nya sendiri:

// Floor division: membulatkan ke minus infinity untuk tanda A dan B apa pun.
// B tak boleh 0, dan FloorDiv(Low(Integer), -1) overflow persis seperti div
function FloorDiv(A, B: Integer): Integer;
begin
  Result := A div B;
  if (A mod B <> 0) and ((A < 0) <> (B < 0)) then
    Dec(Result);
end;

// Arithmetic right shift (si ">>" milik C dan T.88 pada nilai signed).
// Untuk Value negatif, not Value = -Value - 1 tak bernegatif, jadi
// logical shr aman di sana, dan not di luar memetakan hasilnya kembali
function SarInt32(Value: Integer; Shift: Integer): Integer;  // Shift 0..31
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

function SarInt64(Value: Int64; Shift: Integer): Int64;      // Shift 0..63
begin
  if Value >= 0 then
    Result := Value shr Shift
  else
    Result := not ((not Value) shr Shift);
end;

Trik not tak pernah menggeser angka negatif, jadi ia tak bergantung pada cara compiler memperlakukan bit tanda, dan ia tak pernah overflow, termasuk untuk Low(Integer). Kedua helper cocok dengan referensi floor Int64 atas beberapa juta nilai, setiap shift dari 0 sampai 31 serta tepi Low(Integer) dan High(Integer) di Delphi Win32, Delphi Win64, dan Free Pascal x86_64. Sanity check yang layak dipertahankan di unit test mana pun yang menyentuh koordinat:

var
  V: Integer;
begin
  V := -900;
  Writeln(V shr 8);           // 16777212  logical shift, bug lama
  Writeln(V div 256);         // -3        truncation ke arah nol
  Writeln(FloorDiv(V, 256));  // -4        yang dimaksud T.88 dengan >> 8
  Writeln(SarInt32(V, 8));    // -4
end;
Garis bilangan PDFlibPas untuk koordinat -900 yang digeser kanan 8: shr memberi 16777212, div memotong ke -3, sementara FloorDiv dan SarInt32 sama-sama mendarat di nilai floor -4 yang dimaksud ITU-T T.88 oleh shift itu, yang hanya penting ketika pecahan fixed-point-nya non-nol
Truncation dan floor hanya sepakat pada kelipatan eksak, jadi -512 div 256 lolos test singkat dan -900 div 256 memilih piksel yang salah

Math.Floor(V / 256) juga mengembalikan -4, tapi jalan memutarnya lewat Double kehilangan presisi untuk nilai Int64 di atas 253, jadi geometri integer sebaiknya tetap di integers

Panggilan PDFlibPas mana yang menjalankan decoder halftone?

Decoder halftone JBIG2 jalan ketika PDFlibPas merender halaman dengan renderer bawaan, karena render butuh piksel. RenderPageToFile dan RenderPageToStream sama-sama mencapainya lewat image stream JBIG2Decode milik halaman, jadi merender ulang halaman halftone adalah cara langsung memastikan bahwa v3.539.38 mengubah output Anda. Decoder yang sama menangani tipe region JBIG2 lainnya, dibahas di tabel Huffman custom JBIG2 di decoder Pascal murni dan men-decode file JBIG2 random-access di Delphi, dan bitmap hasil render menjadi bahan konversi seperti merender halaman PDF menjadi monokrom 1-bit

uses
  SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  Page: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('scanned-halftone.pdf', '') <> 1 then
      raise Exception.CreateFmt('Load failed, error %d', [Lib.LastErrorCode]);
    for Page := 1 to Lib.PageCount do
      // Rendering men-decode setiap region JBIG2, halftone termasuk
      if Lib.RenderPageToFile(150, Page, PDF_RENDER_PNG,
        Format('page-%.3d.png', [Page])) <> 1 then
        Writeln('Page ', Page, ' was not rendered');
  finally
    Lib.Free;
  end;
end.

Ekstraksi image biasanya mengambil jalur lain. GetPageImageList mengembalikan image JBIG2 dalam bentuk natif, dan SaveImageListItemDataToFile atau GetImageListItemDataToString menyerahkan file JBIG2 standalone yang dibangun dari byte stream: header file, data JBIG2Globals, dan satu segment end-of-file di sekeliling data halaman. Property 400 milik GetImageListItemIntProperty melaporkan 6 untuk item semacam itu. Tak ada yang di-decode di jalur itu, jadi .jb2 hasil ekstraksi yang tampak benar di viewer lain sementara halaman hasil render menampilkan derau adalah tanda khas dari dua bug halftone ini:

var
  ListID, I: Integer;
begin
  Lib.SelectPage(1);
  ListID := Lib.GetPageImageList(0);
  if ListID = 0 then
    Exit;
  try
    for I := 1 to Lib.GetImageListCount(ListID) do
      if Lib.GetImageListItemIntProperty(ListID, I, 400) = 6 then  // standalone JBIG2
        Lib.SaveImageListItemDataToFile(ListID, I, 0,
          Format('page1-image%d.jb2', [I]));
  finally
    Lib.ReleaseImageList(ListID);
  end;
end;

Ketika mask atau konversi warna memaksa fallback hasil render, item kembali sebagai bitmap hasil decode dan decoder halftone memang jalan. Lebih lanjut tentang image list ada di ekstraksi teks, image, dan font PDF Delphi

Referensi cepat: aturan grid halftone JBIG2

  • Indeks mask skip sebagai HSKIP[ng, mg], kolom grid dulu, dan baca kembali dalam urutan yang sama di mana pun sel ditempatkan (T.88 §6.6.5.1, diperbaiki di PDFlibPas v3.539.37)
  • Test code halftone atau grid mana pun dengan grid non-persegi dan set sel out-of-region yang asimetris, karena grid persegi bisa menyembunyikan indeks tertransposisi sepenuhnya
  • Baca HGX dan HGY sebagai nilai 32-bit signed (T.88 §7.4.5.1.2), jangan pernah lewat helper unsigned yang men-clamp negatif
  • Terjemahkan >> 8 milik standar sebagai floor division dengan 256, bukan shr 8 dan bukan div 256
  • Jalankan test skip pada posisi piksel hasil geser; bentuk fixed-point-nya berbeda kapan pun pecahannya non-nol, seperti yang ditunjukkan HGX = -900 dengan pattern 4 piksel
  • Akumulasi koordinat grid di Int64 dan clamp sebelum menyerahkannya ke code bitmap, agar grid korup tak bisa meng-overflow
  • Upgrade ke v3.539.38 atau lebih baru jika dokumen Anda memuat halftone region dengan HENABLESKIP, origin grid negatif, atau grid berotasi

PDFlibPas merender, mengekstrak, dan mengedit dokumen PDF dari Delphi dan C++Builder dengan decoder JBIG2 Pascal native yang kini menangani mask skip halftone, origin grid negatif, dan grid berotasi sesuai spesifikasi T.88. Lihat PDF library Delphi PDFlibPas untuk fitur, edisi, dan unduhan trial