PDFium Component sürüm 3.117.0, iki yana yaslı paragrafları whitespace ile hizalanmış tablolar olarak bildirmeyi bırakıyor: artık her sütun sınırının, ayırdığı hiçbir satırda metin bulunmayan dikey bir koridor olmasını şart koşuyor, çizgili bir ızgaranın zaten sahiplendiği kelimeleri atlıyor ve hücre metnini glyph kutusu merkez mesafesi yerine dikey örtüşmeye göre kuruyor. Üç değişiklik de ExtractTables ile ExtractDocumentTables içinde yaşıyor ve hiçbir seçenek gerektirmiyor
Bunu başlatan bildirim hiç görkemli değildi. Üzerinde hiç tablo bulunmayan bir basın bülteni sayfası ExtractTables fonksiyonundan 5x4'lük bir whitespace tablosu olarak döndü; güven değeri varsayılan MinConfidence 0,5'in rahatça üzerindeydi ve hücreler sıradan gövde metni parçaları tutuyordu. Bir başvuru formu da deneme paragraflarıyla aynısını yaptı ve bir 3x4 ile bir 5x3 üretti. İki belge de iki yana yaslıydı. Bariz tepki eşikleri ayarlamaktır ve bu sürümden çıkan faydalı ders, ayarlamanın bunu düzeltemeyeceğidir, çünkü ayarlanan kural yanlış soruyu soruyordu
uses
PDFium;
// Regresyon kontrolü: bir belgedeki her whitespace tablosunu listele, böylece
// yalnızca düzyazı olduğunu bildiğiniz bir sayfanın temiz olduğu doğrulanır
procedure ReportWhitespaceTables(Pdf: TPdf);
var
Options: TPdfTableExtractionOptions;
Tables: TPdfTables;
I: Integer;
begin
Options := TPdfTableExtractionOptions.Default; // MinColumnGap 12pt
Tables := Pdf.ExtractDocumentTables(Options);
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;
İki yana yaslı metin neden tablo gibi görünüyor?
İki yana yaslı bir paragraf tabloya benzer, çünkü yaslı bir satır, layout motorunun gererek açtığı boşluklarla ayrılmış bir kelime dizisidir ve gerilmiş bir boşluk MinColumnGap eşiğine ulaştığında dedektörün onu sütun ayırıcısından ayırt edecek satır yerelinde bir yolu yoktur. PDFium Component'teki whitespace stratejisi kelime kutularını görsel satırlara böler, her satırı bir önceki kelimeye yatay uzaklık en az MinColumnGap (varsayılan 12 punto) olduğu yerlerden kelime gruplarına ayırır ve en az iki ardışık satır, AlignmentTolerance (3 punto) içinde en az MinColumns kadar sol hizalı grup ankrajını yineliyorsa bunu tablo kabul eder. Tablo algılamaya genel bakış yazısında anlatılan kural budur ve gerçekten hizalı bir tablo için tam olarak doğrudur
Şimdi bunu yirmi satırlık, iki yana yaslı 10 puntoluk düzyazıya uygulayın. Her satır aynı sağ marja kadar gerilir, bu yüzden uzun bir kelimeyle biten bir satır iç boşluklarını açıp genişletir ve birkaç kısa satır içeren bir paragrafta bu boşluklardan bazıları 12 puntonun üzerine çıkar. İki ardışık satırın iki satırlı, iki sütunlu bir aday oluşturması için her birinin aynı X konumunun 3 punto içine düşen birer gerilmiş boşluğa sahip olması yeter. Yeterince satırda bu şanssızlık değil, kesinliğe yaklaşan bir olasılıktır; basın bültenindeki 5x4 de dört böyle boşluğun beş satırda hizalandığı koşudan başka bir şey değildi
Her eşik bir belge sınıfını bir diğeriyle değişir. MinColumnGap değerini 20 puntoya çıkarmak yoğun finansal raporların sıkışık sütunlarını kaybettirir; zaten varsayılanın düşürülmüş olmasının nedeni tam da bu durumdur. MinRows değerini 3'e çıkarmak gerçek iki satırlı tabloları çöpe atar ve uzun paragrafların şansını yalnızca azaltır. AlignmentTolerance değerini 3 puntonun altına indirmek, sol kenarları bundan fazla titreyen OCR kaynaklı kelime kutularını bozar. Satır düzeyindeki sinyal gerçekten belirsizdir, bu yüzden düzeltmenin satırların tek başına taşımadığı bir sinyalden gelmesi gerekir
Bir sütun sınırını gerçek yapan ne?
Gerçek bir sütun sınırı, sayfanın ayırdığı her satır boyunca boş kalan dikey bir şerididir. Bir tabloda her sütun çiftinin arasında böylesi vardır, çünkü hücreler ortak X konumlarına göre yerleştirilmiştir. İki yana yaslı bir paragraf ise kelime boşluklarını her satırda farklı yatay konumlarda gerer, bu yüzden bir iki satırdan fazlasının kesişiminden hiçbir şerit sağ çıkmaz. PDFium Component artık tam da bunu test ediyor: adayın kelime grupları ankraj sütunlarına atandıktan sonra, her komşu sütun çifti için iki hücrede de içerik bulunan her satırda, sol hücrenin kelimelerinin en sağ kenarından sağ hücrenin kelimelerinin en sol kenarına kadar olan aralık alınır, bu aralıklar satırlar boyunca kesiştirilir ve kesişim MinColumnGap değerinin 0,5 katından, yani varsayılanda 6 puntodan darsa adayın tamamı reddedilir
İki ayrıntı önemli. İki hücresi de boş olan satırlar oy vermez, böylece boş hücresi olan ya da başlığı gövdesinden daha az sütuna yayılan bir tablo yine geçer. Koridor genişliği ayrı bir seçenek olarak açılmak yerine MinColumnGap değerinden türetiliyor, çünkü ikisi de aynı fiziksel şeyi tarif eder: bir tasarımcının sütunlar arasında bıraktığı boşluk. Mantık, tablo API'si yerine ham kelime kutuları üzerine kuruluyorsanız yeniden üretilebilecek kadar küçük; aşağıdaki örnek bileşenin içindeki kontrolü yansıtıyor:
uses
Math, PDFium;
type
TIndexList = array of Integer;
TCellIndexes = array of TIndexList; // Row * ColumnCount + Column
// Herhangi bir komşu sütun çifti, kullandığı satırlar boyunca en az
// MinColumnGap / 2 genişliğinde metinsiz dikey koridordan yoksunsa 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 neden iki kez çıkarılıyordu?
Çizgili tablolar iki kez çıkarılıyordu, çünkü whitespace geçişi sayfadaki her kelimeyi, çizgili geçişin zaten bir ızgaraya yerleştirdiği kelimeler dahil, görüyordu ve temiz bir çizgili tablo kuruluşu gereği aynı zamanda kusursuz hizalanmış bir whitespace tablosudur. Zaten var olan bir örtüşme kontrolü, sınırları mevcut bir tablonun yarısından fazlasını kapsayan bir whitespace adayını reddediyordu; ama tablonun alt satırlarını altındaki birkaç hizalı metin satırıyla birleştiren bir aday bu oranın altında kalıp komşusuna taşan, biraz daha büyük ikinci bir tablo olarak hayatta kalabiliyordu. ExtractTables artık bu kelimeleri whitespace geçişi çalışmadan önce kaldırıyor. Bir kelime, merkez noktası çizgili geçişin ürettiği herhangi bir tablonun sınırları içinde kaldığında düşürülüyor; tam kapsama yerine merkezin kullanılmasının nedeni, bir kenarlığı bir puntonun kesri kadar aşan bir kelimenin görsel olarak ait olduğu tabloyu izlemesi. Whitespace stratejisi böylece yalnızca serbest kelimeler üzerinde çalışıyor; bu da doğrudan çizgili bir tablonun altında duran küçük, çizgisiz bir tablonun üstteki ızgarayla kaynaşmak yerine kendi esaslarına göre algılanması anlamına geliyor
"Purpose of Request:" neden "of Purpose Request:" olarak çıktı?
Kelimeler yeniden sıralanmış olarak çıktı, çünkü PDFium Component'in kurduğu kelime kutuları glyph sınırlayıcı kutularının birleşimidir ve "of" kelimesinin alt uzantısı yokken "Purpose" ile "Request:" kelimelerinin var. FPDFText_GetCharBox, glyph mürekkebinin sayfa uzayındaki sıkı kutusunu döndürür; fontun ascent ve descent değerlerine göre doldurulmuş bir kutu değil. Kelime kutusu da karakterlerinin kutularının birleşimidir. Bu yüzden alt uzantısı olmayan bir kelime daha kısadır ve dikey merkezi daha yukarıda oturur; söz konusu formda bu 2 ile 3 punto eder. Eski hücre metni rutini kelimeleri önce merkez Y'ye göre, aynı satır için 1 puntoluk bir toleransla, sonra sol kenara göre sıralıyordu; "of" toleransı aşıp diğerlerinin üstünde kendi satırı olarak sıralandı ve ilk yazıldı
Bu bir PDFium tuhaflığından çok, PDF'in metni nasıl konumlandırdığının bir sonucu. ISO 32000-1 §9.2.2 ile §9.4.4 glyph yerleşimini metin uzayında baseline boyunca yatay yer değiştirme olarak tanımlar ve dosyanın taşıdığı tek dikey metrik font başınadır: §9.8.1'deki font descriptor'ın Ascent, Descent ve FontBBox girdileri. Dosyada iki glyph'in aynı satırı paylaştığını söyleyen hiçbir şey yoktur; bu, geometriden çıkarılmak zorundadır ve seçim vurgusunu doğru göstermesi için kullanılan sıkı glyph kutuları, PDFium char kutularıyla metin satırı seçimi yazısında anlatıldığı gibi, merkez mesafesi karşılaştırması için yanlış girdidir
3.117.0 sürümündeki düzeltme soruyu merkezler ne kadar uzak sorusundan kutular dikey olarak ne kadar örtüşüyor sorusuna çeviriyor. Hücre metni önce hücrenin kelimelerini görsel satırlara gruplayarak kuruluyor; bir kelime, satırın yürüyen sınırlarıyla dikey örtüşmesi iki yükseklikten küçüğünün en az yüzde 25'i olduğunda o satıra katılıyor. Ardından her satır sol kenara göre insertion sort ile sıralanıyor ve satırlar bir satır sonuyla birleştiriliyor. "Purpose" ile "of" tüm x-yüksekliği boyunca örtüşür; bu, kısa kutunun yüzde 25'inden çok daha fazladır, böylece aynı satıra düşerler ve beklendiği gibi X'e göre sıralanırlar
Metin satırlarını merkez mesafesine göre değil örtüşmeye göre gruplayın
Bu hatadan çıkarılacak kural geneldir: aynı satır kararını dikey merkezleri sabit bir toleransla karşılaştırarak veren her PDF metin düzeni kodu gerçek fontlarda başarısız olur ve bu başarısızlık sessizdir: hiçbir şey hata vermez, kelimeler yalnızca yanlış sırayla çıkar. Karışık alt uzantılar bunun en hafif tetikleyicisidir. 10 puntoluk değerlerin yanındaki kalın 12 puntoluk bir etiket, bir üst simge dipnot işareti, bir yedek fonttan çizilen bir para birimi simgesi ve kelime başına yükseklik gürültüsü taşıyan OCR kelime kutuları, merkezleri 12 punto satır aralığındaki 10 puntoluk komşu satırları hâlâ ayıran her toleranstan fazla kaydırır. Örtüşme oranı boyuttan bağımsızdır: aynı baseline üzerindeki iki kutu, ascent ve descent değerleri ne yaparsa yapsın paylaştıkları x-yüksekliği boyunca örtüşür; komşu satırlardaki iki kutu ise hiç örtüşmez
Aynı kuralı tablo çıkarma dışında uygulamak kolaydır. TPdf.PageWordBoxes, etkin sayfadaki her kelimeyi sayfa uzayındaki dikdörtgeniyle döndürür, yani bir sayfayı görsel satırlara gruplamak kısa bir döngüdür:
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 yürüyen birleşim
I, J, Found: Integer;
begin
Words := Pdf.PageWordBoxes;
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;
// Her satırı okumadan önce Rect.Left'e göre sıralayın; PageWordBoxes
// kelimeleri içerik akışı sırasında döndürür, bu da görsel sıra garantisi değildir
end;
Mevcut çağıranlar için ne değişiyor ve sınırlar nerede
O snippet'in amacı döngü değil yüklemdir; hızlı bir dökümün ötesine geçen her şey için, blokları, satırları ve bir okuma sırası kaynağını zaten taşıyan yapılandırılmış metin modelinden başlayın — okuma sıralı yapılandırılmış PDF metin çıkarma yazısında ele alındığı gibi. Mevcut tablo çağıranları üç düzeltmeyi de seçeneklerine dokunmadan alıyor. Koridor eşiği MinColumnGap değerinin yarısına sabitlendi, whitespace stratejisi MinRows 1'e ayarlansa bile iki satırlık tabanını koruyor (çizgili strateji artık 1'i kabul ediyor) ve çizgili önce kelime filtrelemesi iki strateji de etkinken koşulsuz çalışıyor. Sürüm için kullanılan 13 belgelik örnek kümesinde whitespace geçişi daha önce 9 çizgili tablonun yanında 34 fragman ve yanlış pozitif döndürüyordu; sürümden sonra hiç döndürmüyor ve çizgili tablo sayısı 41'e çıktı, gerçi bu artışın çoğu aynı sürümün çizgili dedektöre dolu dikdörtgen olarak çizilmiş kenarlıkları okumayı öğretmesinden geliyor ki o ayrı bir hikâye
Dürüst sınırlar şunlar: koridor testinin herhangi bir şeyi reddedebilmesi için bir sınırın iki yanında da içerik bulunan en az bir satıra ihtiyacı var, yani iki gerilmiş boşluğu şans eseri birbirinin 6 punto içine düşen iki satırlık bir aday yine geçer. Bu, önceki neredeyse kesinlik durumuna kıyasla dar bir rastlantıdır; ama gerçek iki satırlı tablosu olmayan düzyazı ağırlıklı belgeler MinRows değerini 3 yaparak bunu kapatabilir. Sola hizalı düzensiz metin zaten hiç sorun olmadı ve etkilenmiyor. PDF'in hâlâ bir tablo nesnesi yok; ISO 32000-1 §14.8.4.3 bir Table yapı öğesi tanımlar ama onu yalnızca Tagged PDF taşır, yani diğer her şey için ızgara geometriden yapılan bir çıkarımdır ve her TPdfTable üzerindeki güven değerinin bulunmasının nedeni çıkarımın bir puanı hak etmesidir
Tablo çıkarma, yapılandırılmış metin ve kelime kutuları Delphi, C++Builder ve Lazarus'ta aynı sayfa modelinden okur; TPdfTableExtractionOptions ve onunla birlikte gelen TableExtractionLab demosu dahil tam API, PDFium Component for Delphi sayfasında anlatılıyor