Artikel Teknis

Ekstraksi Tabel PDF Bertipe di Delphi Lintas Page Break

HotPDF memulihkan tabel dari PDF yang sudah ada melalui ExtractLoadedTypedTables, API Delphi yang menggabungkan fragmen row hasil layout pass, membangun satu canonical column grid untuk setiap tabel, melanjutkan tabel melewati page break ketika geometri mendukungnya, dan mengembalikan setiap cell sebagai typed value yang membawa page provenance, column span, serta bounds. ExportLoadedTypedTables menulis hasil yang sama langsung ke CSV atau JSON. Skenario yang membuat fitur ini layak dibangun terasa membosankan dan sangat umum. Register invoice sepanjang empat puluh halaman, secara logis satu tabel, dicetak dengan header berulang di bagian atas setiap halaman. Jalankan naive reading-order pass, dan Anda mendapatkan empat puluh tabel, tiga puluh sembilan header row palsu, serta kolom currency yang bergeser satu posisi ke kiri pada setiap row ketika cell tengah kebetulan kosong. Membersihkannya di downstream, di dalam calling application, adalah tempat proyek document-import biasanya mati

Mengapa halaman PDF memberi fragmen, bukan tabel?

Karena halaman PDF tidak membawa table semantic sama sekali kecuali dokumen diberi tag. Content stream hanya memuat text-showing operator dan positioning matrix (ISO 32000-1 §9.4.3) dan tidak memuat apa pun lagi; ruled box yang Anda lihat di layar adalah path painting yang tidak berkaitan dengannya, dan extractor tidak diwajibkan menghubungkan keduanya. Structure element type Table, TR, TH, dan TD hanya hidup dalam logical structure hierarchy tagged PDF (ISO 32000-1 §14.8.4), sedangkan sebagian besar business document yang beredar tidak diberi tag. Semua yang dijelaskan di bawah ini adalah geometric recovery, bukan parsing, dan ini perlu dikatakan terang-terangan sebelum seseorang membangun reconciliation report di atasnya

Karena itu HotPDF lebih dulu menjalankan semantic layout analysis atas glyph hasil ekstraksi, pass yang sama yang menjadi dasar structure-order text extraction dari PDF loaded serta structured HTML dan XML export. Pass tersebut mengelompokkan baseline ke dalam run yang cell-nya sejajar secara vertikal, dan hanya melanjutkan run selama row berturut-turut memiliki jumlah cell yang sama. Untuk layout engine, aturan itu benar dan murah. Untuk caller, bentuknya keliru: satu row dengan interior cell kosong memecah satu visual table menjadi dua source table. Typed table layer berada di atas pass itu tepat untuk menyatukan kembali potongan-potongan tersebut

Canonical column grid dan knob ColumnTolerance

ExtractLoadedTypedTables menggabungkan fragment pada halaman yang sama sebelum melakukan hal lain, dan penggabungan dilakukan berdasarkan geometri kolom, bukan teks row. Dua source table yang berdekatan pada satu halaman bergabung ketika keduanya memiliki sedikitnya dua kolom, jarak vertikal antara row terakhir tabel pertama dan row pertama tabel kedua tetap di dalam tolerance band, dan posisi awal kolomnya sejajar. Column start yang berada dalam jarak ColumnTolerance satu sama lain dilebur menjadi satu canonical column dan dirata-ratakan selama merge. Tolerance default adalah 12 user-space unit, cocok untuk tipografi business biasa dan layak dinaikkan untuk layout dengan tracking lebar atau indentasi dalam

Yang terjadi pada row yang kehilangan nilai interior adalah bagian pentingnya. HotPDF men-snap setiap cell ke column start canonical terdekat lalu menetapkan ColumnSpan ke jarak dari kolom tersebut ke kolom berikutnya yang terisi, bukan menggeser cell yang tersisa ke kiri. Row tiga cell dalam grid lima kolom tetap menyimpan nilainya di bawah heading yang benar dan mencatat persis lokasi gap-nya. Itulah perbedaan antara tabel yang dapat direkonsiliasi dan tabel yang diam-diam salah mengatribusikan uang

