Tekninen artikkeli

Desimaalitarkkuus säilyy Delphin PDF-tallennuksessa

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

Missä PDFlibPas menetti CalRGB-tarkkuuden no-op-tallennuksessa Delphissä: jäsennetty /Gamma-arvo 2.22221 ja matriisialkio 0.71519 asuvat TPDFNumericissa Double-arvona, Output muotoilee ne PLDoubleToStr-kutsulla PDFPrecNum-asetuksella neljään desimaaliin, jokainen rakennetarkistus ilmoittaa asiakirjan muuttumattomaksi, ja vain renderöity vertailu näyttää kaikkien 35 ylätunnistekuvan siirtyneen
Kuvan tavut olivat syyttömiä: TPDFNumeric muotoili niiden ympärillä olevat kalibrointiluvut uudelleen PDFPrecNum-asetuksen läpi, joten streamien tiivisteet ja sormenjälkidiffaus ilmoittivat molemmat identtisistä revisioista, kun taas renderöijä tuotti hieman eri värit jokaisella sivulla

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

Miten PDFlibPas säilyttää jäsennetyn desimaalitekstin Delphissä: TPDFStructure.Decode-funktion tokenisoija pitää lähdeliteraalin FOriginalText-kentässä jokaiselle tokenille, jossa on desimaalipiste eikä eksponenttia, Output kirjoittaa tuon tekstin sellaisenaan PLDoubleToStr-kutsun sijaan, ja SetTo tyhjentää sen koska muokattu luku on uusi luku
Arvo, jonka purat, ja literaali, jonka kirjoitat, ovat kaksi eri asiaa: jäsennetyn tekstin suosiminen pitää 2.22221:n tarkkana, kun taas kirjaston luomat ja muokkaamat luvut noudattavat edelleen PDFPrecNum-asetusta eikä asetus koskaan yllä koskemattomaan syötteeseen
// 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

Miksi PDFlibPasin sisältömalli normalisoi luvut edelleen: NormalizeContentNumbers ajetaan siellä missä jäsennin työntää kunkin operandin ja uudelleen SetOperand-metodin sisällä, etenee rekursiivisesti taulukoiden ja sanakirjojen läpi niin että katkoviivakuviot, TJ-taulukot ja merkityn sisällön ominaisuussanakirjat tulevat katetuiksi, ja SetTo AsDouble -kutsu tyhjentää alkuperäisen tekstin niin että 0.50000 ja 0.5 tuottavat saman
Kanoniset numeeriset operandit ovat sisältömallin lupaus: raaka inline-kuvadata jätetään rauhaan, ja sisältöstreamien ulkopuoliset koskemattomat sanakirjaluvut ovat edelleen sellaisenaan säilyttämisen takeen piirissä, joten pelkkä LoadFromFile- ja SaveToFile-pari säilyttää ne
// 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