Teknik Makale

Delphi'de PDF Ondalık Hassasiyetini Kayıtta Korumak

losLab PDF Developer Library olan PDFlibPas, bir belgedeki her gerçek sayı için ayrıştırdığı ondalık metni aynen saklar ve değer hiç değiştirilmediyse o metni olduğu gibi geri yazar. v3.539.19'dan bu yana SetPrecision ayarı yalnızca kütüphanenin oluşturduğu ya da düzenlediği sayıları yönetir; böylece sıradan bir yükle-kaydet işlemi, 2.22221 değerindeki bir CalRGB /Gamma değerini 2.2222 değerine yuvarlamaz ve kimsenin dokunmadığı bir sayfanın renklerini kaydırmaz. Değişiklik kodda küçük, ayrıştırıcılar hakkında söyledikleri bakımından büyüktür: çözdüğünüz değer ile ürettiğiniz literal iki farklı şeydir ve bir Double gidiş dönüşü bir birim dönüşümü değildir

Hiçbir şeyi değiştirmeyen bir kayıt sayfa renklerini neden kaydırdı?

Çünkü yeniden biçimlendirilen görüntü değil renk uzayı parametreleriydi. Bunu açığa çıkaran dosya, yerel regresyon korpusundaki 35 sayfalık bir ofis belgesi; her sayfada yeniden kullanılan bir başlık görüntüsü var. Belgeyi yükleyip doğrudan geri kaydetmek, girdiyle bayt bayt aynı görüntü akışları üretti ve akış hash'i karşılaştırması belgeyi değişmemiş olarak bildirdi. Render edilmiş bir karşılaştırma aynı fikirde değildi: 35 sayfanın her biri başlıkta piksel farkı gösteriyordu ve başka hiçbir yerde göstermiyordu

Başlık görüntüsü, ISO 32000-1 §8.6.5.3'ün bir /WhitePoint, isteğe bağlı üç elemanlı bir /Gamma dizisi ve isteğe bağlı dokuz elemanlı bir /Matrix ile tanımladığı bir CalRGB renk uzayı üzerinden çizilir. Bu diziler renk uzayı sözlüğündeki sıradan sayısal nesnelerdir. TPDFNumeric her birini bir Double olarak saklıyordu, başka bir şey olarak değil; TPDFNumeric.Output ise o Double değerini varsayılanı dört ondalık basamak olan PDFPrecNum üzerinden biçimlendiriyordu. Böylece /Gamma 2.22221 değerinden 2.2222 değerine, bir matris girdisi 0.71519 değerinden 0.7152 değerine indi ve render edici, biraz farklı kalibrasyondan sadakatle biraz farklı renkler üretti. Görüntü baytları masumdu; çevrelerindeki sayılar değildi. Can sıkıcı olan, bunun ne kadar görünmez olduğudur. Çözülmüş akış baytlarını karşılaştırmak bunu göremez, çünkü sayılar bir akışta değil bir sözlükte yaşar. Ek dosya yüklerini karşılaştırmak da göremez. Değişiklik düzeyi yazısında anlatılan revizyon farkı bile normalleştirilmiş bir nesne gövdesinin parmak izini alır, dolayısıyla iki revizyon da aynı değere hash'lenir ve fark onları özdeş bildirir. Bunu yalnızca render etme yakaladı; korpus temel çizgisinin her sayfayı tek tek render etmesinin ve yalnızca yapısal kontrollere güvenmemesinin sebebi budur

PDFlibPas'ın Delphi'de no-op kayıtta CalRGB hassasiyetini nerede yitirdiği: ayrıştırılan /Gamma 2.22221 ve 0.71519 matris girdisi TPDFNumeric içinde bir Double olarak durur, Output bunları PLDoubleToStr üzerinden PDFPrecNum dört ondalık basamakla biçimlendirir, her yapısal kontrol belgeyi değişmemiş bildirir ve yalnızca render karşılaştırması 35 başlık görüntüsünün de kaydığını gösterir
Görüntü baytları masumdu: TPDFNumeric çevrelerindeki kalibrasyon sayılarını PDFPrecNum üzerinden yeniden biçimlendirdi, bu yüzden akış hash'leri ve parmak izi farkı iki revizyonu da özdeş bildirdi; oysa render edici her sayfada biraz farklı renk üretiyordu

Ayrıştırdığınız değer, yazmanız gereken literal değildir

Bir PDF gerçek sayısı bir ondalık dizedir ve ISO 32000-1 §7.3.3 bunun yalnızca bir ondalık dize olduğunu açıkça söyler: taban gösterimi yok, üs biçimi yok. Ek C ise bir uygulamanın uyması beklenen hassasiyeti, ondalık kısımda yaklaşık beş anlamlı ondalık basamak olarak listeler. Varsayılan dört basamaklı çıktı hassasiyeti bunun zaten altındadır ve sıfıra yakınken durum daha da kötüleşir: PLDoubleToStr değeri ölçekler, bir tamsayıya yuvarlar ve sonuç sıfır çıkarsa 0 yazar; dolayısıyla -0.000012345 değerindeki bir matris girdisi bir basamak kaybetmez, tamamen kaybolur

