Teknik Makale

PDFium Delphi'de Dolu Dikdörtgenlerden Tablo Çizgileri

PDFium Component'in tablo çıkarma özelliği, 3.117.0 sürümünden itibaren ince bir dolu dikdörtgeni bir tablo çizgisi olarak ele alıyor. Varsayılan olan DetectFilledRulings etkinken, MaxRulingThickness (3 punto) değerinden kalın olmayan eksen hizalı dolu bir kutu uzun ekseni boyunca tek bir çizgi oluyor, daha büyük dolu bir kutu dört kenarını katıyor ve ızgara kurulmadan önce her çizgi koordinatı RulingSnapTolerance (4 punto) içinde hizalanıyor. Böylece Word, Google Docs ve tarayıcılardan dışa aktarılan tablolar, whitespace algılamasına fragman olarak düşmek yerine çizgili dedektöre eksiksiz ızgaralar hâlinde ulaşıyor

Tablo algılama ve çıkarma üzerine önceki makale, çizgili algılamanın çizilen çizgileri kullandığını ve çizilen her path parçasının sayfa koordinatlarına dönüştürüldüğünü söylüyordu. O cümle doğruydu ama eksikti. 13 gerçek dünya örnek belgesi üzerinde path nesnelerini saymak, bunlardan 9'unda hiç çizilmiş path bulunmadığını, ama her birinin sayfalarında 0,5 ile 1 punto kalınlığında yüzlerce dolu dikdörtgen taşıdığını gösterdi. Yalnızca stroke'a bakan dedektör hiçbir şey görmedi, her sayfa whitespace algılamasına düştü ve çıktı tablolar yerine küçük fragmanlardan oluşan bir saçılım oldu. 3.116.4'te eklenen compact-columns preset'i bunu fragman düzeyinde yumuşattı; asıl neden, dedektörün yanlış boyama operatörünü okumasıydı

Word'den aktarılan bir tabloda neden çizilmiş çizgi bulunmuyor?

Bir kelime işlemci kenarlığı çizgi olarak düşünmez; onu genişliği olan bir kutu olarak düşünür ve o kutuyu dolguyla boyar. ISO 32000-1 §8.5.2.1, re operatörünü bir dikdörtgen alt yolu eklemek olarak tanımlar ve §8.5.3 boyama operatörlerini ayırır: S yolu geçerli çizgi kalınlığıyla çizer, f içini doldurur. 0,5 puntoluk bir hücre kenarlığı x y w 0.5 re f olarak çıkar ve stroke mekanizması — çizgi kalınlığı, birleşimler ve dash deseni dahil — hiç çalışmaz. Hücre gölgelendirmesi aynı kurgunun daha büyük kutulu hâlidir. m, l ve S ile çizilmiş bir ızgara, orijinal dedektörün beklediği şeydir ve bir ofis uygulamasından dışa aktarılan neredeyse hiçbir şey bunu üretmez:

% kelime işlemci çıktısından bir hücre kenarlığı: 0.5 pt yüksekliğinde dolu bir kutu
72 700 468 0.5 re f
% hücre gölgelendirmesi: hücre boyutunda dolu bir kutu
72 676 117 24 re f
% orijinal dedektörün yazıldığı çizilmiş ızgara çizgisi
72 700 m 540 700 l S

FPDFPath_GetDrawMode fonksiyonuna yalnızca stroke bayrağının ayarlı olup olmadığını soran bir dedektör için iki dolu kutu da görünmezdir. Hücrelerin içindeki kelimeler ardından whitespace algılamasına ulaşır; orada 6 puntoluk bir boşlukla ayrılan sütunlar, varsayılan MinColumnGap değeri olan 12 puntonun altında kalır ve geri dönen şey, MinRows eşiğini geçecek kadar iyi hizalanan satırların rastgele bir alt kümesidir. Fragman davranışı budur ve hiçbir parametre ayarı onu yazarın çizdiği ızgaraya dönüştürmez

PDFium Component dolu bir kutuyu nasıl tablo çizgisine çeviriyor?

