Teknik Makale

Delphi'de PDFium Component ile İçeriğe Duyarlı Tablo Devamı

PDFium Component sürüm 3.117.0, sayfa sınırında bölünen bir tabloyu, ya iki fragman da sayfa kenarlarına değiyorsa ya da ilk fragmanın altında ve ikincinin üstünde gövde metni yoksa birbirine bağlıyor; koşan üstbilgi ve altbilgiler yok sayılıyor. ExtractDocumentTables bu içerik duyarlı testi eski sayfa kenar boşluğu testine alternatif olarak uyguluyor, ilk satırı tüm genişliğe yayılan tek bir başlık hücresi olan sonraki sayfa fragmanını reddediyor ve sonraki sayfaya taşan tek bir satırı devam zincirinin parçası olarak koruyor

Tablo algılama ve çıkarma makalesi, devamı dört katı kapı olarak sunmuş ve sayfa kenarına değiyor olmayı da bunlardan biri saymıştı. O anlatım, kapsadığı sürüm için doğruydu ve aynı zamanda insanların bileşene gerçekten verdiği tabloların çoğu için yanlıştı. Bu makale düzeltmenin kendisi: kenar boşluğu testinin hangi belgelerde işe yaramadığı, yerine ne geldiği ve düzeltmenin beraberinde sürüklediği iki yan durum

Sayfa kenar boşluğu testi Word çıktılarında neden başarısız oluyor?

Çünkü bir kelime işlemci satırları kâğıt kenarında değil, alt kenar boşluğunda düzenlemeyi bırakır. Varsayılan ContinuationMargin değeri 36 punto iken eski kural, önceki fragmanın alt kenarının sayfa altına 36 punto içinde, sonraki fragmanın üst kenarının da sayfa üstüne 36 punto içinde olmasını şart koşuyordu. Word'den varsayılan bir inçlik kenar boşluklarıyla dışa aktarılan bir belge, son satırını sayfa altından en az 72 punto yukarıya koyar, bir altbilgi varsa daha da yukarıya; yani koşul hiçbir zaman sağlanmadı. Böyle bir belgedeki her uzun tablo, ContinuationGroup sıfırda kalan bağımsız fragmanlar olarak geri döndü ve çağıran yine elle birleştirmeye kaldı. Test, tasarlandığı şey için hâlâ anlamlı: sabit bir içerik kutusunu doldurup sonraki sayfaya tepeden başlayan layout motorlarının ürettiği raporlar. Kötü bir kural değil, eksik bir kural; bu yüzden 3.117.0 onu koruyup yerine koymak yerine ikinci bir yol ekledi

İçerik duyarlı test bunun yerine neyi kontrol ediyor?

İçerik duyarlı test, iki fragman arasındaki alanı tablo dışında bir şeyin kaplayıp kaplamadığını, sayfa geometrisi yerine her sayfanın kelime kutularını kullanarak kontrol eder. ExtractDocumentTables belgeyi dolaşırken sayfa başına şunları kaydeder: üstü altbilgi bandının üzerinde kalan herhangi bir kelimenin en düşük alt kenarı ve altı üstbilgi bandının altında kalan herhangi bir kelimenin en yüksek üst kenarı. İki bant da ContinuationMargin punto derinliğindedir, yani aynı seçenek artık çift görev yapıyor: hem sayfa kenarı payı hem de koşan üstbilgi ve altbilgi bölgelerinin yüksekliği. Bir fragman çifti, öncekinin alt kenarı sayfasındaki en düşük gövde metnine eşit ya da ondan aşağıdaysa ve sonrakinin üst kenarı sonraki sayfadaki en yüksek gövde metnine eşit ya da ondan yukarıdaysa geçer; her ikisi de AlignmentTolerance içinde. Sade söyleyişle: tablo N. sayfada en son şeydi ve N+1. sayfada ilk şeydir, kenar boşluğu bandındaki bir sayfa numarası ya da belge başlığı ise sayılmaz. Bu dışlama keyfi değil. ISO 32000-1 §14.8.2.2, koşan üstbilgi ve altbilgileri sayfalama artifact'ı olarak sınıflandırır: sayfa sonu yüzünden var olan, ona rağmen değil. Etiketli bir okuyucunun onları atlamasını sağlayan fikir, bir tablonun onların ötesine devam etmesini sağlayan fikrin aynısıdır. Marked content makalesi, etiketli dosyaların bu artifact'ları açıkça nasıl bildirdiğini ele alıyor; burada sınıflandırma konumdan çıkarılıyor, çünkü dışa aktarılan tabloların çoğu hiç etiket taşımıyor

