HotPDF, Delphi'den THPDFDocComparison üzerinden iki PDF belgesini karşılaştırır; bu sınıf her iki dosyanın nesne grafiğinde kataloğdan dışa doğru gezinir ve istendiğinde her sayfa çiftini de render ederek farklılaşan pikselleri ölçer. Sonuç, bulduğu her farkı, tükettiği bütçeyi ve karşılaştırmanın sonuna kadar çalışıp çalışmadığını adlandıran bir JSON raporudur. Her iki geçiş de önemlidir, çünkü bir yapısal fark ile bir görsel fark farklı sorulara yanıt verir
Bu özelliğin ardındaki soru genellikle bir yayına alma (release) sorusudur. Bir rapor motoruna bir değişiklik gelir, çıktı yeniden üretilir ve birinin herhangi bir şeyin değişip değişmediğine karar vermesi gerekir. İki dosyayı yan yana açmak, dikkat dağılmadan önce yaklaşık üç sayfaya kadar ölçeklenir. Ham baytları karşılaştırmak ise hemen başarısız olur, çünkü aynı üretecin iki çalıştırması, bir okuyucunun gördüğüyle hiç ilgisi olmayan nedenlerle farklı baytlar üretir
PDF'ler neden bayt olarak farklı ama görsel olarak özdeş olabilir?
Aynı şekilde basılan, birbirinden bağımsız olarak üretilmiş iki PDF, baytları bakımından rutin olarak farklılaşır ve bunun nedenleri kozmetik değil yapısaldır. Nesne numaraları, nesnelerin yazıldığı sıraya göre atanır. Yazı tipi alt kümeleri, CID'leri gliflerin ilk karşılaşıldığı sıraya göre ayırır, bu yüzden hafifçe farklı bir gezinme (traversal) sırasında oluşturulan bir alt küme, aynı görünür metin için farklı içerik akışı baytları üretir. Çapraz referans ofsetleri, yukarı akıştaki herhangi bir şey uzunluğunu değiştirdiğinde kayar
İşte bu yüzden nesne numaraları belgeler arası kimlik olarak kullanılamaz. Bunun yerine HotPDF, kataloğdan başlayarak gezinerek, sözlükleri anahtarlarının bayt sırasına göre ve dizileri indekse göre genişleterek her anlık görüntüyü oluşturur, böylece her nesne kendisine ulaşan yol ile adlandırılır. Gezinmenin kökten ulaşamadığı nesneler, nesne numarasını ve nesli (generation) taşıyan sentetik bir $Unreachable[...] yoluna düşer; bu, sahipsiz içeriği sessizce yok olmak yerine raporda görünür tutar
Akışlar (stream) kopyalanarak karşılaştırılmaz. Her akış, sonrasında orijinal akış konumu geri yüklenirken hesaplanan artımlı bir SHA-256 imzası katkısında bulunur, bu yüzden iki yüz megabaytlık dosyaları karşılaştırmak, iki yüz megabaytı iki kez somutlaştırmak anlamına gelmez
Bir belgede ekleme olduğunda sayfaları hizalamak
Sayfa 1'i sayfa 1'e, sayfa 2'yi sayfa 2'ye karşı karşılaştırmak ve böyle devam etmek, yalnızca hiçbir şey eklenmediğinde doğrudur. Bir kapak sayfası ekleyin, saf (naive) bir karşılaştırma her sayfayı değişmiş olarak bildirir; bu teknik olarak doğrudur ama operasyonel olarak işe yaramaz
HotPDF, sayfaları karşılaştırmadan önce hizalar. Çıkarılabilir metinden sayfa başına bir imza oluşturur, metni olmayan sayfalar için yapısal bir imzaya geri döner ve ardından eşleşen hedef indeksler üzerinde en uzun artan alt diziyi (longest increasing subsequence) hesaplar. Bu alt dizinin içindeki sayfalar yalnızca kaymış olanlardır; dışındakiler ise gerçek taşınmalardır. Bu ayrım, 400 sayfalık bir kılavuzun farkını okunabilir kılan şeydir, çünkü rapor dört yüz sayfa değişti demek yerine bir sayfa eklendi der
Bir yapısal karşılaştırma çalıştırmak
En basit çağrı, iki yüklü belge ve bir mod alır. cmStructural nesne grafiği gezinmesini yapar, cmRenderedImage piksel karşılaştırmasını yapar, cmFull ikisini birden yapar ve daha hafif modlar olan cmPageCount, cmPageText ile cmObjectCount, ucuz duman testleri (smoke check) için vardır:
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;
Rapor, bir booleanın ayıramayacağı üç durumu ayırır. identical herhangi bir şeyin farklılaşıp farklılaşmadığını söyler, comparisonComplete gezinmenin bitip bitmediğini söyler ve comparisonBudget, eğer durdurduysa, durduran sınırı adlandırır. Bir bütçeyi tüketen bir karşılaştırma, comparisonComplete=false ve identical=false'u birlikte bildirir, çünkü kesilmiş bir gezinmenin eşitlik iddia etmek için hiçbir dayanağı yoktur. Yalnızca identical'ı okuyan herhangi bir otomasyon, er ya da geç bir bütçe durmasını gerçek bir fark olarak ele alacaktır, bu yüzden üçünü de okuyun
Gezinmeyi hangi sınırlar tutar?
THPDFStructuralCompareLimits.Default içindeki varsayılanlar, kötü niyetli belgeler için değil gerçek belgeler için boyutlandırılmıştır ve anlamsal olarak ilgili her bütçenin kendi tavanı vardır: 250.000 nesne, 2.000.000 kenar, 128 derinlik, 10.000 bildirilen fark, akış başına 64 MB ve toplamda 512 MB akış baytı, değer başına 1 MB ve yol başına 4.096 bayt. Kaynağınızı (corpus) bildiğinizde bunları bilinçli olarak yükseltin, dışarıdan gelen dosyaları karşılaştırırken ise düşürün:
var
Limits: THPDFStructuralCompareLimits;
Options: THPDFRenderedCompareOptions;
begin
Limits := THPDFStructuralCompareLimits.Default;
Limits.MaxDifferences := 200; // CI'de hızlı başarısız ol
Limits.MaxTotalStreamBytes := 128 * 1024 * 1024;
Options := THPDFRenderedCompareOptions.Default;
Options.DPI := 150; // varsayılan 72'dir
Options.ColorTolerance := 2; // 1-2 seviyelik yuvarlama gürültüsünü yok say
Options.MinimumSimilarity := 0.9995;
Options.MaxChangedPixelRatio := 0.0005;
Options.GenerateHeatmaps := True; // inceleme için kaplama (overlay) görüntüleri yaz
Report := THPDFDocComparison.CompareWithOptions(DocA, DocB, cmFull,
Limits, Options);
end;
Render edilen geçiş, herhangi bir bitmap ayrılmadan önce sayfa boyutlarından ve istenen DPI'den piksel sayısını tahmin eder ve ardından gerçek bitmap'i yeniden denetler, böylece kusurlu bir sayfa geometrisi boyutu hakkında yalan söyleyerek bütçeyi atlatamaz. DPI'yi yükseltmek, sadakati ve maliyeti karesel olarak yükseltir: 150 DPI, 72'nin dört katı pikseldir ve sayfa başına ile toplam piksel tavanları tam olarak, 300 DPI'de bir toplu iş aksi halde belleği tükete tükete soruna gireceği için vardır
Ne kadar benzer, yeterince benzerdir?
İki sayfa, yalnızca her iki koşul da sağlandığında benzer sayılır: değişen piksel oranı MaxChangedPixelRatio değerinde veya altında olmalı ve benzerlik MinimumSimilarity değerinde veya üstünde olmalıdır. Tek yerine iki eşik vardır, çünkü bir avuç felaket derecede yanlış piksel ile küçük renk kaymalarının geniş bir dalgası farklı hata türleridir ve her biri tek başına bir iş akışında kabul edilebilirken diğerinde diskalifiye edici olabilir. Eşik testleri yuvarlanmamış değerler kullanır; JSON'daki altı ondalık basamak, karşılaştırmayı tanımlamak için değil, raporları kararlı ve karşılaştırılabilir tutmak için vardır
Değişen pikseller, piksel başına dolgu doldurma (flood fill) yerine, dört yönlü komşuluğa sahip sabit boyutlu döşemeler (tile) düğüm olarak kullanılarak bölgelere gruplanır. Bu, belleği sınırlı tutar ve bölge listesini çalıştırmalar arasında kararlı kılar. Tutulan bölge ayrıntısını kısaltmak yalnızca listelemeyi etkiler, bildirilen bölge sayısını etkilemez, bu yüzden MaxChangedRegions'tan daha fazla değişen bölgesi olan bir sayfa yine de kaç tane olduğunu bildirir
Bir davranışı açıkça belirtmeye değer, çünkü bu, olağan içgüdüyü tersine çevirir. Render hataları, ayırma hataları ve kaplama hataları hiçbir zaman yutulmaz. Bu türden herhangi bir şey renderError veya renderBudget olarak kaydedilir ve renderComparisonComplete=false'u zorlar, çünkü render edilemeyen bir sayfa hiç kimsenin karşılaştırmadığı bir sayfadır ve onu özdeş olarak bildirmek, hiçbir şey bildirmemekten daha kötüdür
Her modun bir işlem hattındaki (pipeline) yeri
Yapısal karşılaştırma neyin değiştiğini yanıtlar ve regresyon paketleri için doğru varsayılandır: yolu, sayfa indeksini ve ilgili nesne numaralarını adlandırır, bu yüzden bir başarısızlık onu üreten koda işaret eder. Render edilmiş karşılaştırma ise birinin fark edip etmeyeceğini yanıtlar; bu, onaylar için ve bir optimizasyon geçişinin gerçekten kayıpsız olduğunu doğrulamak için sorulan sorudur
Bunlar iyi bir şekilde birleşir. cmStructural'ı her derlemede (build) çalıştırın ve beklenmeyen nesne düzeyi değişikliklerde yüksek sesle başarısız olmasına izin verin; bir insanın kaplamalara bakabileceği zamanlarda, bir yayından önce cmFull'u ısı haritalarıyla (heatmap) çalıştırın. Başka nedenlerle zaten sayfa işaretlemesi üreten işlem hatları için, PDF sayfalarını SVG'ye aktarma yazısında anlatılan metin çıktısı, insan tarafından karşılaştırılabilir üçüncü bir görünüm sağlar; preflight rapor otomasyonu yazısındaki otomatik denetimler ise hiçbir fark modunun yanıtlamayı amaçlamadığı uygunluk (conformance) sorularını kapsar
Karşılaştırma, preflight ve render etme aynı yüklü belge nesne modelini paylaşır, bu yüzden bir dosya üzerinde tek bir geçiş üçünü de besleyebilir. Delphi ve C++Builder için eksiksiz özellik listesi HotPDF Delphi PDF bileşeni sayfasındadır