Artikel Teknis

Ekstraksi Teks, Gambar, dan Font dari PDF di Delphi dengan PDF Library for Delphi

Menarik teks, gambar, dan font keluar dari PDF yang sudah ada terdengar seperti masalah yang sudah selesai sampai Anda menjalankan korpus nyata melewatinya. Arahkan pengindeks pencarian ke empat puluh ribu berkas pelanggan dan kerusakannya tersortir menjadi beberapa tumpukan yang mudah dikenali. Kata-kata menyatu karena tak ada yang memberi tahu ekstraktor selebar apa jarak yang dihitung sebagai spasi. Halaman lain kembali sebagai teks kacau karena font tersubset tidak membawa peta dari kode glifnya ke karakter sesungguhnya. Dan "logo perusahaan" ternyata sembilan objek gambar terpisah yang bertumpuk di balik sebuah soft mask. Tidak satu pun dari itu adalah bug pustaka. Itu adalah bedanya antara memanggil fungsi ekstraksi dan memahami apa yang bisa dan tidak bisa dipulihkan fungsi itu dari byte di disk

losLab PDF Library, edisi Pascal, memberi kode Delphi dan C++Builder lebih dari satu cara membaca masing-masing dari ketiga aliran itu, dan tingkatannya berbeda dalam apa yang mereka jamin. Kuncinya adalah mencocokkan tingkat dengan pekerjaannya: indeks pencarian, peninjau redaksi, dan tahap preflight PDF/A sama-sama menginginkan hal yang berbeda dari halaman yang sama, dan menjangkau pemanggilan yang keliru memboroskan tenaga atau menghasilkan keluaran yang tak bisa Anda percaya

Tingkat ekstraksi teks dan apa yang dijanjikan masing-masing

GetPageText menerima nilai opsi dari 0 sampai 8, dan angka itu memilih mesin, bukan format. Nilai 0 sampai 2 menjalankan tahap ringan yang memadai untuk pratinjau cepat. Nilai 3 sampai 8 mengalir lewat mesin yang sadar tata letak, yang membangun ulang baris dan spasi dari posisi glif sesungguhnya di halaman. Di dalam rentang itu variasinya penting: 4 dan 6 memecah keluarannya menjadi kata, 5 dan 6 memancarkan lebar per glif, dan 7 mengembalikan teks polos dengan metadata font, warna, dan blok sengaja dibuang. Opsi 7 adalah yang tepat untuk memasok indeks pencarian, sebab indeks hanya menginginkan kata dan tidak lebih

Tidak ada pengaturan opsi yang bisa menyelamatkan dokumen yang sejak awal memang tidak membawa informasinya. PDF memetakan kode karakter ke bentuk glif, dan satu-satunya yang memetakan kode itu kembali ke teks terbaca adalah ToUnicode CMap milik font (ISO 32000-1 §9.10). Ketika font tersubset dikirim tanpa itu, setiap ekstraktor mentok. Pustaka ini, salin-tempel di sebuah viewer, perkakas pesaing: semuanya tereduksi menjadi menebak dari nama glif atau tidak mengembalikan apa pun. Tanggapan praktisnya adalah deteksi, bukan kepahlawanan. Beri halaman itu skor kepercayaan rendah dan kirim ke OCR, sebab mengindeks sampah secara diam-diam lebih buruk daripada mengakui Anda tidak dapat membacanya

Diagram level ekstraksi teks PDF Delphi: opsi GetPageText 0 hingga 8 mengarahkan ke lintasan ringan atau mesin sadar-tata letak, dan halaman yang font subset-nya tak punya CMap ToUnicode beralih ke OCR
Nilai opsi GetPageText 0 sampai 8 memilih antara lintasan preview ringan dan engine layout-aware, dengan opsi 7 disediakan untuk pengindeksan pencarian dan CMap ToUnicode yang hilang dialihkan ke OCR

