Tehnični članak

Ohranjanje natančnosti decimalk PDF v Delphiju

PDFlibPas, razvijalska knjižnica PDF družbe losLab, obdrži natančno decimalno besedilo, ki ga je razčlenila za vsako realno število v dokumentu, in to besedilo zapiše nazaj dobesedno, kadar vrednost ni bila nikoli spremenjena. Od različice v3.539.19 nastavitev SetPrecision velja le za števila, ki jih knjižnica ustvari ali uredi, zato običajno nalaganje in shranjevanje ne zaokroži več /Gamma barvnega prostora CalRGB z 2.22221 navzdol na 2.2222 in ne premakne barv strani, ki se je nihče ni dotaknil. Sprememba je v kodi majhna, v tem, kar pove o razčlenjevalnikih, pa velika: vrednost, ki jo dekodirate, in literal, ki ga izdate, sta dve različni stvari in pot skozi Double in nazaj ni identitetna preslikava

Zakaj je shranjevanje, ki ni spremenilo nič, premaknilo barve strani?

Ker so se preoblikovali parametri barvnega prostora, ne slika. Datoteka, ki je to razkrila, je 35-stranski pisarniški dokument v lokalnem regresijskem korpusu, s sliko glave, ki se ponovi na vsaki strani. Nalaganje in takojšnje shranjevanje nazaj je dalo tokove slik, ki so bili bajt za bajt enaki vhodu, primerjava zgoščenih vrednosti tokov pa je dokument prijavila kot nespremenjen. Primerjava upodobljenih strani se s tem ni strinjala: vseh 35 strani je pokazalo razlike v pikah v glavi in nikjer drugje

Slika glave se riše skozi barvni prostor CalRGB, ki ga ISO 32000-1 §8.6.5.3 določa z /WhitePoint, neobveznim trielementnim nizom /Gamma in neobveznim deveelementnim /Matrix. Ti nizi so običajni številski objekti v slovarju barvnega prostora. TPDFNumeric je vsakega shranil kot Double in nič drugega, TPDFNumeric.Output pa je ta Double oblikoval skozi PDFPrecNum, ki privzeto pomeni štiri decimalna mesta. Tako je /Gamma šel z 2.22221 na 2.2222, vnos matrike z 0.71519 na 0.7152, upodabljalnik pa je zvesto izdelal nekoliko drugačne barve iz nekoliko drugačne kalibracije. Bajti slike so bili nedolžni; številke okoli njih ne. Neprijetno pri tem je, kako nevidno je bilo. Primerjava dekodiranih bajtov toka tega ne more videti, ker številke živijo v slovarju in ne v toku. Primerjava koristnih obremenitev prilog tega ne more videti. Celo primerjava revizij, opisana v članku o ravneh sprememb, odtisne normalizirano telo objekta, zato obe reviziji dasta isto zgoščeno vrednost in primerjava poroča, da sta enaki. Ujelo ju je le upodabljanje, zato korpusno izhodišče upodobi vsako stran, namesto da bi zaupalo samo strukturnim preverjanjem

Kje je PDFlibPas v Delphiju izgubil natančnost CalRGB ob shranjevanju brez sprememb: razčlenjeni /Gamma 2.22221 in vnos matrike 0.71519 živita v TPDFNumeric kot Double, Output ju oblikuje skozi PLDoubleToStr s PDFPrecNum na štiri decimalna mesta, vsako strukturno preverjanje poroča dokument kot nespremenjen, le primerjava upodobljenih strani pokaže vseh 35 slik glave premaknjenih
Bajti slike so bili nedolžni: TPDFNumeric je preoblikoval kalibracijske številke okoli njih skozi PDFPrecNum, zato sta zgoščene vrednosti tokov in primerjava odtisov poročali enaki reviziji, upodabljalnik pa je na vsaki strani izdelal nekoliko drugačne barve

Vrednost, ki ste jo razčlenili, ni literal, ki bi ga morali zapisati

Realno število PDF je decimalni niz in ISO 32000-1 §7.3.3 je izrecen, da je samo decimalni niz: brez zapisa z osnovo in brez eksponentne oblike. Dodatek C nato našteje natančnost, ki naj bi jo izvedba spoštovala, približno pet pomembnih decimalnih števk v decimalnem delu. Privzeta izhodna natančnost štirih je že pod tem in blizu nič je še slabše: PLDoubleToStr vrednost skalira, zaokroži na celo število in izda 0, kadar je rezultat nič, zato vnos matrike -0.000012345 ne izgubi števke, ampak povsem izgine

