Teknik Makale

Delphi'de PDFium Bileşeni ile PDF Dosyalarından Metin Çıkarma

PDF metin çıkarma, metin katmanının olmadığı, bozulduğu veya anlamlı bir sıra olmaksızın düzinelerce küçük karakter serisine bölündüğü bir belgeye rastlayana kadar basit görünür. PDFium Bileşeni size iki giriş noktası sunar: Bir sayfadaki her glife dizine dayalı ham erişim için Character[] dizisi ve PDF'in etiket ağacından veya sezgisel analizinden paragrafları ve başlıkları yeniden oluşturan yapılandırılmış bir görünüm için ReadablePageContent. İkisinden biri her zaman tek doğru seçim değildir, bu yüzden her birinin neyi ortaya çıkardığını anlamak önemlidir

Belgeyi açma ve sessiz başarısızlık tuzağı

TPdf, FileName'i ayarlayarak ve Active := True değerini çevirerek bir dosyayı açar. Kritik ayrıntı: Active := True asla bir istisna (exception) oluşturmaz. Dosya eksikse, şifre korumalıysa veya bozuksa, PDFium hatayı dahili olarak yakalar ve Active basitçe False olarak kalır. Bu, her çıkarma döngüsünün buna karşı korunması gerektiği anlamına gelir:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('PDF açılamadı (hasarlı veya yanlış şifre)');
    Exit;
  end;
  // metin çıkarma burada devam eder
finally
  Pdf.Active := False;
  Pdf.Free;
end;

Şifre korumalı dosyaların Active := True'dan önce Pdf.Password := '...' olarak ayarlanması gerekir. İkinci bir şans yoktur: Active başarısız olduğunda, kapatıp doğru şifreyle yeniden açarsınız

Character[] ile sayfa sayfa metin çıkarma

En alt düzey yaklaşım, her sayfadaki her karakteri yürür. O sayfanın metin katmanını yüklemek için Pdf.PageNumber'ı ayarlayın, ardından Character[] özelliğini kullanarak CharacterCount girişlerini yineleyin. Her girişteki iki bayrağı kontrol etmeye değer: CharacterGenerated[i], oluşturucu (renderer) tarafından eklenen gerçek bir Unicode değeri olmayan sentetik glifleri (örneğin satır sonlarındaki yumuşak kısa çizgiler) işaretler ve CharacterMapError[i], PDFium'un glifi bir kod noktasıyla eşleştiremediğini bildirir; bu durum bir ToUnicode tablosu bulunmayan yazı tipi kodlamalarında gerçekleşir

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // CR'yi LF'ye normalleştir
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

Sonuç, PDFium'un bunları numaralandırdığı sıradaki Unicode kod noktalarının düz bir dizesidir; bu da mutlaka soldan sağa okuma sırasında değil, içerik akışında göründükleri sıradadır. Standart ofis araçları tarafından üretilen çoğu Latin alfabesi belgesi için bu sorun değildir. Sıra dışı glif dizileriyle OCR uygulanmış taranmış PDF'ler veya sağdan sola yazılan metinler için sıralama yanlış olabilir. İşte o zaman ReadablePageContent daha kullanışlı hale gelir

ReadablePageContent ile yapılandırılmış metin çıkarma

ReadablePageContent bir düzey yukarı çıkar: Fragments dizisi etiketlenmiş içerik parçalarını taşıyan ve her biri paragrafları, başlıkları, liste öğelerini, tablo hücrelerini vb. tanımlayan bir Kind değerine sahip bir TPdfReadableContent kaydı döndürür. PDF bir yapı ağacı (structure tree) taşıdığında (Pdf.IsTagged ile kontrol edin), kaynak rosStructure'dır ve okuma sırası yetkilidir. Etiketlenmemiş dosyalar için PDFium, karakterleri sınırlayıcı kutularına (bounding boxes) göre makul okuma birimlerinde gruplayan ancak doğruluğu garanti edemeyen rosHeuristic'e geri döner

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

Content.Source = rosHeuristic ise ve çıktınız bozuk görünüyorsa, belgenin metin katmanı muhtemelen okuma sırası düşünülerek yazılmamıştır. Bu noktada tek güvenilir düzeltme, kaynak uygulamadan uygun etiketleme ile yeniden dışa aktarmak veya karakter orijinlerini Y'ye ve ardından X'e göre sıralayan bir son işlem (post-processing) adımı çalıştırmaktır

