Tekninen artikkeli

PDF-lukukielioppi vs JSON: NaN, Infinity ja null Delphissä

PDF Library for Delphi (PDFlibPas) emittoi kelvollista JSONia jokaisesta PDF-luvusta versiosta v3.539.31 alkaen. GetObjectJSON kirjoittaa uudelleen tokenit, jotka ISO 32000-1 hyväksyy mutta RFC 8259 torjuu, kuten -.25, +1.5 ja 007.5, muotoihin -0.25, 1.5 ja 7.5 numero kerrallaan; GetDocumentJSON ja analyysiraportit kirjoittavat nullin NaN:lle ja Infinitylle; ja PLDoubleToStr kirjoittaa 0:n NaN:lle sen sijaan, että nostaisi virheen EInvalidOp viennin puolivälissä. Ennen korjausta kirjasto saattoi tuottaa JSONia, jonka oma lukijansa kieltäytyi lataamasta

Miksi kelvollinen PDF-luku rikkoo JSONin?

Siksi, että kaksi kielioppia eriävät neljästä pienestä yksityiskohdasta, ja PDF-jäsennin, joka kunnioittaa lähdetekstiä, vie ne yksityiskohdat suoraan tulosteeseen. ISO 32000-1 §7.3.3 antaa luvun alkaa plusmerkillä, jättää kokonaisosan pois (.5), päättyä paljaaseen pisteeseen (4.) ja kantaa etunollia (007.5). RFC 8259 §6 ei salli mitään tuosta: valinnainen miinus, kokonaisosa, joka on joko 0 tai alkaa numerolla 1–9, ja vähintään yksi numero minkä tahansa desimaalipisteen jälkeen. Tuottajat saavat kirjoittaa PDF-muodot vapaasti, ja paljon generaattoreita sekä käsin muokattuja tiedostoja tekee niin

Vuoto tuli tahallisesta tarkkuusominaisuudesta. Versiosta v3.539.19 alkaen TPDFNumeric.Output palauttaa tokenisoijan reaaliluvuille jäsentämän tarkan tekstin, ja juuri se pitää kalibroidun väriarvon tarkkana tallennuksessa, kuten kirjoituksessa jäsennettyjen PDF-desimaalitarkkuuksien säilyttäminen kuvataan. Tokenisoija korjaa jo tuloaukaisulla .5:n muotoon 0.5 ja 4.:n muotoon 4.0, ja kokonaisluvut muotoillaan arvostaan, joten +3 palaa muodossa 3. Sanasanaisena selviää loput: etumerkillinen alkupiste (-.25), eksplisiittinen plus reaaliluvussa (+1.5) ja etunollat (007.5). Vanha objektikirjoittaja liitti Outputin suoraan perään "value"::n jälkeen, ja kirjaston oman lukijan TJSONParser.ParseNumber pysähtyy jokaiseen niistä ilmoituksella "Invalid JSON number", joten vienti onnistui ja uudelleentuonti kaatui virheeseen PDFLIB_ERROR_OBJECT_JSON_INVALID (105)

PDFlibPas GetObjectJSON kirjoittaa RFC 8259:n torjumat PDF-lukutokenit uudelleen numero kerrallaan: -.25 muuttuu muodoksi -0.25, +1.5 menettää plusmerkkinsä, 007.5 pudottaa etunollansa, ja murtolukunumerot kuten 1.250000 selviävät, koska muotoilu talletetusta Doublesta lisäisi binäärikohinaa
Vanha kirjoittaja liitti liitteenä tarkan jäsennetyn tekstin, kirjaston oma lukija pysähtyi ilmoitukseen Invalid JSON number, ja virhe 105 rikkoi kiekuran, jonka vientipuoli julisti onnistuneeksi
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');

    // Objekti 12 on taulukko, joka on kirjoitettu muodossa [-.25 +1.5 007.5]
    JSON := Lib.GetObjectJSON(12, 0);
    // v3.539.31 ja uudemmat: arvot saapuvat muodossa -0.25, 1.5 ja 7.5

    // SetObjectJSON ei ota vastaan optioita, joten anna 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;

Miten PDFNumberTextToJSON säilyttää jokaisen numeron?

PDFNumberTextToJSON tavuttaa tokenin uudelleen sen sijaan, että laskisi sen uudelleen Doublesta. Funktio PDFlibObjectJSONin sisällä lukee valinnaisen etumerkin, kerää numerot yhden desimaalipisteen molemmin puolin ja tekee sitten vain ne muutokset, joita JSON vaatii: se pudottaa plusmerkin, poistaa etunollat säästäen yhden, toimittaa 0:n, kun kokonaisosa on tyhjä, pudottaa paljaan lopupisteen ja laittaa miinuksen takaisin. Tokeni, joka sisältää minkä tahansa muun merkin tai ei yhtään numeroa, putoaa funktioon PLJSONNumber(Value, 10), joka kirjoittaa nullin, kun arvo ei ole äärellinen

