Teknik Makale

Yanıltıcı PDF tabloları: iki yana yayılmış metin sorunu

İki yana yayılmış bir gövde metni sayfası, bir whitespace tablo dedektöründen birkaç sütunlu bir ızgara olarak dönebilir; her hücrede yarım kalmış bir cümle ve güven puanı, aklınıza gelebilecek her eşiğin hayli üstünde. O sayfada tablo yoktur. Dedektör, sütun ayırıcısı diye geçebilecek kadar geniş sözcük boşlukları ve o boşlukların neredeyse aynı X konumuna düştüğü birkaç ardışık satır bulmuştur; yalnızca hizalamaya bakıldığında tablonun görünümü tam olarak budur. Çare, bir sütun sınırını birer satır mahkûm etmeyi bırakıp sayfanın sütunları arasında boş kâğıttan bir şerit uzayıp uzanmadığını sormaktır

İki yana yayılmış metin bir whitespace tablo testini neden geçiyor?

Yayılmış metin geçer, çünkü yayılmış bir satır, düzenleyicinin gerdiği boşluklarla ayrılmış bir sözcük dizisidir ve gerilmiş bir boşluk bir kez sütun eşiğini aşınca o tek satırda onu bir sütun aralığından ayıran hiçbir şey kalmaz. PDFium Componentteki whitespace stratejisi, çoğu whitespace dedektörü gibi çalışır. TPdf.ExtractTables sözcük kutularını görsel satırlara gruplar, bir sözcükten sonrakine yatay uzaklık en az MinColumnGap (TPdfTableExtractionOptions.Default içinde 12 punto) olduğu her yerde satırı böler ve ortaya çıkan sözcük gruplarının sol kenarlarını AlignmentTolerance — varsayılanı 3 punto — içinde sütun çıpalarına kümeleme yapar. Bir çıpa, en az MinRows satırda yinelenmedikçe hayatta kalmaz ve adayın her satırı en az MinColumns farklı çıpaya isabet etmek zorundadır. Gerçekten hizalı bir tablo için o kural doğrudur ve tablo tespiti genel bakışında anlatılan kural da budur

Düz metin tarafındaki aritmetik göründüğünden affetmez değildir. 180 puntoluk bir bülten sütunundaki 10 puntoluk bir satırın, sonraki sözcük sığmadığı için sağ marja varmadan 30 punto önce bittiğini ve sözcükleri uzun olduğu için yalnızca üç sözcük arası boşluk taşıdığını varsayın. Yayma motoru o 30 puntoyu üç boşluğa yayar; her biri kabaca 2,5 puntodan 12,5 puntoya büyür ve varsayılan MinColumnGapi aşar. İkinci bir satırın, izleyen sözcüğü ilk satırınkinin 3 punto yakınında başlayan tek bir gerilmiş boşluğa ihtiyacı vardır; dedektörün elinde iki satırlı, iki sütunlu bir aday vardır

Germe nasıl kodlanıyorsa kodlansın fark etmez. ISO 32000-1 §9.3.3, sözcük aralığını (Tw) yalnızca tek baytlık 32 karakter koduna uygular; bu yüzden çok baytlı font kullanan üreticiler sözcükleri genellikle bir TJ dizisindeki sayısal ayarlamalarla (§9.4.3) konumlandırır. Hangisi olursa olsun sözcük kutuları birebir aynı çıkar. Her paragrafın son satırı pürüzlü dizilir ve sıradan boşluklar taşır; yanlış pozitiflerin bir paragrafın gövdesini kapsayıp sonundan bir satır önce durması bundandır

İki yana yayılmış düz metnin neden tablo diye puanlandığına dair PDFium Component şeması: her satır aynı marja kadar gerilir, böylece tek boşluklar her satırda farklı bir X üzerinde MinColumnGap'i geçer ve AlignmentTolerance içindeki iki ardışık boşluk, koridor testinin reddettiği yanlış adayları kurar
Gerçek bir tablo sütun çıpalarını her satırda yineler; iki yana yayılmış bir paragraf ise her satırda başka bir boşluğu gerer, satır düzeyinde ince ayarın ikisini ayıramaması bundandır

