Artikel Teknis

Border Tabel Filled Rectangle sebagai Ruling di PDFium

Ekstraksi tabel PDFium Component, sejak versi 3.117.0, memperlakukan filled rectangle tipis sebagai ruling tabel. Dengan DetectFilledRulings aktif, yang merupakan default-nya, kotak sejajar sumbu yang terisi dan tidak lebih tebal dari MaxRulingThickness (3 poin) menjadi satu ruling di sepanjang sumbu panjangnya, kotak terisi yang lebih besar menyumbang keempat tepinya, dan setiap koordinat ruling di-snap dalam RulingSnapTolerance (4 poin) sebelum grid-nya dirakit. Tabel yang diekspor dari Word, Google Docs, dan browser karena itu mencapai detektor ruled sebagai grid yang lengkap alih-alih jatuh ke deteksi whitespace sebagai fragmen

Artikel sebelumnya tentang deteksi dan ekstraksi tabel menyatakan bahwa deteksi ruled memakai garis yang digambar dan bahwa setiap segmen path yang di-stroke ditransformasi ke koordinat halaman. Kalimat itu benar dan tidak lengkap. Menghitung objek path di tiga belas dokumen sampel dunia nyata menunjukkan sembilan di antaranya sama sekali tidak memuat path yang di-stroke, tapi setiap halamannya membawa ratusan filled rectangle setebal 0,5 sampai 1 poin. Detektor yang hanya melihat stroke tidak melihat apa pun, setiap halaman jatuh ke deteksi whitespace, dan output-nya adalah serakan fragmen kecil alih-alih tabel. Preset compact-columns yang ditambahkan di 3.116.4 melunakkan itu di level fragmen; akar masalahnya adalah detektornya membaca operator painting yang salah

Kenapa tabel hasil ekspor Word tidak punya garis yang di-stroke?

Word processor tidak memikirkan border sebagai garis; ia memikirkannya sebagai kotak dengan lebar, dan kotak itu dicat dengan fill. ISO 32000-1 §8.5.2.1 mendefinisikan operator re sebagai penambahan subpath persegi, dan §8.5.3 memisahkan operator painting-nya: S men-stroke path dengan lebar garis saat itu, f mengisi interiornya. Border sel setebal 0,5 poin keluar sebagai x y w 0.5 re f, dan mesin stroke-nya, termasuk line width, join, dan dash pattern, tidak pernah berjalan. Cell shading adalah konstruksi yang sama dengan kotak yang lebih besar. Grid berstroke yang digambar dengan m, l, dan S adalah yang diharapkan detektor aslinya, dan itulah yang hampir tidak pernah dihasilkan aplikasi office mana pun:

% satu border sel dari ekspor word processor: kotak terisi setinggi 0,5 pt
72 700 468 0.5 re f
% cell shading: kotak terisi seukuran selnya
72 676 117 24 re f
% garis grid berstroke yang jadi sasaran detektor aslinya
72 700 m 540 700 l S

Bagi detektor yang menanyakan FPDFPath_GetDrawMode hanya apakah flag stroke-nya diset, kedua kotak terisi itu tidak terlihat. Kata-kata di dalam selnya lalu mencapai deteksi whitespace, tempat kolom yang dipisahkan gutter 6 poin berada di bawah MinColumnGap default 12 poin, dan yang kembali adalah subset baris mana pun yang kebetulan cukup sejajar untuk lolos MinRows. Itulah perilaku fragmennya, dan sebanyak apa pun parameter-nya disetel ulang tidak akan mengubahnya menjadi grid yang digambar penulisnya

Bagaimana PDFium Component mengubah kotak terisi menjadi ruling?

