Artikel Teknis

Sambungan Tabel Content-Aware Antar Halaman PDF di Delphi

PDFium Component versi 3.117.0 menyambungkan tabel yang terpotong batas halaman ketika kedua fragmennya menyentuh tepi halaman, atau ketika tidak ada teks body di bawah fragmen pertama dan di atas fragmen kedua, dengan running header dan footer diabaikan. ExtractDocumentTables menerapkan uji content-aware itu sebagai alternatif uji page-margin yang lebih lama, menolak fragmen halaman berikutnya yang baris pertamanya satu sel caption selebar penuh, dan mempertahankan satu baris yang meluber ke halaman berikutnya sebagai bagian dari rantai sambungannya

Artikel deteksi dan ekstraksi tabel menyajikan continuation sebagai empat gate ketat dan memperlakukan "menyentuh tepi halaman" sebagai salah satunya. Deskripsi itu akurat untuk rilis yang dibahasnya, dan juga keliru untuk sebagian besar tabel yang benar-benar disuapkan orang ke komponen ini. Artikel ini adalah koreksinya: dokumen seperti apa yang tidak bisa ditangani uji margin, apa yang menggantikannya, dan dua kasus sampingan yang ikut terbawa oleh perbaikannya

Kenapa uji page-margin gagal pada ekspor Word?

Uji page-margin gagal karena word processor berhenti menata barisnya di batas bawah margin, bukan di tepi kertas. Dengan ContinuationMargin default 36 poin, aturan aslinya menuntut tepi bawah fragmen sebelumnya berada dalam 36 poin dari dasar halaman dan tepi atas fragmen berikutnya dalam 36 poin dari puncak halaman. Dokumen yang diekspor dari Word dengan margin satu inci default-nya menaruh baris terakhir setidaknya 72 poin di atas dasar halaman, lebih jauh lagi kalau ada footer, jadi syarat itu tidak pernah terpenuhi. Setiap tabel panjang di dokumen seperti itu kembali sebagai fragmen-fragmen mandiri dengan ContinuationGroup nol, dan pemanggilnya kembali menjahit dengan tangan. Uji itu tetap masuk akal untuk apa yang jadi rancangan awalnya: laporan yang dihasilkan layout engine yang mengisi halaman sampai kotak konten tetap lalu memulai halaman berikutnya rapat di puncak. Ia bukan aturan yang buruk, ia aturan yang tidak lengkap, itulah sebabnya versi 3.117.0 mempertahankannya dan menambahkan jalur kedua alih-alih menggantinya

Apa yang diperiksa uji content-aware sebagai gantinya?

Uji content-aware memeriksa apakah ada sesuatu selain tabel yang menempati ruang di antara kedua fragmen, memakai word box setiap halaman alih-alih geometri halamannya. Selama ExtractDocumentTables menelusuri dokumennya ia mencatat, per halaman, tepi bawah terendah dari kata mana pun yang puncaknya berada di atas band footer dan tepi atas tertinggi dari kata mana pun yang dasarnya berada di bawah band header. Kedua band-nya sedalam ContinuationMargin poin, sehingga opsi yang sama kini merangkap dua tugas: sebagai kelonggaran tepi halaman dan sebagai tinggi zona running header serta footer. Sepasang fragmen lolos ketika tepi bawah fragmen sebelumnya berada di atau di bawah teks body terendah di halamannya dan tepi atas fragmen berikutnya berada di atau di atas teks body tertinggi di halaman berikutnya, masing-masing dalam toleransi AlignmentTolerance. Sederhananya: tabel itu adalah hal terakhir di halaman N dan hal pertama di halaman N+1, dan nomor halaman atau judul dokumen di band margin tidak dihitung. Pengecualian itu bukan sembarangan. ISO 32000-1 §14.8.2.2 mengklasifikasikan running header dan footer sebagai artefak paginasi, konten yang ada karena page break, bukan meskipun ada page break, dan gagasan yang sama yang membuat tagged reader bisa melewatinya juga yang membuat tabel bisa berlanjut melewatinya. Artikel marked content membahas bagaimana file bertag mendeklarasikan artefak itu secara eksplisit; di sini klasifikasinya disimpulkan dari posisi, karena sebagian besar tabel hasil ekspor tidak membawa tag sama sekali

Kenapa continuation tabel di PDFium Component butuh dua uji: dengan margin satu inci Word, uji page-margin menuntut tepi fragmen berada di dalam jendela 36 pt yang tidak pernah dicapai tata letaknya, sementara uji content-aware membandingkan word box lalu menyambung ketika tabel adalah konten body terakhir di halaman N dan yang pertama di halaman N+1, mengabaikan band running header dan footer
Salah satu uji saja membuka gate-nya, dan baru setelah itu pemeriksaan sisanya berjalan: halaman yang berdampingan, tidak ada baris caption selebar penuh di fragmen berikutnya, dan batas kolom yang cocok dalam dua kali AlignmentTolerance