Kendi derleminizi hızla denetlemenin yolu, dedektörü yalnızca düz metin içerdiğinden emin olduğunuz belgelere — mektuplar, basın bültenleri — çalıştırıp bildirdiği her whitespace modlu tabloyu yazdırmaktır. Bu belgelerde beklenen çıktı hiçliktir

uses
  SysUtils, PDFium;

// Yüklenen belgedeki her whitespace hizalamalı tabloyu listeler; böylece
// yalnızca düz metin içerdiğini bildiğiniz sayfalar hızla denetlenebilir
procedure ReportWhitespaceTables(Pdf: TPdf; MinRows: Integer = 2);
var
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Options := TPdfTableExtractionOptions.Default;  // MinColumnGap 12, AlignmentTolerance 3
  Options.MinRows := MinRows;
  Tables := Pdf.ExtractDocumentTables(Options);   // etkin sayfayı geri yükler
  for I := 0 to High(Tables) do
    if Tables[I].DetectionMode = ptdmWhitespace then
      Writeln(Format('page %d: %dx%d whitespace table, confidence %.2f, ' +
        'first cell "%s"',
        [Tables[I].PageNumber, Tables[I].RowCount, Tables[I].ColumnCount,
         Tables[I].Confidence, Tables[I].Cells[0].Text]));
end;

Her eşiğin bedeli nedir?

Satır düzeyindeki her eşik, bir belge sınıfını bir başkasına takas eder ve bu sorunda onları rahat bırakmanın gerekçesi budur. MinColumnGapi 20 puntoya yükseltmek, yoğun mali tabloların sıkışık sütunlarını düşürür; etiket sütunuyla değerleri arasındaki boşluk çoğu zaman varsayılanla yarışır. MinRowsu 3e çıkarmak gerçek iki satırlı tabloları çöpe atar ve uzun paragrafların şansını ancak biraz azaltır; düz metin dolu bir sayfa, üç satırın hizalanması için çok fırsat sunar. AlignmentToleranceu 3 puntanın altına sıkmak, sol kenarları hafifçe kayan gerçek sütunları bölmeye başlar; OCR metin katmanlı taranmış sayfalarda kenarlar böyledir

MinConfidence doğal filtre gibi görünür ve dördün en zayıfıdır. PDFium Componentte bir whitespace tablosu, boş olmayan hücrelerin payı times 0,30 artı 0,58 puan alır ve 0,88de tavan yapar; puan, ızgaranın ne kadar dolduğunu ölçer. Bir paragraf parçası her hücreyi doldurur; düz metinden doğan bir yanlış pozitif aralığın tepesine yakın puan alırken, boş hücreli gerçek bir tablo daha düşük alır

İki satır yayılmış düz metinle iki sütunlu bir tablonun iki satırı, aynı çıpalar üzerinde birebir aynı sözcük gruplarını üretebilir. İkisini ayıran kanıt, satırların arasındadır

Gerçek bir sütun sınırı dikey bir koridordur

Gerçek bir sütun sınırı, ayırdığı her satırda metinden arınmış kalan dikey bir şerittir. Bir tabloda her sütun çiftinin arasında bir tanesi olur; hücreler ortak X konumlarına göre dizildiği için bu yapıyla gelir. İki yana yayılmış bir paragraf boşluklarını her satırda başka bir yatay konumda gerer; bir ya da iki satırdan fazla hayatta kalan şerit yoktur. PDFium Component tam da bunu, adayın sözcük grupları çıpa sütunlarına atandıktan sonra sınar. Bitişik her sütun çifti için, iki hücresinde de içerik olan her satır; sol hücredeki sözcüklerin en sağ kenarından sağ hücredeki sözcüklerin en sol kenarına uzanan aralığı bağışlar. Aralıklar satırlar boyunca kesiştirilir ve kesişim MinColumnGapin yarısından dar olduğunda bütün aday reddedilir — varsayılanla 6 punto

ExtractTables'in arkasındaki metinsiz koridor testinin PDFium Component şeması: her satır, sol hücresinin sağ kenarından sağ hücresinin sol kenarına uzanan aralığı bağışlar; kesişim gerçek bir tabloda MinColumnGap'in yarısından geniş kalır, iki yana yayılmış metinde ise hiçliğe çöker
Gerçek bir sütun sınırı, ayırdığı her satırda boştur; satır başına boşlukların kesiştirilmesi bir tablo için paylaşılan bir şerit, gerilmiş düz metin için ise hiçbir şerit bırakmaz