TableCollectObjectRulings memeriksa setiap objek path satu subpath setiap kali. Draw mode-nya berasal dari FPDFPath_GetDrawMode; sebuah path dihitung sebagai terisi ketika DetectFilledRulings aktif dan fill mode-nya bukan none. Setiap titik ditransformasi lewat matriks objeknya lalu dikumpulkan, sampai MaxSubpathPoints (8) per subpath, dan segmen kurva mana pun menandai subpath-nya sebagai melengkung. Ketika subpath-nya menutup atau ada MoveTo baru, FlushSubpath memutuskan apa subpath itu: subpath melengkung dibuang, begitu juga poligon tertutup yang titik-titiknya tidak semuanya berada dalam PointTolerance (0,05 poin) dari tepi bounding box-nya pada setidaknya satu sumbu. Segitiga, chevron, atau tab membulat tidak pernah menjadi ruling, dan itulah yang menjaga ornamen dekoratif tetap di luar grid

Diagram PDFium Component tentang bagaimana TableCollectObjectRulings mengubah subpath tertutup menjadi ruling tabel di Delphi: FlushSubpath membuang outline melengkung dan poligon yang keluar dari tepi bounding box, MaxRulingThickness memecah kotak tipis menjadi satu ruling per sumbu panjangnya, sel yang di-shading memberi empat ruling tepi dan DetectFilledRulings menjaga kotak kecil agar tidak ikut
Subpath tertutup hanya bertahan kalau sejajar sumbu, dan bounding box-nya lalu memutuskan apakah ia satu ruling, empat tepi sel yang di-shading, atau tidak sama sekali

Yang bertahan adalah persegi sejajar sumbu, yang diklasifikasikan lewat bounding box-nya. Lebar pada atau di bawah MaxRulingThickness dengan tinggi di atasnya menghasilkan satu ruling vertikal di titik tengah horizontalnya, membentang kotaknya dari bawah ke atas; kasus cerminnya menghasilkan satu ruling horizontal. Kedua dimensi di atas ambangnya berarti sel yang di-shading, dan kotaknya menyumbang empat ruling, satu per tepinya. Kedua dimensi pada atau di bawah ambangnya tidak menyumbang apa pun, jadi bullet persegi 2 poin tidak disalahartikan sebagai garis. Path yang di-stroke menempuh jalur lama lewat AddLine, satu ruling per segmen sejajar sumbu, jadi grid yang digambar dengan S ditangani persis seperti sebelumnya, dan path yang dicat dengan fill sekaligus stroke menghasilkan potongan-potongan yang saling tumpang tindih yang diruntuhkan oleh pass merge:

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;                     // berbasis 1

    Options := TPdfTableExtractionOptions.Default;
    // ini default 3.117.0, ditulis lengkap agar jelas
    Options.DetectFilledRulings := True;     // kotak terisi yang tipis menjadi ruling
    Options.MaxRulingThickness := 3.0;       // poin; kotak yang lebih tebal dihitung sebagai shading
    Options.RulingSnapTolerance := 4.0;      // poin; 0 menonaktifkan snapping
    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;

Apa fungsi RulingSnapTolerance untuk tabel berbasis sel shading?

RulingSnapTolerance inilah yang membuat tabel yang dibangun hanya dari shading tersambung menjadi satu grid. Sebagian ekspor tidak menggambar border sama sekali: setiap selnya kotak terisi dengan warnanya sendiri, dan kotak-kotak tetangganya dipisahkan gutter putih 1 sampai 3 poin. Setiap kotak menghasilkan empat ruling tepi, tapi tepi kanan satu sel dan tepi kiri sel berikutnya berjarak 2 poin, dan uji konektivitasnya memakai RulingTolerance, yang default-nya 1 poin. Tanpa snapping, setiap sel membentuk komponen terhubungnya sendiri berisi empat ruling, tidak ada komponen yang mencapai MinRows, dan halamannya tidak melaporkan apa pun. TableSnapRulings mengumpulkan setiap koordinat X yang terlibat (posisi setiap ruling vertikal plus awal dan akhir setiap ruling horizontal) dan setiap koordinat Y dengan cara yang sama, mengurutkan tiap daftarnya, mengelompokkannya dengan merantai nilai yang tetangganya berbeda tidak lebih dari toleransinya, mengganti setiap cluster dengan rata-ratanya, lalu memindahkan setiap posisi, awal dan akhir, ke pusat cluster terdekat. Kedua sisi gutter menjadi garis yang sama, dan konektivitasnya terpenuhi