TableCollectObjectRulings her path nesnesini alt yol alt yol inceliyor. Çizim modu FPDFPath_GetDrawMode fonksiyonundan geliyor; bir path, DetectFilledRulings açıkken ve dolgu modu none değilken dolu sayılıyor. Her nokta nesne matrisinden geçirilip toplanıyor, alt yol başına en fazla MaxSubpathPoints (8) kadar; herhangi bir eğri parçası alt yolu eğrisel olarak işaretliyor. Alt yol kapandığında ya da yeni bir MoveTo başladığında FlushSubpath bunun ne olduğuna karar veriyor: eğrisel bir alt yol atılıyor; noktaları en az bir eksende sınırlayıcı kutunun kenarlarına PointTolerance (0,05 punto) içinde oturmayan kapalı her çokgen de atılıyor. Bir üçgen, bir şevron ya da yuvarlatılmış bir sekme asla çizgi olmuyor; dekoratif görselleri ızgaranın dışında tutan da bu

PDFium Component'te TableCollectObjectRulings'in kapalı alt yolları Delphi'de tablo çizgisine nasıl çevirdiğinin şeması: FlushSubpath eğrisel dış hatları ve sınırlayıcı kutu kenarlarının dışındaki çokgenleri atar, MaxRulingThickness ince kutuları uzun eksen başına bir çizgiye böler, gölgeli hücreler dört kenar çizgisi verir ve DetectFilledRulings küçük kareleri dışarıda tutar
Kapalı bir alt yol ancak eksen hizalı olduğunda hayatta kalır ve ardından sınırlayıcı kutu onun tek bir çizgi mi, gölgeli bir hücrenin dört kenarı mı, yoksa hiçbir şey mi olduğuna karar verir

Geriye eksen hizalı bir dikdörtgen kalır ve sınıflandırma sınırlayıcı kutusuna göre yapılır. Genişlik MaxRulingThickness değerine eşit ya da ondan küçük, yükseklik ise ondan büyükse, yatay merkezde ve kutuyu alttan üste kapsayan tek bir dikey çizgi elde edilir; aynadaki durum tek bir yatay çizgi verir. Her iki boyut da eşiğin üzerindeyse bu bir gölgeli hücredir ve kutu dört çizgi, kenar başına bir tane, katkıda bulunur. Her iki boyut da eşiğe eşit ya da ondan küçükse hiçbir katkı olmaz, böylece 2 puntoluk kare bir madde işareti çizgiyle karıştırılmaz. Çizilmiş bir path eski yolu, yani AddLine çağrısını izler ve eksen hizalı parça başına bir çizgi üretir; böylece S ile çizilmiş bir ızgara eskisi gibi işlenir ve hem dolgu hem stroke ile boyanmış bir path, birleştirme geçişinin çökerttiği örtüşen parçalar üretir:

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  Mode: string;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'itinerary-from-word.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // 1 tabanlı

    Options := TPdfTableExtractionOptions.Default;
    // bunlar 3.117.0 varsayılanları, netlik için açık yazıldı
    Options.DetectFilledRulings := True;     // ince dolu kutular çizgi olur
    Options.MaxRulingThickness := 3.0;       // punto; daha kalın kutular gölgelendirme sayılır
    Options.RulingSnapTolerance := 4.0;      // punto; 0 hizalamayı kapatır
    Options.IncludeFormXObjects := True;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
    begin
      if Tables[I].DetectionMode = ptdmRuled then
        Mode := 'ruled'
      else
        Mode := 'whitespace';
      Writeln(Format('%dx%d %s, confidence %.2f',
        [Tables[I].RowCount, Tables[I].ColumnCount, Mode,
         Tables[I].Confidence]));
    end;
  finally
    Pdf.Free;
  end;
end;

RulingSnapTolerance gölgelendirilmiş hücreli tablolar için ne yapıyor?

RulingSnapTolerance, yalnızca gölgelendirmeyle kurulmuş bir tablonun tek bir ızgaraya bağlanmasını sağlayan şeydir. Bazı çıktılar hiç kenarlık çizmez: her hücre kendi renginde dolu bir kutudur ve komşu kutular 1 ile 3 punto genişliğinde beyaz bir boşlukla ayrılır. Her kutu dört kenar çizgisi verir, ama bir hücrenin sağ kenarı ile sonrakinin sol kenarı 2 punto arayla durur ve bağlanabilirlik testi varsayılanı 1 punto olan RulingTolerance değerini kullanır. Hizalama olmadan her hücre dört çizgiden oluşan kendi bağlı bileşenini kurar, hiçbir bileşen MinRows eşiğine ulaşmaz ve sayfa hiçbir şey bildirmez. TableSnapRulings oyundaki her X koordinatını — her dikey çizginin konumu artı her yatay çizginin başlangıcı ve sonu — ve aynı biçimde her Y koordinatını toplar, her listeyi sıralar, komşusu toleranstan fazla farklı olmayan değerleri zincirleyerek kümelere ayırır, her kümeyi ortalamasıyla değiştirir ve ardından her konumu, başlangıcı ve sonu en yakın küme merkezine taşır. Boşluğun iki yakası aynı çizgi olur ve bağlanabilirlik sağlanır

