Techninis straipsnis

PDF skaičiai prieš JSON: NaN, Infinity ir null Delphi

PDF Library for Delphi (PDFlibPas) nuo v3.539.31 išduoda teisėtą JSON kiekvienam PDF skaičiui. GetObjectJSON žetonus, kuriuos ISO 32000-1 priima, o RFC 8259 atmeta, tokius kaip -.25, +1.5 ir 007.5, persirašo į -0.25, 1.5 ir 7.5 skaitmenis po skaitmens; GetDocumentJSON ir analizės ataskaitos NaN ir Infinity rašo kaip null; o PLDoubleToStr NaN atveju rašo 0 vietoj EInvalidOp išmetimo pačiame eksporto viduryje. Iki pataisos biblioteka galėjo pagaminti JSON, kurio paties skaitytuvas atsisakydavo pakrauti

Kodėl teisėtas PDF skaičius laužia JSON?

Todėl, kad dvi gramatikos nesutaria dėl keturių smulkmenų, o šaltinio tekstą gerbiantis PDF analizatorius tas smulkmenas neša tiesiai į išvestį. ISO 32000-1 §7.3.3 leidžia skaičiui prasidėti pliuso ženklu, praleisti sveikąją dalį (.5), baigtis pliku tašku (4.) ir nešti priekinius nulius (007.5). RFC 8259 §6 neleidžia nė vieno iš tų dalykų: neprivalomas minusas, sveikoji dalis, kuri arba 0, arba prasideda skaitmenimis 1–9, ir bent vienas skaitmuo po bet kokio dešimtainio taško. Gamintojai laisvai rašo PDF formas, o nemažai generatorių ir rankomis redaguotų failų tai daro

Prasiskverbimas atėjo iš sąmoningo tikslumo bruožo. Nuo v3.539.19 TPDFNumeric.Output grąžina tikslų tekstą, kurį tokenizatorius išanalysavo tikriesiems skaičiams – būtent todėl sukalibruota spalvos reikšmė išsaugojime lieka tiksli, kaip aprašyta išanalizuoto PDF dešimtainio tikslumo išsaugojime. Tokenizatorius pakeliui į vidų jau sutvarko .5 į 0.5 ir 4. į 4.0, o sveikieji performatuojami iš reikšmės, tad +3 grįžta kaip 3. Pažodžiui išgyvena visa kita: ženklintas priekinis taškas (-.25), atviras pliusas tikrajam skaičiui (+1.5) ir priekiniai nuliai (007.5). Senasis objektų rašytojas prie "value": priklijuodavo Output, o bibliotekos pačios skaitytuvo TJSONParser.ParseNumber ant kiekvieno iš jų sustoja su „Invalid JSON number“, tad eksportas pasisekdavo, o pakartotinis importas krisdavo su PDFLIB_ERROR_OBJECT_JSON_INVALID (105)

PDFlibPas GetObjectJSON RFC 8259 atmetamus PDF skaičių žetonus persirašo skaitmenis po skaitmens: -.25 tampa -0.25, +1.5 praranda pliusą, 007.5 meta priekinius nulius, o trupmeniniai skaitmenys, tokie kaip 1.250000, išgyvena, nes formatavimas iš saugomo Double pridėtų dvejetainio triukšmo
Senasis rašytojas priklijuodavo tikslų išanalysuotą tekstą, bibliotekos paties skaitytuvas sustodavo su Invalid JSON number, o klaida 105 sulauždavo round-trip, kurį eksporto pusė vadino sėkme
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');

    // Objektas 12 yra masyvas, parašytas kaip [-.25 +1.5 007.5]
    JSON := Lib.GetObjectJSON(12, 0);
    // v3.539.31 ir naujesnėse: reikšmės atkeliauja kaip -0.25, 1.5 ir 7.5

    // SetObjectJSON parinkčių nepriima, tad perduokite 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;

Kaip PDFNumberTextToJSON išlaiko kiekvieną skaitmenį?

PDFNumberTextToJSON žetoną perskirba iš naujo, o ne perskaičiuoja jį iš Double. Funkcija faile PDFlibObjectJSON skaito neprivalomą ženklą, surenka skaitmenis aplink vienintelį dešimtainį tašką ir tada pritaiko tik redagavimus, kurių reikalauja JSON: numeta pliusą, nukerpa priekinius nulius palikdama vieną, paduoda 0, kai sveikoji dalis tuščia, numeta pliką galinį tašką ir sugrąžina minusą. Žetonas, kuriame yra bet koks kitas simbolis arba iš viso nėra skaitmenų, grįžta į PLJSONNumber(Value, 10), kuri nebegtinai reikšmei rašo null

