Teknisk artikkel

Bevar desimalpresisjonen fra PDF ved lagring i Delphi

PDFlibPas, losLab PDF Developer Library, beholder den nøyaktige desimalteksten den leste inn for hvert reelt tall i et dokument, og skriver den teksten tilbake ordrett så lenge verdien aldri ble endret. Siden v3.539.19 styrer innstillingen SetPrecision bare tall som biblioteket selv oppretter eller redigerer, så en vanlig last-og-lagre-runde ikke lenger runder et CalRGB /Gamma på 2.22221 ned til 2.2222 og endrer fargene på en side ingen har rørt. Endringen er liten i kode og stor i hva den sier om parsere: verdien du dekoder og literalen du skriver ut, er to forskjellige ting, og en rundtur gjennom en Double er ikke en identitetstransformasjon

Hvorfor endret en lagring uten endringer fargene på siden?

Fordi det var parametrene i fargerommet som ble reformatert, ikke bildet. Filen som avslørte dette, er et 35-siders kontordokument i det lokale regresjonskorpuset, med et topptekstbilde som gjenbrukes på hver side. Å laste den og lagre den rett tilbake ga bildestrømmer som var byte for byte identiske med inndataene, og en sammenligning av strømhasher rapporterte dokumentet som uendret. En gjengitt sammenligning var uenig: alle de 35 sidene viste pikselforskjeller i toppteksten, og ingen andre steder

Topptekstbildet tegnes gjennom et CalRGB-fargerom, som ISO 32000-1 §8.6.5.3 definerer med et /WhitePoint, en valgfri /Gamma-array med tre elementer og en valgfri /Matrix med ni elementer. De arrayene er vanlige numeriske objekter i fargeromsordboken. TPDFNumeric lagret hvert av dem som en Double og ingenting annet, og TPDFNumeric.Output formaterte den Double-en gjennom PDFPrecNum, som standard er fire desimaler. Så /Gamma gikk fra 2.22221 til 2.2222, en matriseverdi gikk fra 0.71519 til 0.7152, og gjengiveren produserte tro mot inndataene litt andre farger fra en litt annen kalibrering. Selve bildebytene var uskyldige; tallene rundt dem var det ikke. Det ubehagelige er hvor usynlig dette var. Å sammenligne dekodede strømbyte kan ikke se det, fordi tallene bor i en ordbok, ikke i en strøm. Å sammenligne vedleggslaster kan ikke se det. Til og med revisjonsdiffen som beskrives i artikkelen om endringsnivå, tar fingeravtrykk av en normalisert objektkropp, så begge revisjonene hasher til samme verdi og diffen rapporterer dem som identiske. Bare gjengivelsen fanget det, og det er derfor korpusbaselinen gjengir hver side i stedet for å stole på strukturelle sjekker alene

Hvor PDFlibPas mistet CalRGB-presisjon ved en lagring uten endringer i Delphi: det innleste /Gamma 2.22221 og en matriseverdi 0.71519 ligger i TPDFNumeric som en Double, Output formaterer dem gjennom PLDoubleToStr med PDFPrecNum på fire desimaler, hver strukturelle sjekk rapporterer dokumentet som uendret, og bare den gjengitte sammenligningen viser at alle 35 topptekstbilder er forskjøvet
Bildebytene var uskyldige: TPDFNumeric reformaterte kalibreringstallene rundt dem gjennom PDFPrecNum, så strømhasher og fingeravtrykkdiffen rapporterte begge identiske revisjoner mens gjengiveren produserte litt andre farger på hver side

Verdien du leste inn, er ikke literalen du bør skrive

Et reelt tall i PDF er en desimalstreng, og ISO 32000-1 §7.3.3 er tydelig på at det bare er en desimalstreng: ingen radiksnotasjon, ingen eksponentform. Vedlegg C lister deretter opp presisjonen en implementasjon forventes å respektere, omtrent fem signifikante desimaler i brøkdelen. En standard utdatapresisjon på fire er allerede under det, og det blir verre nær null: PLDoubleToStr skalerer verdien, runder av til et heltall og skriver 0 når resultatet er null, så en matriseverdi på -0.000012345 mister ikke et siffer, den forsvinner helt