Kedua uji itu bergabung dengan OR. Laporan layout engine yang tabelnya mencapai tepi kertas lolos uji pertama; ekspor Word yang tabelnya berhenti di margin lolos uji kedua; dokumen yang memenuhi keduanya lolos dua kali. Baru setelah salah satunya berhasil, gate sisanya berjalan, dan urutannya tetap: nomor halamannya harus berdampingan, fragmen berikutnya tidak boleh dibuka baris caption, dan batas kolomnya harus cocok dalam dua kali AlignmentTolerance, yaitu 6 poin pada nilai default-nya. Enumerasinya adalah TPdfTableContinuation dengan nilai ptcNone, ptcStart, ptcMiddle, dan ptcEnd. Fragmen yang ditandai ptcEnd lalu menyambung lagi ke halaman berikutnya dipromosikan menjadi ptcMiddle, sehingga tabel tiga halaman terbaca start, middle, end dalam urutan halaman. Nomor grup mulai dari 1 dan 0 berarti tidak tersambung, dan ToJson mengeluarkan informasi yang sama sebagai anggota continuation dan continuationGroup, dan bentuk itulah yang sebaiknya dipilih kalau layanan hilir yang menjahitnya

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;     // default; ditampilkan agar jelas
    Options.ContinuationMargin := 54;        // footer dua baris, kedalamannya ~50 pt

    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;

Bagaimana baris caption mencegah dua tabel menyatu?

Fragmen halaman berikutnya yang baris pertamanya satu sel yang membentang seluruh kolom diperlakukan sebagai tabel baru, tidak pernah sebagai kelanjutan tabel sebelumnya. Aturan ini ada karena uji content-aware, kalau berdiri sendiri, menyambung terlalu bersemangat. Kasus yang membeberkannya adalah formulir bergaya transkrip: sebuah tabel berakhir di dekat dasar halaman 1, tabel kedua dengan lebar kolom identik mulai di dekat puncak halaman 2, tidak ada apa pun selain footer di antara keduanya, dan kolomnya cocok sampai titik terakhir. Di bawah uji margin keduanya tidak pernah bertemu karena tidak ada yang menyentuh tepi; di bawah uji konten keduanya langsung tersambung, dan formulir bersection menjadi satu grid yang tidak koheren. Yang memisahkan keduanya terlihat di struktur selnya. Tabel kedua dibuka dengan caption section seperti "RECIPIENT INFORMATION" yang ditata sebagai satu sel gabungan selebar penuh, dan continuation yang sungguhan tidak pernah begitu, karena caption-nya milik tabel yang sudah dimulai di halaman sebelumnya. TableStartsWithCaptionRow mengodekan persis itu: fragmennya punya minimal dua kolom dan memuat sel dengan RowIndex = 0, ColumnIndex = 0, dan ColumnSpan = ColumnCount. Pemeriksaannya hanya berjalan pada fragmen berikutnya, jadi tabel yang baris caption-nya sendiri berada di halaman pertamanya tidak terpengaruh; caption-nya ada di halaman N, dan hanya fragmen halaman N+1 yang diperiksa

Gate baris caption di PDFium Component: continuation yang sungguhan dibuka dengan sel data dan bergabung ke ContinuationGroup yang sama, sementara fragmen berikutnya yang baris nolnya berisi satu sel gabungan dengan RowIndex 0, ColumnIndex 0 dan ColumnSpan sama dengan ColumnCount ditolak sebagai continuation dan dilaporkan sebagai tabel baru
Pemeriksaannya hanya menyentuh fragmen berikutnya, jadi tabel yang baris caption-nya sendiri ada di halaman pertamanya tidak terpengaruh, dan gate-nya berjalan setelah salah satu dari dua uji tepi sudah menyambungkan pasangannya

Perbandingan kolom yang menyusul, TablesHaveMatchingColumns, lebih ketat dari "jumlah kolom yang sama". Ia membangun ulang posisi batas setiap fragmen dari persegi selnya, menginterpolasi batas yang disembunyikan sel gabungan, dan menolak pasangannya ketika ada batas yang bergeser lebih dari toleransinya. Dua tabel empat kolom dengan proporsi berbeda karena itu tetap terpisah walaupun semua hal lain sejajar

Apa yang terjadi pada satu baris yang meluber ke halaman berikutnya?