Tuhaf gerçek tablolarda iki ayrıntı önem taşır. Hücrelerinden biri boş olan satırlar oy vermez; dolayısıyla boş hücreli bir tablo ya da gövdesinden az sütuna yayılan bir başlık satırı yine de geçer. Ve koridor genişliği ayrı bir seçenek olmak yerine MinColumnGapten türetilir; çünkü ikisi de aynı fiziksel şeyi anlatır: tasarımcının sütunlar arasında bıraktığı boşluğu. Yarıya indirmek, sol sütundaki uzun bir girdinin diğerine yaklaştığı satıra pay bırakır

Denetim, kendi düzen kodunuzda yeniden üretebileceğiniz kadar küçüktür; aşağıdaki hücre listeleri sizin yapınızdır, satır öncelikli sırada hücre başına bir sözcük indeksi dizisi

uses
  Math, PDFium;

type
  TIndexList = array of Integer;
  TCellIndexes = array of TIndexList;   // kendi yapınız: Row * ColumnCount + Column

// Bitişik bir sütun çiftinin, onu kullanan satırlar boyunca en az
// MinColumnGap / 2 genişliğinde metinsiz dikey koridoru yokken False döner
function HasTextFreeCorridors(const Words: TPdfWordBoxes;
  const Cells: TCellIndexes; RowCount, ColumnCount: Integer;
  MinColumnGap: Double): Boolean;
var
  Col, Row, I, LeftCell, RightCell, Supported: Integer;
  CorridorLeft, CorridorRight, RowLeft, RowRight: Double;
begin
  for Col := 0 to ColumnCount - 2 do
  begin
    CorridorLeft := -MaxDouble;
    CorridorRight := MaxDouble;
    Supported := 0;
    for Row := 0 to RowCount - 1 do
    begin
      LeftCell := Row * ColumnCount + Col;
      RightCell := LeftCell + 1;
      if (Length(Cells[LeftCell]) = 0) or (Length(Cells[RightCell]) = 0) then
        Continue;                             // boş hücreler oy vermez
      RowLeft := -MaxDouble;
      RowRight := MaxDouble;
      for I in Cells[LeftCell] do
        RowLeft := Max(RowLeft, Words[I].Rect.Right);
      for I in Cells[RightCell] do
        RowRight := Min(RowRight, Words[I].Rect.Left);
      CorridorLeft := Max(CorridorLeft, RowLeft);
      CorridorRight := Min(CorridorRight, RowRight);
      Inc(Supported);
    end;
    if (Supported > 0) and
       (CorridorRight - CorridorLeft < MinColumnGap * 0.5) then
      Exit(False);
  end;
  Result := True;
end;

Çizgili tabloları whitespace geçişinin dışında tutmak

Çizgili bir tablo, whitespace geçişi sayfadaki her sözcüğü görseydi iki kez bulurdu; temiz bir çizgili tablo, yapısal olarak kusursuz hizalı bir whitespace tablosudur da. Kendi başına, yarıdan fazla örtüşme kuralı, çizgili bir tablonun alt satırlarını altındaki birkaç hizalı metin satiriyle birleştiren bir adayın oranın altında kalmasına ve komşusuna kanayan ikinci, daha büyük bir tablo olarak çıkmasına izin verirdi

Hem DetectRuledTables hem DetectWhitespaceTables açıkken — varsayılan budur — ExtractTables bu yüzden önce çizgili geçişi çalıştırır ve whitespace geçişi başlamadan, merkez noktası bir çizgili tablonun sınırları içinde kalan her sözcüğü çıkarır. Merkez noktası, tam kapsama yerine kullanılır; böylece sınırı birkaç puanlık parçayla bölen bir sözcük, görsel olarak ait olduğu tabloyu izler. Whitespace stratejisi böylece yalnızca serbest sözcükleri görür ve doğrudan çizgili bir ızgaranın altında duran küçük çizgisiz bir tablo kendi satırlarıyla yargılanır

