Teknik Makale

HotPDF ile Delphi'de Yüklenen Bir PDF'ten Metin Ayıklama

HotPDF Bileşeni, Delphi'de yüklediğiniz herhangi bir PDF'ten iki çağrıyla Unicode metin ayıklar: ExtractLoadedPageText bir sayfanın okuma akışındaki metni döndürür ve ExtractLoadedPageTextLayout (v2.263.0 sürümünde eklendi) sayfanın görsel düzenini düz metin olarak yeniden oluşturur; böylece sütunlar, girintiler ve tablo hizalamaları çıktıda korunur. Her iki işlev de HotPDF tarafından oluşturulmamış belgeler üzerinde çalışır; bu da aslında önemli olan durumdur: bir müşterinin size e-posta ile gönderdiği fatura, bir tarama bürosunun teslim ettiği rapor veya artık kimsenin adını hatırlayamadığı bir yazılım tarafından üretilen sözleşme

Oraya ulaşmak, iki imzanın önerdiğinden daha fazla mekanizma gerektirdi; çünkü bir PDF, metni bir metin dosyasının yaptığı gibi depolamaz. Bu makale her iki ayıklama modunu inceleyecek, ardından alttaki üç parçayı —CMap okuyucu, içerik akışı yorumlayıcısı ve yazı tipi kod çözme geri dönüş zinciri— açığa çıkaracaktır; çünkü eşleştirmenin nasıl çalıştığını bilmek, bozuk çıktıya omuz silkmek ile onu teşhis etmek arasındaki farktır

Metin ayıklamak, dosyadan dizeleri okumaktan neden daha zordur?

Bir PDF içerik akışı karakterleri değil, karakter kodlarını kaydeder. Tj and TJ operatörleri (ISO 32000-1 §9.4.3), anlamı tamamen bir önceki Tf tarafından seçilen yazı tipine bağlı olan bayt dizilerini taşır: 0x41 baytı, WinAnsi altında A harfi, bir alt küme yazı tipindeki rastgele bir glif veya kompozit bir CJK yazı tipindeki iki baytlık bir CID'nin yarısı olabilir. ISO 32000-1 §9.10, metin ayıklamayı tam olarak bu kod çözme sorunu olarak tanımlar —her kodu yazı tipi sözlüğünün sağladığı bilgiyi kullanarak Unicode'a geri eşlemek— ve standart, uyumlu bir dosyanın bunu yapmak için yeterli bilgiyi sağlamasının zorunlu olmadığını açıkça belirtir

Bu son madde, bugüne kadar gördüğünüz tüm "bu PDF'ten kopyala-yapıştır yapınca neden anlamsız karakterler çıkıyor" hata raporlarını açıklar. /ToUnicode tablosu olmayan bir alt küme yazı tipini gömen bir üretici, mükemmel şekilde işlenen ancak ayıklandığında anlamsız metin veren bir dosya yazmıştır; çünkü kod-glif eşlemesi mevcuttur ancak kod-Unicode eşlemesi hiçbir zaman gönderilmemiştir. Bu nedenle, dürüst bir ayıklama API'si en iyi çabayı gösteren bir geri dönüş zinciridir ve asıl soru bu zincirin ne kadar derine gittiğidir

ExtractLoadedPageText ile okuma akışlı ayıklama

Arama indeksleme, anahtar kelime eşleştirme veya bir analiz iş hattını metinle beslemek için istediğiniz çağrı ExtractLoadedPageText işlevidir. İmza function ExtractLoadedPageText(PageIndex: Integer; out AText: UnicodeString): boolean şeklindedir —sayfa indeksleri sıfır tabanlıdır, sonuç yerel bir Delphi UnicodeString olarak gelir ve işlev, bir istisna fırlatmak yerine sayfanın okunabilir içerik akışı olmadığında False döndürür

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 now holds the reading-flow text of the document
  finally
    Pdf.Free;
  end;
end;

Çıktıdaki satır sonları kasıtlı olarak basit bir sezgisel yönteme dayanır: bir glifin dikey orijini geçerli yazı tipi boyutunun yarısından fazla hareket ettiğinde —içerik akışındaki bir Td veya T* adımının işareti— bir satır sonu eklenir. Kod çözücünün çözemediği karakterler kaybolmak yerine boşluklara dönüşür, böylece tek tek glifler çözülemese bile kelime sınırları korunur. Bu modun yapmaya çalışmadığı şey ise okuma sırası kümelemesi veya çoklu sütun algılamasıdır: iki sütunlu bir sayfa içerik akışı sırasına göre aralıklı olarak çıkar, bu da genellikle görsel sıradır ancak her zaman öyle olmayabilir

Bunun yerine ne zaman düzeni koruyan ayıklama kullanmalısınız?

