Techninis straipsnis

PDF dešimtainių reikšmių tikslumas išsaugant Delphi

PDFlibPas, losLab PDF Developer Library, saugo tikslų dešimtainį tekstą, kurį perskaitė kiekvienam dokumento realiajam skaičiui, ir tą tekstą rašo atgal pažodžiui, kai reikšmė niekada nebuvo keista. Nuo v3.539.19 SetPrecision nustatymas valdo tik tuos skaičius, kuriuos biblioteka sukuria ar redaguoja, tad įprastas įkėlimas ir išsaugojimas nebeapvalina CalRGB /Gamma reikšmės 2.22221 žemyn į 2.2222 ir nebeslenka puslapio, kurio niekas nelietė, spalvų. Pokytis nedidelis kode ir didelis tame, ką jis sako apie analizatorius: reikšmė, kurią nusidekoduojate, ir literalas, kurį išduodate, yra du skirtingi dalykai, o Double kelionė pirmyn ir atgal nėra tapatumo transformacija

Kodėl nieko nepakeitęs išsaugojimas paslenka puslapio spalvas?

Nes buvo performatuojami spalvų erdvės parametrai, o ne paveikslėlis. Failas, kuris tai atskleidė, yra 35 puslapių biuro dokumentas vietiniame regresijos korpuse su antraštės paveikslėliu, naudojamu kiekviename puslapyje. Jį įkėlus ir iš karto išsaugojus atgal, gauti paveikslėlių srautai buvo identiški įvesčiai baitas į baitą, o srautų maišų palyginimas pranešė, kad dokumentas nepakitęs. Atvaizduotos kopijos palyginimas nesutiko: visuose 35 puslapiuose atsirado pikselių skirtumų antraštėje, ir niekur kitur

Antraštės paveikslėlis braižomas per CalRGB spalvų erdvę, kurią ISO 32000-1 §8.6.5.3 apibrėžia /WhitePoint, neprivalomu trijų elementų /Gamma masyvu ir neprivalomu devynių elementų /Matrix. Tie masyvai yra paprasti skaitiniai objektai spalvų erdvės žodyne. TPDFNumeric kiekvieną jų saugojo kaip Double ir nieko daugiau, o TPDFNumeric.Output tą Double formatuodavo per PDFPrecNum, kurio numatytoji reikšmė yra keturios dešimtainės vietos. Taigi /Gamma iš 2.22221 virto 2.2222, matricos elementas iš 0.71519 virto 0.7152, ir atvaizduoklis stropiai pagamino šiek tiek kitokias spalvas iš šiek tiek kitokios kalibracijos. Paveikslėlio baitai buvo nekalti; skaičiai aplink juos – ne. Nepatogiausia tai, koks nematomas šis dalykas buvo. Palyginimas pagal nusidekodavusius srauto baitus to pamatyti negali, nes skaičiai gyvena žodyne, o ne sraute. Priedų turinio palyginimas to pamatyti negali. Net revizijų skirtumai, aprašyti modifikavimo lygių straipsnyje, paima normalizuoto objekto kūno pirštų atspaudą, tad abi revizijos su maiša duoda tą pačią reikšmę ir skirtumai jas praneša identiškomis. Tai pagavo tik atvaizdavimas – štai kodėl korpuso etalonas atvaizduoja kiekvieną puslapį, o ne pasitiki vien struktūriniais patikrinimais

Kur PDFlibPas prarado CalRGB tikslumą per nieko nekeičiantį išsaugojimą Delphi: perskaityta /Gamma 2.22221 ir 0.71519 matricos elementas gyvena TPDFNumeric kaip Double, Output juos formatuoja per PLDoubleToStr su PDFPrecNum keturių dešimtainių vietų tikslumu, kiekvienas struktūrinis patikrinimas praneša dokumentą nepakitusį, ir tik atvaizduotas palyginimas parodo visus 35 pasislinkusius antraštės paveikslėlius
Paveikslėlio baitai buvo nekalti: TPDFNumeric performatavo aplink juos esančius kalibracijos skaičius per PDFPrecNum, tad srautų maišos ir pirštų atspaudų skirtumai abu pranešė identiškas revizijas, o atvaizduoklis kiekviename puslapyje pagamino šiek tiek kitokias spalvas

Reikšmė, kurią perskaitėte, nėra literalas, kurį reikia rašyti

PDF realusis skaičius yra dešimtainė eilutė, ir ISO 32000-1 §7.3.3 aiškiai sako, kad tai tik dešimtainė eilutė: jokio pagrindo žymėjimo, jokios eksponentinės formos. C priedas tada išvardija tikslumą, kurį implementacija turi gerbti – maždaug penkis reikšminius dešimtainius skaitmenis trupmeninėje dalyje. Numatytasis keturių ženklų išvesties tikslumas jau yra mažesnis už tai, o prie nulio dar blogiau: PLDoubleToStr keičia reikšmės mastelį, apvalina iki sveikojo skaičiaus ir išduoda 0, kai rezultatas lygus nuliui, tad matricos elementas -0.000012345 nepraranda skaitmens – jis išnyksta visai

