Teknik Makale

Delphi'de Görsel PDF Diff: Piksel Kıyas ve Sayfa Eşleştirme

PDFlibPas görsel karşılaştırması iki PDF'yi aynı DPI ile render eder ve bir okuyucunun gerçekte gördüğünü diff'ler. TPDFlib.CompareRenderedPagesWith tek bir sayfa çiftini kıyaslar ve değişen pikselleri, bölgeleri, bir mask ve bir overlay döndürür. TPDFlib.CompareDocumentVisualsWith ile TPDFlib.CompareFileVisuals bütün belgeleri sayfa sayfa eşleştirir, eklenen ve çıkarılan sayfaları saptar ve her şeyi JSON olarak raporlar

Kullanım senaryosu görsel regresyon testidir. Biri fatura şablonunu yeniden yazar, bir font paketini yükseltir ya da render motorunu değiştirir ve soru, baytların değişip değişmediği değil (her zaman değişirler), herhangi bir müşterinin fark görüp görmeyeceğidir. Yapısal bir diff başka bir soruya cevap verir. İki dosya her content stream'de farklılaşabilir ve yine de özdeş sayfalar boyayabilir; kaymış tek bir operand, bir metin çıkarıcının işaretleyeceği hiçbir şeye dokunmadan bir toplamı üç puan oynatabilir. Pikseller, "aynı görünüyor" ifadesinin test edilebildiği tek katmandır

Görsel bir PDF diff gerçekte neyi kıyaslar?

PDFlibPas, ham tampon içeriklerini değil, beyaz bir sayfa üzerindeki görünür sonucu kıyaslar. Her sayfa RenderPageToString üzerinden sıkıştırılmamış bir BMP olarak render edilir, yukarıdan aşağıya bir bitmap'e çözülür ve alfa taşıyan her piksel, kanallar kıyaslanmadan önce beyaz üzerine bileştirilir. Bu yüzden ekranda özdeş görünen bir RGB bitmap ile bir RGBA bitmap özdeş kıyaslanır; iki taraf farklı render yollarından geldiğinde istediğiniz de tam olarak budur

BMP çözücünün bilinmesinde fayda olan kasıtlı bir tuhaflığı vardır. 32 bitlik bir BI_RGB bitmap piksel başına dördüncü bir bayt taşır, ama biçim o baytı tanımsız bırakır: bazı yazanlar 0 ile, bazıları 255 ile, bazıları artıklarla doldurur. PLDecodeVisualBMP o baytı yok sayar ve rengi korur. O baytı opaklık gibi ele almak, sıfır yazan bir render motorunun sayfadaki her pikseli silmesine izin verirdi ve diff, iki boş özdeş belgeyi memnuniyetle raporlardı. Kendi bitmap'lerinizi alt seviye API'ye besliyorsanız, alfa yalnızca biçimi kendiniz vpfRGBA ya da vpfBGRA olarak bildirdiğinizde dikkate alınır

Bir piksel, kırmızı, yeşil ya da mavi kanallarından herhangi biri ColorTolerance değerinden daha fazla farklılaştığında değişmiş sayılır. Tolerans 0, bit düzeyinde özdeşlik demektir. Pratikte tek haneli bir değer, render sürümleri arasındaki anti-aliasing titremesini, değişmiş bir glifi gizlemeden emer; çünkü gerçek bir metin değişikliği kanalları bundan çok daha fazla oynatır. Sayıyı tahminle değil, bilinen-iyi bir temel çalıştırmadan seçin

CompareRenderedPagesWith ile tek bir sayfa çiftini kıyaslamak

TPDFlib.CompareRenderedPagesWith, hangi sayfaların birbirine ait olduğunu zaten bildiğiniz ve ayrıntı istediğiniz zamanki araçtır: bir bounding box, bağlı değişim bölgeleri, isteğe bağlı bir piksel başına mask ve isteğe bağlı bir kırmızı tonlu overlay. Options kaydı ve sonuç türleri PDFlibVisualCompare unit'inde yaşar ve PLDefaultVisualCompareOptions size tolerans 0, 32 piksellik kareler, yok sayma bölgesi yok ve mask ya da overlay olmadan başlangıç değerlerini verir