Diagram PDFium Component tentang RulingSnapTolerance yang menyambungkan tabel sel shading di Delphi: sel tetangga meninggalkan gutter 2 pt, ruling tepinya berada di luar RulingTolerance 1 pt, dan TableSnapRulings merantai kedua nilai X itu menjadi satu rata-rata cluster sehingga uji konektivitasnya akhirnya melihat garis grid yang dipakai bersama
Snapping berjalan sebelum merging dan sebelum detektor ruled, sehingga kedua sisi gutter putih menjadi satu garis dan setiap sel berhenti menjadi pulau berisi empat ruling

Snapping berjalan sebelum TableMergeRulings, yang mengurutkan ruling-nya dan menyambung potongan kolinear yang bersentuhan atau bertumpang tindih dalam RulingTolerance, dan keduanya berjalan sebelum TableDetectRuled melihat datanya sama sekali, sehingga uji konektivitas berpasangannya sebanding dengan jumlah garis grid alih-alih jumlah fragmen per sel. Pada grid berstroke, pass-nya tidak berbahaya, karena koordinat yang sudah identik akan ter-snap ke dirinya sendiri. Satu hal yang perlu diingat adalah clustering berantai tidak punya batas lebarnya sendiri: deretan koordinat yang masing-masing berjarak 3 poin akan runtuh menjadi satu pusat. Pada default 4 poin itu hanya memengaruhi kolom yang lebih sempit dari satu karakter, tapi kalau ada dokumen dengan gutter 3 poin sungguhan yang harus tetap terpisah, turunkan toleransinya atau set ke 0 untuk mematikan snapping:

// Isolasi strategi ruled dan bandingkan apa yang dilihat tiap setelan di satu halaman
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;

// Ekspor Word biasanya melaporkan 0, N lalu kurang dari N:
// mode stroke-only tidak melihat apa pun, snapping menyambungkan sel shading-nya,
// dan mematikan snap meninggalkan setiap sel shading sebagai pulaunya sendiri
Writeln(CountRuledTables(Pdf, False, 4.0));
Writeln(CountRuledTables(Pdf, True, 4.0));
Writeln(CountRuledTables(Pdf, True, 0.0));

Ruling di dalam form XObject

Alat page layout sering membungkus sebuah tabel, atau seluruh body halamannya, dalam form XObject lalu mencatnya dengan Do. ISO 32000-1 §8.10.1 menetapkan bahwa matriks form-nya digabung dengan current transformation matrix saat form-nya dicat, sehingga persegi di dalam form-nya hidup di ruang form dan baru mendarat di halaman setelah dua transformasi atau lebih. TableCollectObjectRulings masuk secara rekursif ke objek form ketika IncludeFormXObjects diset: ia membaca matriks objeknya, menggabungkannya dengan matriks induknya lewat TableMultiplyMatrix, yang urutan argumennya berarti "petakan lewat matriks pertama, lalu matriks kedua", lalu mengenumerasi anaknya dengan FPDFFormObj_CountObjects dan FPDFFormObj_GetObject, sambil menurunkan matriks gabungannya. Penyarangan lebih dalam dari MaxFormDepth (8) dilewati secara senyap, yang merupakan penjaga terhadap file patologis alih-alih batas yang didekati ekspor sungguhan mana pun. Alasan urutan perkaliannya penting sama dengan yang dibahas di prepend versus append matriks: menukar operandnya memindahkan suku translasinya, dan ruling yang seharusnya mendarat di puncak halaman malah mendarat di titik asal

