Artikel Teknis

Ekstrak Gambar dari PDF yang Dimuat di Delphi: HotPDF

Anda punya satu PDF di disk, pelanggan menscan dari tumpukan invoice, dan tugas Anda adalah mengekstrak kembali gambar halaman sebagai bitmap untuk proses OCR. Setelah memuat file, Anda menemukan XObject gambar, lalu ada bagian tersembunyi yang sering tidak disebut: byte dalam stream tersebut bukanlah piksel langsung. Bisa berupa codestream JPEG, blob JPEG 2000 berbasis wavelet, Group 4 fax, atau raster indexed di balik palet di belakang Flate filter. Objek gambar tahu lebar dan tinggi, tetapi sampel sebenarnya tersimpan rapat di filter yang dipilih pembuat file. Untuk mendapatkan TBitmap yang bisa dipakai, filter itu harus dibalik, dan PDF biasanya menyediakan sekitar delapan cara berbeda bagaimana byte disimpan

Di sinilah ExtractLoadedImage dipakai, fitur HotPDF dan komponen VCL PDF asli untuk Delphi dan C++Builder. Fungsi ini menelusuri image XObject dari dokumen yang dimuat, melaporkan tiap jenisnya, lalu mendekode image yang bisa diproses menjadi bitmap 24-bit. Bagian utamanya bukan jumlah method, yang hanya tiga. Yang penting adalah alasan mengapa path decode terpisah memang perlu ada, dan apa yang bisa dan tidak bisa diubah menjadi piksel

Mengapa gambar loaded tidak langsung terdekripsi

Loader HotPDF dirancang untuk pass-through fidelity. Saat Anda memanggil LoadFromFile, stream gambar tetap disimpan persis seperti di file sumber: filter original, byte compressed original, dan dictionary original. Ini disengaja. Tujuan utama loading biasanya adalah menyalin halaman, menggabungkan file, memberi watermark, menyesuaikan izin, lalu menulis ulang lagi; untuk skenario itu yang paling aman dan ekonomis adalah membiarkan stream gambar tetap utuh. Jika setiap gambar langsung didekode ke raster saat load, memori dan CPU akan terpakai besar, sementara kebanyakan caller tidak pernah membutuhkannya, dan re-encode saat save justru menurunkan kualitas gambar yang seharusnya bisa ditiru mentah

Akibatnya graph objek loaded tidak membawa piksel. Image XObject dengan /Filter /DCTDecode hanya berisi byte JPEG. HotPDF tidak memanggil JPEG decoder di saat load karena pada jalur copy-and-rewrite tidak membutuhkannya. Jadi jika Anda benar-benar membutuhkan piksel, API ekstraksi harus melakukan decode sendiri, dari awal, dengan filter yang dipakai oleh gambar tersebut. Ini juga alasan kenapa codec sisi encode dipisah dari loader; artikel adding JPEG 2000 images to PDFs in Delphi menjelaskan bagaimana mesin JPX masuk di sisi pembuatan, dan mesin tersebut memang tidak dipasang di path baca sampai extraction API memerlukannya

API tiga method

Antarmukanya sederhana. GetLoadedImageCount mengembalikan jumlah image XObject dalam dokumen loaded. GetLoadedImageInfo mengisi record descriptor untuk salah satu image berdasarkan index. ExtractLoadedImage mengembalikan bitmap hasil decode, atau nil jika image tersebut tidak dapat didekode. Enumeration berbasis index dan stabil untuk setiap load karena internalnya berjalan lewat tabel indirect object dan mengumpulkan semua stream dengan /Subtype yang resolve ke /Image, sehingga index untuk GetLoadedImageInfo sama dengan index untuk ExtractLoadedImage

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I, Count: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-invoices.pdf', '') <= 0 then
      Exit;
    Count := Pdf.GetLoadedImageCount;
    for I := 0 to Count - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if not Info.Decodable then
        Continue;                       // filter or colour space not supported
      Bmp := Pdf.ExtractLoadedImage(I);
      if Bmp <> nil then
      try
        Bmp.SaveToFile(Format('img_%d.bmp', [I]));
      finally
        Bmp.Free;                       // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Dua detail kontrak penting di sini. Pertama, TBitmap yang dikembalikan harus Anda kelola sendiri dan free saat selesai; dokumen tidak menyimpannya. Kedua, periksa Decodable sebelum memanggil, lalu cek hasil nil sesudahnya. Method ini tidak melempar exception saat filter tidak didukung, hanya mengembalikan nil. Nilai nil yang diam dalam loop besar sangat mudah membuat Anda kehilangan beberapa halaman tanpa sadar