PDFlibPas PDFNumberTextToJSON lukee etumerkin, kerää numerot yhden desimaalipisteen ympäriltä ja tekee vain ne muutokset, joita JSON vaatii, kun taas mikä tahansa muu merkki tai tyhjä numeroputki putoaa funktioon PLJSONNumber, joka kirjoittaa nullin NaN:lle ja Infinitylle luvun sijaan
Uudelleentavuttaminen voittaa uudelleenlaskennan: tokenisoija korjasi jo .5:n ja 4.:n tuloaukaisulla, joten kirjoittaja säästää jokaisen jäljellä olevan numeron ja kiekura rakentaa täsmälleen saman arvon
  • -.25 muuttuu muodoksi -0.25, ja +.5 muuttuu muodoksi 0.5
  • +1.5 muuttuu muodoksi 1.5
  • 007.5 muuttuu muodoksi 7.5, ja 0.75 pysyy sellaisenaan
  • 4. muuttuu muodoksi 4, jos sellainen token joskus päätyy kirjoittajalle
  • 2.22221 ja 1.250000 säilyttävät jokaisen murtolukunumeron, lopulliset nollat mukaan lukien

Muotoilu talletetusta Doublesta olisi ollut lyhyempiä ja väärää, samasta syystä, miksi tarkkuuskorjaus on olemassa: oletustulostetarkkuus on neljä desimaalia, ja jo täystarkkuuden muunnos voi lisätä binäärikohinaa desimaalilitteraaliin. Numeroiden säästäminen tarkoittaa, että SetObjectJSON ja ImportObjectJSON, jotka antavat jokaisen JSON-lukutekstin PDF-tokenisoijalle, rakentavat täsmälleen saman arvon. Takuu kattaa arvon, eivät tavut: uudelleentuonnin jälkeen -.25 on talletettu ja tallennettu muodossa -0.25. Molemmat kirjoitusasut ovat yhtä suuria kohdan §7.3.3 nojalla, mutta tavutason diff liputtaa muutoksen, joten älä käsittele vienti- ja tuontisykliä no-op:na asiakirjassa, jonka tavut ovat allekirjoituksen peitossa

Mitä luvulle tapahtuu, jota JSON ei voi esittää?

GetDocumentJSON kirjoittaa nyt nullin jokaiselle luvulle, joka on NaN tai ääretön, koska RFC 8259 §6 ei tarjoa kummallekaan syntaksia. Äärettömän tuottaminen on helpompaa kuin kuulostaa: PDF-tokenisoija kerryttää numeroita toistuvalla kertolaskulla Doubleen, joka nousee korkeintaan lähelle arvoa 1.8 × 10308, joten yli 300 numeron pituinen kokonaislitteraali muuttuu hiljaa arvoksi +Inf. Rehelliset tiedostot eivät koskaan sisällä sellaista litteraalia; fuzzatut ja vihamieliset tekevät, minkä vuoksi ne kuuluvat samaan testikorpukseen kuin tapaukset kirjoituksessa Pascal-PDF-jäsennimen kovettaminen haitallisia tiedostoja vastaan. Vanha asiakirjakirjoittaja muotoili ei-kokonaisluvut muodossa Str(D:0:6), ja arvolle +Inf se kirjoittaa tekstin +Inf, jota mikään JSON-kuluttaja ei jäsentä

null on tahallaan tappiollinen. GetDocumentJSONin tulosteen kuluttajien on hyväksyttävä null missä tahansa, missä luku voi esiintyä, ja luettava se muodossa "arvo oli läsnä, mutta sitä ei voi esittää", ei puuttuvana avaimena. Alkuperäistä litteraalia ei voi palauttaa asiakirja-JSONista, joten putki, jolla on asiaa, kirjaa objektin ja kohtelee tiedostoa epäiltynä sen sijaan, että korvaisi arvon oletuksella

Miksi yksittäinen NaN saattoi keskeyttää SVG- tai JSON-viennin?

Koska PLDoubleToStr, kirjaston sisältöstreamien, SVG:n, XML:n, CSV:n ja suurimman osan JSONista takana oleva invariantti lukumuotoilija, skaalasi syötteensä ja kutsui funktiota Round, ja Round(NaN) nostaa virheen EInvalidOp kohteissa kuten Win32, joissa Delphi jättää x87:n epäkelvan operaation poikkeuksen peittaamatta. Poikkeus laukes vasta, kun kirjoittaja oli jo emitoinut osan tulosteestaan, joten yksi degeneroitunut mittaus, 0/0 mittarissa tai kutsujan syöttämä NaN jätti jälkeensä katkenneen tiedoston. PLDoubleToStr palauttaa nyt 0:n NaN:lle, ja sen kokonaislukuhaara rajaa itsensä arvoon ±9.2e18 kuten murtolukuhaara, joten myös Infinity tulee ulos äärellisenä litteraalina