Varsayılanı yükseltmek uçurumu yalnızca öteler. Düzeltme, bir Double'ın sayının kendisi olduğu numarasını bırakmaktır. TPDFStructure.Decode içindeki tokenizer standart bir gerçek sayı tanıdığında, yani token bir ondalık nokta içerip üs işareti içermediğinde, kaynak metni dönüştürülmüş değerin yanı sıra yeni FOriginalText alanında saklar. Output ardından o metni tercih eder ve tercih edecek bir şey olmadığında biçimlendirmeye geri döner

PDFlibPas'ın Delphi'de ayrıştırılmış ondalık metni nasıl koruduğu: TPDFStructure.Decode içindeki tokenizer, ondalık nokta içerip üs içermeyen her token için kaynak literal değeri FOriginalText içinde tutar, Output PLDoubleToStr çağırmak yerine o metni aynen yazar, SetTo ise onu temizler çünkü düzenlenmiş bir sayı yeni bir sayıdır
Çözdüğünüz değer ile ürettiğiniz literal iki farklı şeydir: ayrıştırılmış metni tercih etmek 2.22221 değerini tam tutar; kütüphanenin oluşturduğu ve düzenlediği sayılar ise yine PDFPrecNum'a uyar ve ayar el değmemiş girdiye hiç ulaşmaz
// Lib/PDFlibStruct.pas — düzeltmenin çıktı tarafındaki tamamı
Function TPDFNumeric.Output: AnsiString;
Begin
  If FOriginalText<> '' Then
    Result:= FOriginalText
  Else
    Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;

Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
  FOriginalText:= '';   // düzenlenmiş bir sayı yeni bir sayıdır
  FValue:= Value;
  FChanged:= True;
End;

İki sınır bilinçlidir. Tamsayılar korunmaz, çünkü tamsayı biçimlendirmesi zaten kayıpsızdır. 6.02E23 gibi üs biçimleri, bozuk üreticiler hatırına girdide hoş görülür ama çıktıda korunmaz, çünkü onları geri yazmak §7.3.3'ün yasakladığı bir sözdizimini sürdürmek olurdu; kütüphanenin ürettiği her sayı gibi onlar da biçimlendiriciden geçer. Tokenizer ayrıca metni saklamadan önce her zamanki asgari onarımını uygular, böylece .5 gibi baştan noktalı bir literal 0.5, 5. gibi sondan noktalı bir literal ise 5.0 olarak tutulur. İkisi de her okuyucu için aynı sayıdır ve çok daha yaygın kabul görür

v3.539.19'dan sonra SetPrecision neyi garanti eder?

TPDFlib.SetPrecision artık kütüphanenin kendi ürettiği sayıların ondalık basamaklarını denetler: ressam üzerinden çizilen değerler, NewNumeric gibi bir yoldan bir Double'dan oluşturulan sayılar ve o zamandan beri SetTo ile düzenlenmiş her ayrıştırılmış değer. Şunu da not edin: nesne API'si üzerinden çözülen metin, örneğin SetObjectFromString fonksiyonuna verilen bir literal, aynı tokenizer'dan geçer ve aynı şekilde korunur. Hiç değiştirilmemiş ayrıştırılmış bir ondalık, ayardan bağımsız olarak girdi hassasiyetini korur ve ayarı yüklemeden sonra değiştirmek onu geriye dönük etkilemez. SetPrecision referans girdisi de aynı sürümde tam bunu söyleyecek biçimde güncellendi, çünkü eski ifade ayarın dosyadaki her sayıya uygulandığını ima ediyordu

Temizleme, Changed bayrağından türetilmek yerine SetTo içinde yapılır ve bu ayrım önemlidir. Kayıt hattı, nesneler yazıldıktan sonra onların Changed bayrağını sıfırlar; dolayısıyla “değişmediyse özgün metni yaz” biçiminde bir kontrol, aynı oturumda düzenlenip kaydedilen ve yeniden düzenlenen bir değer için bayat metin yazmaya başlardı. Özgün metni doğrudan atamanın kendisine bağlamak, ikisinin çelişmesini olanaksız kılar. Regresyon testi bu davranışların her birini özgün dosyadaki değerlerle sabitler

uses
  PDFlibStruct;

var
  Structure: TPDFStructure;
  Values: TPDFArray;
  Number: TPDFNumeric;
begin
  Structure := TPDFStructure.Create;
  try
    Structure.PDFPrecNum := 4;
    Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
    // Düzenlenmemiş girdi aynen korunur; dört basamaklı
    // biçimlendirmenin 0'a indireceği değer de buna dahil
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // Bir düzenleme özgün metni atar ve PDFPrecNum'a uyar
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Hassasiyeti sonradan düşürmek düzenlenmemiş girdiye ulaşmaz
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