Baca descriptor sebelum decode

THPDFLoadedImageInfo memberi Anda karakter gambar tanpa harus mendekode penuh. Isinya langsung dari image dictionary: Width dan Height dalam sampel, BitsPerComponent, ColorComponents dan ColorSpace yang menjelaskan interpretasi setelah decode (1 untuk abu, 3 untuk RGB, 4 untuk CMYK), Filter sebagai nama kompresi, IsImageMask untuk stencil mask, ObjectNumber untuk indirect object terkait, dan Decodable

Flag terakhir paling jujur. Decodable bernilai True hanya jika kombinasi filter dan color space saat itu benar-benar bisa diubah menjadi bitmap pada versi library yang Anda pakai. Ia merepresentasikan support matrix nyata, bukan perkiraan: image yang Filter-nya tidak didukung oleh build ini akan melaporkan Decodable = False, dan Anda bisa menggunakannya untuk log, skip, atau ambil raw stream sendiri. Anggap ini sebagai prasyarat, bukan sekadar petunjuk

// Triage every image before committing to a decode.
var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  I: Integer;
begin
  // ... Pdf loaded ...
  for I := 0 to Pdf.GetLoadedImageCount - 1 do
  begin
    if not Pdf.GetLoadedImageInfo(I, Info) then
      Continue;
    if Info.Decodable then
      // ExtractLoadedImage(I) will return a TBitmap
    else
      // unsupported filter/colour space: log the object and skip
      Writeln(Format('Image %d obj %d: %dx%d %s/%s not decodable',
        [I, Info.ObjectNumber, Info.Width, Info.Height,
         String(Info.Filter), String(Info.ColorSpace)]));
  end;
end;

Ini satu detail implementasi yang kerap membuat orang yang membangun record descriptor dari scratch keliru. THPDFLoadedImageInfo punya dua field AnsiString untuk Filter dan ColorSpace. Keduanya adalah tipe managed dengan reference counting, sehingga trik mengosongkan record memakai FillChar(Info, SizeOf(Info), 0) akan salah: ini menimpa referensi string tanpa menurunkannya, menimbulkan leak atau korupsi. HotPDF menginisialisasi field demi field untuk alasan ini. Jika Anda menyalin pola ini, lakukan pendekatan yang sama

Satu dispatcher, delapan jalur filter

Feature ini turun bertahap karena PDF tidak punya satu format gambar. PDF punya filter, dan §8.9.5 ISO 32000-1 memungkinkan nama filter apa pun di /Filter, dengan interpretasi sample dipengaruhi /ColorSpace, /BitsPerComponent, dan array /Decode opsional. ExtractLoadedImage membaca nama filter lalu mengarahkan ke decoder khusus. Dukungan yang dibangun sejak v2.229 sampai v2.231 kini mencakup delapan jalur

  • Raw rasters (FlateDecode, LZWDecode, atau tanpa filter) di DeviceRGB 8-bit atau DeviceGray. Byte akan di-inflate menjadi raster padat dan transformasi yang ada hanya pertukaran channel, yang dijelaskan di bawah
  • DCTDecode (JPEG). Codestream dikirim ke TJPEGImage milik VCL, lalu menentukan geometri dan warna, hasil akhirnya ditulis ke bitmap 24-bit
  • JPXDecode (JPEG 2000). Diproses oleh backend OpenJPEG, mesin yang sama dengan artikel JPEG 2000, dan komponen high-bit-depth discaling ke 8 bit
  • Indexed colour. Palet dibaca dari array [/Indexed base hival lookup] dan setiap sample diperluas lewat lookup table menjadi warna asli
  • DeviceCMYK. Empat channel sampel diubah menjadi RGB memakai formula tinta di atas kertas putih
  • Sub-8-bit DeviceGray dan Indexed dengan 1, 2, atau 4 bit per komponen, dibongkar sampel per sampel lalu diskalakan ke rentang 0-255
  • CCITTFaxDecode, filter Group 3 dan Group 4 fax, didekode lewat backend khusus T.4/T.6
  • JBIG2Decode, filter bilevel rasio tinggi, didekode lewat backend JBIG2 terdaftar yang dibahas di artikel native JBIG2 compression article dari sisi encode

Semua jalur berakhir pada format yang sama, yaitu bitmap 24-bit BGR, karena itu format native TBitmap VCL dan format yang diharapkan semua consumer berikutnya

Transformasi yang mengubah piksel secara diam-diam

