Artikel Teknis

Pencarian Teks PDF Aman Unicode di Delphi: NFC dan NFD

PDF Library for Delphi dapat mencocokkan teks berdasarkan kesetaraan kanonis alih-alih berdasarkan code unit, sehingga sebuah query yang diketik sebagai karakter precomposed menemukan konten yang disimpan sebagai huruf dasar ditambah tanda diakritik gabungan, dan sebaliknya. Dua opsi pencarian mengendalikan ini: soCanonicalEquivalent mengaktifkan normalisasi Unicode selama pencocokan, dan soGraphemeClusters membatasi setiap hit dan setiap langkah wildcard ke grapheme cluster utuh

Bug yang diperbaiki ini adalah salah satu yang paling banyak dilaporkan dan paling sedikit dipahami dalam pencarian dokumen. Seorang pengguna mencari sebuah nama, tidak melihat hasil, menyalin nama itu dari dokumen, menempelkannya ke kotak pencarian, dan menemukannya. Tidak ada yang rusak secara nyata: kedua string tampak identik, tercetak identik, dan dibandingkan tidak sama, karena satu adalah U+00E9 dan yang lain adalah U+0065 diikuti U+0301

Mengapa kata yang sama dibandingkan tidak sama?

Unicode mengizinkan beberapa pengkodean untuk karakter abstrak yang sama. Huruf Latin dengan diakritik ada sebagai code point precomposed dan sebagai urutan dasar plus gabungan. Suku kata Hangul ada sebagai suku kata precomposed dan sebagai jamo yang didekomposisi. Yang mana yang dikandung sebuah PDF bergantung pada produsen, platform, dan terkadang font, dan tidak ada satu pun dari itu yang terlihat oleh orang yang melakukan pencarian

Alasan case folding sederhana tidak menyelesaikan ini bersifat struktural, bukan kebetulan. Case folding dan accent folding bersifat satu-ke-satu pada level code unit: string yang di-fold memiliki panjang yang sama dengan aslinya, sehingga posisi kecocokan dalam teks yang di-fold adalah posisi kecocokan dalam aslinya. Normalisasi tidak satu-ke-satu. Satu karakter precomposed menjadi dua atau tiga code unit, urutan yang didekomposisi menciut kembali menjadi satu, dan setelah transformasi itu, posisi tidak lagi selaras dengan teks yang Anda ekstraksi

Menjaga koordinat hit tetap menunjuk ke teks asli

Bagian inilah yang menentukan apakah pencarian yang dinormalisasi dapat digunakan, bukan sekadar benar. Setiap code unit yang dihasilkan normalisasi mencatat posisi awal dan akhir dari teks UTF-16 asli yang menghasilkannya. Dekomposisi rekursif mewarisi rentang sumber induknya, komposisi menggabungkan rentang input-nya, dan ketika sebuah kecocokan ditemukan, library memindai interval pemetaan untuk mencari awal terkecil dan akhir terbesar

Efeknya adalah MatchStart, MatchLength, string konteks, dan kedua entry point penggantian semuanya tetap mengalamatkan teks yang diekstraksi asli, bukan intermediate yang dinormalisasi. Tanpa pemetaan itu, pencarian yang dinormalisasi dapat memberitahu Anda sebuah hit ada tetapi tidak secara andal di mana ia berada, yang membuat highlighting salah dan redaksi berbahaya

Normalizer itu sendiri berdiri sendiri: tabel kompak untuk dekomposisi kanonis, komposisi, dan canonical combining class dari Unicode 15.1, dengan Hangul ditangani oleh aturan algoritmik alih-alih entri tabel. Tidak ada yang dimuat dari berkas data eksternal dan tidak ada API normalisasi platform yang dipanggil, sehingga layanan Windows, daemon Linux, dan build FPC semuanya menghasilkan hasil yang identik pada input yang sama

Mencari dengan kesetaraan kanonis

Opsi berupa sebuah set, sehingga kesetaraan kanonis dapat dikombinasikan dengan perilaku yang sudah ada seperti pencocokan seluruh kata, wildcard, dan folding tak-peduli-diakritik:

uses
  PDFlibrary;

var
  Lib: TPDFlib;
  Hits: array of TPDFlibSearchHit;
  Found, I: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('contracts.pdf', '');
    SetLength(Hits, 500);

    Found := Lib.SearchText('Bäcker', [soCanonicalEquivalent, soWholeWord],
      '', Hits);                       // rentang halaman kosong = seluruh dokumen

    for I := 0 to Found - 1 do
      Log(Format('page %d: "%s" at %d (%d chars)',
        [Hits[I].Page, Hits[I].MatchText, Hits[I].MatchStart,
         Hits[I].MatchLength]));
  finally
    Lib.Free;
  end;
end;

Normalisasi bersifat opt-in dengan alasan. Membangun teks NFD dan pemetaan posisinya membutuhkan kerja, dan sebagian besar pencarian pada dokumen yang hanya ASCII tidak pernah membutuhkannya. Ketika opsinya digunakan, setiap blok teks meng-cache dua bentuk yang ditransformasikan, satu dengan tanda diakritik gabungan dihapus dan satu tanpanya, sehingga sekumpulan query pada blok yang sama menormalisasi sekali alih-alih sekali per query. Case folding tetap menempuh jalur satu-ke-satu yang lebih murah tanpa perubahan