PDFium Component tablo devamının neden iki teste ihtiyaç duyduğu: Word bir inçlik kenar boşluklarıyla kenar boşluğu testi, fragment kenarlarını layout'un asla ulaşmadığı 36 pt pencerelerin içinde ister; içerik duyarlı test ise kelime kutularını karşılaştırır ve tablo N. sayfada son gövde içeriği, N+1. sayfada ilk içerik olduğunda bağlar, koşan üstbilgi ve altbilgi bantlarını yok sayar
İki testten biri kapıyı açar ve ancak ondan sonra kalan kontroller çalışır: bitişik sayfalar, sonraki fragmanda tüm genişliğe yayılan başlık satırı olmaması ve sütun sınırlarının AlignmentToleranceın iki katı içinde eşleşmesi

İki test OR ile birleşiyor. Tabloları kâğıt kenarına kadar uzanan bir layout motoru raporu birinciyi geçer; tabloları kenar boşluğunda duran bir Word çıktısı ikinciyi geçer; ikisini de yapan bir belge iki kez geçer. Kalan kapılar ancak bunlardan biri başarılı olduktan sonra çalışır ve sabit bir sırayla çalışır: sayfa numaraları bitişik olmalı, sonraki fragman bir başlık satırıyla açılmamalı ve sütun sınırları AlignmentTolerance değerinin iki katı içinde eşleşmeli; varsayılanlarda bu 6 puntodur. Numaralandırma TPdfTableContinuation tipindedir ve değerleri ptcNone, ptcStart, ptcMiddle ile ptcEnddir. ptcEnd olarak işaretlenip sonra bir sayfaya daha bağlanan fragman ptcMiddlea terfi eder, böylece üç sayfalık bir tablo sayfa sırasında start, middle, end olarak okunur. Grup numaraları 1'den başlar ve 0 bağlantısız anlamına gelir; ToJson aynı bilgiyi continuation ve continuationGroup üyeleri olarak verir ve birleştirmeyi aşağı akıştaki bir servis yapıyorsa tercih edilecek biçim budur

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'itinerary-from-word.pdf';
    Pdf.LoadDocument;

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectContinuations := True;     // varsayılan; netlik için gösterildi
    Options.ContinuationMargin := 54;        // iki satırlık altbilgi, yaklaşık 50 pt derinliğinde

    Tables := Pdf.ExtractDocumentTables(Options);
    for I := 0 to High(Tables) do
      case Tables[I].Continuation of
        ptcStart:
          Writeln(Format('group %d starts on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
        ptcMiddle, ptcEnd:
          Writeln(Format('group %d continues on page %d (%d rows)',
            [Tables[I].ContinuationGroup, Tables[I].PageNumber,
             Tables[I].RowCount]));
      else
        Writeln(Format('standalone table on page %d (%d rows)',
          [Tables[I].PageNumber, Tables[I].RowCount]));
      end;
  finally
    Pdf.Free;
  end;
end;

Bir başlık satırı iki tablonun kaynaşmasını nasıl engelliyor?

İlk satırı her sütuna yayılan tek bir hücre olan sonraki sayfa fragmanı, öncekinin devamı değil yeni bir tablo sayılır. Bu kural, içerik duyarlı testin tek başına fazla hevesli bağlaması yüzünden var. Bunu ortaya çıkaran durum bir tutanak biçimli formdu: 1. sayfanın altına yakın bir tablo bitiyor, 2. sayfanın üstüne yakın aynı sütun genişliklerine sahip ikinci bir tablo başlıyor, aralarında altbilgiden başka hiçbir şey yok ve sütunlar neredeyse tamı tamına uyuşuyor. Kenar boşluğu testinde ikisi hiç karşılaşmıyordu çünkü hiçbiri bir kenara değmiyordu; içerik testinde ise anında bağlandılar ve bölümleri olan bir form tek bir tutarsız ızgaraya dönüştü. İkisini ayıran şey hücre yapısında görünür. İkinci tablo, tüm genişlik boyunca tek bir birleşik hücre olarak yerleştirilmiş "RECIPIENT INFORMATION" gibi bir bölüm başlığıyla açılır ve gerçek bir devam bunu asla yapmaz, çünkü başlık önceki sayfada başlamış olan tabloya aittir. TableStartsWithCaptionRow tam olarak bunu kodlar: fragmanın en az iki sütunu vardır ve içinde RowIndex = 0, ColumnIndex = 0 ile ColumnSpan = ColumnCount koşullarını sağlayan bir hücre bulunur. Kontrol yalnızca sonraki fragman üzerinde çalışır, bu yüzden kendi başlık satırı ilk sayfasında duran bir tablo etkilenmez; başlık N. sayfadadır ve yalnızca N+1. sayfa fragmanı incelenir

PDFium Component içindeki başlık satırı kapısı: gerçek bir devam veri hücreleriyle açılır ve aynı ContinuationGroup'a katılır; sıfırıncı satırı RowIndex 0, ColumnIndex 0 ve ColumnCount değerine eşit ColumnSpan taşıyan tek bir birleşik hücre olan sonraki fragman ise devam olarak reddedilir ve yeni tablo olarak bildirilir
İnceleme yalnızca sonraki fragmana dokunur, bu yüzden kendi başlık satırı ilk sayfasında duran bir tablo etkilenmez; kapı, iki kenar testinden biri çifti zaten bağladıktan sonra çalışır

Ardından gelen sütun karşılaştırması TablesHaveMatchingColumns, aynı sütun sayısı olmaktan daha katıdır. Her fragmanın sınır konumlarını hücre dikdörtgenlerinden yeniden kurar, birleşik hücrelerin gizlediği sınırları interpolasyonla bulur ve herhangi bir sınır toleranstan fazla kaydığında çifti reddeder. Bu yüzden farklı oranlara sahip iki dört sütunlu tablo, geri kalan her şey uyuşsa bile ayrı kalır

Sonraki sayfaya taşan tek bir satıra ne oluyor?

Bir satırını sonraki sayfaya taşıyan çizgili bir ızgara artık algılanıyor ve bağlanıyor, yeter ki bir devam zincirine dahil olsun; tek başınaysa atılıyor. Varsayılan MinRows değeri 2, başıboş bir çizgi çiftinin tablo olarak bildirilmesini engellemek için var; ama sayfa sonunda öteki tarafa itilen son satır, 2'lik katı bir tabanın sessizce düşürdüğü gerçek bir satırdır ve tablonun geri kalanı eksik olduğu hâlde tam görünür. Belge düzeyindeki tarama bunu üç adımda hallediyor. DetectContinuations ile DetectRuledTables birlikte ayarlandığında, sayfa başına geçiş çizgili dedektörü satır tabanı geçici olarak 1'e indirilmiş hâlde çalıştırır — ExtractTablesın çizgili ızgaralar için artık MinRows değeri 1'i kabul etmesinin, whitespace algılamasının ise içeride 2 tabanını korumasının nedeni budur. Devam işaretleri tam sonuç üzerinde konur. Sonra çağıranın MinRows değerinden kısa olan ve hiçbir zincire dahil olmayan her tablo kaldırılır. Tek satırlık fragman yalnızca bağlandığı için hayatta kalır ve aksi hâlde sıradan olan bir sayfanın ortasındaki tek satırlık ızgara eskisi gibi filtrelenir

PDFium Component sayfa sonundan taşan çizgili bir satırı nasıl koruyor: DetectContinuations ve DetectRuledTables ayarlıyken sayfa başına çizgili geçiş satır tabanı birde çalışır, devam işaretleri tam sonuç üzerinde konur ve yalnızca MinRows değerinden kısa olup hiçbir zincirin dışında kalan fragmanlar kaldırılır
Taşan satır, zinciri onu bağladığı için hayatta kalır; sıradan bir sayfadaki bağımsız tek satırlık ızgara ise eskisi gibi filtrelenir ve whitespace ile algılanan tablolar böyle bir rahatlama olmadan iki satırlık tabanını korur
// Her zinciri tek bir CSV olarak yeniden kur, yinelenen başlık satırlarını
// devam fragmanlarında düşürerek
procedure ExportChains(const Tables: TPdfTables; const Folder: string);
var
  I, R: Integer;
  Lines: TStringList;
  Csv: TStringList;
begin
  Csv := TStringList.Create;
  Lines := TStringList.Create;
  try
    for I := 0 to High(Tables) do
    begin
      if Tables[I].Continuation in [ptcNone, ptcStart] then
        Csv.Clear;
      Lines.Text := string(Tables[I].ToCsv);
      if (Tables[I].Continuation in [ptcMiddle, ptcEnd]) and
         (Lines.Count > 1) and (Tables[I].RowCount > 1) then
        Lines.Delete(0);            // kelime işlemcinin yinelediği başlık
      for R := 0 to Lines.Count - 1 do
        Csv.Add(Lines[R]);
      if Tables[I].Continuation in [ptcNone, ptcEnd] then
        Csv.SaveToFile(Format('%s\page%d-group%d.csv',
          [Folder, Tables[I].PageNumber, Tables[I].ContinuationGroup]));
    end;
  finally
    Lines.Free;
    Csv.Free;
  end;
end;

Bu rutinde iki ayrıntı bilinçli. Tek satırlık taşma asla kırpılmaz, çünkü RowCount üzerindeki koruma onu tutar; her sayfada başlık satırını yineleyen bir kelime işlemci ise ilk satırı yeniden başlık olan bir fragman üretir, yani middle ve end fragmanlarında sıfırıncı satırı düşürmek o durum için doğru, başlıkları yinelemeyen bir üretici için yanlıştır. Rutini bir klasöre salmadan önce tek bir belgeyi kontrol edin

Kuralların hâlâ durduğu yer

İçerik duyarlı test ancak okuduğu metin katmanı kadar iyidir. Hiç metni olmayan taranmış bir sayfada kaydedilen gövde metni uçları sayfa sınırlarına düşer, arada hiçbir şey yok koşulu boş yere sağlanır ve geriye yalnızca başlık satırı ile sütun kapıları kalır; böyle bir sayfadaki çizgili bir ızgara yine boş bir iskelet olarak bulunur, yani zincir doğru bağlanabilir ama çevredeki metne dair hiçbir şey gerçekten doğrulanmamıştır. Bu önemliyse önce bir metin katmanı ekleyin. Metin yerine resim olarak render edilen altbilgiler bant mantığına görünmez ve aynı nedenle zararsızdır

Bantlar tek bir sayıdır. ContinuationMargin değerinden derin bir altbilgi, alt satırlarını gövde bölgesinin içinde bırakır; bu da önceki fragmanı metinle takip ediliyor gibi gösterip bağlantıyı engeller — ilk örnekte yapıldığı gibi seçeneği gerçek bant derinliğine çıkarın. Fazla yükseltirseniz sayfanın altına yakın kısa bir kapanış paragrafı banda kayar ve yok sayılır; bu da tabloyu kendisini izleyen şeye bağlar. Başlık kuralının ayna görüntüsü bir arızası var: her devam fragmanının ilk satırı olarak birleşik bir "continued" banner'ı yazan bir üreticinin bu fragmanları yeni tablo olarak reddedilir ve bugün tek çare, hiçbir şeyi gevşetmeden ContinuationGroup üzerinden kendiniz birleştirmektir, çünkü kuralın bir anahtarı yok

Whitespace ile algılanan tablolar tek satırlık rahatlamanın hiçbirini almıyor. Whitespace stratejisi bir tabloyu görebilmek için iki hizalı satıra ihtiyaç duyar, bu yüzden bir satır taşan çizgisiz bir tablo yine o satır kadar eksik bildirilir. Bununla karşılaştığınızda yapılandırılmış metin blokları ve okuma sırası arkasındaki kelime kutuları, o satırı kurtarmak için ham konumları verir. Bu çalışmayı tetikleyen örnek kümesinde — on üç kelime işlemci ve tarayıcı çıktısı — gerçekten çok sayfalı tablolar içeren beş belge de tek zincir hâlinde bağlandı ve daha önce kaynaşan tutanak formu ayrı kaldı; bu, sürümün ölçüldüğü çıtadır, her düzen hakkında bir vaat değil

Devam işaretleme, başlık kuralı ve tek satır geçişi, Delphi, C++Builder ve Lazarus derlemelerinin paylaştığı belge düzeyi yolda yaşıyor; tam tablo çıkarma API'si PDFium Component for Delphi sayfasında anlatılıyor