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) >> 8y = (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
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
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 diperbaiki | Region hasil decode |
|---|---|
| Tak ada (sebelum v3.539.38) | Grid tertarik ke origin, seluruh gambar dua baris terlalu rendah |
| Hanya pembacaan HGX / HGY signed | Baris grid pertama hilang, sisanya tergarble oleh drift test skip |
| Pembacaan signed, floor shift, dan test skip di ruang piksel | Identik, 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;
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
HGXdanHGYsebagai nilai 32-bit signed (T.88 §7.4.5.1.2), jangan pernah lewat helper unsigned yang men-clamp negatif - Terjemahkan
>> 8milik standar sebagai floor division dengan 256, bukanshr 8dan bukandiv 256 - Jalankan test skip pada posisi piksel hasil geser; bentuk fixed-point-nya berbeda kapan pun pecahannya non-nol, seperti yang ditunjukkan
HGX = -900dengan pattern 4 piksel - Akumulasi koordinat grid di
Int64dan 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