Zvišanje privzete vrednosti bi le premaknilo rob. Popravek je v tem, da prenehamo delati, kot da je Double tisto število. Ko tokenizator v TPDFStructure.Decode prepozna standardno realno število, torej žeton, ki vsebuje decimalno piko in nobene oznake eksponenta, izvorno besedilo shrani v novo polje FOriginalText poleg pretvorjene vrednosti. Output nato raje uporabi to besedilo in se na oblikovanje zateče le, kadar ni česa, kar bi preferiral

Kako PDFlibPas v Delphiju ohrani razčlenjeno decimalno besedilo: tokenizator v TPDFStructure.Decode shrani izvorni literal v FOriginalText za vsak žeton z decimalno piko in brez eksponenta, Output to besedilo zapiše dobesedno namesto klica PLDoubleToStr, SetTo pa ga počisti, ker je urejeno število novo število
Vrednost, ki jo dekodirate, in literal, ki ga izdate, sta dve različni stvari: prednost razčlenjenega besedila ohrani 2.22221 natančen, števila, ki jih ustvari knjižnica, in urejena števila pa še vedno sledijo PDFPrecNum, nastavitev pa nikoli ne doseže nedotaknjenega vhoda
// Lib/PDFlibStruct.pas — celoten popravek na izhodni 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:= '';   // urejeno število je novo število
  FValue:= Value;
  FChanged:= True;
End;

Dve meji sta namerni. Celih števil se ne ohranja, ker je oblikovanje celih števil že brez izgube. Eksponentne oblike, kot je 6.02E23, se na vhodu tolerirajo zaradi pokvarjenih producentov, na izhodu pa se ne ohranjajo, saj bi njihovo prepisovanje ohranjalo skladnjo, ki jo §7.3.3 prepoveduje; gredo skozi oblikovalnik kot vsako število, ki ga ustvari knjižnica. Tokenizator pred shranjevanjem besedila uporabi tudi svoje običajno minimalno popravljanje, zato se literal z vodilno piko, kot je .5, obdrži kot 0.5, literal s končno piko, kot je 5., pa kot 5.0. Za vsakega bralca je to isto število in je veliko bolj razširjeno sprejeto

Kaj SetPrecision zagotavlja po različici v3.539.19?

TPDFlib.SetPrecision zdaj nadzoruje decimalna mesta števil, ki jih ustvari knjižnica sama: vrednosti, narisane skozi risalnik, števila, ustvarjena iz Double, na primer skozi NewNumeric, in vsako razčlenjeno vrednost, ki je bila pozneje urejena s SetTo. Upoštevajte, da gre besedilo, dekodirano skozi objektni API, na primer literal, predan v SetObjectFromString, skozi isti tokenizator in se ohrani na enak način. Razčlenjena decimalka, ki ni bila nikoli spremenjena, obdrži natančnost vhoda ne glede na nastavitev, sprememba nastavitve po nalaganju pa se je ne dotakne za nazaj. Vnos v referenci za SetPrecision je bil v isti izdaji posodobljen, da pravi natanko to, ker je staro besedilo namigovalo, da nastavitev velja za vsako število v datoteki

Čiščenje se zgodi v SetTo in ni izpeljano iz zastavice Changed, ta razlika pa je pomembna. Veriga shranjevanja zastavico Changed na objektih ponastavi, ko so ti zapisani, zato bi preverjanje v obliki »izdaj izvirno besedilo, razen če je spremenjeno« začelo izdajati zastarelo besedilo za vrednost, ki je bila v isti seji urejena, shranjena in znova urejena. Če je izvirno besedilo vezano na samo dodelitev, se ta dva ne moreta razhajati. Regresijski test vsako od teh vedenj pripne z vrednostmi iz izvirne 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]'));
    // Neurejeni vhod preživi dobesedno, tudi vrednost, ki bi jo
    // oblikovanje na štiri mesta strlo na 0
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // Ureditev zavrže izvirno besedilo in sledi PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Poznejše znižanje natančnosti ne doseže neurejenega vhoda
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

Zakaj model vsebine števila še vedno normalizira?