PDFlibPas PDFNumberTextToJSON skaito ženklą, surenka skaitmenis aplink vienintelį dešimtainį tašką ir pritaiko tik redagavimus, kurių reikalauja JSON, o bet koks kitas simbolis ar tuščias skaitmenų ruožas grįžta į PLJSONNumber, kuri NaN ir Infinity vietoj skaičiaus rašo null
Perskirbimas nugali perskaičiavimą: tokenizatorius pakeliui į vidų jau buvo sutvarkęs .5 ir 4., tad rašytojas išlaiko kiekvieną išgyvenusį skaitmenį, o round-trip atkuria lygiai tą pačią reikšmę
  • -.25 tampa -0.25, o +.5 tampa 0.5
  • +1.5 tampa 1.5
  • 007.5 tampa 7.5, o 0.75 lieka toks, koks yra
  • 4. tampa 4, jei toks žetonas kada nors pasieks rašytoją
  • 2.22221 ir 1.250000 išlaiko kiekvieną trupmeninį skaitmenį, galinius nulius įskaitant

Formatavimas iš saugomo Double būtų buvęs trumpesnis ir klaidingas – dėl tos pačios priežasties egzistuoja tikslumo pataisa: numatytasis išvesties tikslumas yra keturi dešimtainiai, o net pilno tikslumo konversija dešimtainiui literalui gali pridėti dvejetainio triukšmo. Skaitmenų išlaikymas reiškia, kad SetObjectJSON ir ImportObjectJSON, kurie kiekvieną JSON skaičiaus tekstą perduoda PDF tokenizatoriui, atkuria lygiai tą pačią reikšmę. Garantija dengia reikšmę, o ne baitus: po pakartotinio importo -.25 saugojama ir išsaugoma kaip -0.25. Abi rašybos pagal §7.3.3 lygiavertės, bet baitų lygio diff pakeitimą pažymės, tad eksporto ir importo ciklo laikykite ne no-op dokumente, kurio baitus dengia parašas

Kas nutinka skaičiui, kurio JSON negali išreikšti?

GetDocumentJSON dabar bet kokiam NaN ar begalybiniam skaičiui rašo null, nes RFC 8259 §6 neturi sintaksės nei vienam, nei kitam. Infinity pasigaminti lengviau, nei skamba: PDF tokenizatorius skaitmenis kaupia pakartotine daugyba į Double, kurios riba šalia 1.8 × 10308, tad sveikasis literalas, ilgesnis už maždaug 300 skaitmenų, tyliai tampa +Inf. Sąžiningi failai tokio literalo niekada neturi; fuzzing'o ir priešiškų failų tarpe jie pasitaiko – todėl jie priklauso tam pačiam testų korpusui kaip Pascal PDF analizatoriaus sukietinimo prieš kenkėjiškus failus atvejai. Senasis dokumentų rašytojas netikrus skaičius formatuodavo su Str(D:0:6), o +Inf atveju tai rašo tekstą +Inf, kurio joks JSON vartotojas neišanalysuos

null čia sąmoningai prarandantis duomenis. GetDocumentJSON išvesties vartotojai turi priimti null bet kurioje vietoje, kur gali atsirasti skaičius, ir skaityti jį kaip „reikšmė buvo, bet jos išreikšti negalima“, o ne kaip dingusį raktą. Pirminio literalo iš dokumento JSON atgauti neįmanoma, tad pipeline, kuriam tai rūpi, turėtų užregistruoti objektą ir failą laikyti įtartinu, vietoj to, kad įkištų numatytąją reikšmę

Kodėl vienas NaN galėjo nutraukti SVG arba JSON eksportą?

Todėl, kad PLDoubleToStr – invariantinis skaičių formatuotojas, stovintis už turinio srautų, SVG, XML, CSV ir didžiosios dalies bibliotekos JSON – savo įvedimą padaugindavo ir kviestų Round, o Round(NaN) tokiose taikinio platformose kaip Win32, kur Delphi palieka x87 netinkamos operacijos išimtį neužmaskuotą, kelia EInvalidOp. Išimtis sprogdavo tada, kai rašytojas jau buvo išdavęs dalį išvesties, tad vienas degradavęs matavimas, 0/0 metikoje arba kvietėjo perduotas NaN palikdavo po savęs nukirstą failą. PLDoubleToStr dabar NaN grąžina 0, o jo sveikųjų skaičių šaka apkarpoma iki ±9.2e18 kaip ir trupmeninė, tad Infinity taip pat išeina kaip baigtinis literalas