Å heve standarden ville bare flytte stupet. Fiksen er å slutte å late som om en Double er tallet. Når tokenizeren i TPDFStructure.Decode gjenkjenner et standard reelt tall, det vil si at tokenet inneholder et desimalpunkt og ingen eksponentmarkør, lagrer den kildeteksten i det nye FOriginalText-feltet ved siden av den konverterte verdien. Output foretrekker deretter den teksten og faller tilbake til formatering bare når det ikke er noe å foretrekke

Hvordan PDFlibPas bevarer innlest desimaltekst i Delphi: tokenizeren i TPDFStructure.Decode beholder kildeliteralen i FOriginalText for ethvert token med desimalpunkt og uten eksponent, Output skriver den teksten ordrett i stedet for å kalle PLDoubleToStr, og SetTo tømmer den fordi et redigert tall er et nytt tall
Verdien du dekoder og literalen du skriver ut er to forskjellige ting: å foretrekke den innleste teksten holder 2.22221 eksakt, mens tall biblioteket har opprettet og redigert fortsatt følger PDFPrecNum, og innstillingen når aldri urørt inndata
// Lib/PDFlibStruct.pas — hele fiksen på utdatasiden
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:= '';   // et redigert tall er et nytt tall
  FValue:= Value;
  FChanged:= True;
End;

To grenser er bevisste. Heltall bevares ikke, fordi heltallsformatering allerede er tapsfri. Eksponentformer som 6.02E23 tolereres på inndata for ødelagte produsenters skyld, men bevares ikke på utdata, siden det å skrive dem tilbake ville videreføre en syntaks §7.3.3 forbyr; de går gjennom formatoren som ethvert tall biblioteket genererer. Tokenizeren bruker også sin vanlige minimale reparasjon før den lagrer teksten, så en literal med ledende punktum som .5 beholdes som 0.5, og en literal med avsluttende punktum som 5. som 5.0. Begge er samme tall for enhver leser og er langt mer allment akseptert

Hva garanterer SetPrecision etter v3.539.19?

TPDFlib.SetPrecision styrer nå antall desimaler for tall biblioteket selv produserer: verdier tegnet gjennom painteren, tall opprettet fra en Double, for eksempel via NewNumeric, og enhver innlest verdi som siden er redigert med SetTo. Merk at tekst som dekodes gjennom objekt-API-et, for eksempel en literal sendt til SetObjectFromString, går gjennom den samme tokenizeren og bevares på samme måte. En innlest desimal som aldri ble endret, beholder inndatapresisjonen uavhengig av innstillingen, og å endre innstillingen etter lastingen rører den ikke i ettertid. Referanseoppføringen for SetPrecision ble oppdatert i samme utgivelse for å si nøyaktig dette, fordi den gamle ordlyden antydet at innstillingen gjaldt hvert tall i filen

Tømmingen skjer i SetTo i stedet for å utledes fra Changed-flagget, og den forskjellen betyr noe. Lagringspipelinen nullstiller Changed på objekter når de er skrevet, så en sjekk av formen «skriv ut originalteksten med mindre endret» ville begynne å skrive ut foreldet tekst for en verdi som ble redigert, lagret og redigert igjen i samme sesjon. Å knytte originalteksten til selve tilordningen gjør det umulig for de to å være uenige. Regresjonstesten fester hver av disse oppførslene med verdiene fra originalfilen

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]'));
    // Urørt inndata overlever ordrett, inkludert verdien som
    // fire-desimalers formatering ville ha kollapset til 0
    Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');

    // En redigering forkaster originalteksten og følger PDFPrecNum
    Number := TPDFNumeric(Values.Item[0]);
    Number.SetTo(0.123456);
    Assert(Number.Output = '0.1235');
    Assert(Structure.NewNumeric(0.123456).Output = '0.1235');

    // Å senke presisjonen etterpå når ikke urørt inndata
    Structure.PDFPrecNum := 2;
    Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
  finally
    Structure.Free;
  end;
