Artikel Teknis

Grid Rowspan dan Header Tabel Berulang di HotPDF

HotPDF merender tabel HTML lewat profil HTML5 paged-media-nya dengan grid okupansi sungguhan untuk rowspan dan colspan, tinggi baris hasil pengukuran alih-alih estimasi hitung karakter, dan baris header yang diulang di setiap halaman lanjutan. Dua situasi membuatnya menolak mengulang header, dan mengetahuinya dari awal lebih murah daripada debugging sel yang terduplikasi nanti

Kelas dokumen yang memaksa semua ini adalah yang pada akhirnya dikirim setiap tim pelaporan: faktur atau laporan compliance tempat sumber kebenarannya adalah HTML, tabelnya membentang empat halaman, dan header harus terbaca di semuanya. Apa pun yang kurang dari layout tabel sungguhan menghasilkan dua kegagalan yang langsung diperhatikan pembaca: header yang hanya muncul sekali di halaman satu, dan tinggi baris yang ditebak dari hitungan karakter

Mengapa kemampuan tabel pindah ke HTML renderer?

Karena alternatifnya kehilangan rich text, dan rich text adalah alasan kontennya berupa HTML sejak awal. Rencana yang tampak jelas terlihat seperti reuse: HotPDF sudah punya objek tabel layout DOM dengan grid yang benar, jadi jembatani parser HTML ke sana dan dapatkan spanning gratis. Masalahnya ada pada apa yang digambar objek tabel itu. Sel-selnya membawa teks dan style, dan jalur gambar memancarkan output teks polos, sehingga apa pun yang benar-benar dikandung HTML melampaui font dan warna — link, superscript, perubahan ukuran inline, warna per-run — lenyap pada saat mencapai halaman

Arah yang bertahan ketika berhadapan dengan dokumen nyata adalah arah sebaliknya. Pindahkan kemampuan engine tabel — grid okupansi, pengukuran sungguhan, pengulangan header, dan pembobotan kolom — ke HTML renderer, dan biarkan rendering rich text tetap di tempatnya yang sudah bekerja. Ini perubahan yang lebih besar daripada jembatan, dan ini perubahan yang menjaga hyperlink di dalam sel tabel tetap menjadi hyperlink

Rowspan tanpa union-find

Sel yang spanning menciptakan grup baris atomik, tapi penutupan atas grup-grup itu tidak butuh struktur disjoint-set umum, sebab okupansinya selalu interval kontinu. Sel dengan rowspan="3" yang mulai di baris K menduduki baris K sampai K+2 dan bukan yang lain, jadi informasi grupnya menyusut menjadi penanda akhir per baris

Algoritmanya dua baris maksud. Saat Anda menempatkan sel spanning yang mulai di K dan berakhir di E, catat GroupEnd[K] := Max(GroupEnd[K], E). Lalu telusuri baris sekali dari belakang dan terapkan G[R] := G[G[R]], yang mempropagasikan tiap akhir baris ke belakang melalui span yang tumpang tindih dan menghasilkan transitive closure dalam satu pass. Yang Anda dapat, untuk setiap baris, adalah baris terakhir yang wajib berada di halaman yang sama dengannya — persis yang dibutuhkan langkah paginasi untuk memutuskan di mana break boleh jatuh

Membagikan tinggi adalah separuh lainnya. Saat sel spanning butuh ruang vertikal lebih daripada yang baris-baris yang dicovernya sedang sediakan, kelebihannya diberikan ke baris terakhir span, bukan dibagi rata. Proses sel spanning setelah tinggi baris biasa selesai ditetapkan, lalu tambahkan sisa ruang ke baris terakhir tiap span. Membagi kelebihan itu rata terkesan lebih adil dan menghasilkan output yang kelihatan salah: baris yang hanya berisi sel satu baris pendek jadi menggelembung karena beberapa sel tak berkaitan tiga baris di atasnya kebetulan tinggi