Konum anlam taşıdığında ExtractLoadedPageTextLayout doğru çağrıdır: tablolar, formlar, kod listeleri, diff yapmak, grep yapmak veya sütuna göre ayrıştırmak istediğiniz her şey. Glifleri bir akış haline getirmek yerine, onları taban çizgilerinde kümeler, her taban çizgisini X eksenine göre sıralar ve ortalama glif ilerlemesi ile yazı tipi boyutuna göre ölçeklendirilmiş tek aralıklı bir karakter ızgarası üzerinde yatay ve dikey boşlukları yeniden üretir. Aynı taban çizgisindeki çalışmalar arasındaki geniş boşluklar boşluk dizilerine dönüşür; taban çizgilerinde oluşan büyük boşluklar ise boş satırlar haline gelir. Sonuç, sayfanın göründüğü gibi okunmasını sağlar

var
  Grid: UnicodeString;
begin
  if Pdf.ExtractLoadedPageTextLayout(0, Grid) then
    TFile.WriteAllText('page1.txt', Grid, TEncoding.UTF8);
  // Columns, indentation and table alignment survive as
  // spaces and blank lines on a character grid
end;

Her iki mod da kod çözme mekanizmasının her baytını paylaşır ve yalnızca çözülen glifleri nasıl düzenledikleri konusunda farklılık gösterir, bu nedenle seçimin doğruluğa herhangi bir maliyeti yoktur. Yalnızca kelimeler önemli olduğunda ExtractLoadedPageText, düzen önemli olduğunda ise ExtractLoadedPageTextLayout seçeneğini tercih edin. Çoklu sütun okuma sırası algılaması her ikisi için de kapsam dışıdır —iki sütunlu bir sayfanın ızgara görünümü size her iki sütunu da yan yana, aslına sadık bir şekilde gösterir; bu, diff yapmak için tam olarak doğrudur ancak düz metin akışı için doğru değildir

HotPDF karakter kodlarını Unicode'a nasıl çözer?

HotPDF Bileşeni, her karakter kodunu öncelik sırasına göre bir geri dönüş zinciri aracılığıyla çözer: önce yazı tipinin gömülü /ToUnicode CMap'i, ardından /Encoding girdisi (akış veya adlandırılmış CMap), ardından —kompozit yazı tipleri için— Adobe-GB1, Adobe-CNS1, Adobe-Japan1 ve Adobe-KR gibi karakter koleksiyonları için Adobe standart CMap dosyaları ve son olarak basit yazı tipleri için yerleşik WinAnsi ve MacRoman tabloları. Bir yanıt veremeyen strateji bir istisna fırlatmak yerine sessizce bir sonrakine düşer ve tüm zinciri tüketen bir kod 0'a çözümlenir, böylece arayan kişi tahmin etmek yerine eksiklikleri sayabilir

/ToUnicode CMap (ISO 32000-1 §9.10.3) ilk sırada yer alır çünkü üreticinin özellikle ayıklama için yazdığı eşleştirmedir. Adobe standart CMap yolu, hiçbir şey gömmek yerine UniGB-UTF16-H gibi önceden tanımlanmış CMap'leri kullanan CJK belgeleri için önem taşır: HotPDF, koleksiyon dosyalarını resources\CMap dizini altında taşır, çalışma zamanında bunları yürütülebilir dosyaya göre bulur ve her ayrıştırılmış eşlemeyi işlem başına önbelleğe alır; bilinmeye değerdir çünkü bunların en büyüğü olan Adobe-GB1 eşlemesi, sayfa başına yeniden ayrıştırmak istemeyeceğiniz yaklaşık 2 MB'lık bir kaynak metindir. Dizin mevcut değilse, kod çözücü disk tabanlı CMap'leri atlar ve gömülü tablolar ile yerleşik kodlamalarla çalışır. Bu, yazma zamanında aynı kod-glif ayrımının yaşandığı HotPDF ile karmaşık yazı tipi metin şekillendirme makalesinde ele alınan şekillendirme sorununun okuma tarafındaki yansımasıdır

Bilinmesi gereken iki CMap sözdizimi tuzağı

CMap dosyaları kolayca ayrıştırılabilir görünür ancak öyle değildir ve iki ayrıntı çoğu ilk ayrıştırıcı denemesi başarısızlığının nedenidir. İlki, kayıt sayısının bölüm anahtar kelimesinden önce gelmesidir: bir bölüm beginbfchar 2 değil, 2 beginbfchar şeklinde okunur. Sayıyı anahtar kelimeden sonra bekleyen bir ayrıştırıcı, sayıyı başıboş bir belirteç olarak tüketir ve ardından her bölümde sıfır giriş bulur. Sağlam yaklaşım —HotPDF okuyucusunun benimsediği yaklaşım— kayıt sayısını tamamen görmezden gelmek ve eşleşen endbfchar / endbfrange anahtar kelimesine kadar döngü yapmaktır; bu yaklaşım, sayımları yanlış olan gerçek dünya dosyalarını tolere etme avantajına da sahiptir

