Teknik Makale

PDFium Delphi'de Dolu Dikdörtgenlerden Tablo Çizgileri

Word'den ya da Google Docs'tan dışa aktarılan bir tablo, tek bir sayfada yüzlerce kenar parçası taşıyabilir ve tek bir çizgili çizgi taşımaz. Sayfayı qpdf --qdf ile açtığınızda içerik akışı, her biri yarım punto yüksekliğinde bir kutuyu anlatan bir re f çiftleri sütunu gibi okunur. Yalnızca çizgili segmentlere bakan bir tablo dedektörü hiçbir şey bulamaz, sözcükleri aralarındaki beyaz boşluğa göre dizmeye geri düşer ve yazarın tek bir temiz ızgara çizdiği yerde iki-üç satırlık parçalar savurur. Kenarlar hep oradadır. Dolguyla boyanırlar; bu yüzden dedektörün dolu dikdörtgenleri çizgi olarak okuması ve sonra birbirine yaklaşan kenarları paylaşılan ızgara çizgilerine çekmesi gerekir

Bir Word tablosunun neden çizgili çizgisi yoktur?

Bir kelime işlemci, hücre kenarını genişlikli bir kutu sayar ve o kutuyu fill operator'üyle boyar. ISO 32000-1 §8.5.2.1, re'yi kapalı bir dikdörtgen alt yolu eklemek olarak tanımlar ve §8.5.3 boyama operator'lerini ayırır: S yolu geçerli çizgi genişliğiyle çizer, f içini doldurur. 0.5 puntoluk bir kenar, x y w 0.5 re f olarak çıkar ve stroke mekanizması (çizgi genişliği, birleşimler, kesik çizgi deseni) hiç çalışmaz. Hücre gölgelendirmesi, daha büyük kutulu aynı yapıdır. Çizgi-odaklı bir dedektörün beklediği çizgili biçim — m, l, S — office çıktılarının nadiren kullandığı biçimdir:

% bir kelime işlemci çıktısından bir hücre kenarı: 0.5 pt yüksekliğinde dolu kutu
72 700 468 0.5 re f
% hücre gölgelendirmesi: hücre boyutunda dolu bir kutu
72 676 117 24 re f
% yalnızca-stroke arayan bir dedektörün beklediği çizgili ızgara çizgisi
72 700 m 540 700 l S

Ham PDFium API'si üzerinde kendi dedektörünüzü yazıyorsanız ilk taslak genelde FPDFPath_GetDrawMode çağırır, stroke bayrağı setken yolu tutar ve yoluna devam eder. Yukarıdaki iki dolu kutu da o testte kalır. Hücrelerin içindeki sözcükler boşluk algılamasına ulaşır ve sıkı bir çıktıda sütunlar belki 6 punto arayla durur. PDFium Component'ta varsayılan MinColumnGap 12 puntodur; o sütunlar kayda geçmez. Kayda geçene kadar MinColumnGap'i düşürmek, iki yana yaslanmış paragrafların gerilmiş sözcük aralığını da kayda geçirir ve yazarın çizdiği ızgara asla geri gelmez; çünkü onu tanımlayan bilgi dolu kutularda yaşar

PDFium Component dolu bir kutuyu nasıl çizgi olarak okur?

PDFium Component tablo geometrisini, sayfadaki her path nesnesinden bir alt yol okur ve TPdfTableExtractionOptions.DetectFilledRulings varsayılan olarak True olduğundan, fill modu none olmayan her yol, çizgili yolların yanında çizgiler için incelenir. Arkadan gelen filtre bilerek katıdır. Dolu bir alt yol, yalnızca dört ile sekiz arası noktası olduğunda, curve segmenti içermediğinde ve her noktası, alt yolunun sınırlayıcı kutusunun bir köşesinde 0.05 punto içinde oturduğunda hak kazanır. re ile çizilen bir dikdörtgen geçer; tekrarlanan bir köşeyle beş açık nokta olarak çizilen de geçer. Bir üçgen, bir chevron, L biçimli bir kontur ya da yuvarlatılmış bir sekme testi geçemez ve hiçbir şey katmaz

Delphi'de PDFium Component'ın kapalı dolu bir alt yolu nasıl sınıflandırdığı şeması: eğri konturlar ve köşeleri sınırlayıcı kutu köşelerinin dışında kalan çokgenler elenir, MaxRulingThickness'ten kalın olmayan bir kutu uzun ekseninde bir çizgi verir, gölgeli bir hücre dört kenar çizgisi verir ve minicik bir kare hiç vermez
Kapalı bir alt yol yalnızca eksene hizalı olduğunda hayatta kalır; sınırlayıcı kutu da bir çizgi mi, gölgeli bir hücrenin dört kenarı mı yoksa hiçbir şey mi olduğuna karar verir

