Teknik Makale

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

Kalibre edilmiş bir PDF yükleyin, hiçbir şey değiştirmeyin, yeni adla kaydedin ve renk uzayı sözlüğünü diff'leyin: /Gamma [2.22221 2.22221 2.22221] girdi, /Gamma [2.2222 2.2222 2.2222] çıktı. Yazıcı, dokunması hiç gerekmeyen bir sayıyı yeniden biçimlendirdi ve sayfa artık hafifçe farklı renklerde render oluyor. Çare, ayrıştırılan her ondalığı aynı anda iki şey olarak görmek — aritmetik için bir değer, çıktı için bir literal — ve biri gerçekten yeni bir değer atayana kadar literali yazmaya devam etmektir

No-op bir kayıt CalRGB renklerini neden kaydırabilir

Pikseller kayar; çünkü görüntü baytları özdeş kalırken render ediciye hafifçe farklı kalibrasyon verisi verilir. ISO 32000-1 §8.6.5.3, bir CalRGB renk uzayını zorunlu bir /WhitePoint, isteğe bağlı üç elemanlı bir /Gamma dizisi ve isteğe bağlı dokuz elemanlı bir /Matrix ile tanımlar; hepsi renk uzayı sözlüğündeki düz sayısal nesnelerdir. Bunları bir Double'dan ve dört ondalık basamağa sabitlenmiş bir biçimlendiriciden geçirin: 2.22221, 2.2222 olur; 0.71519 değerindeki bir matris girdisi 0.7152 olur. Gamma değeri, o uzaydan geçen her pikselin her bileşenine uygulanan bir üstür; değişiklik uzayın kullanıldığı her yerde görünür

35 sayfada yeniden kullanılan bir başlık görüntüsü için tek bir CalRGB uzayı bildiren bir dosyada 35 başlığın hepsi kaymış renklerle render olur ve o sayfalarda başka hiçbir şey değişmez. İnsanların önce uzandığı denetimler temiz döner. Çözülmüş akış baytlarının hash'i görüntü verisini karşılaştırır ve görüntü verisi gerçekten özdeştir; hasar bir sözlükte oturur. Değişiklik düzeyi makalesinde anlatılan revizyon diff'i, normalleştirilmiş bir nesne gövdesinin parmak izini alır; iki revizyon orada da aynı değere hash'lenir. Bunu yakalayan, render edilmiş sayfa karşılaştırmasıdır. Başka yazılımların ürettiği PDF'leri yeniden yazan her hat için sayfa başına bir render karşılaştırmasını her yapısal denetimin önüne koyardık

Yalnızca Double tutan bir sayı modelinin Delphi'de no-op kayıtta CalRGB hassasiyetini nasıl yitirdiği: ayrıştırılan /Gamma 2.22221 ve 0.71519 matris girdisi Double olarak saklanıp çıktıda dört ondalık basamakla biçimlendirilir, her yapısal denetim 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ı masumdur: çevrelerindeki kalibrasyon sayılarını sabit bir hassasiyette yeniden biçimlendirmek yeter; akış hash'leri ve parmak izi diff'i iki revizyonu da özdeş bildirirken render edici her sayfada biraz farklı renk üretir

Bir PDF gerçel sayısı neye benzeyebilir

ISO 32000-1 §7.3.3 gerçel sayıyı ondalık bir dize olarak tanımlar: isteğe bağlı işaret, rakamlar, nokta, yine rakamlar. Taban gösterimi yok. Üstel biçim yok. Uygulama sınırlarını listeleyen Ek C, kesirli kısımda kabaca beş anlamlı ondalık basamağın korunmasını bekler. Dört olan varsayılan çıktı hassasiyeti bu yüzden spesifikasyonun okuyuculara tutmasını söylediğinin bir basamak altındadır ve dosya beş kullanmakla yanlış bir şey yapmamıştır

Sıfıra yakınken naif bir biçimlendirici bir basamak kaybetmekten daha kötüsünü yapar. Bir Double'ı N basamakla yazmanın yaygın yolu 10^N ile çarpmak, tamsayıya yuvarlamak ve rakamları yazmaktır; o tamsayı sıfırken çıktı 0 olur. 0.00001 0 0 1 0 0 cm'in ilk operandını dört basamakla verin, sonuç 0 0 0 1 0 0 cm olur: altında çizilen her şeyi yok eden tekil bir matris. -0.000012345 değerindeki bir /Matrix girdisi de aynı yolu izler

