Tehnični članak

Števila PDF proti JSON: NaN, Infinity in null v Delphiju

PDF Library for Delphi (PDFlibPas) od v3.539.31 izdaja veljaven JSON za vsako število PDF. GetObjectJSON prepiše žetone, ki jih ISO 32000-1 sprejme, RFC 8259 pa odkloni, kot so -.25, +1.5 in 007.5, v -0.25, 1.5 in 7.5, števko za števko; GetDocumentJSON in analitična poročila za NaN in Infinity zapišejo null; PLDoubleToStr pa za NaN zapiše 0, namesto da bi na polovici izvoza sprožil EInvalidOp. Pred popravkom je lahko knjižnica izdelala JSON, ki ga njen lastni bralec ni hotel naložiti nazaj

Zakaj veljavno število PDF pokvari JSON?

Ker se dve skladnji ne strinjata o štirih drobnih podrobnostih, razčlenjevalnik PDF, ki spoštuje izvorno besedilo, pa bo te podrobnosti neposredno prenesel v izhod. ISO 32000-1 §7.3.3 dovoli, da se število začne s predznakom plus, izpusti celoštevilski del (.5), konča na goli piki (4.) in nosi vodilne ničle (007.5). RFC 8259 §6 tega ne dovoli ničesar: neobvezen minus, celoštevilski del, ki je bodisi 0 bodisi se začne s 1 do 9, in vsaj ena števka za vsako decimalno piko. Proizvajalci smejo prosto zapisati PDF oblike, in precej generatorjev ter ročno urejenih datotek to tudi počne

Uhajanje je prišlo iz namernih natančnostnih zmožnosti. Od v3.539.19 TPDFNumeric.Output vrne točno besedilo, ki ga je tokenizer razčlenil za realna števila, kar je tisto, ki kalibrirano barvno vrednost obdrži točno ob shranjevanju, kot opisuje ohranjanje razčlenjene decimalne natančnosti PDF. Tokenizer že na poti noter popravi .5 v 0.5 in 4. v 4.0, cela števila pa se preoblikujejo iz svoje vrednosti, zato se +3 vrne kot 3. Kar preživi dobesedno, je preostanek: podpisana vodilna pika (-.25), izrecni plus pri realnem (+1.5) in vodilne ničle (007.5). Stari pisalec objektov je pripel Output takoj za "value":, TJSONParser.ParseNumber v lastnem bralcu knjižnice pa se na vsakem od njih ustavi z »Invalid JSON number«, zato je izvoz uspel, ponovni uvoz pa odpovedal z PDFLIB_ERROR_OBJECT_JSON_INVALID (105)

PDFlibPas GetObjectJSON prepiše žetone števil PDF, ki jih odkloni RFC 8259, števko za števko: -.25 postane -0.25, +1.5 izgubi plus, 007.5 odvrže vodilne ničle, ulomkovne števke, kot je 1.250000, pa preživijo, ker bi oblikovanje iz shranjenega Double dodalo dvojiški šum
Stari pisalec je pripel točno razčlenjeno besedilo, lastni bralec knjižnice se je ustavil z Invalid JSON number, napaka 105 pa je polomila povratno pot, ki jo je strana izvoza razglasila za uspešno
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
  JSON: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('legacy-drawing.pdf', '') = 0 then
      raise Exception.Create('load failed');

    // Objekt 12 je polje, zapisano kot [-.25 +1.5 007.5]
    JSON := Lib.GetObjectJSON(12, 0);
    // v3.539.31 in kasneje: vrednosti prispejo kot -0.25, 1.5 in 7.5

    // SetObjectJSON ne sprejme možnosti, zato podajte 0
    if Lib.SetObjectJSON(12, JSON, 0) = 0 then
      raise Exception.CreateFmt('round trip rejected, error %d',
        [Lib.LastErrorCode]);

    Lib.SaveToFile('legacy-drawing-roundtrip.pdf');
  finally
    Lib.Free;
  end;
end;

Kako PDFNumberTextToJSON obdrži vsako števko?

PDFNumberTextToJSON žeton znova črkuje, namesto da bi ga znova izračunal iz Double. Funkcija v PDFlibObjectJSON prebere neobvezen predznak, pobere števke pred in za enojno decimalno piko in nato uveljavi samo popravke, ki jih zahteva JSON: odvrže plus, odstrani vodilne ničle in obdrži eno, prispe 0, ko je celoštevilski del prazen, odvrže golo končno piko in vrne minus nazaj. Žeton, ki vsebuje kateri koli drug znak ali pa sploh nič števk, pade nazaj na PLJSONNumber(Value, 10), ki za nefinitno vrednost zapiše null