Hayatta kalan dikdörtgen, sınırlayıcı kutusuyla MaxRulingThickness'e karşı sınıflandırılır; o, varsayılan olarak 3 puntodur. Genişlik eşiğin altında ya da eşitken yükseklik üzerindeyse, kutunun bütün yüksekliğince akan, yatay merkezde tek bir dikey çizgi verilir; ayna durum tek bir yatay çizgi verir. Her iki boyut da eşiği aştığında kutu gölgeli bir hücre sayılır ve kenar başına bir olmak üzere dört çizgi katlar. Her iki boyut da eşiğin altındayken ya da eşitken kutu hiçbir şey katmaz; böylece bir listedeki 2 puntoluk kare madde imi, asla masa ızgarasının ucu olmaz. Dolgu renginin bunda hiçbir payı yoktur. Beyaz sayfadaki beyaz kutu, siyah olanın verdiği çizgilerin aynısını verir; bu yüzden tablo boyutunda bir arka plan paneli dış çerçeve olarak görünebilir

Çizgili yollar kendi rotalarını korur. Uçları tek bir eksende RulingTolerance'ten (varsayılan 1 punto) fazlasıyla farklılaşmayan her düz segment, öteki eksen boyunca bir çizgiye dönüşür; curve segmentleri atlanır ve kapalı bir alt yolunun kapanış segmenti de sayılır. Hem fill hem stroke ile boyanan bir yol iki rotadan da katkı verir ve üst üste binen parçaları daha sonra birleştirme adımı çöker. Çizgili yuvarlatılmış bir dikdörtgen, düz kenarları için yine çizgi verir; yalnızca köşelerdeki yaylar düşer. Çıkarma çağrısının kendisinin özel bir kurulumu gerekmez:

uses
  System.SysUtils, PDFium;

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

    Options := TPdfTableExtractionOptions.Default;
    // dolu-çizgi alanları, varsayılan değerleriyle açıkça yazılmış
    Options.DetectFilledRulings := True;
    Options.MaxRulingThickness := 3.0;       // punto; 0'dan büyük olmalı
    Options.RulingSnapTolerance := 4.0;      // punto; 0 yaslamayı 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';
      WithText := 0;
      for J := 0 to High(Tables[I].Cells) do
        if Tables[I].Cells[J].Text <> '' then
          Inc(WithText);
      Writeln(Format('%dx%d %s, confidence %.2f, %d of %d cells hold text',
        [Tables[I].RowCount, Tables[I].ColumnCount, Mode,
         Tables[I].Confidence, WithText, Length(Tables[I].Cells)]));
    end;
  finally
    Pdf.Free;
  end;
end;

MaxRulingThickness'i 0 yapmak özelliği kapatmaz. ExtractTables geometri option'larını baştan doğrular ve sıfır ya da altındaki bir eşik için "Tablo çıkarma geometri option'ları geçersiz" iletisiyle EPdfError yükseltir. Yalnızca-stroke davranışını almak için DetectFilledRulings'i False yapın. Bir office paketinden ya da bir tarayıcı yazdırma iletişiminden çıkan her şey için açık bırakın; maliyeti düşük tutan, katı dikdörtgen filtresidir, çünkü dekoratif dolgular neredeyse hiç ondan geçmez

RulingSnapTolerance gölgeli-hücreli tablolar için ne yapar?

RulingSnapTolerance, yalnızca gölgelendirmeden kurulmuş bir tabloyu tek bir ızgaraya bağlayan şeydir. Bazı çıktılar hiç kenar çizmez: her hücre, komşularından 1-3 puntoluk beyaz bir aralıkla ayrılmış dolu bir kutudur. Her kutu dört kenar çizgisini verir ama bir hücrenin sağ kenarı ile bir sonrakinin sol kenarı, diyelim ki, 2 punto arayla durur ve iki çizginin aynı ızgaraya ait olup olmadığına karar veren test yalnızca RulingTolerance'e izin verir — 1 punto. Yaslama olmadan her hücre, 1x1 bir ızgarayı anlatan dört çizgili bir adadır; hiçbir şey MinColumns'a ya da MinRows'a (ikisi de varsayılan 2) ulaşmaz ve çizgili geçiş, açıkça tablo taşıyan bir sayfada hiçbir tablo raporlamaz

Delphi için PDFium Component'ta RulingSnapTolerance şeması: komşu gölgeli hücreler 2 pt aralık bırakır, X 230.0 ve 232.0'daki kenar çizgileri 1 pt RulingTolerance'ün ötesinde durur ve yaslamak iki değeri 231.0'da kümeler; böylece her iki hücre bir ızgara çizgisini paylaşır
Yaslama, birleştirmeden ve çizgili dedektörden önce çalışır; böylece beyaz bir aralığın iki kenarı tek bir çizgiye dönüşür ve her hücre dört çizgili bir ada olmayı bırakır