Nulis yra teisingas atsakymas turinio srautui, kur skaičiaus vietoje turi gulėti skaičius, ir neteisingas atsakymas ataskaitai, kur 0 yra įtikinamas matavimas. JSON rašytojai, kuriems privaloma išlaikyti skirtumą, naudoja PLJSONNumber(Value, Decimals) iš PDFlibExtra, kuri NaN ar Infinity rašo null, o kitu atveju – invariantinius skaitmenis. PLJSONNumber dabar paremia GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON ir brūkšninių kodų, išlyginimo, struktūrinio teksto bei PDF/VCR ataskaitas; išlyginimo ataskaita anksčiau nebegtinam kampui rašė 0, o dabar rašo null

PDFlibPas NaN ir Infinity stabdo trimis būdais: AddPageMatrix, ScalePage ir RedactRegion nebegtinius argumentus atmeta prie pat durų, PLDoubleToStr turinio srautų vietose rašo 0, o PLJSONNumber ataskaitose rašo null, kur nulis skaitytųsi kaip įtikinamas matavimas – anksčiau Round(NaN) eksporto viduryje keldavo EInvalidOp
Nulis yra teisingas atsakymas turinio srautui ir neteisingas atsakymas ataskaitai, tad ataskaitų rašytojai kiekvieną Double perduoda PLJSONNumber ir leidžia null pasakyti, jog reikšmė buvo, bet jos išreikšti negalima
uses
  SysUtils, PDFlibTypes, PDFlibExtra;

function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
  B: PLStringBuilder;
begin
  B := PLStringBuilder.Create(128);
  try
    // Kiekvieną Double pirmiausiai suformatuokite į tekstą; PLJSONNumber rašo null
    // NaN ar Infinity atveju ir visuomet vartoja dešimtainį tašką
    B.Append('{"page":').Append(Page)
     .Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
     .Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
     .Append('}');
    // Niekada B.Append(Angle): Double perkroda seka vartotojo lokalę
    Result := B.ToString;
  finally
    B.Free;
  end;
end;

Kur vartotojo lokalė vis dar išsmunka į JSON?

Pro bet kurį formatuotoją, kuris pasižiūri į regioninius nustatymus, o pilnas mašinai skiriamo išvesties auditas rado lygiai vieną likusį: maxAcceptedMeanError faile GetSimilarImageDeduplicationReportJSON, rašytą su PLFloatToStr – plonu FloatToStr apvalkalu. Darbalaukyje, kurio dešimtainis skirtukas kablelis, ataskaitoje gulėjo "maxAcceptedMeanError":1,5, ką JSON analizatorius skaito kaip reikšmę 1 ir po jos plaukiojantį žetoną. Laukas praneša blogiausią priimtą pikselių klaidą iš suvokiamos vaizdų dedublikacijos, ir dabar jis eina per PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Likęs spąstai yra PLStringBuilder: Delphi jis yra paprastas System.SysUtils.TStringBuilder alternatyvusis vardas, kurio Append(Double) perkroda formatuoja per vartotojo lokalę, o FPC versijos vietoj to naudoja bibliotekos klasę, tad testas su Free Pascal arba en-US mašinoje jo niekada nepagaus

uses
  System.SysUtils, System.JSON, PDFlibrary;

var
  Lib: TPDFlib;
  Report: WideString;
  Parsed: TJSONValue;
begin
  // Atkurkite vokišką arba prancūzišką darbalaukį pačiame testo paleidime
  FormatSettings.DecimalSeparator := ',';
  Lib := TPDFlib.Create;
  try
    // Naudokite fikstūrą, kuri iš tiesų turi beveik dubliuotų vaizdų,
    // kitaip vidutinė klaida lygi 0 ir klaida lieka paslėpta
    Lib.LoadFromFile('scanned-batch.pdf', '');
    // Bandomasis paleidimas su slenksčiais 2, 2, 4: dokumentas nekeičiamas
    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 išvesties regresijos rinkiniui, kad jis liktų sąžiningas, reikia trijų fikstūrų: puslapio su -.25, +1.5 ir 007.5, objekto, laikančio 400 skaitmenų sveikąjį skaičių, ir bet kokios ataskaitos, paleistos kablelio lokalėje, kiekvienos patikrintos griežtu analizatoriumi, o ne akimis. Objektų JSON, dokumentų JSON ir analizės ataskaitos PDF Library for Delphi dalijasi tais pačiais skaičių taisyklėmis per Delphi, C++Builder ir Free Pascal; pilnas bruožų sąrašas yra PDF Library for Delphi produkto puslapyje