PDFlibPas PDFNumberTextToJSON prebere predznak, pobere števke okoli enojne decimalne pike in uveljavi samo popravke, ki jih zahteva JSON, vsak drug znak ali prazen tek števk pa pade nazaj na PLJSONNumber, ki za NaN in Infinity namesto števila zapiše null
Znova črkovanje premaga znova računanje: tokenizer je že na poti noter popravil .5 in 4., pisalec pa obdrži vsako preživelo števko in povratna pot znova ustvari točno isto vrednost
  • -.25 postane -0.25, +.5 pa 0.5
  • +1.5 postane 1.5
  • 007.5 postane 7.5, 0.75 pa ostane, kakršen je
  • 4. postane 4, če tak žeton sploh kdaj doseže pisalca
  • 2.22221 in 1.250000 obdržita vsako ulomkovno števko, vključno z ničlami na koncu

Oblikovanje iz shranjenega Double bi bilo krajše in narobe, iz istega razloga, iz katerega obstaja popravek natančnosti: privzeta izhodna natančnost je štiri decimalki, tudi pretvorba s polno natančnostjo pa lahko decimalnemu literalu doda dvojiški šum. Obdržati števke pomeni, da SetObjectJSON in ImportObjectJSON, ki vsako besedilo števila JSON podata tokenizerju PDF, znova ustvarita točno isto vrednost. Zagotovilo pokriva vrednost, ne bajtov: po ponovnem uvozu je -.25 shranjeno in zapisano kot -0.25. Obe črkovanji sta enaki pod §7.3.3, bajtni diff pa bo spremembo označil, zato cikla izvoz in uvoz ne obravnavajte kot no-op na dokumentu, katerega bajte pokriva podpis

Kaj se zgodi s številom, ki ga JSON ne more predstaviti?

GetDocumentJSON zdaj za vsako število, ki je NaN ali neskončno, zapiše null, ker RFC 8259 §6 za nobeno nima skladnje. Neskončnost je lažje izdelati, kot se sliši: tokenizer PDF nabira števke s ponavljajočim množenjem v Double, ki se izčrpa blizu 1.8 × 10308, zato celoštevilski literal, malo čez 300 števk dolg, tiho postane +Inf. Poštene datoteke takega literala nikoli ne vsebujejo; fuzzirane in sovražne pa ga, zato sodijo v isti testni korpus kot primeri v utrjevanju razčlenjevalnika PDF v Pascalu pred zlonamernimi datotekami. Stari pisalec dokumentov je ne-cela števila oblikoval s Str(D:0:6), za +Inf pa to zapiše besedilo +Inf, ki ga noben porabnik JSON ne bo razčlenil

null je namerno izgubni. Porabniki izhoda GetDocumentJSON morajo sprejeti null kjerkoli se lahko pojavi število, brati pa naj ga kot »vrednost je bila prisotna, a je ni mogoče predstaviti«, ne kot manjkajoči ključ. Izvirnega literala iz JSON dokumenta ni mogoče povrniti, zato naj cevovod, ki mu je mar, zabeleži objekt in datoteko obravnava kot osumljenčko, namesto da bi vstavil privzeto vrednost

Zakaj je lahko en sam NaN prekinil izvoz SVG ali JSON?

Ker je PLDoubleToStr, invariantni formatirnik števil za tokove vsebine, SVG, XML, CSV in večino JSON v knjižnici, skaliral svoj vhod in poklical Round, Round(NaN) pa sproži EInvalidOp na ciljih, kot je Win32, kjer Delphi pusti izjemo neveljavne operacije x87 nemaskirano. Izjema je strelila, potem ko je pisalec že izdal del svojega izhoda, zato je ena degenerirana meritev, 0/0 v metriki ali NaN, ki ga poda klicatelj, pustila za seboj odsekano datoteko. PLDoubleToStr zdaj za NaN vrne 0, njegova celoštevilska veja pa se, tako kot ulomkovna, stisne na ±9.2e18, zato tudi Infinity izide kot finitni literal

