PDFlibPas eli losLab PDF Developer Library säilyttää tarkan desimaalitekstin, jonka se jäsensi jokaisesta asiakirjan reaaliluvusta, ja kirjoittaa tuon tekstin takaisin sellaisenaan aina kun arvoa ei koskaan muokattu. Versiosta v3.539.19 lähtien SetPrecision-asetus koskee vain lukuja, jotka kirjasto luo tai muokkaa, joten tavallinen lataus ja tallennus ei enää pyöristä CalRGB:n /Gamma-arvoa 2.22221 alaspäin arvoon 2.2222 eikä siirrä sivun värejä, joihin kukaan ei koskenut. Muutos on koodissa pieni ja siinä, mitä se kertoo jäsentimistä, suuri: arvo, jonka purat, ja literaali, jonka kirjoitat, ovat kaksi eri asiaa, eikä Double-edestakaismatka ole identiteettimuunnos
Miksi mitään muuttamaton tallennus siirsi sivun värejä?
Koska uudelleenmuotoiltavana olivat väriavaruuden parametrit, ei kuva. Tämän paljastanut tiedosto on paikallisessa regressiokorpuksessa oleva 35-sivuinen toimistoasiakirja, jossa sama ylätunnisteen kuva toistuu joka sivulla. Sen lataaminen ja tallentaminen suoraan takaisin tuotti kuvastreamit, jotka olivat tavu tavulta identtiset syötteen kanssa, ja streamien tiivisteiden vertailu ilmoitti asiakirjan muuttumattomaksi. Renderöity vertailu oli eri mieltä: jokainen 35 sivusta näytti pikselieroja ylätunnisteessa eikä missään muualla
Ylätunnisteen kuva piirtyy CalRGB-väriavaruuden kautta, jonka ISO 32000-1 §8.6.5.3 määrittelee /WhitePoint-arvolla, valinnaisella kolmialkioisella /Gamma-taulukolla ja valinnaisella yhdeksänalkioisella /Matrix-taulukolla. Nuo taulukot ovat väriavaruuden sanakirjassa tavallisia numeerisia objekteja. TPDFNumeric tallensi jokaisen niistä Double-arvona eikä minään muuna, ja TPDFNumeric.Output muotoili tuon Double-arvon PDFPrecNum-asetuksen läpi, jonka oletus on neljä desimaalia. Niinpä /Gamma meni arvosta 2.22221 arvoon 2.2222, eräs matriisialkio arvosta 0.71519 arvoon 0.7152, ja renderöijä tuotti uskollisesti hieman eri värit hieman eri kalibroinnista. Kuvan tavut olivat syyttömiä; niiden ympärillä olevat luvut eivät. Epämukava osa on se, miten näkymätöntä tämä oli. Purettujen streamien tavujen vertailu ei näe sitä, koska luvut asuvat sanakirjassa eivätkä streamissa. Liitteiden hyötykuormien vertailu ei näe sitä. Jopa muokkaustasojen artikkelissa kuvattu revisiodiffaus sormenjäljittää normalisoidun objektirungon, joten molemmat revisiot tiivistyvät samaan arvoon ja diffaus ilmoittaa ne identtisiksi. Vain renderöinti nappasi sen, minkä takia korpuksen perustaso renderöi jokaisen sivun sen sijaan että luottaisi pelkkiin rakennetarkistuksiin
Jäsentämäsi arvo ei ole se literaali, joka sinun pitäisi kirjoittaa
PDF:n reaaliluku on desimaalimerkkijono, ja ISO 32000-1 §7.3.3 on yksiselitteinen siitä, että se on vain desimaalimerkkijono: ei kantaluvun merkintää, ei eksponenttimuotoa. Liite C luettelee sitten sen tarkkuuden, jota toteutuksen odotetaan noudattavan, noin viisi merkitsevää desimaalia murto-osassa. Neljän desimaalin oletustarkkuus on jo tuon alapuolella, ja nollan lähellä se käy pahemmaksi: PLDoubleToStr skaalaa arvon, pyöristää kokonaisluvuksi ja tuottaa 0, kun tulos on nolla, joten matriisialkio -0.000012345 ei menetä yhtä numeroa, vaan katoaa kokonaan
Oletuksen nostaminen vain siirtäisi jyrkännettä. Korjaus on lopettaa sen teeskenteleminen, että Double olisi se luku. Kun TPDFStructure.Decode-funktion tokenisoija tunnistaa standardin reaaliluvun eli tokenin, jossa on desimaalipiste eikä eksponenttimerkkiä, se tallentaa lähdetekstin uuteen FOriginalText-kenttään muunnetun arvon rinnalle. Output suosii sitten tuota tekstiä ja turvautuu muotoiluun vain silloin, kun suosittavaa ei ole
// Lib/PDFlibStruct.pas — koko korjaus tulostuspuolella
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:= ''; // muokattu luku on uusi luku
FValue:= Value;
FChanged:= True;
End;
Kaksi rajausta on tarkoituksellinen. Kokonaislukuja ei säilytetä, koska kokonaislukujen muotoilu on jo häviötöntä. Eksponenttimuodot kuten 6.02E23 sietää syötteessä rikkinäisten tuottajien takia, mutta niitä ei säilytetä tulosteessa, koska niiden takaisin kirjoittaminen jatkaisi syntaksia, jonka §7.3.3 kieltää; ne kulkevat muotoilijan läpi kuin mikä tahansa kirjaston tuottama luku. Tokenisoija tekee myös tavanomaisen vähimmäiskorjauksensa ennen tekstin tallentamista, joten etupisteellä alkava literaali kuten .5 säilyy muodossa 0.5 ja loppupisteellä päättyvä literaali kuten 5. muodossa 5.0. Molemmat ovat jokaiselle lukijalle sama luku ja huomattavasti laajemmin hyväksyttyjä
Mitä SetPrecision takaa version v3.539.19 jälkeen?
TPDFlib.SetPrecision ohjaa nyt niiden lukujen desimaalien määrää, jotka kirjasto itse tuottaa: piirtimen kautta piirretyt arvot, Double-arvosta luodut luvut esimerkiksi NewNumeric-kutsun kautta sekä mikä tahansa jäsennetty arvo, jota on sittemmin muokattu SetTo-kutsulla. Huomaa, että objektirajapinnan kautta purettu teksti, esimerkiksi SetObjectFromString-kutsulle annettu literaali, kulkee saman tokenisoijan läpi ja säilyy samalla tavalla. Jäsennetty desimaali, jota ei koskaan muokattu, pitää syötetarkkuutensa asetuksesta riippumatta, eikä asetuksen muuttaminen latauksen jälkeen kosketa siihen takautuvasti. SetPrecision-viitemerkintä päivitettiin samassa julkaisussa sanomaan täsmälleen tämä, koska vanha sanamuoto antoi ymmärtää, että asetus koskisi jokaista tiedoston lukua
Tyhjennys tapahtuu SetTo-metodissa sen sijaan että se johdettaisiin Changed-lipusta, ja tuo ero on merkityksellinen. Tallennusputki nollaa objektien Changed-lipun heti kun ne on kirjoitettu, joten muotoa ”kirjoita alkuperäinen teksti ellei muutettu” oleva tarkistus alkaisi tuottaa vanhentunutta tekstiä arvolle, joka muokattiin, tallennettiin ja muokattiin uudelleen samassa istunnossa. Alkuperäisen tekstin sitominen itse sijoitukseen tekee mahdottomaksi, että ne olisivat eri mieltä. Regressiotesti kiinnittää jokaisen näistä käyttäytymisistä alkuperäisen tiedoston arvoilla
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]'));
// Muokkaamaton syöte säilyy sellaisenaan, myös arvo, jonka
// neljän desimaalin muotoilu olisi kutistanut nollaksi
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Muokkaus hylkää alkuperäisen tekstin ja noudattaa PDFPrecNum-asetusta
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Tarkkuuden laskeminen jälkikäteen ei yllä muokkaamattomaan syötteeseen
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Miksi sisältömalli normalisoi luvut edelleen?
Koska TPDFContentProgram lupaa kanoniset numeeriset operandit, ja tuo lupaus on arvokkaampi kuin sellaisenaan säilyvä teksti sisältöstreamin sisällä. Muokattava sisältömalli, sama jonka päälle grafiikkatilan seuraaja on rakennettu, on olemassa siksi, että NormalizeContentStreams, optimoija ja Emit tuottavat vakaata ja vertailukelpoista tulostetta mielivaltaisesta syötteestä. Jos jäsennetty operandi kuljettaisi alkuperäisen tekstinsä malliin, operaattorisekvenssi kuten 0.50000 0 0 RG tuottaisi eri tavalla kuin 0.5 0 0 RG, ja jokainen alavirran vertailu ajautuisi tuottajan muotoilutottumusten mukana
Niinpä malli riisuu alkuperäisen tekstin kahdessa sisääntulopisteessään. NormalizeContentNumbers ajetaan jokaiselle operandille, kun jäsennin työntää sen, ja uudelleen SetOperand-metodin sisällä, kun kutsujan toimittamaa lähdettä puretaan, ja se etenee rekursiivisesti taulukoiden ja sanakirjojen läpi, jotta katkoviivakuviot, TJ-taulukot ja merkityn sisällön ominaisuussanakirjat tulevat katetuiksi. SetTo(AsDouble)-kutsu jokaiselle numeeriselle riittää, koska se on täsmälleen se operaatio, joka tyhjentää tekstin. Raaka inline-kuvadata jätetään rauhaan, kuten aina ennenkin
// Lib/PDFlibContentModel.pas — sisältömalli pitää sopimuksensa
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;
Kutsujien käytännön sääntö on siis yksinkertainen. Pelkkä LoadFromFile ja sen perään SaveToFile jättää koskemattomat sisältöstreamit ja koskemattomat sanakirjaluvut sellaisiksi kuin ne olivat. Sivu, joka kulkee NormalizeContentStreams-kutsun läpi tai jonka muokkaus tehdään sisältömallin kautta, tulee ulos kanonisena suunnittelun mukaan, ja muu asiakirja on silti edelleen säilytetty. Ne ovat kaksi eri pyyntöä, ja ne tekevät nyt kahta eri asiaa
Mitä se maksaa ja mihin takeet päättyvät
Jokainen TPDFNumeric kuljettaa nyt yhtä ylimääräistä AnsiString-viittausta, ja jokainen jäsennetty desimaali pitää lähdetekstinsä elossa objektin koko eliniän. Asiakirjassa, jossa on miljoonia reaalilukuja, se on oikeaa muistia, ja se kuuluu jokaiseen suurten asiakirjojen mittaukseen sen sijaan että se ohitetaan kintaalla. Takuu on myös rajattu luvun omaan asiakirjaan: objektien kopioiminen asiakirjojen välillä tai arvojen rekonstruointi objektirajapinnan kautta tuottaa uusia lukuja, jotka noudattavat tulostustarkkuutta kuin mikä tahansa muu uusi luku. On syytä olla täsmällinen siitä, mitä julkaisu lupaa ja mitä ei. Koskemattoman asiakirjan lataus ja tallennus säilyttää nyt ne kalibrointiluvut, joita renderöijä todella kuluttaa, ja juuri sitä ominaisuutta korpuksen perustaso tarkistaa. Se ei lupaa tavuidenttistä tulostetta, joka riippuu myös objektien numeroinnista, streamien pakkauksesta ja trailerin tunnisteesta, jota käsitellään deterministisen PDF-ID:n artikkelissa. Eikä se saa sormenjälkidiffausta näkemään pyöristyseroja muiden ohjelmistojen tuottamissa tiedostoissa, koska ne tiivistyvät edelleen normalisoituun runkoon. Oppi yleistyy hyvin CalRGB:n ulkopuolelle: kun jäsennin pitää vain muunnetun arvon, jokainen tallennus on muokkaus, ja ainoa tapa huomata se on katsoa renderöityä tulosta. Lukujen käsittely ja SetPrecision-semantiikka on dokumentoitu losLab PDF Developer Library -tuotesivulla