Padidinus numatytąją reikšmę, uola tik pasislinktų. Pataisa yra nustoti apsimesti, kad Double ir yra tas skaičius. Kai TPDFStructure.Decode žetonizatorius atpažįsta standartinį realųjį skaičių, tai yra žetonas turi dešimtainį tašką ir jokio eksponento žymens, jis šaltinio tekstą įrašo į naują FOriginalText lauką greta konvertuotos reikšmės. Output tada teikia pirmenybę tam tekstui ir grįžta prie formatavimo tik tada, kai nėra kam teikti pirmenybės

Kaip PDFlibPas išsaugo perskaitytą dešimtainį tekstą Delphi: TPDFStructure.Decode žetonizatorius šaltinio literalą laiko FOriginalText bet kuriam žetonui su dešimtainiu tašku ir be eksponento, Output rašo tą tekstą pažodžiui vietoje to, kad kviestų PLDoubleToStr, o SetTo jį išvalo, nes paredaguotas skaičius yra naujas skaičius
Reikšmė, kurią nusidekoduojate, ir literalas, kurį išduodate, yra du skirtingi dalykai: pirmenybė perskaitytam tekstui išlaiko 2.22221 tikslų, o bibliotekos sukurti ir paredaguoti skaičiai vis tiek seka PDFPrecNum, ir nustatymas niekada nepasiekia nepaliestos įvesties
// Lib/PDFlibStruct.pas — visa pataisa išvesties pusėje
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:= '';   // paredaguotas skaičius yra naujas skaičius
  FValue:= Value;
  FChanged:= True;
End;

Dvi ribos yra sąmoningos. Sveikieji skaičiai neišsaugomi, nes sveikųjų skaičių formatavimas ir taip yra be nuostolių. Eksponentinės formos, tokios kaip 6.02E23, įvestyje toleruojamos dėl sulaužytų generatorių, bet išvestyje neišsaugomos, nes jų įrašymas atgal įtvirtintų sintaksę, kurią §7.3.3 draudžia; jos eina per formatuotoją kaip bet kuris bibliotekos sukurtas skaičius. Žetonizatorius taip pat prieš įrašydamas tekstą pritaiko savo įprastą minimalų taisymą, tad literalas su pradiniu tašku kaip .5 paliekamas kaip 0.5, o literalas su baigiamuoju tašku kaip 5. – kaip 5.0. Abu kiekvienam skaitytuvui yra tas pats skaičius ir yra daug plačiau priimami

Ką SetPrecision garantuoja po v3.539.19?

TPDFlib.SetPrecision dabar valdo dešimtaines vietas skaičių, kuriuos pagamina pati biblioteka: reikšmių, nubraižytų per painter, skaičių, sukurtų iš Double, pavyzdžiui per NewNumeric, ir bet kurios perskaitytos reikšmės, kuri nuo to laiko buvo paredaguota su SetTo. Atkreipkite dėmesį, kad tekstas, nusidekoduotas per objektų API, pavyzdžiui literalas, perduotas SetObjectFromString, eina per tą patį žetonizatorių ir yra išsaugomas taip pat. Perskaitytas dešimtainis skaičius, kuris niekada nebuvo keistas, išlaiko savo įvesties tikslumą nepriklausomai nuo nustatymo, o nustatymo pakeitimas po įkėlimo jo atgaline data neliečia. SetPrecision nuorodos įrašas buvo atnaujintas tame pačiame leidime, kad pasakytų būtent tai, nes senoji formuluotė leido suprasti, kad nustatymas taikomas kiekvienam failo skaičiui

Išvalymas įvyksta SetTo viduje, o ne išvedamas iš Changed vėliavėlės, ir tas skirtumas svarbus. Išsaugojimo grandinė atstato Changed objektuose, kai jie jau buvo įrašyti, tad patikrinimas formos „išduok originalų tekstą, jei nepakeista“ pradėtų išduoti pasenusį tekstą reikšmei, kuri buvo paredaguota, išsaugota ir vėl paredaguota toje pačioje sesijoje. Originalaus teksto susiejimas su pačiu priskyrimu padaro neįmanoma, kad jie nesutartų. Regresijos testas kiekvieną iš šių elgsenų prisega reikšmėmis iš originalaus failo

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]'));
    // Nepaliesta įvestis išlieka pažodžiui, įskaitant reikšmę, kurią
    // keturių ženklų formatavimas būtų sutraukęs į 0
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // Redagavimas išmeta originalų tekstą ir seka PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Tikslumo sumažinimas po to nepasiekia nepaliestos įvesties
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

Kodėl turinio modelis vis tiek normalizuoja skaičius?