Nolla on oikea vastaus sisältöstreamille, jossa lukupaikan on pidettävä sisällään luku, ja väärä vastaus raportille, jossa 0 on uskottava mittaus. JSON-kirjoittajat, joiden on säilytettävä ero, käyttävät funktiota PLJSONNumber(Value, Decimals) moduulista PDFlibExtra, joka kirjoittaa nullin NaN:lle tai Infinitylle ja invariantit numerot muuten. PLJSONNumber takaa nyt raportit GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON sekä viivakoodin, kallistuksen korjauksen, jäsennetyn tekstin ja PDF/VCR-raportit; kallistuskorjauksen raportti kirjoitti aiemmin arvon 0 ei-äärelliselle kulmalle ja kirjoittaa nyt nullin

PDFlibPas pysäyttää NaN:n ja Infinityn kolmella tavalla: AddPageMatrix, ScalePage ja RedactRegion torjuvat ei-äärelliset argumentit heti alkuun, PLDoubleToStr kirjoittaa 0:n sisältöstreamipaikkoihin, ja PLJSONNumber kirjoittaa nullin raporteissa, joissa nolla lukeutuisi uskottavana mittauksena, sen jälkeen kun Round(NaN) nosti EInvalidOpin viennin puolivälissä
Nolla on oikea vastaus sisältöstreamille ja väärä vastaus raportille, joten raporttikirjoittajat antavat jokaisen Doublen funktiolle PLJSONNumber ja päästävät nullin sanomaan, että arvo oli läsnä mutta esittämätön
uses
  SysUtils, PDFlibTypes, PDFlibExtra;

function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
  B: PLStringBuilder;
begin
  B := PLStringBuilder.Create(128);
  try
    // Muotoile jokainen Double tekstiksi ensin; PLJSONNumber kirjoittaa nullin
    // NaN:lle tai Infinitylle ja käyttää aina desimaalipistettä
    B.Append('{"page":').Append(Page)
     .Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
     .Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
     .Append('}');
    // Älä koskaan kutsu B.Append(Angle): Double-overload noudattaa käyttäjän lokaalia
    Result := B.ToString;
  finally
    B.Free;
  end;
end;

Missä käyttäjän lokaali yhä piiloutuu JSONiin?

Minkä tahansa alueasetuksia kyselevän muotoilijan kautta, ja koko auditointi koneluettavasta tulosteesta löysi täsmälleen yhden jäljellä: kentän maxAcceptedMeanError funktiossa GetSimilarImageDeduplicationReportJSON, kirjoitettuna funktiolla PLFloatToStr, ohut kääre funktion FloatToStr ympärillä. Työpöydällä, jonka desimaalierotin on pilkku, raportti sisälsi kentän "maxAcceptedMeanError":1,5, jonka JSON-jäsennin lukee arvona 1 ja sitä seuraavana harhaisena tokenina. Kenttä raportoi kirjoituksen havaintopohjaisen kuvien kaksoiskappaleiden tunnistuksen pahimman hyväksytyn pikselivirheen, ja se kulkee nyt funktion PLJSONNumber(Stats.MaxAcceptedMeanError, 6) läpi. Jäljellä oleva sudenkuoppa on PLStringBuilder: Delphillä se on paljas alias luokalle System.SysUtils.TStringBuilder, jonka overload Append(Double) muotoilee käyttäjän lokaalin kautta, kun taas FPC-buildit käyttävät kirjaston omaa luokkaa, joten testi Free Pascalilla tai en-US-koneella ei koskaan nappaa sitä

uses
  System.SysUtils, System.JSON, PDFlibrary;

var
  Lib: TPDFlib;
  Report: WideString;
  Parsed: TJSONValue;
begin
  // Toista saksalainen tai ranskalainen työpöytä testiajon sisällä
  FormatSettings.DecimalSeparator := ',';
  Lib := TPDFlib.Create;
  try
    // Käytä fixtureä, joka oikeasti sisältää lähes identtisiä kuvia,
    // muuten keskimääräinen virhe on 0 ja vika jää piiloon
    Lib.LoadFromFile('scanned-batch.pdf', '');
    // Kuiva ajo kynnysarvoilla 2, 2, 4: asiakirjaa ei muuteta
    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;

JSON-tulosteen regressiosarja tarvitsee pysyäkseen rehellisenä kolme fixtureä: sivun, joka kantaa arvoja -.25, +1.5 ja 007.5, objektin, joka pitää sisällään 400-numeroisen kokonaisluvun, ja minkä tahansa raportin ajettuna pilkkulokaalilla, jokainen validoituna tiukalla jäsennyksellä eikä silmämääräisesti. Objekti-JSON, asiakirja-JSON ja analyysiraportit PDF Library for Delphissä jakavat samat lukusäännöt Delphin, C++Builderin ja Free Pascalin välillä; koko ominaisuusluettelo on PDF Library for Delphi -tuotesivulla