Tehnički članak

Očuvanje preciznosti parsiranih PDF decimala pri spremanju

PDFlibPas, losLab PDF Developer Library, čuva točan decimalni tekst koji je parsirao za svaki realni broj u dokumentu i zapisuje taj tekst natrag doslovno kad god vrijednost nikad nije bila izmijenjena. Od v3.539.19 postavka SetPrecision upravlja samo brojevima koje biblioteka stvara ili uređuje, pa obično učitavanje i spremanje više ne zaokružuje CalRGB /Gamma s 2.22221 na 2.2222 niti pomiče boje stranice koju nitko nije dirao. Promjena je mala u kodu, a velika u onome što govori o parserima: vrijednost koju dekodirate i literal koji emitirate dvije su različite stvari, a Double round trip nije identitetska transformacija

Zašto je spremanje koje nije promijenilo ništa pomaknulo boje stranice?

Zato što su se parametri color spacea preformatirali, a ne slika. Datoteka koja je to razotkrila uredski je dokument od 35 stranica u lokalnom regresijskom korpusu, s header slikom koja se ponavlja na svakoj stranici. Učitavanje i izravno spremanje proizvelo je image streamove bajt identične ulazu, a usporedba hashova streamova prijavila je dokument kao nepromijenjen. Usporedba renderiranog izgleda nije se složila: svaka od 35 stranica pokazala je razlike u pikselima u headeru, i nigdje drugdje

Header slika crta kroz CalRGB color space, koji ISO 32000-1 §8.6.5.3 definira pomoću /WhitePoint, opcionalnog /Gamma niza s tri elementa i opcionalnog /Matrix s devet elemenata. Ti nizovi obični su numerički objekti u rječniku color spacea. TPDFNumeric spremao je svaki kao Double i ništa drugo, a TPDFNumeric.Output formatirao je taj Double kroz PDFPrecNum, koji prema zadanim postavkama ima četiri decimalna mjesta. Tako je /Gamma otišao s 2.22221 na 2.2222, unos matrice s 0.71519 na 0.7152, a renderer je vjerno proizveo malo drukčije boje iz malo drukčije kalibracije. Bajtovi slike bili su nevini; brojevi oko njih nisu. Neugodan dio je koliko je to bilo nevidljivo. Usporedba dekodiranih bajtova streama to ne može vidjeti, jer brojevi žive u rječniku, a ne u streamu. Usporedba payloada privitaka to ne može vidjeti. Čak i revision diff opisan u članku o razinama izmjena uzima fingerprint normaliziranog tijela objekta, pa obje revizije hashiraju u istu vrijednost i diff ih prijavljuje kao identične. Samo je renderiranje to uhvatilo, zato baseline korpusa renderira svaku stranicu umjesto da vjeruje isključivo strukturnim provjerama

Gdje je PDFlibPas izgubio CalRGB preciznost pri spremanju bez izmjena u Delphiju: parsirani /Gamma 2.22221 i unos matrice 0.71519 žive u TPDFNumeric kao Double, Output ih formatira kroz PLDoubleToStr uz PDFPrecNum na četiri decimalna mjesta, svaka strukturna provjera prijavljuje dokument kao nepromijenjen, a samo renderirana usporedba pokazuje sve 35 header slika pomaknute
Bajtovi slike bili su nevini: TPDFNumeric je preformatirao kalibracijske brojeve oko njih kroz PDFPrecNum, pa su i hashovi streamova i fingerprint diff prijavili identične revizije, dok je renderer davao malo drukčije boje na svakoj stranici

Vrijednost koju ste parsirali nije literal koji trebate zapisati

PDF realni broj je decimalni string, i ISO 32000-1 §7.3.3 izričit je da je to samo decimalni string: bez radix notacije, bez eksponentnog oblika. Annex C zatim navodi preciznost koju implementacija treba poštovati, otprilike pet značajnih decimalnih znamenki u razlomljenom dijelu. Zadana izlazna preciznost od četiri već je ispod toga, a blizu nule postaje još gore: PLDoubleToStr skalira vrijednost, zaokružuje na cijeli broj i emitira 0 kad je rezultat nula, pa unos matrice -0.000012345 ne izgubi znamenku, nego nestane u cijelosti