Diagram PDFium Component tentang ruling di dalam form XObject di Delphi: persegi tipis yang ditulis sebagai 72 700 468 0.5 re f hidup di ruang form dan baru mendarat di halaman setelah TableMultiplyMatrix menggabungkan CTM induknya dengan matriks form-nya, secara rekursif lewat FPDFFormObj_CountObjects sampai MaxFormDepth
Perseginya ditulis di ruang form dan baru mencapai puncak halaman setelah matriksnya dikalikan dalam urutan yang menjaga suku translasinya tetap di tempatnya

Kenapa anggaran ruling-nya naik empat kali?

MaxRulingSegments default naik dari 4096 ke 16384 di 3.117.0 karena border per sel datang dalam jumlah yang jauh lebih besar daripada garis grid berstroke. Tabel berstroke 30 baris 6 kolom adalah 38 segmen garis. Tabel yang sama bila diekspor sebagai kotak terisi menjadi sampai empat border per sel, 720 potongan sebelum digabungkan, dan formulir dengan sel shading menggandakannya. Dua tabel seperti itu dalam satu halaman akan menghabiskan anggaran yang lama. Anggarannya ditegakkan di TableAppendRuling lewat Check, yang memunculkan EPdfError dengan pesan "Table ruling-segment budget exceeded"; tidak ada hasil yang menurun kualitasnya, tidak ada grid separuh, dan pass whitespace-nya juga tidak berjalan. Kalau Anda menyetel anggaran yang lebih ketat untuk input yang tidak tepercaya, tangkap exception-nya lalu putuskan, jangan membaca hasil kosong sebagai "tidak ada tabel":

Options := TPdfTableExtractionOptions.Default;
Options.MaxRulingSegments := 2048;        // sengaja ketat untuk input yang tidak tepercaya
try
  Tables := Pdf.ExtractTables(Options);
except
  on E: EPdfError do
  begin
    Log(E.Message);                       // 'Table ruling-segment budget exceeded'
    Options.MaxRulingSegments := 16384;   // default 3.117.0
    Tables := Pdf.ExtractTables(Options);
  end;
end;

Hasil terukur dan di mana pendekatannya berhenti

Pada tiga belas dokumen sampel yang sama, ekstraksinya bergerak dari 43 tabel, 9 di antaranya ruled dan 34 fragmen whitespace atau positif palsu, menjadi 41 tabel ruled tanpa positif palsu whitespace. Sebagian pembersihan itu berasal dari dua perubahan pendamping di 3.117.0: kata-kata yang sudah diklaim grid ruled dibuang sebelum deteksi whitespace berjalan, sehingga sebuah tabel tidak pernah dilaporkan dua kali, dan batas kolom whitespace kini harus berupa koridor bebas teks di seluruh baris yang dipisahkannya, dan itulah yang menghentikan paragraf rata kanan-kiri dinilai sebagai tabel 5x4. Pembaca filled rectangle-lah yang memindahkan tabelnya sendiri dari kolom fragmen ke kolom ruled

Batasnya layak dinyatakan terang-terangan. Halaman tanpa text layer tetap menghasilkan kerangka grid-nya, setiap selnya kosong, karena ruling-nya berasal dari geometri sementara teksnya dari text page; halaman hasil scan butuh OCR dulu. Bentuk terisi dengan kurva, sudut membulat, atau outline tak persegi dibuang seluruhnya, jadi tabel yang bordernya digambar sebagai outline persegi membulat tetap butuh deteksi whitespace seperti sebelumnya. Tabel yang tidak punya border maupun shading tidak berubah oleh semua ini dan tetap menjadi wilayah strategi whitespace yang dijelaskan di artikel ekstraksi tabel; ketika itu pun tidak cukup, word box dan block dari structured text dan reading order adalah bahan mentah untuk reader khusus domain. Demo TableExtractionLab yang disertakan bersama komponen ini memaparkan DetectFilledRulings di panel opsinya, dan itu cara tercepat untuk melihat seperti apa suatu ekspor dengan dan tanpa opsi itu; API lengkapnya dijelaskan di halaman PDFium Component for Delphi