uses
  PDFlibrary, PDFlibVisualCompare;

var
  Baseline, Candidate: TPDFlib;
  Options: TPDFVisualCompareOptions;
  Comparison: TPDFVisualCompareResult;
  ErrorText: WideString;
begin
  Baseline := TPDFlib.Create;
  Candidate := TPDFlib.Create;
  try
    if (Baseline.LoadFromFile('invoice-v1.pdf', '') = 1) and
       (Candidate.LoadFromFile('invoice-v2.pdf', '') = 1) then
    begin
      Options := PLDefaultVisualCompareOptions;
      Options.ColorTolerance := 8;
      Options.IncludeOverlay := True;
      // Baskı zaman damgasını maskele; piksel birimleri aşağıdaki DPI'da
      SetLength(Options.IgnoreRects, 1);
      Options.IgnoreRects[0].Left := 600;
      Options.IgnoreRects[0].Top := 20;
      Options.IgnoreRects[0].Right := 790;
      Options.IgnoreRects[0].Bottom := 50;

      if Baseline.CompareRenderedPagesWith(Candidate, 1, 1, 96, Options,
        Comparison, ErrorText) then
      begin
        if Comparison.ChangedPixels > 0 then
          Writeln(PLVisualCompareResultToJSON(Comparison));
      end
      else
        Writeln('Compare failed: ', ErrorText);
    end;
  finally
    Candidate.Free;
    Baseline.Free;
  end;
end;

Burada üç ayrıntı insanı ısırır. Yok sayma dikdörtgenleri, sol üst köken ve yarı açık aralıklarla render edilmiş piksellerdedir; bu yüzden Right ile Bottom dışlayıcıdır ve DPI'yı değiştirirseniz aynı dikdörtgen sayfanın başka bir kısmını kapsar. Çakışan yok sayma dikdörtgenleri bir kez sayılır ve yok sayılan pikseller yok olmak yerine IgnoredPixels içinde görünür; bu da ChangedRatio'yu dürüst tutar, çünkü o yalnızca kıyaslanan piksellere böler. Son olarak, tek sayfalık çağrı her iki render'ın aynı boyutlara sahip olmasını ister; boyutu değişen bir sayfa, sığdırılmak üzere gerilmek yerine bir hata ile başarısız olur

Bölgeler bir kare geçişinden gelir: değişen pikseller TileSize karelerine ayrıştırılır ve bitişik kareler, kendi sınırları ve piksel sayısı olan tek bir bölgede birleştirilir. Daha büyük kareler daha az ve daha kaba bölge verir; test raporunun istediği genellikle budur. Overlay, ikinci sayfanın değişen pikselleri kırmızıya boyanmış bir RGB kopyasıdır ve Comparison.Overlay içinde ham baytlar olarak döndürülür; onu CI çıktılarınız için bir PNG'ye yazmak sizin kodunuza iş olur, kütüphanenin değil

Belge seviyesi sayfa eşleştirmesi eklenen sayfaları nasıl ele alır?

TPDFlib.CompareDocumentVisualsWith sayfaları, tam piksel parmak izleri üzerine bir en uzun ortak alt dizi ile eşleştirir; böylece tek bir eklenen sayfa, kendisinden sonraki her sayfayı sahte bir değişime çevirmez. Şablon bir kapak sayfası kazandığı anda, naif bir 1. sayfayı 1. sayfaya, 2. sayfayı 2. sayfaya bağlayan döngü işe yaramaz hâle gelir: eklemeden sonraki her sayfa bir kayar ve rapor tepeden tırnağa kırmızı yanar

Her sayfa render edilir ve piksel genişliği, yüksekliği ile görünür piksellerin SHA-256 özetinden oluşan bir anahtara indirgenir. Boyut, göründüğünden daha önemlidir: aynı alana sahip, tertemiz beyaz bir dikey sayfa ile bir yatay sayfanın piksel tamponları bayt düzeyinde özdeştir ve anahtar içinde boyutlar olmasaydı birbirlerine çapa olurlardı. İki taraftaki özdeş anahtarlar çapaya dönüşür, LCS bunların en uzun sıralı zincirini seçer ve iki ardışık çapa arasındaki sayfalar sırayla eşleştirilir. Bir boşluk bir tarafta daha fazla sayfa tuttuğunda, fazlalık added ya da removed olarak raporlanır