"Purpose of Request:" neden "of Purpose Request:" olarak çıkabilir?

Hücrenin içindeki sözcükler, "aynı satır" kararını veren kod dikey merkezleri karşılaştırdığında yanlış sırada çıkar; çünkü glif kutularından kurulan sözcük kutuları aynı taban çizgisini paylaştıklarında bile merkez paylaşmazlar. FPDFText_GetCharBox, her glifin mürekkebinin sayfa uzayındaki sıkı kutusunu döndürür; fontun ascender ve descenderına göre şişirilmiş bir kutu değil ve bir sözcük kutusu, karakterlerinin kutularının birleşimidir. "Purpose of Request:" gibi bir form etiketinde "Purpose" ile "Request:" descender taşır, "of" taşımaz; "of"un kutusu daha kısadır ve merkezi birkaç punto yukarıdadır. Merkez Y'yi 1 puntoluk aynı-satır toleransıyla, sonra sol kenara göre sıralayan bir yordam "of"u diğer ikisinin üstünde ayrı bir satıra koyar ve onu ilk olarak verir

PDF kestirme yol sunmaz. ISO 32000-1 §9.4.4 glifleri taban çizgisi boyunca yer değiştirmeyle yerleştirir ve dosyadaki tek dikey ölçüler font başınadır: font tanımlayıcısındaki Ascent, Descent ve FontBBox (§9.8.1). İki glifin aynı satırı paylaştığını söyleyen hiçbir şey yoktur. PDFium char kutularıyla metin satırı seçimi yazısında işlenen, seçim vurgusunun doğru görünmesini sağlayan sıkı kutular, merkez uzaklığı karşılaştırması için kötü girdidir

Purpose of Request sözcük sırasının PDFium Component şeması: FPDFText_GetCharBox sıkı glif kutuları, descendersız of sözcüğüne eski 1 pt merkez-Y toleransının kendi satırına ayırdığı yüksek bir merkez verirken; yüzde 25 dikey örtüşme kuralı onu taban çizgisinde ve doğru sözcük sırasında tutar
Merkez Y, mürekkebin taşıdığı ascender ve descenderlarla oynar; aynı taban çizgisindeki iki kutu ise yükseklikleri ne olursa olsun ortak x-height boyunca örtüşür

PDFium Component hücre metnini bunun yerine örtüşmeyle kurar. Bir sözcük, dikey örtüşmesi satırın ilerleyen sınırlarıyla karşılaştırıldığında iki yükseklikten küçüğünün en az yüzde 25i olduğunda ya da iki merkez bir punto içinde olduğunda görsel satıra katılır; her satır sonra sol kenara göre sıralanır ve satırlar bir CR LF çiftiyle birleştirilir. "Purpose" ile "of", bütün x-height boyunca örtüşür; kısa kutunun dörtte birinden çok daha fazla. Aynı satıra düşerler ve sırayla okunurlar

Kendi kodunuzda metin satırlarını örtüşmeyle gruplamak

Aynı tuzak, "aynı satır" kararını merkezi sabit bir toleransla karşılaştırarak veren her PDF düzen kodunda bekler. En yumuşak tetikleyici descenderlardır. Küçük değerlerin yanındaki kalın etiketler, üst simge dipnot işaretleri ve gürültülü OCR kutuları, 12 punto satır aralıklı 10 puntoluk metnin bitişik satırlarını hâlâ ayırabilen her toleranstan fazla merkez kaydırır. Örtüşme oranı boyutu umursamaz: aynı taban çizgisindeki iki kutu ortak x-height boyunca örtüşür, bitişik satırlardaki iki kutu ise hiç örtüşmez

TPdf.PageWordBoxes, etkin sayfadaki her sözcüğü Rect içindeki sayfa uzayı dikdörtgeniyle döndürür; bir sayfayı görsel satırlara gruplamak bir doğruluk fonksiyonu ve kısa bir döngü ister

uses
  Math, PDFium;

function SameVisualLine(const A, B: TPdfRectangle): Boolean;
var
  Overlap, MinHeight: Double;
begin
  Overlap := Min(A.Top, B.Top) - Max(A.Bottom, B.Bottom);
  MinHeight := Min(A.Top - A.Bottom, B.Top - B.Bottom);
  Result := (MinHeight > 0) and (Overlap >= MinHeight * 0.25);