var
  Pdf: THotPDF;
  Options: THPDFTypedTableExtractionOptions;
  Tables: THPDFTypedTables;
  Info: THPDFTypedTableExtractionInfo;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('register.pdf', '') <= 0 then
      Exit;
    Options := THPDFTypedTableExtractionOptions.Default;
    Options.ColumnTolerance := 12;           // user-space unit
    Options.MinimumTableConfidence := 0.55;  // di bawah ini, tabel dibuang
    Options.DateOrder := ttdoDMY;            // 03/04/2026 adalah 3 April
    Options.DecimalSeparator := ',';
    Options.ThousandsSeparator := '.';
    if Pdf.ExtractLoadedTypedTables([0, 1, 2, 3], Options, Tables, Info) then
      // Info.TableCount vs Info.SourceTableCount menunjukkan jumlah yang digabung
      ProcessTables(Tables)
    else if Info.Status = ttesBudgetExceeded then
      Log(string(Info.Diagnostic));
  finally
    Pdf.Free;
  end;
end;

Apa yang sebenarnya dijamin oleh cross-page merging?

Yang dijamin adalah sikap konservatif, dan itu disengaja. HotPDF hanya menggabungkan dua tabel melewati batas halaman ketika MergeAcrossPages aktif, tabel kedua dimulai tepat pada page index setelah halaman terakhir tabel pertama, keduanya memiliki sedikitnya dua kolom, dan sedikitnya dua canonical column start sejajar dalam ColumnTolerance. Syarat halaman berurutan adalah penentu utamanya. Caller dapat memberikan PageIndices sebagai open array dalam urutan apa pun, dan tanpa pemeriksaan tersebut request untuk halaman 3, 9, dan 14 dapat mengelas tiga tabel yang tidak berkaitan menjadi satu hasil yang terlihat sepenuhnya masuk akal. Biayanya, continuation yang benar tetapi melewati satu halaman, appendix yang disisipkan, atau duplex scan dengan verso kosong akan kembali sebagai dua tabel dan tidak ada option yang melonggarkannya. Menggabungkan kembali kasus tersebut adalah policy call yang hanya dapat dibuat calling application, sehingga API mengekspos FirstPageIndex, LastPageIndex, SourceTableCount, dan PageIndex per row, lalu membiarkan keputusan berada di tempat yang tepat

Repeated header diberi label, tidak dihapus

ExtractLoadedTypedTables tidak pernah menghapus repeated header row dari result. Ketika cross-page merge menemukan bahwa tabel yang masuk dibuka dengan header text yang identik dengan tabel yang sudah terakumulasi, setelah trimming dan case folding, method menandai row tersebut IsHeader dan IsRepeatedHeader lalu tetap menambahkannya dalam source order. Deletion adalah keputusan lossy dan irreversible, sementara consumer berbeda membutuhkan jawaban berbeda: CSV import ingin repeat dihapus, audit trail ingin repeat tetap ada bersama nomor halaman, dan diffing tool ingin source order dipertahankan byte demi byte. Karena itu library melaporkan dan caller yang memutuskan

var
  T, R, C: Integer;
  Row: THPDFTypedTableRow;
  Total: Double;
begin
  Total := 0;
  for T := 0 to High(Tables) do
    for R := 0 to High(Tables[T].Rows) do
    begin
      Row := Tables[T].Rows[R];
      if Row.IsRepeatedHeader then
        Continue;                    // pertahankan hanya blok header pertama
      for C := 0 to High(Row.Cells) do
        if Row.Cells[C].ValueKind = ttvkCurrency then
          Total := Total + Row.Cells[C].NumberValue;
    end;
end;

Typed value dan separator yang harus Anda berikan

Type inference berjalan dalam urutan tetap yang menyelesaikan ambiguitas ke arah yang masuk akal: boolean lebih dulu, kemudian date, percentage, currency, lalu plain number, sedangkan yang tidak cocok tetap menjadi string. Urutan inilah yang mencegah 2026 di date column diputuskan oleh number parser sebelum date parser melihatnya. Currency dikenali dari leading $, £, ¥, atau , atau dari ISO 4217 code tiga huruf yang diikuti spasi, dan code tersebut dipertahankan dalam CurrencyCode. Yang penting, HotPDF tidak menebak locale Anda. DecimalSeparator, ThousandsSeparator, dan DateOrder berasal dari options, karena 1.234 bisa berarti satu angka atau seribu dua ratus tiga puluh empat bergantung pada fakta yang tidak ada di PDF. Raw Unicode Text dipertahankan pada setiap cell bersama typed value, sehingga tebakan yang salah selalu dapat dipulihkan tanpa extraction pass kedua