Grid bergaris yang membawa satu barisnya ke halaman berikutnya kini terdeteksi dan tersambung, asalkan ia berakhir di sebuah rantai continuation; kalau berdiri sendiri ia dibuang. MinRows default 2 ada untuk menjaga sepasang garis nyasar agar tidak dilaporkan sebagai tabel, tapi baris terakhir yang terdorong melewati page break adalah baris sungguhan yang diam-diam dijatuhkan oleh batas minimum 2, dan bagian tabel lainnya tampak lengkap padahal tidak. Pemindaian level dokumen menanganinya dalam tiga langkah. Ketika DetectContinuations dan DetectRuledTables keduanya aktif, pass per halaman menjalankan detektor ruled dengan batas barisnya diturunkan sementara ke 1, itulah sebabnya ExtractTables kini menerima MinRows bernilai 1 untuk grid bergaris sementara deteksi whitespace tetap memakai batas internal 2. Continuation-nya ditandai atas hasil yang lengkap. Lalu setiap tabel yang lebih pendek dari MinRows milik pemanggil dan tidak menjadi bagian rantai mana pun dihapus. Fragmen satu barisnya bertahan hanya karena tersambung, dan grid satu baris di tengah halaman yang biasa-biasa saja disaring persis seperti sebelumnya

Bagaimana PDFium Component mempertahankan baris bergaris yang meluber melewati page break: pass ruled per halaman berjalan dengan batas baris satu ketika DetectContinuations dan DetectRuledTables diset, continuation-nya ditandai atas hasil lengkapnya, dan hanya fragmen yang lebih pendek dari MinRows dan berada di luar setiap rantai yang dihapus
Baris yang meluber bertahan karena rantainya menyambungkannya, sementara grid satu baris yang berdiri sendiri di halaman biasa disaring persis seperti sebelumnya, dan tabel hasil deteksi whitespace tetap memakai batas dua barisnya tanpa keringanan seperti itu
// Bangun ulang setiap rantai sebagai satu CSV, buang baris header yang berulang
// pada fragmen continuation-nya
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);            // header yang diulang oleh word processor
      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;

Dua detail di rutin itu disengaja. Baris tunggal yang meluber tidak pernah dipotong, karena guard pada RowCount menjaganya, dan word processor yang mengulang baris header di setiap halaman menghasilkan fragmen yang baris pertamanya adalah header lagi, jadi membuang baris nol pada fragmen middle dan end tepat untuk kasus itu dan keliru untuk generator yang tidak mengulang header. Periksa satu dokumen dulu sebelum melepas rutin itu ke seluruh folder

Di mana aturannya masih berhenti

Uji content-aware hanya sebaik text layer yang dibacanya. Di halaman hasil scan yang tidak punya teks sama sekali, ekstrem teks body yang tercatat jatuh kembali ke batas halaman, syarat "tidak ada apa pun di antaranya" terpenuhi secara hampa, dan yang tersisa hanya gate caption dan kolom; grid bergaris di halaman seperti itu tetap ditemukan sebagai kerangka kosong, jadi rantainya bisa saja tersambung dengan benar, tapi tidak ada apa pun soal teks di sekitarnya yang benar-benar terverifikasi. Tambahkan text layer dulu kalau itu penting. Footer yang digambar sebagai gambar alih-alih teks tidak terlihat oleh logika band-nya dan tidak berbahaya karena alasan yang sama

Band-nya hanya satu angka. Footer yang lebih dalam dari ContinuationMargin meninggalkan baris bawahnya di dalam zona body, yang membuat fragmen sebelumnya tampak diikuti teks lalu memblokir sambungannya; naikkan opsi itu ke kedalaman band yang sebenarnya, seperti yang dilakukan contoh pertama. Naikkan terlalu jauh dan paragraf penutup pendek di dekat dasar halaman menyelinap ke band-nya dan diabaikan, yang menyambungkan tabel ke apa pun yang menyusulnya. Aturan caption punya kegagalan yang bercermin: generator yang menulis banner gabungan "continued" sebagai baris pertama setiap fragmen continuation akan membuat fragmen-fragmen itu ditolak sebagai tabel baru, dan satu-satunya obat hari ini adalah menjahit sendiri lewat ContinuationGroup setelah tidak melonggarkan apa pun, karena aturannya tidak punya switch

Tabel hasil deteksi whitespace tidak mendapat keringanan satu baris itu. Strategi whitespace butuh dua baris yang sejajar untuk bisa melihat tabel sama sekali, jadi tabel tanpa garis yang meluber satu baris tetap dilaporkan kurang sebanyak baris itu. Kalau Anda menemui itu, word box di balik structured text block dan reading order memberi Anda posisi mentahnya untuk memulihkannya. Pada set sampel yang menggerakkan pekerjaan ini, tiga belas ekspor word processor dan browser, lima dokumen dengan tabel multi-halaman sungguhan semuanya tersambung menjadi rantai tunggal dan formulir transkrip yang sebelumnya menyatu tetap terpisah, dan itulah patokan yang dipakai mengukur rilisnya, bukan janji tentang setiap tata letak

Penandaan continuation, aturan caption, dan pass satu barisnya semuanya ada di jalur level dokumen yang dipakai bersama build Delphi, C++Builder, dan Lazarus; API ekstraksi tabel lengkapnya dijelaskan di halaman PDFium Component for Delphi