Yaslama adımı, oyundaki her X koordinatını toplar — her dikey çizginin konumu artı her yatay çizginin başlangıcı ve sonu — ve her Y koordinatını da aynı şekilde. Her liste sıralanır ve iki komşu toleransten fazlasıyla farklılaştığı her yerde kümeler halinde bölünür; her küme ortalamaıyla değiştirilir ve her konum, başlangıç ile uç en yakın merkeze taşınır. 230.0 ve 232.0'daki kenarlar ikisi de 231.0 olur, iki hücre bir çizgiyi paylaşır ve yatay çizgiler onlarla birlikte gerilir, köşeler buluşsun diye

RulingTolerance içinde dokunan ya da üst üste binen aynı-doğrultudaki parçalar tek çizgilerde birleştirilir. Izgara dedektörü ancak ondan sonra çalışır ve çizgileri ikili karşılaştırır; 720 kenar parçası olarak gelen 30 satırlık bir tablo o karşılaştırmaya bir düzine çizgi olarak ulaşır

Çizgili bir ızgarada adım hiçbir şeyi değiştirmez, çünkü zaten eşleşen koordinatlar kendilerine yaslanır. Peki zincirlemenin kendi genişlik sınırı yoktur. 100, 103, 106 ve 109'daki, her biri 3 punto arayla giden bir değer dizisi, dış değerler 9 punto arayla dururken 104.5'te tek bir merkeze çöker. 4 puntoluk varsayılanla bu, yalnızca bir karakterden dar sütunlarda sokar; office çıktısı 4 puntoluk varsayılanı tutabilir. Bir form kasıtlı olarak dar sütunlar taşıdığında (onay kutuları, hücre-başına-rakam hesap numarası alanları) 1 ya da 2'ye doğru indirin ve yaslamayı kapatmak için 0 yapın. Çizgili stratejiyi üç ayar altında tek başına çalıştırmak, bir sayfanın hangi mekâna bel bağladığını gösterir:

// Yalnızca çizgili strateji, böylece boşluk algısı sonucu maskeler
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;

// Yalnızca gölgeli hücrelerle 2 pt aralıklı çizilmiş bir tablo için beklenen:
//   yalnızca stroke          -> 0  (hiç çizgili geometri yok)
//   dolu kutular, snap 4     -> 1  (aralık kenarları bir çizgiyi paylaşır)
//   dolu kutular, snap 0     -> 0  (her hücre 1x1 bir ada olarak kalır)
Writeln('stroke only:          ', CountRuledTables(Pdf, False, 4.0));
Writeln('filled boxes, snap 4: ', CountRuledTables(Pdf, True, 4.0));
Writeln('filled boxes, snap 0: ', CountRuledTables(Pdf, True, 0.0));

Üçüncü sayım ikincinin altına düştüğünde sayfa yaslamaya bel bağlar ve toleransı indirmeden gözle kontrol edilecek sayfa da odur

Form XObject'lerinin içindeki çizgiler

Sayfa-düzeni araçları sık sık bir tabloyu ya da bütün sayfa gövdesini bir form XObject'ine sarar ve onu Do ile boyar. ISO 32000-1 §8.10.1, form boyandığında form matrisinin geçerli dönüşüm matrisiyle birleştirildiğini söyler; bu yüzden formun içindeki bir dikdörtgen form uzayında yazılır ve sayfa uzayına ancak iki ya da daha fazla dönüşümden sonra ulaşır. Varsayılan olan IncludeFormXObjects setken PDFium Component her form nesnesinin içine yürür, nesne matrisini ebeveyninin matrisiyle birleştirir ve birleşik matrisi her çocuğa devreder; böylece ötelenmiş bir formun içinde 72 700 468 0.5 re f olarak çizilen bir kenar, görüntüleyicinin gösterdiği yere düşer. Yürüyüş, sekiz düzey yuvalanmış formda durur

Delphi için PDFium Component'ta form XObject'inin içindeki çizgiler şeması: 72 700 468 0.5 re f olarak yazılmış ince bir dikdörtgen form uzayında yaşar ve sayfanın tepesine ancak form matrisi ile ebeveyn dönüşümü doğru sırada birleştirildikten sonra düşer
Dikdörtgen form uzayında yazılır ve matrisler, öteleme terimini ait olduğu yerde tutan bir sırada çarpıldıktan sonra ancak sayfanın tepesine ulaşır

O birleşmenin sırası önem taşır. Formun içindeki bir nokta önce nesne matrisinden, sonra ebeveyn matrisinden geçer; operandları değiştirin, öteleme terimi yanlış uzayda uygulanır ve sayfanın tepesine ait bir çizgi orijinin ya da sayfanın dışında biter. Aynı tuzak, matrisleri kendiniz kurduğunuz her seferde karşınıza çıkar ve matris prepend ile append karşılaştırması yazısı onu bir döndürme ekseniyle gezer