var
  Stream: TFileStream;
  Info: THPDFTypedTableExtractionInfo;
begin
  Stream := TFileStream.Create('tables.json', fmCreate);
  try
    if not Pdf.ExportLoadedTypedTables([0, 1, 2], ttefJSON,
      Stream, Options, Info) then
      case Info.Status of
        ttesInvalidOptions:   ReportBadConfiguration;
        ttesBudgetExceeded:   ReportOversizedDocument;
        ttesCancelled:        ReportUserCancelled;
        ttesWriteFailed:      ReportDestinationProblem;
      else
        ReportExtractionFailure;
      end;
  finally
    Stream.Free;
  end;
end;

Kedua format export menjawab pertanyaan yang berbeda dan sengaja tidak dibuat ekuivalen. CSV menulis continuation column dari merged span sebagai empty field, sesuai kebutuhan spreadsheet atau bulk loader. JSON mempertahankan semua yang diketahui extraction: typed value menurut kind-nya sendiri, columnSpan, confidence per-cell dan per-row, cell bounds, serta page dan source-table provenance. Kedua format men-stage seluruh dokumen ke bounded in-memory buffer lalu baru menerbitkannya ke destination stream, mengembalikan byte, length, dan position semula jika write gagal di tengah, sehingga export yang gagal tidak pernah meninggalkan file setengah tertulis. Budget untuk page, glyph per page, table, row, cell, character, dan output byte semuanya dihitung terpisah, dan row dihitung sebelum allocation karena SetLength per-row dapat berubah menjadi copying kuadratik jauh sebelum ceiling default satu juta row

Di mana geometric table recovery menyerah?

Menjelaskan failure mode secara eksplisit lebih berguna daripada feature list, karena setiap poin ini adalah tempat caller membutuhkan policy sendiri, bukan nilai option yang lebih baik

  • Vertical merge tidak dipulihkan. HotPDF melaporkan ColumnSpan untuk horizontal span dan membiarkan RowSpan tetap 1, sehingga cell yang mencakup tiga row dalam printed table tiba sebagai satu cell ditambah dua gap
  • Header detection berbasis data, bukan visual. Header block adalah rangkaian row sebelum row pertama yang memiliki typed value non-string, sehingga tabel yang body-nya seluruhnya teks melaporkan HeaderRowCount nol apa pun stylenya
  • Tabel di bawah MinimumTableConfidence dibuang dari result tanpa error. Bandingkan Info.TableCount dengan Info.SourceTableCount ketika Anda perlu tahu bahwa sesuatu dibuang
  • Sebuah run membutuhkan sedikitnya dua row dan dua kolom sebelum layout pass menyebutnya tabel, sehingga pseudo-table satu baris atau layout dua kolom berisi prosa panjang dengan benar, meskipun tidak membantu, bukan tabel
  • Scanned page tidak memiliki text operator, sehingga tidak ada yang dapat dipulihkan secara geometris sampai OCR text layer ada di halaman

Jika PDF Anda keluar dari reporting stack sendiri, perbaikan termurah untuk semuanya ada di upstream: emit tagged table atau simpan source data, dan perlakukan extraction sebagai fallback untuk dokumen yang tidak Anda produksi. Untuk kasus lain, pipeline ini layak dipelajari dengan urutan berikut karena setiap layer dibangun di atas layer di bawahnya: mulai dari plain text extraction dari PDF loaded, naik ke typed table API ketika geometri perlu dipertahankan, lalu lihat rendering data table ke PDF baru ketika Anda berada di sisi generating dan dapat menentukan seberapa mudah output dipulihkan

ExtractLoadedTypedTables dan ExportLoadedTypedTables tersedia sebagai bagian dari native HotPDF Delphi PDF Component untuk Delphi dan C++Builder, tanpa DLL eksternal dan tanpa runtime dependency; product page memuat referensi lengkap untuk option, status, dan record pada typed table API