Untuk kasus yang tidak dicakup opsi datar, seperti tokenisasi kustom, forensik aliran konten, atau corong teks yang dibangun menurut aturan Anda sendiri, dekodernya tersedia satu lapis di bawah. TPDFExtractor dikonstruksi di atas kamus sumber daya dan koleksi font sebuah halaman. Metode ExtractTextW-nya menjalankan operasi teks aliran konten mentah kembali melalui mesin font yang sama untuk memulihkan Unicode, dan event OnFindObject-nya menyerahkan tiap objek kepada Anda saat ia mengalir lewat. Kebanyakan kode tidak pernah perlu turun sedalam ini. Aplikasi yang perlu adalah yang bersyukur lapisan itu bersifat publik alih-alih terkubur

Blok berposisi: satuan bagi hasil pencarian dan tinjauan redaksi

Teks polos memberi tahu apa yang dikatakan halaman itu. Cepat atau lambat sebuah produk juga perlu tahu di mana ia mengatakannya, agar bisa menyorot hasil pencarian, menggambar kotak di sekeliling kandidat redaksi, atau menambatkan anotasi di titik yang tepat. ExtractPageTextBlocks mengembalikan handle ke sebuah daftar deretan teks, dan tiap deretan membawa teksnya, kotak pembatasnya, serta nama dan ukuran font yang dipakainya:

var
  Pdf: TPDFlib;
  Blocks, I: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('contract.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    Pdf.SelectPage(1);
    Blocks := Pdf.ExtractPageTextBlocks(0);
    for I := 0 to Pdf.GetTextBlockCount(Blocks) - 1 do
      Writeln(Format('%s  [%s %.1f pt at %.0f,%.0f]',
        [Pdf.GetTextBlockText(Blocks, I),
         Pdf.GetTextBlockFontName(Blocks, I),
         Pdf.GetTextBlockFontSize(Blocks, I),
         Pdf.GetTextBlockBound(Blocks, I, 0),
         Pdf.GetTextBlockBound(Blocks, I, 1)]));
    Pdf.ReleaseTextBlocks(Blocks);
  finally
    Pdf.Free;
  end;
end;

Satu detail di wilayah ini menjegal integrasi lebih sering daripada yang lain. SetTextExtractionArea, SetTextExtractionWordGap, dan SetTextExtractionOptions adalah keadaan tingkat dokumen yang bertahan, bukan argumen yang Anda oper per pemanggilan. Konfigurasikan pembatasan area untuk satu fitur, misalnya membaca hanya pita header untuk mengklasifikasi dokumen, dan ia diam-diam memangkas setiap ekstraksi berikutnya pada handle yang sama, termasuk tingkat GetPageText yang sadar tata letak yang Anda jangkau kemudian. Entah reset keadaan ekstraksinya di antara tugas logis, atau beri tiap tugas handle dokumennya sendiri

Ambang jarak antar kata adalah tuas untuk tumpukan kegagalan pertama tadi, yaitu kata-kata yang menyatu. SetTextExtractionWordGap memberi tahu mesin tata letak seberapa banyak ruang horizontal, diukur terhadap spasi glif halaman itu sendiri, yang memisahkan satu kata dari kata berikutnya. Tabel yang padat menginginkan jarak lebih kecil daripada halaman pemasaran yang longgar, jadi ambang yang disetel per kelas dokumen mengalahkan satu konstanta global. Ia bertahan pada dokumen seperti sisa keadaan ekstraksi lainnya, jadi rencanakan untuk menetapkannya secara sengaja alih-alih sekali lalu dilupakan

Diagram yang menunjukkan status ekstraksi PDF tingkat dokumen di Delphi yang bertahan melintasi pemanggilan pada satu handle hingga reset, yang mencegah pemotongan senyap pada ekstraksi berikutnya
Area ekstraksi, word-gap, dan setelan opsi bertahan pada handle dokumen, sehingga region yang dibatasi untuk satu fitur diam-diam memotong setiap ekstraksi berikutnya sampai state di-reset atau handle diganti

Gambar: aliran aslinya, bukan tangkapan layar

Cara yang keliru untuk mengeluarkan gambar dari PDF adalah merender halamannya lalu memotongnya. Itu menyampel ulang pikselnya, memanggang rotasi apa pun ke dalamnya, dan membuang apa pun bentuk aslinya. GetPageImageList justru mendaftar sumber daya gambar yang sungguh dirujuk halaman itu, dan tiap itemnya menyerahkan properti berikut datanya yang asli dan tak terusik:

var
  ImgList, I: Integer;
begin
  Pdf.SelectPage(1);
  ImgList := Pdf.GetPageImageList(0);
  for I := 0 to Pdf.GetImageListCount(ImgList) - 1 do
  begin
    Writeln(Pdf.GetImageListItemFormatDesc(ImgList, I, 0));
    Pdf.SaveImageListItemDataToFile(ImgList, I, 0,
      Format('page1-img%.2d.bin', [I]));
  end;
  Pdf.ReleaseImageList(ImgList);
end;

Periksa GetImageListItemFormatDesc sebelum Anda mengandaikan apa pun tentang sebuah item, sebab yang dirujuk sebuah halaman jarang berupa satu gambar rapi per citra yang terlihat. Sebuah soft mask muncul sebagai entri terpisahnya sendiri. XObject yang sama kerap berulang di banyak halaman, jadi deduplikasi berdasarkan hash konten sebelum Anda mengarsipkan ekspor "semua gambar", kalau tidak Anda akan menulis logo yang sama seratus kali. JPEG CMYK perlu manajemen warna diterapkan di hilir, kalau tidak ia dirender terbalik di viewer yang menerima kanalnya begitu saja. Ketika yang Anda inginkan adalah inventaris seluruh dokumen alih-alih satu halaman demi satu halaman, FindImages bersama SetFindImagesMode memindai seluruh berkas dalam satu tahap

Ada satu batas yang layak diangkat bersama pemangku kepentingan sebelum siapa pun menulis kriteria penerimaan: ekstraksi gambar hanya mengembalikan sumber daya raster. Logo atau bagan yang digambar sebagai jalur vektor bukanlah gambar dalam pengertian sumber daya dan tidak akan pernah muncul di daftar gambar mana pun, sejelas apa pun ia terbaca sebagai gambar di layar. Ketika persyaratannya memang menyerahkan bagan itu sebagai berkas, pendekatan yang jujur adalah merender wilayah halamannya menjadi bitmap, dan itu operasi berbeda dengan kesetiaan yang berbeda. Kedua jenis keluaran itu tidak layak berada di folder ekspor yang sama tanpa label yang menyatakan mana yang mana

Perbandingan antara merender halaman PDF Delphi untuk merebut gambar versus mengekstrak stream gambar asli dengan GetPageImageList, termasuk catatan soft-mask, XObject duplikat, dan CMYK
Rendering dan cropping me-resample piksel dan membuang data gambar aslinya, sementara GetPageImageList mengenumerasi resource gambar yang tersimpan beserta properti dan stream mereka yang tak terganggu

Font: permukaan audit, bukan fitur ekspor

API font menjawab pertanyaan tentang font. Ia tidak menyerahkan berkas font-nya kepada Anda, dan pembedaan itu membentuk segala hal yang bisa Anda bangun di atasnya. Setelah FindFonts memindai dokumen, enumerasinya menyusuri font berdasarkan ID, dan pemanggilan propertinya melaporkan font mana pun yang sedang terpilih:

var
  I: Integer;
begin
  Pdf.FindFonts;
  for I := 1 to Pdf.FontCount do        // indeks font mulai dari 1, bukan 0
    if Pdf.SelectFont(Pdf.GetFontID(I)) = 1 then
      Writeln(Format('%s  type=%d  embedded=%d  subset=%d',
        [Pdf.FontName, Pdf.FontType,
         Pdf.GetFontIsEmbedded, Pdf.GetFontIsSubsetted]));
end;

Perhatikan batas loop-nya. Indeks font berjalan dari 1 sampai FontCount, sementara indeks blok teks dan daftar gambar beberapa paragraf di atas bermula dari nol. Bawa satu konvensi ke dalam yang lain dan Anda mendapat kesalahan meleset-satu yang entah melewatkan font pertama atau melewati ujungnya, dan itu akan lolos pengujian sambil lalu karena kebanyakan dokumen punya beberapa font dan font yang salah pun masih tampak masuk akal. Jelas pula soal cakupannya. API ini tidak punya ekspor font di tingkat byte. Tidak ada pemanggilan yang mengembalikan program font tertanam sebagai berkas TTF atau OTF, dan enumerasi ditambah pemeriksaan metadata adalah keseluruhan model yang dimaksudkan. Model itu tetap mencakup apa yang sungguh dituntut pekerjaan produksi dari font: deteksi subset lewat pola nama, audit penanaman sebelum konversi arsip (font yang tidak tertanam adalah penghalang keras PDF/A, sebagaimana dibahas preflight PDF/A dan PDF/UA di Delphi), dan diagnostik pengodean untuk saat kepercayaan ekstraksi menurun. Ada pula alasan lisensi mengapa batasnya duduk di sini. Program font tersubset adalah materi berlisensi dan, karena kehilangan sebagian besar glifnya, toh tak berguna sebagai font yang bisa dipasang. Memperlakukannya sebagai metadata audit alih-alih aset yang dapat diekstrak adalah posisi yang bisa Anda pertahankan

Pemanggilan terakhir itu membuktikan nilainya saat triase. Jalankan GetFontEncoding pada tiap font, baca berdampingan dengan flag subset-nya, dan Anda dapat meramalkan kualitas ekstraksi sebelum menarik satu karakter pun. Halaman yang seluruh font-nya tersubset dengan pengodean nonstandar adalah kandidat OCR hanya dari pemeriksaan itu saja, dan itu memungkinkan pipeline batch merutekannya dengan benar tanpa lebih dulu memboroskan satu tahap ekstraksi yang gagal

Ekstraksi pada skala besar tanpa memuat dokumen

Dalam pipeline batch, memuat seluruh dokumen hanya untuk membaca satu halaman adalah I/O yang terbuang, dan itu menumpuk dengan cepat di sepanjang korpus. Varian sekali-panggil, ExtractFilePageText dan ExtractFilePageTextBlocks, menerima nama berkas, kata sandi, dan nomor halaman secara langsung serta melewati pemuatan penuh. Untuk berkas berskala gigabyte masih ada gigi yang lebih rendah lagi. Jalur direct-access membuka berkas lewat pembacaan xref secara streaming, sehingga DAOpenFileReadOnly yang disusul DAExtractPageText hanya menyentuh objek yang benar-benar dibutuhkan satu halaman itu. Ia datang dengan pergeseran konvensi yang layak dihafalkan: fungsi DA mengalamati halaman lewat PageRef, sebuah handle referensi objek yang Anda peroleh dari DAFindPage, tidak pernah lewat nomor halaman mentah. Oper nomornya di tempat handle seharusnya berada dan pemanggilan itu bekerja pada objek yang salah tanpa memunculkan kesalahan, dan itu jenis kekeliruan terburuk untuk di-debug. Sisa perkakas direct-access diuraikan di gabung, pecah, dan direct access untuk PDF besar

Kalau ada satu kebiasaan yang memisahkan kode ekstraksi yang bertahan menghadapi korpus nyata dari kode yang terseok, itu adalah memperlakukan halaman sebagai masukan yang tidak tepercaya alih-alih sumber data yang bersih. Teks yang berselisih dengan apa yang dirender viewer hampir selalu merupakan masalah pengodean, ligatur yang runtuh menjadi satu glif atau font subset yang kehilangan entri ToUnicode-nya, dan perbaikannya adalah mengukur kepercayaan lalu mengalihkan halaman buruk ke OCR, bukan melawan byte-nya. API font tidak akan pernah menghasilkan TTF atau OTF, dan itu memang dirancang begitu, jadi bangunlah alur kerja font di seputar pertanyaan audit. Dan keadaan ekstraksi yang bertahan, terutama persegi panjang areanya, adalah pengaturan yang Anda miliki sepanjang umur sebuah handle dokumen, bukan parameter yang Anda lupakan setelah satu pemanggilan. Benahi ketiga refleks itu dan sisa API-nya akan bersikap baik

Build evaluasi, proyek demo, dan referensi API ekstraksi selengkapnya ada di halaman produk losLab PDF Library for Delphi