PDFium Component'te RulingSnapTolerance'ın Delphi'de gölgeli hücreli bir tabloyu nasıl bağladığının şeması: komşu hücreler 2 pt boşluk bırakır, kenar çizgileri 1 pt RulingTolerance değerinin ötesinde kalır ve TableSnapRulings iki X değerini tek bir küme ortalamasında zincirler, böylece bağlanabilirlik testi sonunda paylaşılan bir ızgara çizgisi görür
Hizalama birleştirmeden ve çizgili dedektörden önce çalışır, böylece beyaz bir boşluğun iki yakası tek çizgi olur ve her hücre dört çizgiden oluşan bir ada olmaktan çıkar

Hizalama TableMergeRulings öncesinde çalışır; o da çizgileri sıralayıp RulingTolerance içinde değen ya da örtüşen collinear parçaları birleştirir ve ikisi de TableDetectRuled veriyi görmeden önce çalışır, böylece ikili bağlanabilirlik kontrolü hücre başına fragman sayısıyla değil ızgara çizgisi sayısıyla orantılıdır. Çizilmiş bir ızgarada bu geçişler zararsızdır, çünkü zaten birebir aynı olan koordinatlar kendilerine hizalanır. Akılda tutulacak tek şey, zincir kümelemesinin kendi başına bir genişlik sınırı olmaması: birbirinden 3 punto uzaklıkta dizilen koordinatlar tek bir merkeze çöker. 4 puntoluk varsayılanda bu yalnızca bir karakterden dar sütunları etkiler; ama bir belgede ayrı kalması gereken gerçek 3 puntoluk boşluklar varsa toleransı düşürün ya da hizalamayı kapatmak için 0 yapın:

// Çizgili stratejiyi yalıt ve her ayarın tek sayfada ne gördüğünü karşılaştır
function CountRuledTables(Pdf: TPdf; FilledRulings: Boolean;
  SnapTolerance: Double): Integer;
var
  Options: TPdfTableExtractionOptions;
begin
  Options := TPdfTableExtractionOptions.Default;
  Options.DetectWhitespaceTables := False;
  Options.DetectFilledRulings := FilledRulings;
  Options.RulingSnapTolerance := SnapTolerance;
  Result := Length(Pdf.ExtractTables(Options));
end;

// Tipik bir Word çıktısı 0, N ve sonra N'den az bildirir:
// yalnızca stroke hiçbir şey görmez, hizalama gölgeli hücreleri bağlar,
// hizalamayı kapatmak her gölgeli hücreyi kendi adası olarak bırakır
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Form XObject içindeki çizgiler

Sayfa düzeni araçları bir tabloyu ya da tüm sayfa gövdesini sık sık bir form XObject içine sarıp Do ile boyar. ISO 32000-1 §8.10.1, form boyandığında form matrisinin geçerli dönüşüm matrisiyle birleştirildiğini belirtir; bu yüzden form içindeki bir dikdörtgen form uzayında yaşar ve sayfaya ancak iki ya da daha fazla dönüşümden sonra oturur. TableCollectObjectRulings, IncludeFormXObjects ayarlıyken form nesnelerinin içine özyinelemeli olarak iner: nesne matrisini okur, onu ebeveyn matrisle TableMultiplyMatrix üzerinden birleştirir — bu fonksiyonun argüman sırası önce birinci matristen, sonra ikinciden geçir anlamına gelir — ve çocukları FPDFFormObj_CountObjects ile FPDFFormObj_GetObject kullanarak numaralandırıp birleşik matrisi aşağı geçirir. MaxFormDepth (8) değerinden derin iç içe geçme sessizce atlanır; bu, gerçek hiçbir çıktının yaklaşmadığı bir sınır değil, patolojik dosyalara karşı bir korumadır. Çarpma sırasının önemli olmasının nedeni matris prepend ve append karşılaştırmasında ele alınanla aynı: operandların yerini değiştirmek öteleme terimini kaydırır ve sayfanın üstüne oturması gereken bir çizgi bunun yerine orijine oturur