Grid tabel HTML HotPDF tempat satu sel dengan rowspan 3 yang mulai di baris 2 menduduki baris 2 sampai 4 sebagai satu persegi atomik, berdampingan dengan nilai akhir grup per baris G dari R hasil satu penelusuran terbalik yang menunjukkan baris 2, 3 dan 4 terikat ke halaman yang sama
Okupansi spanning selalu interval kontinu, sehingga penanda akhir per baris dan satu penelusuran terbalik menggantikan union-find dan memberi tahu paginasi persis di mana break boleh jatuh
var
  Pdf: THotPDF;
  Importer: THPDFHTMLImporter;
  Stats: THPDFHTMLImportStatistics;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'audit-report.pdf';
    Pdf.BeginDoc;
    Importer := THPDFHTMLImporter.Create(Pdf);
    try
      Importer.Margin := 48;
      Importer.BaseFontName := 'Arial';
      Importer.BaseFontSize := 10;
      Importer.MaxDOMNodes := 200000;
      Importer.MaxLayoutOperations := 2000000;
      if Importer.RenderHTML5(SourceHtml, PrintStyleSheet) then
      begin
        Stats := Importer.Statistics;
        Writeln('tables ', Stats.TableCount,
                '  page breaks ', Stats.PageBreakCount);
      end;
    finally
      Importer.Free;
    end;
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

RenderHTML5 menerima author style sheet opsional sebagai argumen keduanya, dan ke sanalah print rule diletakkan. Jauhkan style sheet layar dari situ. Profilnya diveresionkan, dan HTML5ProfileMilestones melaporkan grup kemampuan mana yang diimplementasikan build saat ini — himParserCascade, himPagedLayout, himTablesForms dan himBoundedResources — sehingga aplikasi bisa menurunkan kemampuan secara sengaja alih-alih menemukan celah di produksi

Pengukuran harus sepakat dengan penggambaran, persis

Tinggi baris hanya benar ketika kode yang mengukur baris ter-wrap mem-wrap-nya dengan aturan yang sama dengan kode yang menggambarnya. Kedengarannya jelas, dan inilah sumber paling umum tabel yang border-nya tak sejalan dengan isinya. HotPDF mengukur dengan penghitung baris greedy, dan penghitung itu harus cocok dengan semantik wrapping jalur output rich text dalam tiga hal spesifik: ia memutus hanya di spasi, tidak pernah membelah kata, dan kata yang lebih lebar dari kolom mendapat satu baris sendiri

Persyaratan keduanya adalah font. Pengukuran harus berjalan dengan font milik sel itu sendiri — diatur lewat SetFont dengan nama, set style dan ukuran yang sebenarnya sebelum memanggil fungsi lebar — bukan dengan font apa pun yang kebetulan aktif. Teks bold rutin lebih dari sepuluh persen lebih lebar daripada regular pada ukuran sama, cukup untuk mengubah sel tiga baris menjadi sel empat baris. Tabel yang sel headernya bold dan sel body-nya tidak, yang diukur dengan satu font, akan salah persis di baris yang pertama dilihat pembaca

Membenarkan ini mengubah apa yang bisa Anda assert dalam test. Efek terobservasi dari pengukuran akurat adalah jarak antarbaris, bukan hitungan glyph: baris satu baris itu sekitar 20 points tingginya sementara estimasi hitung karakter atas konten yang sama memprediksi dua baris dan sekitar 35. Assert pada jarak vertikal antar baris. Dan ingat user space PDF punya Y yang bertambah ke atas, jadi header yang duduk di atas baris body berarti nilai Y header lebih besar — kebalikan dari yang ditulis naluri koordinat layar

Kapan HotPDF menolak mengulang header?

