Artikel Teknis

Ekstrak Teks dari PDF yang Dimuat di Delphi dengan HotPDF

HotPDF Component mengekstrak teks Unicode dari PDF mana pun yang Anda muat di Delphi melalui dua panggilan: ExtractLoadedPageText mengembalikan teks alur bacaan dari suatu halaman, dan ExtractLoadedPageTextLayout (ditambahkan di v2.263.0) merekonstruksi susunan visual halaman sebagai teks biasa, sehingga kolom, indentasi, dan perataan tabel tetap bertahan dalam keluaran. Keduanya bekerja pada dokumen yang tidak dibuat oleh HotPDF, yang merupakan kasus yang benar-benar penting: faktur yang dikirimkan pelanggan melalui email kepada Anda, laporan yang dikirimkan oleh biro pemindai, kontrak yang dihasilkan oleh perangkat lunak yang tidak dapat disebutkan namanya lagi

Untuk mencapainya diperlukan lebih banyak mekanisme daripada yang disarankan oleh kedua tanda tangan fungsi tersebut, karena PDF tidak menyimpan teks dengan cara yang same seperti file teks. Artikel ini membahas kedua mode ekstraksi, lalu membuka bagian dalam dari tiga bagian di bawahnya — pembaca CMap, penerjemah aliran konten, dan rantai fallback decoding font — karena mengetahui cara kerja pemetaan adalah perbedaan antara sekadar pasrah pada keluaran sampah dan mendiagnosisnya

Mengapa ekstraksi teks lebih sulit daripada membaca string dari file?

Aliran konten PDF mencatat kode karakter, bukan karakter. Operator Tj dan TJ (ISO 32000-1 §9.4.3) membawa string bita yang maknanya sepenuhnya bergantung pada font yang dipilih oleh Tf sebelumnya: bita 0x41 mungkin berupa huruf A di bawah WinAnsi, glif sembarang dalam font subset, atau setengah dari dua bita CID dalam font CJK komposit. ISO 32000-1 §9.10 mendefinisikan ekstraksi teks persis seperti masalah decoding ini — memetakan setiap kode kembali ke Unicode menggunakan informasi apa pun yang disediakan oleh kamus font — dan standar tersebut secara eksplisit menyatakan bahwa file yang sesuai tidak diwajibkan untuk menyediakan informasi yang cukup untuk melakukannya

Klausul terakhir tersebut menjelaskan setiap laporan bug "mengapa salin-tempel dari PDF ini menghasilkan teks tidak jelas" yang pernah Anda lihat. Produsen yang menyematkan font subset tanpa tabel /ToUnicode telah menulis file yang merender dengan sempurna dan mengekstrak sebagai omong kosong, karena pemetaan kode-ke-glif ada tetapi pemetaan kode-ke-Unicode tidak pernah disertakan. Oleh karena itu, API ekstraksi yang jujur adalah rantai fallback upaya terbaik, dan pertanyaan yang berguna adalah seberapa dalam rantai tersebut berjalan

Ekstraksi alur bacaan dengan ExtractLoadedPageText

Untuk indeks pencarian, pencocokan kata kunci, atau memasukkan teks ke alur analisis, ExtractLoadedPageText adalah panggilan yang Anda inginkan. Tanda tangannya adalah function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean — indeks halaman berbasis nol, hasilnya tiba sebagai UnicodeString bawaan Delphi, dan fungsi mengembalikan False ketika halaman tidak memiliki aliran konten yang dapat dibaca daripada memicu pengecualian

var
  Pdf: THotPDF;
  PageCount, I: Integer;
  PageText, AllText: UnicodeString;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('invoice.pdf');
    AllText := '';
    for I := 0 to PageCount - 1 do
      if Pdf.ExtractLoadedPageText(I, PageText) then
        AllText := AllText + PageText + #13#10;
    // AllText sekarang menyimpan teks alur bacaan dari dokumen
  finally
    Pdf.Free;
  end;
end;

Pemisah baris dalam keluaran berasal dari heuristik sederhana yang disengaja: ketika asal vertikal suatu glif bergeser lebih dari setengah ukuran font saat ini — tanda dari langkah Td atau T* dalam aliran konten — sebuah baris baru akan dimasukkan. Karakter yang tidak dapat diuraikan oleh dekoder akan menjadi spasi alih-alih menghilang, sehingga batas kata tetap bertahan meskipun glif individual tidak. Yang tidak dicoba oleh mode ini adalah pengelompokan urutan membaca or pendeteksian multi-kolom: halaman dua kolom akan keluar secara berseling sesuai urutan aliran konten, yang biasanya (tetapi tidak selalu) merupakan urutan visual