PDFium Component'te bir form XObject içindeki çizgilerin Delphi'de nasıl işlendiğinin şeması: 72 700 468 0.5 re f olarak yazılmış ince bir dikdörtgen form uzayında yaşar ve sayfaya ancak TableMultiplyMatrix ebeveyn CTM ile form matrisini birleştirdikten sonra oturur; özyineleme FPDFFormObj_CountObjects üzerinden MaxFormDepth değerine kadar sürer
Dikdörtgen form uzayında yazılmıştır ve sayfanın üstüne ancak matrisler öteleme terimini yerinde tutan bir sırayla çarpıldıktan sonra ulaşır

Çizgi bütçesi neden dört katına çıktı?

Varsayılan MaxRulingSegments değeri 3.117.0'da 4096'dan 16384'e çıktı, çünkü hücre başına kenarlıklar çizilmiş ızgara çizgilerinden çok daha kalabalık gelir. Çizilmiş 30 satırlık, 6 sütunlu bir tablo 38 çizgi parçasıdır. Aynı tablonun dolu kutu olarak dışa aktarılmış hâli hücre başına dört kenarlığa kadar çıkar, birleştirme öncesi 720 parça eder ve gölgeli hücreleri olan bir form bunu iki katına çıkarır. Bir sayfada böyle iki tablo eski bütçeyi tüketirdi. Bütçe, TableAppendRuling içinde Check üzerinden uygulanır; bu da EPdfError istisnasını Table ruling-segment budget exceeded iletisiyle fırlatır. Bozulmuş bir sonuç yok, kısmi bir ızgara yok ve whitespace geçişi de çalışmaz. Güvenilmeyen girdi için kendi daha sıkı bütçenizi koyuyorsanız, boş sonucu tablo yok diye okumak yerine istisnayı yakalayıp karar verin:

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // güvenilmeyen girdi için bilinçli olarak dar
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // 3.117.0 varsayılanı
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Ölçülen sonuçlar ve yaklaşımın durduğu yer

Aynı 13 örnek belgede çıkarma, 43 tablodan — 9'u çizgili, 34'ü whitespace fragmanı ya da yanlış pozitif — 41 çizgili tabloya ve sıfır whitespace yanlış pozitife geçti. Bu temizliğin bir kısmı 3.117.0'daki iki eşlikçi değişikliğe ait: çizgili bir ızgaranın zaten sahiplendiği kelimeler whitespace algılaması çalışmadan önce çıkarılıyor, böylece bir tablo hiçbir zaman iki kez bildirilmiyor; ayrıca bir whitespace sütun sınırı artık ayırdığı her satır boyunca metinsiz bir koridor olmak zorunda, tam da bu sayede iki yana yaslı paragraflar 5x4 tablo olarak puanlanmayı bıraktı. Tabloların kendisini fragman sütunundan çizgili sütuna taşıyan ise dolu dikdörtgen okuyucu

Sınırları açıkça söylemek gerekir. Metin katmanı olmayan bir sayfa yine ızgara iskeletini verir, her hücre boştur, çünkü çizgiler geometriden, metin ise metin sayfasından gelir; taranmış sayfalar için önce OCR gerekir. Eğriler, yuvarlatılmış köşeler ya da dikdörtgen olmayan dış hatlar taşıyan dolu şekiller tamamen atılır, yani kenarlıkları yuvarlatılmış dikdörtgen dış hatları olarak çizilmiş bir tablo eskisi gibi whitespace algılamasına muhtaçtır. Ne kenarlığı ne gölgelendirmesi olan bir tablo bütün bunlardan etkilenmez ve tablo çıkarma makalesinde anlatılan whitespace stratejisinin alanı olarak kalır; bu bile yetmediğinde yapılandırılmış metin ve okuma sırası üzerinden gelen kelime kutuları ve bloklar, alana özgü bir okuyucu için ham malzemedir. Bileşenle birlikte gelen TableExtractionLab demosu DetectFilledRulings seçeneğini ayar panelinde sunuyor; belirli bir çıktının onunla ve onsuz nasıl göründüğünü görmenin en hızlı yolu bu. Tam API PDFium Component for Delphi sayfasında anlatılıyor