Nes TPDFContentProgram žada kanoninius skaitinius operandus, ir tas pažadas vertas daugiau nei pažodinis tekstas turinio srauto viduje. Redaguojamas turinio modelis – tas pats, ant kurio pastatytas grafinės būsenos sekiklis – egzistuoja tam, kad NormalizeContentStreams, optimizatorius ir Emit iš bet kokios įvesties duotų stabilų, palyginamą rezultatą. Jei perskaitytas operandas į modelį atsineštų savo originalų tekstą, operatorių seka kaip 0.50000 0 0 RG būtų išduodama kitaip nei 0.5 0 0 RG, ir kiekvienas tolimesnis palyginimas imtų plaukti pagal generatoriaus formatavimo įpročius

Tad modelis nuima originalų tekstą savo dviejuose įėjimo taškuose. NormalizeContentNumbers paleidžiamas kiekvienam operandui, kai analizatorius jį įstumia, ir dar kartą SetOperand viduje, kai nusidekoduojamas kviečiančiojo pateiktas šaltinis, ir jis rekursyviai eina per masyvus bei žodynus, tad brūkšnių šablonai, TJ masyvai ir žymėto turinio savybių žodynai taip pat uždengiami. Pakanka kiekvienam skaitiniam objektui iškviesti SetTo(AsDouble), nes būtent ši operacija ir išvalo tekstą. Neapdoroti įterpto paveikslėlio duomenys paliekami ramybėje, kaip visada buvo

Kodėl PDFlibPas turinio modelis vis tiek normalizuoja skaičius: NormalizeContentNumbers paleidžiamas ten, kur analizatorius įstumia kiekvieną operandą, ir dar kartą SetOperand viduje, rekursyviai eina per masyvus ir žodynus, tad brūkšnių šablonai, TJ masyvai ir žymėto turinio savybių žodynai uždengiami, o SetTo AsDouble išvalo originalų tekstą, tad 0.50000 ir 0.5 išduodami identiškai
Kanoniniai skaitiniai operandai yra turinio modelio pažadas: neapdoroti įterpto paveikslėlio duomenys paliekami ramybėje, o nepaliesti žodyno skaičiai už turinio srautų ribų išlaiko pažodinę garantiją, tad paprasta LoadFromFile ir SaveToFile pora juos vis tiek išsaugo
// Lib/PDFlibContentModel.pas — turinio modelis laikosi savo kontrakto
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;

Praktinė taisyklė kviečiantiesiems todėl paprasta. Paprastas LoadFromFile ir po jo SaveToFile palieka nepaliestus turinio srautus ir nepaliestus žodyno skaičius tokius, kokie buvo. Puslapis, praėjęs per NormalizeContentStreams, arba bet koks redagavimas, atliktas per turinio modelį, išeina kanoninis pagal sumanymą, o likusi dokumento dalis vis tiek išsaugoma. Tai du skirtingi prašymai, ir dabar jie daro du skirtingus dalykus

Kiek tai kainuoja ir kur garantija sustoja

Kiekvienas TPDFNumeric dabar nešasi vieną papildomą AnsiString nuorodą, o kiekvienas perskaitytas dešimtainis skaičius laiko savo šaltinio tekstą gyvą visą objekto gyvavimo laiką. Dokumente su milijonais realiųjų skaičių tai yra tikra atmintis, ir ji turi būti įtraukta į bet kokį didelių dokumentų matavimą, o ne nurašyta mostelint ranka. Garantija taip pat apribota paties skaičiaus dokumentu: objektų kopijavimas tarp dokumentų ar reikšmių atkūrimas per objektų API pagamina naujus skaičius, o tie seka išvesties tikslumą kaip bet kuris kitas naujas skaičius. Verta būti tiksliems dėl to, ką leidimas teigia ir ko ne. Nepaliesto dokumento įkėlimas ir išsaugojimas dabar išsaugo kalibracijos skaičius, kuriuos atvaizduoklis iš tikrųjų vartoja – tai savybė, kurią tikrina korpuso etalonas. Jis neteigia identiškos išvesties baitu lygmenyje, kuri taip pat priklauso nuo objektų numeravimo, srautų glaudinimo ir trailer identifikatoriaus, aptarto deterministinio PDF ID straipsnyje. Ir jis nepadaro taip, kad pirštų atspaudų palyginimas matytų apvalinimo skirtumus kitoje programinėje įrangoje pagamintuose failuose, nes tie vis tiek maišuoja normalizuotą kūną. Pamoka apibendrinama kur kas plačiau nei CalRGB: kai analizatorius laiko tik konvertuotą reikšmę, kiekvienas išsaugojimas yra redagavimas, o vienintelis būdas tai pastebėti – pažiūrėti į atvaizduotą rezultatą. Skaičių apdorojimas ir SetPrecision semantika aprašyti losLab PDF Developer Library produkto puslapyje