İçerik modeli sayıları neden hâlâ normalleştiriyor?

Çünkü TPDFContentProgram kurallı sayısal operandlar vaat eder ve o vaat, bir içerik akışının içinde aynen korunan metinden daha değerlidir. Grafik durumu izleyicisinin üzerine kurulduğu düzenlenebilir içerik modeli, NormalizeContentStreams, iyileştirici ve Emit fonksiyonlarının rastgele girdiden kararlı ve karşılaştırılabilir çıktı üretmesi için vardır. Ayrıştırılmış bir operand özgün metnini modele taşısaydı, 0.50000 0 0 RG gibi bir operatör dizisi 0.5 0 0 RG dizisinden farklı yazılır ve aşağı yöndeki her karşılaştırma üreticinin biçimlendirme alışkanlıklarıyla birlikte kayardı

Bu yüzden model özgün metni iki giriş noktasında temizler. NormalizeContentNumbers, ayrıştırıcı her operandı yerleştirirken ve çağıranın verdiği kaynak çözüldüğünde SetOperand içinde bir kez daha çalışır; dash desenleri, TJ dizileri ve işaretli içeriğin özellik sözlükleri kapsansın diye diziler ile sözlüklerin içine özyinelemeyle iner. Her sayısal üzerinde SetTo(AsDouble) çağırmak yeterlidir, çünkü metni temizleyen işlem tam olarak budur. Ham satır içi görüntü verisine, her zaman olduğu gibi, dokunulmaz

PDFlibPas içerik modeli neden sayıları hâlâ normalleştirir: NormalizeContentNumbers, ayrıştırıcının her operandı yerleştirdiği noktada ve bir kez daha SetOperand içinde çalışır, dash desenleri, TJ dizileri ve işaretli içerik özellik sözlükleri kapsansın diye diziler ile sözlüklerin içine iner ve SetTo AsDouble özgün metni temizlediği için 0.50000 ile 0.5 özdeş yazılır
Kurallı sayısal operandlar içerik modelinin vaadidir: ham satır içi görüntü verisi el değmemiş bırakılır ve içerik akışlarının dışındaki dokunulmamış sözlük sayıları aynen korunma garantisini sürdürür; sade bir LoadFromFile ile SaveToFile çifti onları hâlâ korur
// Lib/PDFlibContentModel.pas — içerik modeli sözleşmesini korur
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
  K: Integer;
Begin
  If Obj is TPDFNumeric Then
    TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
  Else If Obj is TPDFArray Then
    For K:= 0 To TPDFArray(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFArray(Obj).Item[K])
  Else If Obj is TPDFDictionary Then
    For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
      NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;

Çağıranlar için pratik kural bu yüzden basittir. Düz bir LoadFromFile ardından SaveToFile, dokunulmamış içerik akışlarını ve dokunulmamış sözlük sayılarını oldukları gibi bırakır. NormalizeContentStreams üzerinden geçen bir sayfa ya da içerik modeli üzerinden yapılan herhangi bir düzenleme, tasarım gereği kurallı olarak çıkar; belgenin geri kalanı yine korunur. Bunlar iki farklı istedir ve artık iki farklı şey yaparlar

Maliyeti ne ve garanti nerede bitiyor

Her TPDFNumeric artık bir fazladan AnsiString başvurusu taşır ve ayrıştırılan her ondalık, nesnenin ömrü boyunca kaynak metnini canlı tutar. Milyonlarca gerçek sayı içeren bir belgede bu gerçek bir bellektir ve geçiştirilmek yerine her büyük belge ölçümüne girmelidir. Garanti ayrıca bir sayının kendi belgesiyle sınırlıdır: nesneleri belgeler arasında kopyalamak ya da nesne API'si üzerinden değerleri yeniden kurmak yeni sayılar üretir ve onlar da her yeni sayı gibi çıktı hassasiyetine uyar. Sürümün neyi iddia edip neyi etmediği konusunda kesin olmakta fayda var. Dokunulmamış bir belgenin yüklenip kaydedilmesi artık render edicinin gerçekten tükettiği kalibrasyon sayılarını korur ki korpus temel çizgisinin kontrol ettiği özellik budur. Bayt bayt özdeş çıktı iddia edilmez; o ayrıca nesne numaralandırmasına, akış sıkıştırmasına ve belirlenimci PDF ID yazısında tartışılan trailer tanımlayıcısına bağlıdır. Ve başka bir yazılımın ürettiği dosyalardaki yuvarlama farklarını parmak izi karşılaştırmasına göstertmez, çünkü orada hâlâ normalleştirilmiş gövde hash'lenir. Ders CalRGB'nin çok ötesine genellenir: bir ayrıştırıcı yalnızca dönüştürülmüş değeri saklıyorsa her kayıt bir düzenlemedir ve bunu fark etmenin tek yolu render edilmiş sonuca bakmaktır. Sayı işleme ve SetPrecision semantiği losLab PDF Developer Library ürün sayfasında belgelenmiştir