CharacterOrigin ve CharacterRectangle size ne verir?

Her iki özellik de sayfa boşluğundaki bir karakterin konumunu döndürür (noktalar, sol alt köşedeki orijin, Y yukarı doğru artar). CharacterOrigin[i] glifin taban çizgisi (baseline) çapa noktasıdır; CharacterRectangle[i] tam sınırlayıcı kutudur (bounding box). Bunlar, düz metnin ötesindeki her şey için yapı taşlarıdır: Sütun sınırlarını algılama, karakterleri bir tolerans dahilinde Y koordinatlarını karşılaştırarak satırlar halinde gruplandırma veya bir görüntüleyicide metin seçimi için bir isabet testi haritası (hit-test map) oluşturma. Bir fare tıklamasının altında hangi karakterin oturduğunu bulmanız gerekiyorsa, CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY), dikdörtgenleri yinelemenize gerek kalmadan bu aramayı doğrudan yapar

DLL dosyasını yerleştirmek

PDFium Bileşeni, tüm PDF ayrıştırmasını hedef platformunuza bağlı olarak pdfium32.dll veya pdfium64.dll olan yerel bir DLL'e devreder. Bileşen, doğru dosyayı Windows sistem dizinine kopyalayan bir CopyDlls.bat betiği ile birlikte gönderilir. Geliştirme makinesinde bir kez Yönetici olarak çalıştırmak yeterlidir; dağıtım için DLL'i uygulama yürütülebilir dosyasının yanına kopyalarsınız. V8 etkinleştirilmiş varyantlar (pdfium32v8.dll, pdfium64v8.dll) önemli ölçüde daha büyüktür ve yalnızca PDF'leriniz yürütülmesi gereken JavaScript içeriyorsa gereklidir. Saf metin çıkarma için standart yapı doğru seçimdir

DLL çalışma zamanında mevcut değilse, Active := True tıpkı eksik bir dosyada olduğu gibi sessizce başarısız olur, çünkü bileşen yükleme hatasını dahili olarak yakalar. Göndermeden önce her zaman temiz bir makinede test edin

Düzen analizi için Character[]'in yanında FontSize[] kullanma

Düz metnin ötesinde, karakter düzeyindeki API, her glifin işlenen nokta boyutunu döndüren FontSize[i] özelliğini sunar. CharacterOrigin[i] ve CharacterRectangle[i] ile birleştirildiğinde, bu, yapı ağacına güvenmeden gövde metnini başlıklardan ayırmanıza olanak tanır. Yazı tipi boyutunun bir eşiğin üzerine çıktığı bir karakter serisi, etiketlenmemiş bir belgede kesinlikle bir başlıktır. Aynı teknik, altyazıları (resim sınırlayıcı kutusunun altındaki küçük metinler) veya dipnotları (sayfanın altına yakın küçük metinler) algılamak için de geçerlidir. Bunların hiçbiri oluşturma gerektirmez; her üç özellik de PDFium'un Active := True sırasında oluşturduğu metin katmanından doğrudan okunur

Bir nüans: FontSize[i] sayfanın CTM'si (mevcut dönüştürme matrisi - current transformation matrix) uygulandıktan sonraki boyutu yansıtır; bu nedenle yazarın tüm sayfayı ölçeklendirdiği bir belge, orantılı olarak ayarlanmış boyutlar bildirecektir. Farklı sayfa boyutlarına sahip sayfalar arasındaki boyutları karşılaştırıyorsanız, eşik kararları vermeden önce her sayfanın MediaBox yüksekliğine göre normalleştirin

Çıktıyı bir dosyaya yazma

Delphi'nin TStringList sınıfı XE'den beri UTF-8 çıktısını temiz bir şekilde işler. BOM'suz bir dosyaya ihtiyacınız varsa WriteBOM := False olarak ayarlayın (birmany downstream tüketici baştaki BOM'da boğulur):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

Belleğin sorun teşkil ettiği çok büyük belgeler için, önce her şeyi bir listede biriktirmek yerine sayfa döngüsü içinde doğrudan TEncoding.UTF8 ile bir TStreamWriter'a yazın

Burada gösterilen Character[], CharacterCount, CharacterOrigin[], CharacterRectangle[], ReadablePageContent ve CharacterIndexAtPos API'leri Delphi ve C++Builder için PDFium Bileşeni'nin bir parçasıdır