İkinci tuzak ise bfchar ve bfrange hedeflerinin tamsayılar değil, UTF-16BE dizeleri olmasıdır. <D83DDE00> hedefi U+1F600 anlamına gelir —tek bir kod noktasına birleştirilmesi gereken bir yedek çift (surrogate pair)— ve bu dört baytı büyük endian bir tamsayı olarak okumak, Temel Çok Dilli Düzlem dışındaki her kod noktasında anlamsız bir değer üretir. PDF'lerdeki emojiler artık egzotik olmadığından, yedek çift birleştirmeyi atlayan bir kod çözücü, kullanıcılarınızın gerçekten sahip olduğu dosyalarda başarısız olur. HotPDF, hex sabit değerini önce ham baytlara ayrıştırır, ardından UTF-16BE kod birimlerini birleştirir, bu da ligatür eşleştirmelerinin ürettiği çoklu karakter hedeflerini de kapsar

ExtractLoadedPageGlyphs ile glif düzeyine inme

Her iki metin çağrısı da ExtractLoadedPageGlyphs üzerine kuruludur ve alttaki THPDFGlyphArray kodunuz için de mevcuttur. Her THPDFGlyphRecord, çözülen Unicode kod noktasını, ham karakter kodu, kodun bayt genişliği (CMap'in codespacerange girdisine göre belirlenen 1, 2 veya 4), aktif yazı tipi kaynak anahtarı ve boyutu, kullanıcı alanı X ve Y orijini ve yatay ilerleme ile birlikte taşır. Bu, içerik akışına kendiniz dokunmadan kelime sınırı algılama, konumlandırılmış vurgulama veya özel bir düzen algoritması oluşturmak için yeterlidir

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 of %d glyphs have no Unicode mapping',
        [Unresolved, Length(Glyphs)]);
  end;
end;

Yukarıdaki gibi Unicode = 0 kayıtlarını saymak, metni ardıl olarak kullanmadan önce belirli bir belge üzerinde ayıklama kalitesini ölçmenin dürüst yoludur. Glif kayıtları ayrıca her karakteri içerik akışındaki kaynak operanda sabitler, bu da HotPDF'in yüklenen belge metin arama ve değiştirme özelliklerini aynı temel üzerinde mümkün kılan şeydir

Hangi PDF'ler metinlerini vermez?

Bazı dosyalar her türlü ayıklayıcıyı bozguna uğratır ve çıktılarını kullanmaktansa bunları tespit etmek daha iyidir. Taranmış belgeler en belirgin durumdur: tamamen büyük bir görüntüden oluşan bir sayfa hiçbir metin operatörü içermez, bu nedenle ayıklama haklı olarak boş bir dize döndürür —çözüm OCR'dir ve yüklenen PDF'ten sayfa görüntülerini ayıklamak bu iş hattının ilk adımıdır. /ToUnicode tablosu olmayan alt küme yazı tipleri ise daha zor olan durumdur: /Encoding yolu ve standart CMap'ler de boş kalırsa, bu glifler 0'a çözümlenir ve metin çağrılarında boşluk olarak görünür. Şifrelenmiş belgeler, onları LoadFromFile aşırı yüklemesi aracılığıyla şifreleriyle yüklediğiniz sürece normal şekilde ayıklanır, böylece akışlar yorumlayıcı daha görmeden çözülür

Daha dar bir sınırın açıkça belirtilmesi gerekir: kod çözme zinciri, CMap ve içerik akışlarını HotPDF'in Flate yolu üzerinden okur, bu nedenle ToUnicode akışı sıra dışı bir filtre kullanan bir yazı tipi, sayfanın başarısız olması yerine bir sonraki stratejiye düşer. Uygulamada, FlateDecode son yirmi yılda üretilen hemen hemen her şeyi kapsar ve bu düşüş kasıtlı olarak sessizdir —istisna yerine dosyanın izin verdiği en iyi metni alırsınız. Burada yazı tipi sözlüklerini çözen aynı okuma tarafı nesne mekanizması, yüklenen belgelerdeki meta verileri düzenleme özelliğini de destekler, böylece bir belge alım iş hattı tek bir geçişte ayıklayabilir, inceleyebilir ve açıklama ekleyebilir

Metin ayıklama, düzeni koruyan görselleştirme, glif düzeyinde erişim ve bunların üzerine inşa edilen arama-değiştirme özellikleri, Delphi ve C++Builder için standart HotPDF Bileşeni'nin bir parçasıdır —harici DLL'ler yok, işletim sistemi metin servisleri yok, yalnızca kuyruğunuza garip bir dosya düştüğünde adım adım inceleyebileceğiniz Object Pascal kodu