Ker TPDFContentProgram obljublja kanonične številske operande in ta obljuba je vredna več kot dobesedno besedilo znotraj toka vsebine. Urejalni model vsebine, isti, na katerem je zgrajen sledilnik grafičnega stanja, obstaja zato, da NormalizeContentStreams, optimizator in Emit iz poljubnega vhoda dajo stabilen, primerljiv izhod. Če bi razčlenjeni operand v model prinesel svoje izvirno besedilo, bi se zaporedje operatorjev, kot je 0.50000 0 0 RG, izdalo drugače kot 0.5 0 0 RG, vsaka dolvodna primerjava pa bi se premikala skupaj z oblikovalskimi navadami producenta

Zato model izvirno besedilo odstrani na obeh vstopnih točkah. NormalizeContentNumbers se izvede na vsakem operandu, ko ga razčlenjevalnik potisne, in znova znotraj SetOperand, ko se dekodira izvorna koda, ki jo poda klicatelj, rekurzivno pa gre skozi nize in slovarje, tako da so pokriti vzorci črtkanja, nizi TJ in slovarji lastnosti označene vsebine. Dovolj je, da se na vsakem številu pokliče SetTo(AsDouble), saj je prav to operacija, ki besedilo počisti. Surovi podatki vgrajenih slik ostanejo nedotaknjeni, kot so vedno bili

Zakaj model vsebine PDFlibPas števila še vedno normalizira: NormalizeContentNumbers se izvede tam, kjer razčlenjevalnik potisne vsak operand, in znova znotraj SetOperand, rekurzivno gre skozi nize in slovarje, tako da so pokriti vzorci črtkanja, nizi TJ in slovarji lastnosti označene vsebine, SetTo AsDouble pa počisti izvirno besedilo, zato se 0.50000 in 0.5 izdata enako
Kanonični številski operandi so obljuba modela vsebine: surovi podatki vgrajenih slik ostanejo nedotaknjeni, nedotaknjena števila v slovarjih zunaj tokov vsebine pa ohranijo jamstvo dobesednosti, zato jih navaden par LoadFromFile in SaveToFile še vedno ohrani
// Lib/PDFlibContentModel.pas — model vsebine drži svojo pogodbo
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 klicatelje je torej preprosto. Navaden LoadFromFile, ki mu sledi SaveToFile, pusti nedotaknjene tokove vsebine in nedotaknjena števila v slovarjih takšna, kot so bila. Stran, ki gre skozi NormalizeContentStreams, ali kakršno koli urejanje skozi model vsebine, izide kanonična po zasnovi, preostali del dokumenta pa je še vedno ohranjen. To sta dve različni zahtevi in zdaj delata dve različni stvari

Koliko to stane in kje se jamstvo ustavi

Vsak TPDFNumeric zdaj nosi en sklic na AnsiString več in vsaka razčlenjena decimalka ohranja svoje izvorno besedilo pri življenju toliko časa, kolikor živi objekt. Pri dokumentu z milijoni realnih števil je to resničen pomnilnik in sodi v vsako meritev velikih dokumentov, ne pa da ga odmahnemo. Jamstvo je tudi omejeno na dokument, ki mu število pripada: kopiranje objektov med dokumenti ali rekonstruiranje vrednosti skozi objektni API ustvari nova števila, ta pa sledijo izhodni natančnosti kot vsako drugo novo število. Velja biti natančen glede tega, kaj izdaja trdi in česa ne. Nalaganje in shranjevanje nedotaknjenega dokumenta zdaj ohrani kalibracijske številke, ki jih upodabljalnik resnično porabi, in prav to lastnost preverja korpusno izhodišče. Ne trdi bajtno enakega izhoda, ki je odvisen tudi od številčenja objektov, stiskanja tokov in identifikatorja v repu, obravnavanega v članku o determinističnem ID PDF. Prav tako ne naredi tega, da bi primerjava odtisov videla razlike v zaokroževanju v datotekah druge programske opreme, saj te še vedno zgoščujejo normalizirano telo. Nauk se posploši daleč onkraj CalRGB: kadar razčlenjevalnik obdrži samo pretvorjeno vrednost, je vsako shranjevanje urejanje in edini način, da to opazite, je pogled na upodobljeni rezultat. Ravnanje s števili in semantika SetPrecision sta dokumentirana na strani izdelka losLab PDF Developer Library