PDF Library for Delphi (PDFlibPas) emituje platný JSON pre každé PDF číslo od v3.539.31. GetObjectJSON prepisuje tokeny, ktoré ISO 32000-1 prijíma, ale RFC 8259 odmieta, ako -.25, +1.5 a 007.5, na -0.25, 1.5 a 7.5 číslicu za číslicu; GetDocumentJSON a analytické reporty zapisujú pre NaN a Infinity null; a PLDoubleToStr zapíše pre NaN 0 namiesto vyhodenia EInvalidOp v polovici exportu. Pred opravou dokázala knižnica vyrobiť JSON, ktorý jej vlastný čítač odmietol načítať späť
Prečo pokazí platné PDF číslo JSON?
Pretože sa obe gramatiky nezhodnú na štyroch drobných detailoch a PDF parser, ktorý rešpektuje zdrojový text, zanesie tie detaily rovno do výstupu. ISO 32000-1 §7.3.3 dovoľuje číslu začať plusom, vynechať celú časť (.5), skončiť na holom bode (4.) a niesť úvodné nuly (007.5). RFC 8259 §6 nedovoľuje nič z toho: voliteľné mínus, celú časť buď 0, alebo začínajúcu číslicou 1 až 9, a aspoň jednu číslicu po desatinnej bodke. Producenti môžu PDF tvary pokojne zapisovať a kopec generátorov aj ručne upravovaných súborov to robí
Únik prišiel z cielenej funkcie pre presnosť. Od v3.539.19 vracia TPDFNumeric.Output pre reálne čísla presný text, ktorý tokenizer naparsoval, a práve to drží kalibrovanú farebnú hodnotu pri ukladaní presnú, ako popisuje článok o zachovaní naparsovanej PDF desatinnej presnosti. Tokenizer už na vstupe opraví .5 na 0.5 a 4. na 4.0 a celé čísla sa reformátujú z ich hodnoty, takže +3 sa vráti ako 3. Slovo od slova prežije zvyšok: bodka so znamienkom na začiatku (-.25), explicitný plus pri reálnom čísle (+1.5) a úvodné nuly (007.5). Starý objektový zapisovač pripojil Output hneď za "value": a TJSONParser.ParseNumber vo vlastnom čítači knižnice na každom z nich zastane s hláškou „Invalid JSON number“, takže export uspel a re-import zlyhal s PDFLIB_ERROR_OBJECT_JSON_INVALID (105)
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 pole zapísané ako [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 a novšie: hodnoty dorazia ako -0.25, 1.5 a 7.5
// SetObjectJSON neprijíma žiadne options, preto 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;
Ako PDFNumberTextToJSON zachová každú číslicu?
PDFNumberTextToJSON token prepíše po písmenách namiesto toho, aby ho prepočítavala z Double. Funkcia v PDFlibObjectJSON prečíta voliteľné znamienko, zozbiera číslice pred a po jedinej desatinnej bodke a potom aplikuje len úpravy, ktoré JSON vyžaduje: zloží plus, zoškrtnie úvodné nuly s tým, že jednu ponechá, dodá 0, keď je celá časť prázdna, zloží holú koncovú bodku a mínus vráti naspäť. Token obsahujúci akýkoľvek iný znak alebo žiadne číslice spadne k PLJSONNumber(Value, 10), ktoré zapíše null, keď hodnota nie je konečná
-.25sa stane-0.25a+.5sa stane0.5+1.5sa stane1.5007.5sa stane7.5, zatiaľ čo0.75ostanú, aké je4.sa stane4, ak taký token niekedy dorazí k zapisovaču2.22221a1.250000ponechajú každú desatinnú číslicu, koncové nuly vrátane
Formátovanie z uloženého Double by bolo kratšie a zlé, a to z rovnakého dôvodu, pre ktorý existuje oprava presnosti: predvolená výstupná presnosť je štyri desatinné miesta a aj konverzia s plnou presnosťou dokáže do desatinného literálu pridať binárny šum. Ponechanie číslic znamená, že SetObjectJSON a ImportObjectJSON, ktoré podávajú text každého JSON čísla PDF tokenizeru, znovu vytvoria presne tú istú hodnotu. Garancia sa týka hodnoty, nie bajtov: po re-importe sa -.25 ukladá a zapisuje ako -0.25. Obe hláskovania sú podľa §7.3.3 rovnaké, ale bajtovoúrovňový diff zmenu označí, takže cyklus exportu a importu neberte ako no-op na dokumente, ktorého bajty kryje podpis
Čo sa stane s číslom, ktoré JSON nedokáže vyjadriť?
GetDocumentJSON teraz zapisuje null pre každé číslo, ktoré je NaN alebo nekonečné, lebo RFC 8259 §6 pre žiadne z nich nemá syntax. Nekonečno sa vyrobiť dá ľahšie, než znie: PDF tokenizer zbieha číslice opakovaným násobením do Double, ktorý vrcholí pri 1.8 × 10308, takže celočíselný literál trochu cez 300 číslic sa potichu stane +Inf. Poctivé súbory taký literál nikdy neobsahujú; fuzzerom vyrobené a nepriateľské áno, a preto patria do toho istého testovacieho korpusu ako prípady z článku o posilňovaní Pascal PDF parsera proti škodlivým súborom. Starý dokumentový zapisovač formátoval neceločíselné hodnoty cez Str(D:0:6) a pre +Inf zapísal text +Inf, ktorý nerozparseuje žiadny JSON konzument
null je zámerne stratové. Konzumenti výstupu GetDocumentJSON musia akceptovať null hocikde, kde sa môže objaviť číslo, a mali by ho čítať ako „hodnota bola prítomná, ale nedá sa vyjadriť“, nie ako chýbajúci kľúč. Pôvodný literál sa z dokumentového JSON nedá obnoviť, takže pipeline, ktorej na tom záleží, má objekt zalogovať a súbor považovať za podozrivý namiesto náhrady predvolenou hodnotou
Prečo dokázalo jediné NaN prerušiť SVG či JSON export?
Lebo PLDoubleToStr, invariantný formatter čísel stojaci za content streammi, SVG, XML, CSV a väčšinou JSON v knižnici, prenásobil vstup a volal Round a Round(NaN) hodí EInvalidOp na cieľoch ako Win32, kde Delphi necháva výnimku x87 invalid-operation nezamaskovanú. Výnimka vystrelila potom, čo zapisovač už emitoval časť výstupu, takže jedno degenerované meranie, 0/0 v metrike alebo NaN podané volajúcim, zanechalo za sebou skrátený súbor. PLDoubleToStr teraz vracia pre NaN 0 a celočíselná vetva sa svorkuje na ±9.2e18 ako vetva desatinná, takže aj Infinity vychádza ako konečný literál
Nula je správna odpoveď pre content stream, kde číselný slot musí držať číslo, a zlá odpoveď pre report, kde 0 je vierohodné meranie. JSON zapisovače, ktoré musia zachovať ten rozdiel, používajú PLJSONNumber(Value, Decimals) z PDFlibExtra, ktoré pre NaN či Infinity zapíše null a inak invariantné číslice. PLJSONNumber teraz stojí za GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON a reportmi barcode, deskew, structured text a PDF/VCR; deskew report predtým zapisoval pre nekonečný uhol 0 a teraz zapisuje null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Každé Double najprv naformátuj na text; PLJSONNumber zapíše null
// pre NaN či Infinity a vždy používa desatinnú bodku
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Nikdy B.Append(Angle): overload pre Double sa riadi locale používateľa
Result := B.ToString;
finally
B.Free;
end;
end;
Kde sa do JSON ešte vkráda locale používateľa?
Cez ktorýkoľvek formatter, ktorý nazrie do regionálnych nastavení, a úplný audit strojovo čitateľného výstupu našiel zostávajúci presne jeden: maxAcceptedMeanError v GetSimilarImageDeduplicationReportJSON, zapisovaný cez PLFloatToStr, tenký obal nad FloatToStr. Na ploche s čiarkou ako desatinným oddeľovačom obsahoval report "maxAcceptedMeanError":1,5, čo JSON parser prečíta ako hodnotu 1 nasledovanú túlavým tokenom. Pole hlási najhoršiu akceptovanú pixelovú chybu z perceptuálnej deduplikácie obrázkov a teraz prechádza cez PLJSONNumber(Stats.MaxAcceptedMeanError, 6). Zvyšná pasca je PLStringBuilder: na Delphi je to obyčajný alias System.SysUtils.TStringBuilder, ktorého overload Append(Double) formátuje cez locale používateľa, zatiaľ čo FPC zostavenia používajú namiesto toho triedu z knižnice, takže test na Free Pascale alebo stroji en-US ho nikdy nechytí
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Reprodukcia nemeckej či francúzskej plochy vnútri testového behu
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Použite fixtúru, ktorá naozaj obsahuje takmer duplicitné obrázky,
// inak je priemerná chyba 0 a chyba zostane skrytá
Lib.LoadFromFile('scanned-batch.pdf', '');
// Suchý beh s prahmi 2, 2, 4: dokument sa neupraví
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;
Regresná sada pre JSON výstup potrebuje, aby ostala poctivá, tri fixtúry: stranu nesúcu -.25, +1.5 a 007.5, objekt držiaci 400-ciferné celé číslo a ktorýkoľvek report bežiaci pod čiarkovým locale, každú validovanú prísnym parserom namiesto odhadu okom. Object JSON, document JSON aj analytické reporty v PDF Library for Delphi zdieľajú tie isté číselné pravidlá naprieč Delphi, C++Builder a Free Pascal; kompletný zoznam funkcií je na produktovej stránke PDF Library for Delphi