PDF Library for Delphi (PDFlibPas) sänder ut giltig JSON för varje PDF-tal sedan v3.539.31. GetObjectJSON skriver om token som ISO 32000-1 accepterar men RFC 8259 avvisar, som -.25, +1.5 och 007.5, till -0.25, 1.5 och 7.5 siffra för siffra; GetDocumentJSON och analysrapporterna skriver null för NaN och Infinity; och PLDoubleToStr skriver 0 för NaN i stället för att kasta EInvalidOp halvvägs genom en export. Före fixen kunde biblioteket producera JSON som dess egen läsare vägrade läsa in
Varför bryter ett giltigt PDF-tal JSON?
För att de två grammatikerna är oense om fyra små detaljer, och en PDF-parser som respekterar källtexten får med sig de detaljerna rakt in i utdatan. ISO 32000-1 §7.3.3 låter ett tal börja med plustecken, utelämna heltalsdelen (.5), sluta på en naken punkt (4.) och bära inledande nollor (007.5). RFC 8259 §6 tillåter inget av det: ett valfritt minus, en heltalsdel som antingen är 0 eller börjar med 1 till 9, och minst en siffra efter varje decimalpunkt. Producenter får gärna skriva PDF-formerna, och gott om generatorer och handredigerade filer gör det
Läckan kom från en medveten precisionsfunktion. Sedan v3.539.19 returnerar TPDFNumeric.Output den exakta text som tokenizeren parsade för reella tal, vilket är det som håller ett kalibrerat färgvärde exakt vid sparande, som beskrivs i att bevara parsad PDF-decimalprecision. Tokenizeren patchar redan .5 till 0.5 och 4. till 4.0 på vägen in, och heltal formateras om från sitt värde, så +3 kommer tillbaka som 3. Det som överlever ordagrant är resten: ett tecken framför en inledande punkt (-.25), ett explicit plus på ett reellt tal (+1.5) och inledande nollor (007.5). Den gamla objektsskrivaren bifogade Output direkt efter "value":, och TJSONParser.ParseNumber i bibliotekets egen läsare stannar vid vare ett av dem med "Invalid JSON number", så exporten lyckades och återimporten fallerade med 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 är en array skriven som [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 och senare: värdena anländer som -0.25, 1.5 och 7.5
// SetObjectJSON tar inga alternativ, så skicka 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;
Hur håller PDFNumberTextToJSON varje siffra?
PDFNumberTextToJSON stavar om token i stället för att räkna om den från en Double. Funktionen i PDFlibObjectJSON läser ett valfritt tecken, samlar siffror före och efter en enskild decimalpunkt och tillämpar sedan bara de redigeringar JSON kräver: den släpper ett plus, klipper inledande nollor men behåller en, levererar 0 när heltalsdelen är tom, släpper en naken avslutande punkt och sätter tillbaka minustecknet. En token som innehåller något annat tecken, eller inga siffror alls, faller tillbaka till PLJSONNumber(Value, 10), som skriver null när värdet inte är ändligt
-.25blir-0.25, och+.5blir0.5+1.5blir1.5007.5blir7.5, medan0.75förblir som det är4.blir4om en sådan token någonsin når skrivaren2.22221och1.250000behåller varje bråkdelssiffra, avslutande nollor inkluderade
Att formatera från den lagrade Double:en hade varit kortare och fel, av samma skäl precisionsfixen finns: standardprecisionen för utdata är fyra decimaler, och även en fullprecisionskonvertering kan addera binärt brus till en decimalliteral. Att behålla siffrorna innebär att SetObjectJSON och ImportObjectJSON, som räcker varje JSON-taltext till PDF-tokenizeren, återskapar exakt samma värde. Garantin täcker värdet, inte bytena: efter en återimport lagras och sparas -.25 som -0.25. Båda stavningarna är lika under §7.3.3, men en diff på bytenivå kommer att flagga ändringen, så behandla inte en export- och importcykel som en no-op på ett dokument vars byte täcks av en signatur
Vad händer med ett tal JSON inte kan representera?
GetDocumentJSON skriver nu null för varje tal som är NaN eller oändligt, för RFC 8259 §6 har ingen syntax för något av dem. Oändlighet är lättare att åstadkomma än det låter: PDF-tokenizeren ackumulerar siffror genom upprepad multiplikation i en Double, som tar slut runt 1.8 × 10308, så en heltalsliteral något över 300 siffror lång blir tyst +Inf. Ärliga filer innehåller aldrig en sådan literal; fuzzade och fientliga gör det, vilket är därför de hör hemma i samma testkorpus som fallen i att härda en Pascal-PDF-parser mot skadliga filer. Den gamla dokumentskrivaren formaterade icke-heltal med Str(D:0:6), och för +Inf skriver det texten +Inf, som ingen JSON-konsument kommer att parsa
null innebär medvetet en förlust. Konsumenter av GetDocumentJSON-utdata måste acceptera null varhelst ett tal kan förekomma, och bör läsa det som "ett värde fanns men kan inte representeras", inte som en saknad nyckel. Den ursprungliga literalen går inte att återfå ur dokument-JSON:en, så en pipeline som bryr sig bör logga objektet och behandla filen som misstänkt i stället för att byta in ett standardvärde
Varför kunde en ensam NaN avbryta en SVG- eller JSON-export?
För att PLDoubleToStr, den invarianta talsformateraren bakom innehållsströmmar, SVG, XML, CSV och det mesta JSON:et i biblioteket, skalade sin indata och anropade Round, och Round(NaN) kastar EInvalidOp på mål som Win32, där Delphi lämnar x87:ans invalid-operation-undantag omaskerat. Undantaget utlöstes efter att skrivaren redan hade satt ut en del av sin utdata, så en enda degenererad mätning, en 0/0 i ett mått eller en NaN skickad in av en anropare, lämnade en avhuggen fil efter sig. PLDoubleToStr returnerar nu 0 för NaN, och dess heltalsgren clampar till ±9.2e18 som bråkdelsgrenen, så Infinity kommer också ut som en ändlig literal
Noll är det rätta svaret för en innehållsström, där en talsplats måste hålla ett tal, och det felaktiga svaret för en rapport, där 0 är en rimlig mätning. JSON-skrivare som måste behålla skillnaden använder PLJSONNumber(Value, Decimals) från PDFlibExtra, som skriver null för NaN eller Infinity och invarianta siffror i övrigt. PLJSONNumber står nu bakom GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON och rapporterna för barcode, deskew, structured text och PDF/VCR; deskew-rapporten skrev tidigare 0 för en icke-ändlig vinkel och skriver nu null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Formatera varje Double till text först; PLJSONNumber skriver null
// för NaN eller Infinity och använder alltid en decimalpunkt
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Aldrig B.Append(Angle): Double-överloaden följer användarens locale
Result := B.ToString;
finally
B.Free;
end;
end;
Var smyger användarens locale fortfarande in i JSON?
Genom vilken formaterare som helst som frågar de regionala inställningarna, och en fullständig granskning av maskinläsbar utdata fann exakt en kvarvarande: maxAcceptedMeanError i GetSimilarImageDeduplicationReportJSON, skriven med PLFloatToStr, en tunn wrapper runt FloatToStr. På ett skrivbord vars decimaltecken är komma innehöll rapporten "maxAcceptedMeanError":1,5, som en JSON-parser läser som värdet 1 följt av en vilsekommen token. Fältet rapporterar det sämsta accepterade pixelfelet från perceptuell bilddeduplicering, och det går nu genom PLJSONNumber(Stats.MaxAcceptedMeanError, 6). En återstående fälla är PLStringBuilder: i Delphi är det ett rent alias för System.SysUtils.TStringBuilder, vars Append(Double)-överload formaterar genom användarens locale, medan FPC-byggen använder en biblioteksklass i stället, så ett test på Free Pascal eller en en-US-maskin kommer aldrig att fånga den
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Återskapa ett tyskt eller franskt skrivbord inuti testkörningen
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Använd en fixture som verkligen innehåller nästan-dubblettbilder,
// annars är medelfelet 0 och buggen förblir gömd
Lib.LoadFromFile('scanned-batch.pdf', '');
// Torrkörning med trösklar 2, 2, 4: dokumentet ändras inte
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;
En regressionssvit för JSON-utdata behöver tre fixtures för att förbli ärlig: en sida som bär -.25, +1.5 och 007.5, ett objekt som håller ett 400-siffrigt heltal, och vilken rapport som helst körd under en komma-locale, var och en validerad med en strikt parser i stället för ögonmått. Objekt-JSON, dokument-JSON och analysrapporterna i PDF Library for Delphi delar samma talregler över Delphi, C++Builder och Free Pascal; den fullständiga funktionslistan finns på produktsidan för PDF Library for Delphi