PDF Library for Delphi (PDFlibPas) bir Double'ı kendisi biçimlendirmek zorunda kaldığında, hassasiyet adımından küçük sıfır olmayan bir değer için 0 yazmak yerine istenen sayıda anlamlı basamağı korur. Dört basamakta 0.00001, 0.00001 olarak; 0.0000123, 0.0000123 olarak yazılır; yalnızca kabaca 5E-16 altındaki büyüklükler hâlâ 0 çıkar. Bu, yeni yazılan sayıları aklı başında tutar. Yeniden biçimlendirilmesi hiç gerekmeyen ayrıştırılmış bir sayı başka bir meseledir ve kütüphane onu hiç yeniden biçimlendirmeyerek halleder

PDFlibPas ayrıştırdığı literali nasıl korur

TPDFStructure.Decode, belirteçlere ayırdığı her standart gerçel sayının — yani ondalık nokta taşıyan ve üs işareti taşımayan her belirtecin — kaynak metnini dönüştürülmüş Double'ın hemen yanında tutar. TPDFNumeric.Output o metin varsa onu geri yazar; Double'ı yapının PDFPrecNum değeriyle ancak metin yokken biçimlendirir. TPDFNumeric.SetTo metni atar; çünkü düzenlenmiş sayı yeni sayıdır ve geçerli çıktı hassasiyetine uymalıdır

PDFlibPas ayrıştırılan ondalık metni Delphi'de nasıl korur: TPDFStructure.Decode içindeki belirteçleyici, ondalık nokta taşıyan ve üs taşımayan her belirteç için kaynak literali dönüştürülmüş değerin yanında tutar, Output o metni Double'ı PDFPrecNum ile yeniden biçimlendirmek yerine olduğu gibi yazar, SetTo onu temizler çünkü düzenlenmiş sayı yeni sayıdır
Çözdüğünüz değerle yazdığınız literal iki ayrı şeydir: ayrıştırılan metni yeğlemek 2.22221'i tam tutar, kütüphanenin yarattığı ve düzenlenen sayılar yine PDFPrecNum'a uyar ve ayar dokunulmamış girdiye asla ulaşmaz

Sıfırlamayı SetTo'ya bağlamak bilinçliydi. Cazip kısayol, nesnenin Changed bayrağı kurulu olmadıkça özgün metni yazmaktır; o bayrak zaten her nesnede vardır. Ne var ki kayıt hattı bir nesne yazıldığında Changed'i temizler; aynı oturumda düzenlenip kaydedilen ve yeniden düzenlenen bir değer ikinci kayıtta bayat literalini yazmaya geri dönerdi. Metni atamanın kendi içinde temizlemek, literal ile değerin asla çelişmemesi demektir

Birkaç sınır bilinçlidir. Tamsayılar metin olarak saklanmaz; çünkü tamsayı biçimlendirmesi zaten kayıpsızdır. 6.02E23 gibi üstel biçimler, onları yine de yazan üreticiler uğruna girdide kabul edilir; ama çıktıda kütüphanenin ürettiği herhangi bir sayı gibi biçimlendirilir, çünkü onları geri kopyalamak §7.3.3'ün dışladığı bir söz dizimini sürdürürdü. Belirteçleyici, metni saklamadan önce her zamanki küçük onarımını da uygular: .5 gibi baştaki noktalı literal 0.5 olarak, 5. gibi sondaki noktalı literal 5.0 olarak tutulur. Her okuyucu iki çifti de aynı sayı sayar ve onarılmış yazım çok daha geniş kabul görür

Aşağıdaki parça CalRGB değerlerini doğrudan nesne katmanından geçirir; böylece her kural PDF dosyası açmadan görülebilir. Assert satırları kendi testlerinize eklemenizi önerdiğimiz denetimlerdir; kütüphaneye yapılan çağrılar değildir

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]'));

    // Kendi denetiminiz: düzenlenmemiş girdi olduğu gibi sağ çıkar; naif dört
    // basamaklı bir biçimlendiricinin 0'a çökerteceği değer dâhil
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // 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');

    // Minik yeni değerler 0 olmak yerine anlamlı basamakları korur
    Assert(Structure.NewNumeric(0.0000123).Output = '0.0000123');

    // 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;