Kapan Anda harus menggunakan ekstraksi yang mempertahankan tata letak sebagai gantinya?

ExtractLoadedPageTextLayout adalah panggilan yang tepat setiap kali posisi membawa arti: tabel, formulir, daftar kode, apa pun yang ingin Anda bandingkan (diff), grep, atau uraikan per kolom. Alih-alih meratakan glif ke dalam aliran, fungsi ini mengelompokkannya ke dalam garis dasar (baselines), mengurutkan setiap garis dasar berdasarkan X, dan mereproduksi ruang kosong horizontal dan vertikal pada kisi karakter monospaced yang berukuran dari median pergeseran glif dan ukuran font. Kesenjangan lebar antara baris pada garis dasar yang same menjadi baris spasi; kesenjangan besar antara garis dasar menjadi baris kosong. Hasilnya dibaca seperti tampilan halaman tersebut

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Kolom, indentasi, dan perataan tabel bertahan sebagai
  // spasi dan baris kosong pada kisi karakter
end;

Kedua mode berbagi setiap bita dari mekanisme decoding dan hanya berbeda dalam cara mereka mengatur glif yang didekodekan, sehingga pilihan tersebut tidak mengorbankan ketepatan. Pilih ExtractLoadedPageText ketika hanya kata-kata yang penting dan ExtractLoadedPageTextLayout ketika susunannya yang penting. Pendeteksian urutan bacaan multi-kolom tetap berada di luar cakupan untuk keduanya — rendering kisi dari halaman dua kolom menunjukkan kedua kolom secara berdampingan, secara setia, yang untuk diffing sangat tepat dan untuk re-flow prosa tidak

Bagaimana HotPDF mendekode kode karakter ke Unicode?

HotPDF Component menyelesaikan setiap kode karakter melalui rantai fallback yang berurutan berdasarkan prioritas: CMap /ToUnicode tersemat font terlebih dahulu, kemudian entri /Encoding (aliran atau CMap bernama), lalu — untuk font komposit — file CMap standar Adobe untuk koleksi karakter seperti Adobe-GB1, Adobe-CNS1, Adobe-Japan1, dan Adobe-KR, dan terakhir tabel WinAnsi dan MacRoman bawaan untuk font sederhana. Strategi yang tidak dapat memberikan jawaban akan diturunkan secara senyap ke strategi berikutnya daripada memicu pengecualian, dan kode yang menghabiskan seluruh rantai akan diselesaikan menjadi 0 sehingga pemanggil dapat menghitung kesalahan alih-alih menebak

CMap /ToUnicode (ISO 32000-1 §9.10.3) berada di urutan pertama karena ini adalah pemetaan yang ditulis oleh produsen secara khusus untuk ekstraksi. Jalur CMap standar Adobe penting untuk dokumen CJK yang menggunakan CMap standar yang telah ditentukan sebelumnya seperti UniGB-UTF16-H alih-alih menyematkan apa pun: HotPDF menyertakan file koleksi tersebut di bawah direktori resources\CMap-nya, menentukannya secara relatif terhadap file eksekutabel saat runtime, dan menyimpan cache setiap peta yang diuraikan per proses — patut diketahui karena yang terbesar di antaranya, peta Adobe-GB1, berukuran sekitar 2 MB teks sumber yang tidak ingin Anda uraikan ulang di setiap halaman. Jika direktori tersebut tidak ada, dekoder cukup melewatkan CMap berbasis disk dan bekerja dengan tabel tersemat ditambah pengodean bawaan. Ini adalah cermin sisi pembaca dari masalah pembentukan yang dibahas dalam pembentukan teks skrip kompleks dengan HotPDF, di mana perbedaan kode-versus-glif yang sama dihadapi pada saat penulisan

Dua jebakan sintaksis CMap yang patut diketahui

File CMap terlihat mudah diurai padahal tidak, dan dua detail menjadi penyebab sebagian besar kegagalan pengurai pada upaya pertama. Yang pertama adalah bahwa jumlah rekaman muncul sebelum kata kunci bagian: sebuah bagian dibaca 2 beginbfchar, bukan beginbfchar 2. Pengurai yang mengharapkan jumlah setelah kata kunci akan mengonsumsi angka tersebut sebagai token nyasar, lalu menemukan nol entri di setiap bagian. Pendekatan yang kuat — yang digunakan pembaca HotPDF — adalah mengabaikan jumlah tersebut sepenuhnya dan mengulang hingga kata kunci endbfchar / endbfrange yang cocok ditemukan, yang memiliki keuntungan tambahan berupa toleransi terhadap file dunia nyata yang jumlahnya salah