Dalam dua kasus, yang keduanya akan menghasilkan output kelihatan salah bila tetap dilanjutkan. Yang pertama, blok header yang mengandung sel spanning yang menjulur melampaui header ke baris body. Mengulang header akan menggambar isi sel itu sekali lagi di posisi yang tak lagi menjadi tempatnya, jadi header digambar sekali dan tabel berlanjut tanpanya. Yang kedua, header yang lebih tinggi dari 90 persen tinggi halaman yang bisa dipakai, tempat pengulangan menyisakan hampir tak ada ruang untuk data dan tabel tidak membuat kemajuan apa pun

Alur keputusan HotPDF untuk mengulang header tabel HTML lintas page break: header yang rowspan-nya menerobos ke baris body digambar sekali, header yang lebih tinggi dari 90 persen tinggi halaman yang bisa dipakai digambar sekali, dan setiap header lainnya diulang di tiap halaman lanjutan
Dua penolakan itu disengaja: mengulang header yang memiliki sel body spanning atau memenuhi sebagian besar halaman akan menggambar konten di tempat yang tak lagi menjadi miliknya atau menyisakan tak ada ruang untuk data

Kedua penolakan itu disengaja dan senyap memang rancangannya, karena alternatifnya lebih buruk. Kalau header Anda tidak berulang padahal Anda mengharapkannya, periksa markup mencari rowspan yang menerobos batas thead sebelum mencurigai engine-nya. Satu pola markup ini menjelaskan sebagian besar kejutan

// Bobot kolom berasal dari markup, jadi print style sheet adalah tempat
// untuk mengendalikannya. Lebar diperlakukan sebagai bobot, bukan piksel
const
  PrintStyleSheet =
    'table { width: 100%; }' +
    'thead th { font-weight: bold; background: #eee; }' +
    'td.amount { text-align: right; }';

// Baris header yang membawa rowspan menerobos ke body menekan
// pengulangan header. Jaga span tetap di dalam satu section:
//   <thead><tr><th rowspan="2">Item</th>...</tr></thead>  ok
//   <tr><th rowspan="3">Item</th>...  menerobos ke tbody, tanpa repeat

Lebar kolom berperilaku sebagai bobot alih-alih pengukuran absolut, dan itulah perilaku yang menjaga tabel tetap terpakai saat kontennya tidak cocok dengan estimasi penulisnya. Kolom yang dideklarasikan 30 persen mendapat sekitar 30 persen lebar yang tersedia, tapi distribusinya menghormati lebar minimum yang benar-benar dibutuhkan tiap kolom, sehingga kolom sempit yang menampung token panjang yang tak bisa dipatah tidak meluap dari box tabel secara diam-diam

Di mana ini menempati posisi dalam pipeline dokumen

Pekerjaan tabel ini duduk di dalam profil paged-media yang lebih luas, dan aturan paginasi, anggaran resource, serta penanganan CSS yang dibahas di jalur impor HTML5 paged-media berlaku tanpa perubahan pada dokumen yang mengandung tabel. Kalau data Anda tidak mulai sebagai HTML, jalur konstruksi langsung di membangun tabel langsung ke PDF menghindari lapisan parsing sepenuhnya dan memberi Anda perilaku grid yang sama lewat API. Dan karena tinggi baris pada akhirnya bergantung pada di mana baris patah, diskusi pengukuran di text justification dan pemenggalan baris adalah pasangan wajib bagi siapa pun yang menyetel output tabular yang padat

Pelajaran yang bisa dipinjam di sini sebenarnya bukan soal tabel. Ketika subsystem baru butuh kemampuan yang sudah dimiliki subsystem lama, tanyakan mana dari keduanya yang memiliki hal yang paling sulit diimplementasikan ulang. Aritmetika grid hanya beberapa puluh baris dan gampang dipindah. Rendering rich text dengan link inline, superscript, dan styling per-run tidak, jadi grid yang pindah dan teks yang tinggal. HotPDF mengirim kedua jalur itu sebagai bagian dari komponen PDF Delphi HotPDF, sehingga pilihan antara input HTML dan konstruksi langsung adalah keputusan proyek, bukan keputusan pustaka