Dua jalur di antaranya memiliki transformasi yang mudah salah meski terlihat sederhana. Pertama adalah penukaran urutan warna. PDF DeviceRGB menyimpan sample dalam urutan merah-hijau-biru dari baris atas ke bawah. TBitmap VCL 24-bit menyimpan baris dalam urutan biru-hijau-merah. Maka decode gambar RGB biasa bukan sekadar memcpy, byte pertama dan ketiga harus dipertukarkan saat masuk scanline. Jika dibalik, merah dan biru tertukar; ini mungkin terlihat wajar pada gambar abu-abu dan buruk sekali pada foto berwarna. Untuk urutan baris, PDF dan VCL berbaris sama arah, ScanLine[0] sudah baris visual teratas, jadi tidak perlu flip vertikal

Kedua adalah CMYK. Gambar DeviceCMYK membawa empat ink, dan konversi ke RGB dilakukan per channel dengan formula (255 - ink) * (255 - K) / 255. Ini adalah aproksimasi perangkat, bukan color-managed conversion lewat profil ICC, jadi hasilnya cukup untuk display dan re-rasterisasi, tetapi tidak untuk kebutuhan print color yang presisi. Jika alur kerja Anda menuntut fidelitas, anggap bitmap yang diekstrak sebagai preview dan jaga stream CMYK original untuk pipeline warna terkelola

Path Indexed punya jebakan parsing lain. Color space /Indexed dapat tersimpan sebagai string literal atau string hex, dan HotPDF menyimpan nilai string hex sebagai teks hex, bukan byte terdecode. Jadi saat palet berupa hex string, lookup table harus lewat decode hex terlebih dahulu; string literal sudah berisi raw byte. Kalau melewatkan cabang ini, indexed image berwarna empat akan rusak karena seluruh entry palet dibaca dari boundary byte yang salah

Filter chain: filter terakhir adalah format gambar sebenarnya

Satu /Filter adalah kasus mudah. PDF juga mengizinkan chain filter, di mana stream dilewatkan beberapa filter berurutan dalam array /Filter seperti [/ASCII85Decode /FlateDecode] atau [/ASCIIHexDecode /DCTDecode] sesuai ISO 32000-1 §7.4. Urutannya jelas: filter diterapkan kiri ke kanan saat encode, sehingga saat decode dibalik dari kanan ke kiri. Filter terakhir dalam array menentukan format gambar akhir, sedangkan filter awal hanya pembungkus transport

Extractor menangani ini dengan membongkar filter. Semua filter dalam chain kecuali yang terakhir diproses dulu agar outputnya menjadi input untuk filter terakhir, lalu dispatch dilakukan pada filter itu. Maka [/ASCII85Decode /DCTDecode] akan didekode ASCII85 dulu, lalu masuk ke jalur JPEG, dan [/FlateDecode] di sekitar raw raster akan di-inflate lalu masuk jalur raster. Ini membuat delapan decoder tetap sederhana karena tidak perlu tahu soal ASCII85 atau hex transport. Artinya, jika filter terakhir tidak didukung, kegagalan terjadi bersih di langkah dispatch, bukan di tengah decode

Di mana ekstraksi berhenti dan apa yang harus dilakukan

Sejujurnya tentang batasan. Jika filter terakhir di luar set yang didukung, hasilnya nil; hal yang sama jika color space tidak dapat diinterpretasi oleh build Anda. Soft mask dan alpha juga tidak direkonstruksi menjadi bitmap, Anda hanya melihat base image tanpa komposisi. Kedalaman bit di atas 8 dari JPEG 2000 akan di-resample ke bawah yang memang lossy, dan ini salah untuk arsip ulang karena kualitasnya turun. Image mask, stensil 1 bit tanpa warna sendiri, secara descriptor berbeda dari gambar foto; jika Anda mendekodenya seperti foto hasilnya akan mengejutkan

Jika ekstraksi tidak cukup, raw stream tetap tersedia di loaded object graph lengkap dengan filternya. Anda bisa ambil byte per byte untuk diproses di codec khusus Anda sendiri. Inilah fallback sengaja dijaga oleh desain pass-through: byte original tidak pernah dibuang, jadi skenario terburuk adalah Anda yang melakukannya sendiri, bukan datanya hilang. Untuk banyak kerja riil, delapan filter yang didukung sudah mencakup pola output scanner, office suite, dan engine laporan. Loop GetLoadedImageCount dengan guard Decodable sudah cukup untuk menjadikan PDF loaded menjadi folder bitmap dalam beberapa baris

API ekstraksi gambar loaded dan lengkap set filter decode yang dijelaskan di sini tersedia dalam HotPDF Component untuk Delphi dan C++Builder