var
  Options: TPDFVisualCompareOptions;
  Report: WideString;
begin
  // Baseline ve Candidate önceki gibi yüklendi, eşleşen render ayarlarıyla
  Options := PLDefaultVisualCompareOptions;
  Options.ColorTolerance := 8;
  Options.TileSize := 64;
  Report := Baseline.CompareDocumentVisualsWith(Candidate, 96, Options);
  if Baseline.LastErrorCode <> 0 then
    Writeln('Visual diff failed: ', Report)
  else
    Writeln(Report);
end;

Çapalar bit düzeyinde özdeşliktir ve bu bir ihmal değil, bilinçli bir tasarım tercihidir. ColorTolerance yalnızca eşleştirilmiş sayfaların kıyasına uygulanır, asla parmak izine değil. Yalnızca anti-aliasing gürültüsü içeren bir sayfa çapa olmaz, ama yine de bir boşluğa düşer, karşılığıyla sırayla eşleştirilir ve tolerans altında unchanged olarak kıyaslanır. Bulanık çapalar daha affedici ve çok daha öngörülemez olurdu; çünkü birbirine yakın kopya sayfalar (boş ayraçlar, tekrarlayan koşullar sayfaları) birbirleriyle eşleşmeye başlardı

Bu hizalamanın ne olduğuna net bakın. JSON, ona bilerek exact-raster-lcs-gap-order adını verir: bu bir raster hizalamasıdır, semantik sayfa tanıma değil. Bir boşluk yeniden yazılmış bir sayfa ile eklenmiş bir sayfa içeriyorsa, sırayla eşleştirme yeniden yazımı eklemeyle kıyaslayabilir ve özgünü çıkarılmış olarak raporlayabilir. Özdeş sayfalarla dolu belgeler benzer biçimde belirsizdir. Eşleştirme bir kararda önem taşıdığında yalnızca sayaçlara değil, çiftlere bakın

JSON raporunu okumak

Belge raporu, bir test koşucusunda üzerinde iddia kurabileceğiniz sabit bir şekle sahiptir. Başarı durumunda "status":"complete", "complete":true, hizalama adı, firstPageCount, secondPageCount ve changedPages, addedPages, removedPages ile unchangedPages sayaçlarını taşır. pairs dizisi ardından her satırı, firstPage, secondPage (eksik taraf için 0) ve unchanged, changed, size_changed, added ya da removed değerlerinden biri olan kind ile listeler

changed ve unchanged türündeki satırlar, width, height, comparedPixels, changedPixels, ignoredPixels, changedRatio, boundingBox (hiçbir şey değişmediğinde null) ve bounds artı changedPixels içeren bir regions listesiyle bir comparison nesnesi gömer. Bir size_changed satırının kıyası hiç yoktur; firstWidth, firstHeight, secondWidth ve secondHeight bildirir ve changedPages'e sayılır. Boyutu değişen bir sayfayı kıyaslamak için germek anlamsız değişen piksellerden bir duvar üretirdi; bu yüzden kütüphane geometri değişimini raporlar ve orada durur

Belge raporu asla mask ya da overlay içermez. CompareDocumentVisualsWith, seçenekleriniz onları istese bile IncludeMask ile IncludeOverlay seçeneklerini kapatır; böylece bellek, her çift için tam sayfa önizleme tutmak yerine sayfa sayısı büyürken düz kalır. Raporu ilginç sayfaları bulmak için kullanın, sonra overlay'i almak için yalnızca o sayfalarda CompareRenderedPagesWith çağırın. Başarısızlıklar, bir error metniyle "status":"error" ve "complete":false olarak geri döner ve LastErrorCode 401'e ayarlanır

CompareFileVisuals ile bir dosyaya karşı kıyaslama