Böyle assertion'lar yazarken bir tuzak: Delphi'nin Round'u bankacı yuvarlaması kullanır ve 0.000012345 * 1e8, ikilikte 1234.5 değil 1234.4999… olarak hesaplanır. Sıfıra yakın biçimlendirme yolunu denetlerken o değere tam olarak çarptık. Bir assertion son basamağın nasıl yuvarlandığına bağlıysa ikilik orta nokta olmayan test girdileri seçin

SetPrecision neyi denetler?

TPDFlib.SetPrecision, kütüphanenin yazdığı sayıların ondalık basamak sayısını belirler: painter üzerinden çizilen grafikler, NewNumeric gibi yollarla Double'dan yaratılan sayılar ve SetTo ile sonradan değiştirilen ayrıştırılmış değerler. 2'den 8'e kadar kabul eder, aralık dışı her şeye 0 döndürür. Kimsenin düzenlemediği ayrıştırılmış bir ondalık, ayar ne olursa olsun girdi metnini korur ve ayarı LoadFromFile'dan sonra değiştirmek ona geriye dönük ulaşmaz. SetObjectFromString'e geçilen kaynak da aynı belirteçleyiciden geçer ve aynı biçimde korunur

Önerimiz, ürettiğiniz geometri daha ince adımlar istemedikçe SetPrecision'a dokunmamaktır. Girdi dosyalarını korumak için yükseltmek hiçbir şey kazandırmaz; dokunulmamış girdi zaten kendi basamaklarını korur ve çizdiğiniz her sayıyı uzatır. Beş basamak point altı çizim işleri için makul bir seçimdir; sekiz çoğunlukla içerik akışlarını şişirir

uses
  PDFlibrary;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('calibrated.pdf', '') <> 1 then
      Exit;

    // Bundan sonra yazılan sayıları yönetir; ayrıştırılan /Gamma,
    // /Matrix ve /WhitePoint dizileri özgün metnini korur
    PDF.SetPrecision(5);

    PDF.SelectPage(1);
    PDF.SetLineWidth(0.123456);       // çizilen sayılar beş basamak kullanır
    PDF.DrawLine(72, 72, 300, 72);

    PDF.SaveToFile('calibrated-out.pdf');
  finally
    PDF.Free;
  end;
end;

İçerik akışları: düzenlemeler operandları korur, normalleştirme isteğe bağlı

Düzenlenebilir içerik modeli varsayılan olarak operand metnini korur ve sayıları yalnızca kanonik çıktı çağrının amacı olduğunda kanonikleştirir. TPDFContentProgram.CanonicalNumbers varsayılan olarak False'tur; ParsePageContentLayer'ın döndürdüğü bir katman — grafik durumu izleyicisinin üzerine kurulduğu modelin aynısı — dokunulmamış her sayısal operandı yazıldığı gibi yeniden yazar. Bütün bir katmanı yeniden yazan sayfa düzenlemeleri — ScalePage, TransformPageElement, DeletePageElement, RedactRegion, ConvertTextToPaths ve WritePageContentLayer ile geri yazdığınız kendi programınız dâhil — yalnızca düzenlemenin dokunduğu operandları değiştirir

Öbür varsayılan daha derli toplu görünür: her ayrıştırmada kanonikleştir, böylece düzenleme yolu ile NormalizeContentStreams hep aynı yazımı üretsin. Düzenleme için bunun yanlış varsayılan olduğunu düşünüyoruz. 0.123456 w içeren bir katmanda ScalePage çağrısı 0.1235 w yazar ve katmandaki öbür her operand da yeniden yazılırdı; oysa çağıran yalnızca ölçekleme istemişti. Kanonik biçim, amacı karşılaştırılabilir çıktı olan işlemlere aittir

O işlemler onu yine alır. NormalizeContentStreams ve kayıt anındaki içerik iyileştiricisi CanonicalNumbers'ı True kurarak ayrıştırır; çünkü işleri keyfi girdiden kararlı çıktı üretmektir. 0.50000 0 0 RG ile 0.5 0 0 RG farklı yazılsaydı her aşağı akış karşılaştırma üreticinin biçimlendirme alışkanlıklarıyla kayardı. Kanonikleştirme dizilerde ve sözlüklerde özyineler; böylece dash desenleri, TJ dizileri ve marked-content özellik sözlükleri kapsanır, ham inline görüntü verisine ise dokunulmaz. SetOperand ile verilen operandlar bayrak ne derse desin kanonikleştirilir. Bir içerik programı, SetPrecision'dan bağımsız, dört ondalık basamağa sabit kendi sayı deposunu taşır; düzenlenen operand dört basamakla çıkar. Bayrağı yalnızca Parse okur; yani ayrıştırmadan önce kurulmalıdır. ParsePageContentLayer'ın çoktan ayrıştırdığı bir program için bayrağı kurun ve Parse(Emit) çağırın