end;

Hvorfor normaliserer innholdsmodellen fortsatt tall?

Fordi TPDFContentProgram lover kanoniske numeriske operander, og det løftet er verdt mer enn ordrett tekst inne i en innholdsstrøm. Den redigerbare innholdsmodellen, den samme som sporingen av grafikkstatus er bygget på, finnes for at NormalizeContentStreams, optimalisereren og Emit skal produsere stabile, sammenlignbare utdata fra vilkårlige inndata. Hvis en innlest operand tok med seg originalteksten inn i modellen, ville en operatorsekvens som 0.50000 0 0 RG bli skrevet ut annerledes enn 0.5 0 0 RG, og enhver sammenligning nedstrøms ville drive med produsentens formateringsvaner

Så modellen fjerner originalteksten ved sine to inngangspunkter. NormalizeContentNumbers kjører på hver operand når parseren skyver den inn, og igjen inne i SetOperand når kallergitt kilde dekodes, og den rekurserer gjennom arrayer og ordbøker slik at stiplete mønstre, TJ-arrayer og egenskapsordbøkene til markert innhold dekkes. Å kalle SetTo(AsDouble) på hvert tall er nok, siden det nettopp er operasjonen som tømmer teksten. Rå innebygde bildedata røres ikke, som alltid

Hvorfor innholdsmodellen i PDFlibPas fortsatt normaliserer tall: NormalizeContentNumbers kjører der parseren skyver inn hver operand og igjen inne i SetOperand, rekurserer gjennom arrayer og ordbøker slik at stiplete mønstre, TJ-arrayer og egenskapsordbøker for markert innhold dekkes, og SetTo AsDouble tømmer originalteksten så 0.50000 og 0.5 skrives ut likt
Kanoniske numeriske operander er innholdsmodellens løfte: rå innebygde bildedata røres ikke, og urørte ordboktall utenfor innholdsstrømmer beholder ordrett-garantien, så et vanlig par med LoadFromFile og SaveToFile bevarer dem fortsatt
// Lib/PDFlibContentModel.pas — innholdsmodellen beholder kontrakten sin
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;

Den praktiske regelen for kallere er derfor enkel. En vanlig LoadFromFile etterfulgt av SaveToFile lar urørte innholdsstrømmer og urørte ordboktall være som de var. En side som går gjennom NormalizeContentStreams, eller enhver redigering gjort gjennom innholdsmodellen, kommer ut kanonisk med vilje, og resten av dokumentet er fortsatt bevart. Det er to forskjellige forespørsler, og de gjør nå to forskjellige ting

Hva det koster, og hvor garantien stopper

Hver TPDFNumeric bærer nå én ekstra AnsiString-referanse, og hver innlest desimal holder kildeteksten sin i live så lenge objektet lever. På et dokument med millioner av reelle tall er det virkelig minne, og det hører hjemme i enhver måling på store dokumenter i stedet for å viftes bort. Garantien er også begrenset til et talls eget dokument: å kopiere objekter mellom dokumenter eller rekonstruere verdier gjennom objekt-API-et gir nye tall, som følger utdatapresisjonen som ethvert annet nytt tall. Det er verdt å være presis på hva utgivelsen gjør og ikke gjør krav på. En last-og-lagre av et urørt dokument bevarer nå kalibreringstallene som gjengiveren faktisk bruker, og det er egenskapen korpusbaselinen sjekker. Den gjør ikke krav på byte-identiske utdata, som også avhenger av objektnummerering, strømkomprimering og trailer-identifikatoren som diskuteres i artikkelen om deterministisk PDF-ID. Og den får ikke fingeravtrykkdiffen til å se avrundingsforskjeller i filer produsert av annen programvare, siden de fortsatt hasher den normaliserte kroppen. Lærdommen generaliserer godt utover CalRGB: når en parser bare beholder den konverterte verdien, er hver lagring en redigering, og den eneste måten å oppdage det på er å se på det gjengitte resultatet. Tallhåndteringen og semantikken til SetPrecision er dokumentert på produktsiden til losLab PDF Developer Library