Podizanje zadane vrijednosti samo bi pomaknulo liticu. Popravak je prestati se pretvarati da je Double taj broj. Kad tokenizer u TPDFStructure.Decode prepozna standardni real, što znači da token sadrži decimalnu točku i nema oznaku eksponenta, sprema izvorni tekst u novo polje FOriginalText uz konvertiranu vrijednost. Output zatim preferira taj tekst, a na formatiranje se vraća samo kad nema što preferirati

Kako PDFlibPas čuva parsirani decimalni tekst u Delphiju: tokenizer u TPDFStructure.Decode drži izvorni literal u FOriginalText za svaki token s decimalnom točkom i bez eksponenta, Output zapisuje taj tekst doslovno umjesto da pozove PLDoubleToStr, a SetTo ga čisti jer je uređeni broj novi broj
Vrijednost koju dekodirate i literal koji emitirate dvije su različite stvari: preferiranje parsiranog teksta drži 2.22221 točnim, dok brojevi koje je biblioteka stvorila ili uredila i dalje slijede PDFPrecNum, a postavka nikad ne dopire do nediranog ulaza
// Lib/PDFlibStruct.pas — cijeli popravak na izlaznoj strani
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:= '';   // uređeni broj je novi broj
  FValue:= Value;
  FChanged:= True;
End;

Dvije granice su namjerne. Cijeli brojevi se ne čuvaju, jer je formatiranje cijelih brojeva već bez gubitka. Eksponentni oblici poput 6.02E23 toleriraju se na ulazu zbog pokvarenih proizvođača, ali se ne čuvaju na izlazu, jer bi njihovo vraćanje ovjekovječilo sintaksu koju §7.3.3 zabranjuje; prolaze kroz formatter kao i svaki broj koji je generirala biblioteka. Tokenizer prije spremanja teksta primjenjuje i svoj uobičajeni minimalni popravak, pa se literal s vodećom točkom poput .5 čuva kao 0.5, a literal sa završnom točkom poput 5. kao 5.0. Oba su isti broj za svakog čitača i daleko su šire prihvaćeni

Što SetPrecision jamči nakon v3.539.19?

TPDFlib.SetPrecision sada kontrolira decimalna mjesta brojeva koje sama biblioteka proizvodi: vrijednosti iscrtane kroz painter, brojeve stvorene iz Double poput onih kroz NewNumeric, i svaku parsiranu vrijednost koja je od tada uređena s SetTo. Imajte na umu da tekst dekodiran kroz object API, na primjer literal predan u SetObjectFromString, prolazi kroz isti tokenizer i čuva se na isti način. Parsirani decimal koji nikad nije izmijenjen zadržava svoju ulaznu preciznost bez obzira na postavku, a promjena postavke nakon učitavanja ne dira ga retroaktivno. Referentni unos za SetPrecision ažuriran je u istom izdanju da kaže upravo to, jer je stara formulacija implicirala da se postavka primjenjuje na svaki broj u datoteci

Čišćenje se događa u SetTo, a ne izvodi se iz zastavice Changed, i ta razlika je važna. Pipeline spremanja resetira Changed na objektima nakon što su zapisani, pa bi provjera oblika "emitiraj izvorni tekst osim ako je promijenjen" počela emitirati zastarjeli tekst za vrijednost koja je uređena, spremljena i ponovno uređena u istoj sesiji. Vezanje izvornog teksta na samu dodjelu čini nemogućim da se njih dvoje raziđu. Regresijski test prikiva svako od tih ponašanja vrijednostima iz izvorne datoteke

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]'));
    // Nedirani ulaz preživljava doslovno, uključujući vrijednost koju bi
    // formatiranje na četiri mjesta svelo na 0
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // Uređivanje odbacuje izvorni tekst i slijedi PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Naknadno snižavanje preciznosti ne dopire do nediranog ulaza
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

Zašto content model i dalje normalizira brojeve?