Varsayılan çizgi bütçesi neden 16384?

16384'lük varsayılan MaxRulingSegments, hücre-başına bir kenar çıktısının kaç parça ürettiğini yansıtır. Çizgili 30 satırlı, 6 sütunlu bir tablo 38 çizgi segmenti ister: 31 yatay, 7 dikey. Dolu kutular olarak dışa aktarılan aynı tablo, hücre başına dört kenar parçası taşıyabilir — birleştirmeden önce 720 — ve her hücrenin gölgelendirilmesi hücre başına dört kenar çizgisi daha ekler; toplam 1440. Böyle iki tablo tek bir sayfada 3000'e yakın ister

Bütçeyi aşmak bir hatadır ve öyle yüzeye çıkar. ExtractTables, "Tablo çizgi-segment bütçesi aşıldı" iletisiyle EPdfError yükseltir, kısmi bir ızgara döndürmez ve o sayfanın boşluk geçişi de çalışmaz. Sıkı bir bütçe, güvenilmeyen girdi için makuldür — işlenmiş bir sayfa milyonlarca minicik kutu taşıyabilir — ama o zaman çağıranın "bu bütçe için fazla karmaşık" ile "bu sayfada tablo yok"u ayırt etmesi gerekir:

function TryExtractTables(Pdf: TPdf; Budget: Integer;
  out Tables: TPdfTables): Boolean;
var
  Options: TPdfTableExtractionOptions;
begin
  Options := TPdfTableExtractionOptions.Default;
  Options.MaxRulingSegments := Budget;     // varsayılan 16384
  try
    Tables := Pdf.ExtractTables(Options);
    Result := True;
  except
    on E: EPdfError do
    begin
      // LogWarning sizin loglama rutininizdir, kütüphanenin parçası değildir
      LogWarning(Format('Page %d: %s', [Pdf.PageNumber, E.Message]));
      Tables := nil;
      Result := False;                     // bütçe aşımı ya da geçersiz option'lar
    end;
  end;
end;

// False bir sonuç "karara bağlanmadı" demektir; bu, boş bir diziden farklıdır
if not TryExtractTables(Pdf, 4096, Tables) then
  QueueForReview(Pdf.FileName, Pdf.PageNumber);  // bu da sizin rutininiz

Aynı istisna türü kelime bütçesini ve geçersiz option'ları da kapsar; gözden geçirme kuyruğunda onları ayırt eden şey günlüğe kaydedilen iletidir

Çizgili yolun devrettiği yer

Çizgili algı önce, boşluk algısı sonra çalışır ve iki kural ikisinin birbirinin üstüne basmasını önler. Çizgili geçişin zaten raporladığı bir ızgaranın içine merkezleri düşen sözcükler, boşluk algısı başlamadan kaldırılır; böylece bir tablo asla iki kez raporlanmaz. Bir boşluk sütun sınırının ayrıca, ayırdığı her satır boyunca metinsiz bir dikey koridor olması gerekir; bu, iki yana yaslanmış paragrafların tablo sayılmasını önler — boşluk tablosu yanlış pozitifleri yazısı o kuralı kapsar. Dolu dikdörtgenleri okumak, office çıktısı tabloları boşluk yolundan çizgili yola taşır

Çizgiler path nesnelerinden, metin metin sayfasından geldiği için metin katmanı olmayan bir sayfa, her hücresi boş ızgara iskeletini yine üretir. Çizgili bir ızgaranın güveni, 0.88 artı metin tutan hücrelerin oranının 0.12 katıdır; boş bir iskelet tam olarak 0.88 rapor eder ve ilk listedeki hücre sayımı metinli 0 hücre gösterir. O bileşim, önce OCR çalıştırma sinyalidir — OCR ile aranabilir bir metin katmanı eklemede anlatıldığı gibi. Eğrili ya da dikdörtgen-olmayan konturlu dolu şekiller hiçbir şey katmaz; hücreleri dolu yuvarlatılmış dikdörtgenler olan bir tablo boşluk algısına düşer. Ne kenarı ne gölgelendirmesi olan bir tablo, tablo algılama ve çıkarma yazısında anlatılan boşluk stratejisine aittir; o bile yetmediğinde yapılandırılmış metin ve okuma sırasındaki kelime kutuları ve bloklar, tek bir belge ailesi için yazılmış bir okuyucunun ham maddesidir

Bileşenle gelen TableExtractionLab demosunda DetectFilledRulings için bir onay kutusu vardır; bir çıktı yükleyip ızgaranın belirmesini ve kayboluşunu hiç kod yazmadan görebilirsiniz. Option'lar, kayıt türleri ve çıkarma API'sinin geri kalanı Delphi için PDFium Component ürün sayfasında listelenir