Jebakan kedua adalah target bfchar dan bfrange adalah string UTF-16BE, bukan integer. Tujuan <D83DDE00> berarti U+1F600 — pasangan pengganti (surrogate pair) yang harus digabungkan kembali menjadi satu titik kode — dan membaca empat bita tersebut sebagai integer big-endian menghasilkan nilai yang tidak berarti pada setiap titik kode di luar Basic Multilingual Plane. Emoji dalam PDF tidak lagi eksotis, sehingga dekoder yang melewatkan penggabungan kembali pengganti akan gagal pada file yang sebenarnya dimiliki pengguna Anda. HotPDF mengurai literal heksadesimal ke bita mentah terlebih dahulu, lalu menggabungkan kembali unit kode UTF-16BE, yang juga mencakup target multi-karakter yang dihasilkan oleh pemetaan ligatur

Turun ke tingkat glif dengan ExtractLoadedPageGlyphs

Kedua panggilan teks dibangun di atas ExtractLoadedPageGlyphs, dan THPDFGlyphArray yang mendasarinya juga tersedia untuk kode Anda. Setiap THPDFGlyphRecord membawa titik kode Unicode yang diselesaikan bersama dengan kode karakter mentah, lebar bita kode (1, 2, atau 4, diputuskan oleh codespacerange CMap), kunci dan ukuran sumber daya font aktif, koordinat X dan Y ruang pengguna, serta pergeseran horizontal. Itu cukup untuk membangun pendeteksian batas kata, penyorotan posisi, atau algoritma tata letak khusus tanpa Anda harus menyentuh aliran konten sendiri

var
  Glyphs: THPDFGlyphArray;
  I, Unresolved: Integer;
begin
  if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
  begin
    Unresolved := 0;
    for I := 0 to High(Glyphs) do
      if Glyphs[I].Unicode = 0 then
        Inc(Unresolved);
    if Unresolved > 0 then
      ShowMessageFmt('%d dari %d glif tidak memiliki pemetaan Unicode',
        [Unresolved, Length(Glyphs)]);
  end;
end;

Menghitung rekaman Unicode = 0, seperti di atas, adalah cara jujur untuk mengukur kualitas ekstraksi pada dokumen tertentu sebelum Anda mempercayai teks tersebut di alur kerja hilir. Rekaman glif juga menambatkan setiap karakter ke operan sumber dalam aliran konten, yang memungkinkan pencarian dan penggantian teks dokumen HotPDF yang dimuat dapat dilakukan di atas fondasi yang sama

PDF mana yang tidak akan menyerahkan teksnya?

Beberapa file mengalahkan pengekstrak mana pun, dan lebih baik mendeteksinya daripada mengirimkan hasilnya. Dokumen terpindai adalah kasus paling jelas: halaman yang merupakan satu gambar besar tidak berisi operator teks sama sekali, sehingga ekstraksi mengembalikan string kosong dengan benar — perbaikannya adalah OCR, dan mengekstrak gambar halaman dari PDF yang dimuat adalah langkah pertama dari alur kerja tersebut. Font subset tanpa tabel /ToUnicode adalah kasus yang lebih sulit: jika jalur /Encoding dan CMap standar juga kosong, glif tersebut diselesaikan menjadi 0 dan muncul sebagai spasi dalam panggilan teks. Dokumen terenkripsi diekstrak secara normal asalkan Anda memuatnya dengan kata sandinya melalui kelebihan beban LoadFromFile, sehingga aliran data didekripsi sebelum penerjemah melihatnya

Satu batasan yang lebih sempit patut dinyatakan dengan jelas: rantai dekode membaca CMap dan aliran konten melalui jalur Flate HotPDF, sehingga font yang aliran ToUnicode-nya menggunakan filter yang tidak biasa akan diturunkan ke strategi berikutnya alih-alih menggagalkan halaman. Dalam praktiknya FlateDecode mencakup hampir semua hal yang diproduksi dalam dua dekade terakhir, dan penurunan tersebut berjalan senyap secara sengaja — Anda mendapat teks terbaik yang dimungkinkan oleh file tersebut daripada sebuah pengecualian. Mekanisme objek sisi pembaca yang same yang menyelesaikan kamus font di sini juga mendukung pengeditan metadata pada dokumen yang dimuat, sehingga alur penerimaan dokumen dapat mengekstrak, memeriksa, dan memberi anotasi dalam satu lintasan

Ekstraksi teks, rendering yang mempertahankan tata letak, akses tingkat glif, serta fitur pencarian dan penggantian yang dibangun di atasnya semuanya adalah bagian dari standar HotPDF Component untuk Delphi dan C++Builder — tidak ada DLL eksternal, no layanan teks OS, hanya Object Pascal yang dapat Anda telusuri langkah demi langkah ketika file aneh mendarat di antrean Anda