Zato što TPDFContentProgram obećava kanonske numeričke operande, a to obećanje vrijedi više od doslovnog teksta unutar content streama. Uredivi content model, isti onaj na kojem je izgrađen tracker grafičkog stanja, postoji kako bi NormalizeContentStreams, optimizer i Emit davali stabilan, usporediv izlaz iz proizvoljnog ulaza. Kad bi parsirani operand nosio svoj izvorni tekst u model, operatorski slijed poput 0.50000 0 0 RG emitirao bi se drukčije od 0.5 0 0 RG, i svaka bi nizvodna usporedba plivala s navikama formatiranja proizvođača

Zato model skida izvorni tekst na svojim dvama ulaznim točkama. NormalizeContentNumbers vrti se nad svakim operandom dok ga parser gura i ponovno unutar SetOperand kad se dekodira izvor koji je dao pozivatelj, i rekurzivno prolazi kroz nizove i rječnike tako da su pokriveni dash patterns, TJ nizovi i property rječnici označenog sadržaja. Poziv SetTo(AsDouble) nad svakim numerikom dovoljan je, jer je to upravo operacija koja čisti tekst. Sirovi podaci inline slike ostaju na miru, kao i uvijek dosad

Zašto PDFlibPas content model i dalje normalizira brojeve: NormalizeContentNumbers vrti se ondje gdje parser gura svaki operand i ponovno unutar SetOperand, rekurzivno prolazi kroz nizove i rječnike pa su pokriveni dash patterns, TJ nizovi i property rječnici označenog sadržaja, a SetTo AsDouble čisti izvorni tekst pa se 0.50000 i 0.5 emitiraju identično
Kanonski numerički operandi obećanje su content modela: sirovi podaci inline slike ostaju na miru, a nedirani brojevi u rječnicima izvan content streamova zadržavaju jamstvo doslovnosti, pa običan par LoadFromFile i SaveToFile i dalje čuva njih
// Lib/PDFlibContentModel.pas — content model drži svoj ugovor
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;

Praktično pravilo za pozivatelje zato je jednostavno. Običan LoadFromFile nakon kojeg slijedi SaveToFile ostavlja nedirane content streamove i nedirane brojeve u rječnicima onakvima kakvi su bili. Stranica koja prođe kroz NormalizeContentStreams, ili bilo koje uređivanje obavljeno kroz content model, izlazi kanonska po dizajnu, a ostatak dokumenta i dalje je sačuvan. To su dva različita zahtjeva, i sada rade dvije različite stvari

Što to košta i gdje jamstvo prestaje

Svaki TPDFNumeric sada nosi jednu dodatnu AnsiString referencu, a svaki parsirani decimal drži svoj izvorni tekst na životu cijeli životni vijek objekta. Na dokumentu s milijunima realnih brojeva to je stvarna memorija, i to pripada u svako mjerenje velikih dokumenata, a ne da se odmahne rukom. Jamstvo je također ograničeno na vlastiti dokument broja: kopiranje objekata između dokumenata ili rekonstrukcija vrijednosti kroz object API proizvodi nove brojeve, koji slijede izlaznu preciznost kao i svaki drugi novi broj. Vrijedi biti precizan oko toga što izdanje tvrdi, a što ne. Učitavanje i spremanje nediranog dokumenta sada čuva kalibracijske brojeve koje renderer doista troši, a to je svojstvo koje baseline korpusa provjerava. Ne tvrdi se bajt identičan izlaz, koji ovisi i o numeriranju objekata, kompresiji streamova i identifikatoru trailera opisanom u članku o determinističkom PDF ID-u. I ne čini da fingerprint diff vidi razlike u zaokruživanju u datotekama koje je proizveo drugi softver, jer one i dalje hashiraju normalizirano tijelo. Lekcija se dobro generalizira daleko izvan CalRGB-a: kad parser čuva samo konvertiranu vrijednost, svako je spremanje uređivanje, a jedini način da se to primijeti jest pogledati renderirani rezultat. Rukovanje brojevima i semantika SetPrecision dokumentirani su na stranici proizvoda losLab PDF Developer Library