TPDFlib.CompareFileVisuals, CI için tek çağrılık biçimdir: diğer PDF'yi yükler, geçerli render ayarlarını üzerine kopyalar ve aynı belge JSON'ını döndürür. İmza CompareFileVisuals(OtherFileName, Password, DPI, ColorTolerance) biçimindedir; tolerans 0-255 arasında ve DPI sonlu ve pozitif olmalıdır

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('baseline\statement.pdf', '') = 1 then
    begin
      Report := Lib.CompareFileVisuals('build\statement.pdf', '', 96, 8);
      Writeln(Report);
    end;
  finally
    Lib.Free;
  end;
end;

Render kalıtımı, ikinci dosyayı kendiniz yüklemek yerine bu çağrıyı tercih etmenizin nedenidir. Yüklenen belge, geçerli örneğin render ölçeğini, crop türünü ve grafik ayarlarını, ayrıca derlenmişlerse Cairo ya da PDFium motor seçimini devralır. CompareDocumentVisualsWith için iki TPDFlib örneğini elle kurarsanız ikisini de özdeş yapılandırmak size kalmıştır ve bir uyuşmazlık, her sayfa değişmiş olarak görünür. Bu çağrı ayrıca yok sayma bölgesi alamaz; onlara ihtiyacınız olduğunda tipli API'ye uzanın

Sınırlar, maliyet ve yapısal diff'in yeri

PDFlibPas işi açıkça sınırlandırır; böylece düşmanca ya da devasa bir girdi, belleği tüketmek yerine bir hata ile başarısız olur. Render edilen bir sayfa 64 Mi pikseli aşamaz, kare ızgarası 4 Mi kareyle sınırlıdır ve sayfa hizalaması, iki sayfa sayısının toplamı 8192'yi geçen ya da LCS matrisi 4 Mi hücreyi aşan girdileri reddeder. Belge karşılaştırması ayrıca her sayfayı iki kez render eder: bir kez parmak izi için, bir kez de eşleştirilmiş kıyas için; 150 DPI ve üzerinde bunu bütçeye yazın. Çoğu regresyon seti için 72-96 DPI, yerleşim ve metin değişikliklerini zamanın küçük bir kesriyle yakalar

Görsel diff ile yapısal diff farklı sorulara cevap verir ve ciddi bir hat genellikle ikisini de ister. Piksel kıyası, bir okuyucunun bir şey fark edip etmeyeceğini söyler. İmzalı bir belgenin sayfa dışına render edilen bir ek açıklama kazanıp kazanmadığını ya da artımlı bir güncellemenin bir form alanı değerini aynı görünür metinle sessizce değiştirip değiştirmediğini söyleyemez. O katman için bir PDF imzalandıktan sonra neyin değiştiğini sınıflandırma yazısı revizyon seviyesindeki diff'i anlatır. Ve pikselleri render motoru seçimi belirlediği için, bir temeli tek motora sabitlemeden önce Delphi'de çok motorlu PDF rendering yazısını okumaya değer

FAQ

PDFlibPas görsel karşılaştırması boyutu değişen sayfaları gerer mi?

Hayır. CompareRenderedPagesWith, farklı piksel boyutlu sayfalar için hata döndürür ve belge raporu böyle çiftleri kıyaslamak yerine her iki boyutla size_changed olarak işaretler

Birbirine yakın özdeş sayfalar neden çapa olarak kullanılmaz?

Çapalar, görünür piksellerin ve sayfa boyutunun tam bir SHA-256 parmak izini kullanır; böylece hizalama öngörülebilir kalır. Tolerans sonradan, eşleştirilmiş sayfalar kıyaslandığında uygulanır; bu yüzden yalnızca gürültü içeren sayfalar da unchanged olarak sonuçlanır

Bir belgenin her sayfası için overlay görüntüsü alabilir miyim?

Belge çağrısından alamazsınız; o, belleği düz tutmak için maskleri ve overlayleri bilerek dışarıda bırakır. JSON'u değişen sayfaları bulmak için kullanın, sonra o sayfalarda IncludeOverlay ile CompareRenderedPagesWith çağırın

Görsel karşılaştırma, sayfa eşleştirme ve rendering ile belge API'lerinin geri kalanı Delphi için PDFlibPas PDF kütüphanesi ile birlikte gelir