HotPDF membandingkan dua dokumen PDF dari Delphi melalui THPDFDocComparison, yang menelusuri object graph kedua berkas dari catalog ke luar dan, jika diminta, juga merender setiap pasangan halaman serta mengukur piksel yang berbeda. Hasilnya adalah laporan JSON yang menyebutkan setiap perbedaan yang ditemukan, budget yang terpakai, dan apakah perbandingan berjalan hingga selesai. Kedua tahap ini penting, karena diff struktural dan diff visual menjawab pertanyaan yang berbeda
Pertanyaan di balik fitur ini biasanya adalah pertanyaan rilis. Sebuah mesin laporan menerima perubahan, output diregenerasi, dan seseorang harus memutuskan apakah ada yang berpindah. Membuka kedua berkas berdampingan hanya efektif hingga sekitar tiga halaman sebelum perhatian mulai goyah. Membandingkan byte mentah gagal seketika, karena dua kali jalan dari generator yang sama menghasilkan byte berbeda karena alasan yang sama sekali tidak berkaitan dengan apa yang dilihat pembaca
Mengapa PDF bisa berbeda byte tetapi identik secara visual?
Dua PDF yang dihasilkan secara independen dan tercetak identik rutin berbeda byte-nya, dan alasannya struktural, bukan kosmetik. Nomor objek ditetapkan sesuai urutan objek kebetulan ditulis. Subset font mengalokasikan CID sesuai urutan glyph pertama kali ditemukan, sehingga subset yang dibangun selama traversal yang sedikit berbeda menghasilkan byte content stream yang berbeda untuk teks yang tampak sama. Offset cross-reference bergeser setiap kali sesuatu di hulu berubah panjangnya
Inilah sebabnya nomor objek tidak dapat digunakan sebagai identitas lintas dokumen. HotPDF sebagai gantinya membangun setiap snapshot dengan menelusuri dari catalog, memperluas dictionary berdasarkan urutan byte kuncinya dan array berdasarkan indeks, sehingga setiap objek dinamai berdasarkan jalur yang mencapainya. Objek yang tidak dapat dijangkau traversal dari root jatuh kembali ke jalur sintetis $Unreachable[...] yang membawa nomor objek dan generasinya, yang menjaga konten yatim tetap terlihat dalam laporan alih-alih diam-diam hilang
Stream tidak dibandingkan dengan cara disalin. Setiap stream menyumbangkan signature SHA-256 inkremental, dihitung sambil memulihkan posisi stream aslinya sesudahnya, sehingga membandingkan dua berkas berukuran ratusan megabita tidak berarti memateriasi dua ratus megabita dua kali
Menyelaraskan halaman ketika satu dokumen mengalami penyisipan
Membandingkan halaman 1 terhadap halaman 1, halaman 2 terhadap halaman 2, dan seterusnya hanya benar ketika tidak ada yang disisipkan. Sisipkan satu halaman sampul dan perbandingan naif akan melaporkan setiap halaman sebagai berubah, yang secara teknis benar tetapi tidak berguna secara operasional
HotPDF menyelaraskan halaman sebelum membandingkannya. Ia membangun signature per halaman dari teks yang dapat diekstrak, jatuh kembali ke signature struktural untuk halaman tanpa teks, lalu menghitung longest increasing subsequence atas indeks target yang cocok. Halaman di dalam subsequence itu adalah halaman yang sekadar bergeser; halaman di luarnya adalah perubahan sungguhan. Perbedaan inilah yang membuat diff manual 400 halaman dapat dibaca, karena laporan mengatakan satu halaman disisipkan, bukan empat ratus halaman berubah
Menjalankan perbandingan struktural
Panggilan paling sederhana menerima dua dokumen yang dimuat dan sebuah mode. cmStructural melakukan penelusuran object graph, cmRenderedImage melakukan perbandingan piksel, cmFull melakukan keduanya, dan mode yang lebih ringan cmPageCount, cmPageText, dan cmObjectCount tersedia untuk pemeriksaan smoke test yang murah:
uses
HPDFDoc, HPDFDocCompare;
var
DocA, DocB: THotPDF;
Report: AnsiString;
begin
DocA := THotPDF.Create(nil);
DocB := THotPDF.Create(nil);
try
if (DocA.LoadFromFile('baseline.pdf') <= 0) or
(DocB.LoadFromFile('candidate.pdf') <= 0) then
Exit;
Report := THPDFDocComparison.Compare(DocA, DocB, cmStructural);
with TFileStream.Create('diff.json', fmCreate) do
try
WriteBuffer(Report[1], Length(Report));
finally
Free;
end;
finally
DocB.Free;
DocA.Free;
end;
end;
Laporan membedakan tiga status yang tidak dapat dinyatakan oleh sebuah boolean. identical menyatakan apakah ada yang berbeda, comparisonComplete menyatakan apakah penelusuran selesai, dan comparisonBudget menyebutkan batas yang menghentikannya jika memang terjadi. Perbandingan yang menghabiskan budget melaporkan comparisonComplete=false dan identical=false secara bersamaan, karena penelusuran yang terpotong tidak memiliki dasar untuk mengklaim kesetaraan. Otomasi mana pun yang hanya membaca identical pada akhirnya akan memperlakukan penghentian budget sebagai perbedaan sungguhan, jadi bacalah ketiganya
Batasan apa yang menjaga penelusuran tetap terbatas?
Nilai default dalam THPDFStructuralCompareLimits.Default disesuaikan untuk dokumen nyata, bukan untuk dokumen yang bersifat adversarial, dan setiap budget yang relevan secara semantik memiliki batas atasnya sendiri: 250.000 objek, 2.000.000 edge, kedalaman 128, 10.000 perbedaan yang dilaporkan, 64 MB per stream dan total 512 MB byte stream, 1 MB per nilai, dan 4.096 byte per path. Naikkan secara sengaja ketika Anda tahu korpus Anda, dan turunkan ketika membandingkan berkas yang datang dari luar:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // gagal cepat di CI
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // default adalah 72
Options.ColorTolerance := 2; // abaikan noise pembulatan level 1-2
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // tulis gambar overlay untuk ditinjau
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Tahap rendered mengestimasi jumlah piksel dari dimensi halaman dan DPI yang diminta sebelum bitmap apa pun dialokasikan, dan memeriksa ulang bitmap sesungguhnya sesudahnya, sehingga geometri halaman yang cacat tidak dapat lolos dari budget dengan berbohong soal ukurannya. Menaikkan DPI menaikkan fidelitas dan biaya secara kuadratik: 150 DPI adalah empat kali jumlah piksel dari 72, dan batas piksel per halaman serta total ada persis karena job batch pada 300 DPI jika tidak akan mengalokasikan dirinya sendiri menuju masalah
Seberapa mirip yang disebut cukup mirip?
Dua halaman dihitung mirip hanya ketika kedua kondisi terpenuhi: rasio piksel yang berubah berada di atau di bawah MaxChangedPixelRatio dan similarity berada di atau di atas MinimumSimilarity. Dua ambang batas, bukan satu, karena segelintir piksel yang salah secara katastrofik dan hamparan luas pergeseran warna kecil merupakan kegagalan yang berbeda, dan salah satunya bisa saja dapat diterima dalam satu alur kerja dan mendiskualifikasi dalam alur kerja lain. Pengujian ambang batas menggunakan nilai yang tidak dibulatkan; enam desimal dalam JSON ada untuk menjaga laporan tetap stabil dan dapat di-diff, bukan untuk mendefinisikan perbandingan
Piksel yang berubah dikelompokkan menjadi region menggunakan tile berukuran tetap sebagai node dengan adjacency empat arah, bukan flood fill per piksel. Ini menjaga memori tetap terbatas dan daftar region tetap stabil di berbagai jalankan. Memotong detail region yang disimpan hanya memengaruhi daftar, bukan jumlah region yang dilaporkan, sehingga halaman dengan region berubah lebih banyak daripada MaxChangedRegions tetap melaporkan berapa banyak jumlahnya
Ada satu perilaku yang layak dinyatakan secara terus terang karena membalikkan naluri biasa. Kegagalan renderer, kegagalan alokasi, dan kegagalan overlay tidak pernah ditelan begitu saja. Segala jenis kegagalan tersebut dicatat sebagai renderError atau renderBudget dan memaksa renderComparisonComplete=false, karena halaman yang gagal dirender adalah halaman yang tidak dibandingkan oleh siapa pun, dan melaporkannya sebagai identik lebih buruk daripada tidak melaporkan apa-apa
Di mana setiap mode berada dalam sebuah pipeline
Perbandingan struktural menjawab apa yang berubah dan merupakan default yang tepat untuk suite regresi: ia menyebutkan path, indeks halaman, dan nomor objek yang terlibat, sehingga kegagalan menunjuk langsung ke kode yang menghasilkannya. Perbandingan rendered menjawab apakah ada yang akan menyadarinya, pertanyaan yang tepat untuk approval dan untuk memverifikasi bahwa sebuah pass optimisasi benar-benar lossless
Keduanya berpadu baik. Jalankan cmStructural pada setiap build dan biarkan ia gagal dengan keras pada perubahan level objek yang tidak terduga; jalankan cmFull dengan heatmap sebelum rilis, ketika manusia tersedia untuk melihat overlay-nya. Untuk pipeline yang sudah menerbitkan markup halaman untuk alasan lain, output teks yang dijelaskan dalam mengekspor halaman PDF ke SVG memberikan tampilan ketiga yang dapat di-diff manusia, dan pemeriksaan otomatis dalam otomasi laporan preflight mencakup pertanyaan konformansi yang tidak dimaksudkan untuk dijawab oleh kedua mode diff tersebut
Perbandingan, preflight, dan rendering berbagi model objek loaded-document yang sama, sehingga satu kali pemrosesan atas sebuah berkas dapat memberi makan ketiganya. Daftar fitur lengkap untuk Delphi dan C++Builder ada di halaman komponen PDF Delphi HotPDF