PDFlibPas içerik modeli sayıları ne zaman normalleştirir: CanonicalNumbers True iken her sayısal operand ayrıştırıcı onu iterken kanonikleştirilir, SetOperand ile geçilen operandlar her zaman kanonikleştirilir, geçiş dizilerde ve sözlüklerde özyineler böylece dash desenleri, TJ dizileri ve marked-content özellik sözlükleri kapsanır ve SetTo AsDouble özgün metni temizler böylece 0.50000 ile 0.5 aynı yazılır
Kanonik sayısal operandlar NormalizeContentStreams ile iyileştiricinin istediğidir: ham inline görüntü verisine dokunulmaz ve içerik akışlarının dışındaki dokunulmamış sözlük sayıları olduğu gibi kalma güvencesini korur; düz bir LoadFromFile ve SaveToFile çifti onları yine korur
uses
  PDFlibrary, PDFlibContentModel, PDFlibContentOps;

var
  PDF: TPDFlib;
  Prog: TPDFContentProgram;
  I: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('calibrated.pdf', '') <> 1 then
      Exit;
    PDF.SelectPage(1);
    Prog := PDF.ParsePageContentLayer(1);
    if Prog = nil then
      Exit;
    try
      // Bir çizgi genişliğini değiştir; öbür her operand kaynak metnini korur
      I := Prog.FindNextOp(0, coLineWidth);
      if I >= 0 then
        Prog.SetOperand(I, 0, '0.123456');   // 0.1235 olarak yazılır
      PDF.WritePageContentLayer(1, Prog);

      // Yalnızca karşılaştırma için kanonik kopya, geri yazılmaz:
      // bayrağı yalnızca Parse okur; kurduktan sonra yeniden ayrıştır
      Prog.CanonicalNumbers := True;
      Prog.Parse(Prog.Emit);
      // Prog.Emit artık 0.50000 ile 0.5'i aynı yazar
    finally
      Prog.Free;
    end;
    PDF.SaveToFile('calibrated-out.pdf');
  finally
    PDF.Free;
  end;
end;

Bir araya getirince çağıranlar için kural kısadır. Düz bir LoadFromFile ardından SaveToFile, dokunulmamış içerik akışlarını ve dokunulmamış sözlük sayılarını olduğu gibi bırakır. İçerik modeliyle düzenlenen bir sayfa, düzenlemenin değiştirmediği her operandın metnini korur. NormalizeContentStreams, içerik kayıtta sıkıştırıldığında koşan iyileştirici ve CanonicalNumbers'ı True kurulmuş olarak ayrıştırılan her program tasarım gereği kanonik sayılar üretir; belgenin geri kalanı yine korunur

Maliyeti ne ve garanti nerede bitiyor

Her TPDFNumeric bir fazladan AnsiString başvurusu taşır ve ayrıştırılan her ondalık, nesne yaşadığı sürece kaynak metnini canlı tutar. Milyonlarca gerçel sayı taşıyan bir belgede bu ölçülebilir bellektir ve koştuğunuz her büyük belge benchmark'ına girmelidir. Güvence ayrıca sayının kendi belgesiyle sınırlıdır. Nesneleri belgeler arasında kopyalamak ya da nesne API'siyle değerleri yeniden kurmak yeni sayılar üretir ve yeni sayılar çıktı hassasiyetine uyar

Korunan sayılar ile bayt bayt özdeş çıktı ayrı özelliklerdir. Dokunulmamış bir belgenin yüklenip kaydedilmesi render edicinin tükettiği kalibrasyon sayılarını korur; ama bayt özdeşliği ayrıca nesne numaralandırmasına, akış sıkıştırmasına ve belirlenimci PDF ID makalesinde işlenen trailer tanımlayıcısına bağlıdır. Parmak izi diff'i normalleştirilmiş gövdeyi hash'lemeye devam eder; yani başka araçların yazdığı dosyalardaki yuvarlama farkları onun karşılaştırdığının dışındadır. Onlar için denetim, render karşılaştırması olmaya devam eder

Sayı işleme ve kesin SetPrecision semantiği, yukarıda kullanılan içerik modeli çağrılarıyla birlikte losLab PDF Developer Library başvurusunda işlenir