Apa yang rusak tanpa batas grapheme cluster?

Code unit bukan karakter, dan karakter bukan apa yang dipersepsikan pengguna. Sebuah emoji bendera adalah dua code point regional indicator. Sebuah emoji keluarga adalah beberapa code point yang digabungkan zero-width joiner. Sebuah conjunct Indic adalah sebuah konsonan, sebuah virama, dan konsonan lain. Sebuah huruf dengan dua aksen bertumpuk adalah tiga code point. Mencocokkan atau memotong di tengah salah satu dari ini menghasilkan fragmen yang tampil sebagai sampah

soGraphemeClusters membatasi kedua ujung setiap hit, literal atau wildcard, ke batas extended grapheme cluster yang lengkap. Segmentasinya mengimplementasikan aturan extended: pemasangan CR dan LF, karakter kontrol, kelas suku kata Hangul, Extend dan SpacingMark, Prepend, urutan emoji ZWJ, pemasangan regional indicator, dan pemisahan conjunct Indic. Sebuah batas tidak pernah dihasilkan di dalam surrogate pair, yang saja sudah menghilangkan satu kelas hasil rusak pada konten apa pun di luar basic multilingual plane

Opsi ini juga mengatur konsumsi wildcard, yaitu tempat implementasi naif masih akan salah memotong. Wildcard satu karakter memajukan tepat satu cluster lengkap, dan backtracking untuk wildcard run hanya berpindah di antara batas cluster:

// Tanpa soGraphemeClusters, "?" dapat mengonsumsi separuh cluster dan
// mengembalikan hit yang teksnya berakhir dengan tanda gabungan menggantung
Found := Lib.SearchText('c?té',
  [soWildcards, soCanonicalEquivalent, soGraphemeClusters], '', Hits);

// Batas yang sama melindungi penggantian, sehingga redaksi dan
// penulisan ulang konten tidak pernah memotong sebuah emoji atau huruf beraksen
Replaced := Lib.SearchAndReplaceText('naïve', 'plain',
  [soCanonicalEquivalent, soGraphemeClusters], '1-20');

Memilih opsi untuk beban kerja nyata

Tiga kombinasi mencakup sebagian besar kasus. Untuk kotak pencarian dokumen internal, soCanonicalEquivalent ditambah soDiacriticInsensitive memberikan perilaku yang toleran yang diharapkan pengguna, mencocokkan kedua bentuk pengkodean serta ejaan beraksen dan tidak beraksen. Untuk pencarian legal atau kepatuhan, di mana false positive memiliki biaya, gunakan soCanonicalEquivalent dengan soCaseSensitive dan soWholeWord dan biarkan accent folding mati, sehingga kesetaraannya persis dan tidak bergantung pengkodean

Untuk apa pun yang memodifikasi dokumen, tambahkan soGraphemeClusters tanpa kecuali. Pencarian yang mengembalikan rentang yang sedikit salah hanya menyesatkan pembaca; penggantian atau redaksi yang menggunakan rentang salah yang sama menuliskan kesalahan itu ke dalam berkasnya. Konsekuensi dari rentang penghapusan yang salah dibahas dalam redaksi sesungguhnya dan penghapusan konten

Ketika throughput penting, utamakan entry point batch. SearchTextBatch menjalankan setiap query non-kosong selagi blok teks setiap halaman berada di memori, yang menghindari ekstraksi ulang sebuah halaman per query dan menggunakan kembali normalisasi yang di-cache, dan varian streaming-nya menerbitkan hit tanpa buffer berukuran pemanggil. Model ekstraksi yang mendasarinya dijelaskan dalam pencarian teks dan enumerasi elemen halaman

Skrip di mana ini bukan opsional

Untuk bahasa Korea, kesetaraan kanonis adalah perbedaan antara menemukan sebuah nama dan tidak menemukannya, karena suku kata precomposed dan jamo yang didekomposisi keduanya lazim dalam dokumen nyata. Untuk bahasa Vietnam, diakritik bertumpuk membuat bentuk komposisi sepenuhnya bergantung pada produsen. Untuk skrip Indic, penanganan conjunct memutuskan apakah sebuah batas hit mendarat di tempat yang terbaca. Untuk bahasa Jepang dan Tionghoa, sisi pencarian relatif sederhana, meskipun sisi layout tidak, seperti dijelaskan dalam penulisan vertikal untuk bahasa Jepang dan Tionghoa

Aturan praktisnya singkat: jika korpus mengandung bahasa apa pun selain bahasa Inggris, nyalakan kesetaraan kanonis dan ukur biayanya sebelum memutuskan itu terlalu mahal. Pada sebagian besar kumpulan dokumen, itu tidak mahal, dan alternatifnya adalah fitur pencarian yang diam-diam gagal tepat pada nama yang paling ingin ditemukan pengguna Anda

Pencarian yang sadar Unicode, ekstraksi, redaksi, dan penulisan ulang teks berbagi satu engine untuk Delphi, C++Builder, dan Free Pascal; daftar fitur lengkapnya ada di halaman PDF Library for Delphi