Nič je pravi odgovor za tok vsebine, kjer mora reža za število držati število, in napačen odgovor za poročilo, kjer je 0 verodostojna meritev. Piscalci JSON, ki morajo obdržati razliko, uporabljajo PLJSONNumber(Value, Decimals) iz PDFlibExtra, ki za NaN ali Infinity zapiše null, sicer pa invariantne števke. PLJSONNumber zdaj stoji za GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON in poročili za črtne kode, deskew, strukturirani tekst in PDF/VCR; poročilo deskew je prej za nefinitni kot zapisalo 0, zdaj pa zapiše null

PDFlibPas ustavi NaN in Infinity na tri načine: AddPageMatrix, ScalePage in RedactRegion odklonijo nefinitne argumente že na vratih, PLDoubleToStr zapiše 0 za reže tokov vsebine, PLJSONNumber pa zapiše null v poročilih, kjer bi nič prebrali kot verodostojno meritev, potem ko je Round(NaN) nekoč sprožil EInvalidOp na polovici izvoza
Nič je pravi odgovor za tok vsebine in napačen odgovor za poročilo, zato pisci poročil vsak Double dajo PLJSONNumber in pustijo null, da pove, da je bila vrednost prisotna, a je ni mogoče predstaviti
uses
  SysUtils, PDFlibTypes, PDFlibExtra;

function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
  B: PLStringBuilder;
begin
  B := PLStringBuilder.Create(128);
  try
    // Najprej oblikujte vsak Double v besedilo; PLJSONNumber zapiše null
    // za NaN ali Infinity in vedno uporabi decimalno piko
    B.Append('{"page":').Append(Page)
     .Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
     .Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
     .Append('}');
    // Nikoli B.Append(Angle): preobremenitev Double sledi uporabniškemu locale
    Result := B.ToString;
  finally
    B.Free;
  end;
end;

Kod se uporabniški locale še vedno prikrade v JSON?

Skozi kateri koli formatirnik, ki sprašuje regionalne nastavitve, in polna revizija strojno berljivega izhoda je našla točno enega, ki je ostal: maxAcceptedMeanError v GetSimilarImageDeduplicationReportJSON, zapisan s PLFloatToStr, tanko ovijalko okoli FloatToStr. Na namizju, kjer je decimalno ločilo vejica, je poročilo vsebovalo "maxAcceptedMeanError":1,5, kar razčlenjevalnik JSON prebere kot vrednost 1, ki ji sledi osamljen žeton. Polje poroča najslabšo sprejeto napako pikslov iz perceptualne deduplikacije slik in zdaj gre skozi PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Preostala past je PLStringBuilder: na Delphiju je čista alias System.SysUtils.TStringBuilder, katerega preobremenitev Append(Double) oblikuje skozi uporabniški locale, verzije FPC pa namesto tega uporabljajo knjižnični razred, zato ga preizkus na Free Pascal ali računalniku en-US nikoli ne ujame

uses
  System.SysUtils, System.JSON, PDFlibrary;

var
  Lib: TPDFlib;
  Report: WideString;
  Parsed: TJSONValue;
begin
  // Reproducirajte nemško ali francosko namizje znotraj preizkusnega poganjanja
  FormatSettings.DecimalSeparator := ',';
  Lib := TPDFlib.Create;
  try
    // Uporabite fiksno datoteko, ki res vsebuje skoraj podvojene slike,
    // sicer je povprečna napaka 0 in hrošč ostane skrit
    Lib.LoadFromFile('scanned-batch.pdf', '');
    // Suhi tek s pragovi 2, 2, 4: dokument ni spremenjen
    Lib.GetSimilarImageDeduplicationReportJSON(2, 2, 4, Report);
    Parsed := TJSONObject.ParseJSONValue(Report);
    if Parsed = nil then
      raise Exception.Create('report is not valid JSON on a comma locale');
    Parsed.Free;
  finally
    Lib.Free;
  end;
end;

Regresijska zbirka za izhod JSON potrebuje tri fiksne datoteke, da ostane iskrena: stran, ki nosi -.25, +1.5 in 007.5, objekt s 400-številskim celim številom in katero koli poročilo, pognano pod locale z vejico, vsako validirano s strogim razčlenjevalnikom, namesto da bi jo pregledali z očmi. JSON objektov, JSON dokumentov in analitična poročila v PDF Library for Delphi si delijo ista pravila za števila čez Delphi, C++Builder in Free Pascal; popoln seznam zmožnosti je na strani izdelka PDF Library for Delphi