end;

procedure GroupPageIntoLines(Pdf: TPdf; out Lines: TArray<TPdfWordBoxes>);
var
  Words: TPdfWordBoxes;
  Bounds: TArray<TPdfRectangle>;   // satır başına ilerleyen birleşim
  I, J, Found: Integer;
begin
  Words := Pdf.PageWordBoxes;            // etkin bir sayfa gerekir (Pdf.PageNumber)
  Lines := nil;
  Bounds := nil;
  for I := 0 to High(Words) do
  begin
    Found := -1;
    for J := High(Lines) downto 0 do
      if SameVisualLine(Bounds[J], Words[I].Rect) then
      begin
        Found := J;
        Break;
      end;
    if Found < 0 then
    begin
      SetLength(Lines, Length(Lines) + 1);
      SetLength(Bounds, Length(Bounds) + 1);
      Found := High(Lines);
      Bounds[Found] := Words[I].Rect;
    end;
    SetLength(Lines[Found], Length(Lines[Found]) + 1);
    Lines[Found][High(Lines[Found])] := Words[I];
    Bounds[Found].Left := Min(Bounds[Found].Left, Words[I].Rect.Left);
    Bounds[Found].Right := Max(Bounds[Found].Right, Words[I].Rect.Right);
    Bounds[Found].Top := Max(Bounds[Found].Top, Words[I].Rect.Top);
    Bounds[Found].Bottom := Min(Bounds[Found].Bottom, Words[I].Rect.Bottom);
  end;
  // Okumadan önce her satırı Rect.Left ile sıralayın: PageWordBoxes
  // sözcükleri content stream sırasında verir; görsel sırayla uyuşmayabilir
end;

Kopyalanmaya değer parça doğruluk fonksiyonudur. Hızlı bir dökümden ötesi için yapılandırılmış metin modelinden başlayın; blokları, satırları ve okuma sırası kaynağını hâlihazırda taşır, okuma sıralı yapılandırılmış PDF metni çıkarma yazısında anlatıldığı gibi

Tablo çağıranlarının hâlâ elinde tuttukları

Tablo çağıranları, seçeneklerine dokunmadan koridor testini, önce-çizgili sözcük filtresini ve örtüşme tabanlı hücre metnini alır. Koridor eşiği MinColumnGapin yarısına sabitlenmiştir; sütun aralığı için ayarladığınız tek değer ikisini de yönetir. Whitespace stratejisi, MinRows 1e çekilse bile iki satırlık bir taban tutar; tek satırlı ızgaraları yalnızca çizgili strateji kabul eder ve ExtractDocumentTables onları bir sayfanın tepesinde devam eden tablo parçaları için kullanır. Aynı seçenek kaydı TPdfReflowOptions.TableOptionsa da gider; DetectTables açıkken yeniden akış çıktısı aynı kararları miras alır

Koridor testi, bir sınırın iki yanında da içeriği olan satırlara bakarak karar verir; iki satırlı bir aday yalnızca iki böyle satırla yargılanır. Çoğu düz metin olan ve gerçek iki satırlı tabloları seyrek barındıran arşivler için — sözleşmeler, yazışmalar, denemeler — MinRowsu 3 yapın ve kaybedilen iki satırlı tabloları kabul edin. Mali ve form yoğun belgelerde 2 varsayılanını tutun; kısa gerçek tablolar orada sıktır. Pürüzlü sağ kenarlı metin sıradan tek boşluklar taşır ve 12 puntoya yaklayan bir boşluk nadiren üretir

PDFin genel bir tablo nesnesi yoktur. ISO 32000-1 §14.8.4.3.4 bir Table yapı elementi tanımlar ama onu yalnızca Tagged PDF taşır; geri kalan her şey için ızgara geometriden çıkarılan bir sonuçtur ve bir whitespace tablosunun Confidenceu hiçbir zaman 0,88i aşmaz

Tespit seçenekleri, TPdfTable.ToCsv ile ToJson serileştirmesi ve Delphi, C++Builder ve Lazarus için TableExtractionLab demosu, Delphi için PDFium Component ürün sayfasında ele alınır