PDF Library for Delphi (PDFlibPas) udsender gyldig JSON for hvert PDF-tal siden v3.539.31. GetObjectJSON omskriver tokens, som ISO 32000-1 accepterer, men RFC 8259 afviser, som -.25, +1.5 og 007.5, til -0.25, 1.5 og 7.5 ciffer for ciffer; GetDocumentJSON og analyserapporterne skriver null for NaN og Infinity; og PLDoubleToStr skriver 0 for NaN i stedet for at rejse EInvalidOp halvvejs inde i en eksport. Før fixet kunne biblioteket producere JSON, som dets egen reader nægtede at indlæse
Hvorfor bryder et gyldigt PDF-tal JSON?
Fordi de to grammatikker er uenige om fire små detaljer, og en PDF-parser, der respekterer kildeteksten, transporterer de detaljer direkte ud i outputtet. ISO 32000-1 §7.3.3 tillader, at et tal starter med et plustegn, udelader heltalsdelen (.5), slutter på et bart punktum (4.) og bærer indledende nuller (007.5). RFC 8259 §6 tillader intet af det: et valgfrit minus, en heltalsdel, der enten er 0 eller starter med 1 til 9, og mindst ét ciffer efter et decimalpunktum. Producenter står frit til at skrive PDF-formerne, og masser af generatorer og håndredigerede filer gør det
Lækken kom fra en bevidst præcisionsfunktion. Siden v3.539.19 returnerer TPDFNumeric.Output den eksakte tekst, tokenizeren parsed for reelle tal, hvilket er det, der holder en kalibreret farveværdi eksakt ved gemning, som beskrevet i at bevare parsede PDF-decimalers præcision. Tokenizeren patcher allerede .5 til 0.5 og 4. til 4.0 på vej ind, og heltal formateres om fra deres værdi, så +3 kommer tilbage som 3. Det, der overlever ordret, er resten: et fortegnet indledende punktum (-.25), et eksplicit plus på et reelt tal (+1.5) og indledende nuller (007.5). Den gamle object-writer satte Output direkte efter "value":, og TJSONParser.ParseNumber i bibliotekets egen reader stopper ved hver af dem med "Invalid JSON number", så eksporten lykkedes, og re-importen fejlede 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 er et array skrevet som [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 og senere: værdierne ankommer som -0.25, 1.5 og 7.5
// SetObjectJSON tager ingen options, så giv 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;
Hvordan holder PDFNumberTextToJSON hvert ciffer?
PDFNumberTextToJSON staver tokenen om i stedet for at genberegne den fra en Double. Funktionen i PDFlibObjectJSON læser et valgfrit fortegn, samler cifre før og efter ét decimalpunktum og anvender derefter kun de redigeringer, JSON kræver: den dropper et plus, fjerner indledende nuller, mens den beholder én, leverer 0, når heltalsdelen er tom, dropper et bart afsluttende punktum og sætter minuset tilbage. En token med ethvert andet tegn, eller slet ingen cifre, falder tilbage til PLJSONNumber(Value, 10), som skriver null, når værdien ikke er endelig
-.25bliver til-0.25, og+.5bliver til0.5+1.5bliver til1.5007.5bliver til7.5, mens0.75forbliver, som det er4.bliver til4, hvis en sådan token nogensinde når writeren2.22221og1.250000beholder hvert fraktionsciffer, nuller i enden inkluderet
Formatering fra den gemte Double ville have været kortere og forkert, af samme grund som præcisionsfixet findes: standardoutputpræcisionen er fire decimaler, og selv en fuldpræcisionskonvertering kan tilføje binær støj til en decimal literal. At beholde cifrene betyder, at SetObjectJSON og ImportObjectJSON, som giver hver JSON-taltekst til PDF-tokenizeren, genskaber præcis samme værdi. Garantien dækker værdien, ikke bytes: efter en re-import gemmes -.25 og skrives som -0.25. Begge stavemåder er lige under §7.3.3, men en byte-level diff vil markere ændringen, så behandl ikke en eksport- og importcyklus som en no-op på et dokument, hvis bytes er dækket af en signatur
Hvad sker der med et tal, JSON ikke kan repræsentere?
GetDocumentJSON skriver nu null for ethvert tal, der er NaN eller uendelig, for RFC 8259 §6 har ingen syntaks for nogen af dem. Infinity er lettere at frembringe, end det lyder: PDF-tokenizeren akkumulerer cifre ved gentagen multiplikation ind i en Double, som topper omkring 1,8 × 10308, så en integer literal lidt over 300 cifre bliver i stilhed til +Inf. Ærlige filer indeholder aldrig en sådan literal; fuzzede og fjendtlige gør, hvilket er grunden til, at de hører hjemme i samme testkorpus som tilfældene i hærdning af en Pascal-PDF-parser mod ondsindede filer. Den gamle document-writer formaterede ikke-heltal med Str(D:0:6), og for +Inf skriver den teksten +Inf, som ingen JSON-forbruger parser
null'et er bevidst tabsgivende. Forbrugere af GetDocumentJSON-output skal acceptere null alle steder, hvor et tal kan optræde, og bør læse det som "en værdi var til stede, men kan ikke repræsenteres", ikke som en manglende nøgle. Den oprindelige literal kan ikke gendannes fra dokument-JSON'en, så en pipeline, der holder af det, bør logge objektet og behandle filen som mistænkelig i stedet for at erstatte med en default
Hvorfor kunne én enkelt NaN afbryde en SVG- eller JSON-eksport?
Fordi PLDoubleToStr, den invariante talformatter bag content streams, SVG, XML, CSV og det meste JSON i biblioteket, skalerede sit input og kaldte Round, og Round(NaN) rejser EInvalidOp på targets som Win32, hvor Delphi lader x87 invalid-operation-exceptionen være umasket. Exceptionen affyredes, efter writeren allerede havde udsendt en del af sit output, så én degenereret måling, en 0/0 i en metrik eller en NaN givet videre af en caller efterlod en afkortet fil. PLDoubleToStr returnerer nu 0 for NaN, og dens heltalsgren clamper til ±9.2e18 ligesom grenen for fraktionerede tal, så Infinity også kommer ud som en endelig literal
Nul er det rigtige svar for en content stream, hvor en talslot skal holde et tal, og det forkerte svar for en rapport, hvor 0 er en plausibel måling. JSON-writere, der skal holde forskellen adskilt, bruger PLJSONNumber(Value, Decimals) fra PDFlibExtra, som skriver null for NaN eller Infinity og invariante cifre ellers. PLJSONNumber ligger nu bag GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON og barcode-, deskew-, structured text- og PDF/VCR-rapporterne; deskew-rapporten skrev tidligere 0 for en ikke-endelig vinkel og skriver nu null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Formatér hver Double til tekst først; PLJSONNumber skriver null
// for NaN eller Infinity og bruger altid et decimalpunktum
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-overloaden følger brugerens locale
Result := B.ToString;
finally
B.Free;
end;
end;
Hvor smyger brugerens locale sig stadig ind i JSON?
Gennem enhver formatter, der spørger regionale indstillinger, og en fuld audit af maskinlæsbart output fandt præcis én tilbage: maxAcceptedMeanError i GetSimilarImageDeduplicationReportJSON, skrevet med PLFloatToStr, en tynd wrapper over FloatToStr. På en desktop, hvis decimaltegn er et komma, indeholdt rapporten "maxAcceptedMeanError":1,5, som en JSON-parser læser som værdien 1 efterfulgt af en løs token. Feltet rapporterer den værste accepterede pixelfejl fra perceptuel billed-deduplikering og går nu gennem PLJSONNumber(Stats.MaxAcceptedMeanError, 6). En resterende fælde er PLStringBuilder: på Delphi er den en ren alias af System.SysUtils.TStringBuilder, hvis Append(Double)-overload formaterer gennem brugerens locale, mens FPC-builds bruger en biblioteksklasse i stedet, så en test på Free Pascal eller en en-US-maskine fanger den aldrig
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Genskab en tysk eller fransk desktop i testkørslen
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Brug en fixture, der virkelig indeholder næsten-duplikerede billeder,
// ellers er middelfejlen 0, og fejlen forbliver skjult
Lib.LoadFromFile('scanned-batch.pdf', '');
// Dry run med tærskler 2, 2, 4: dokumentet ændres ikke
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 regression-suite for JSON-output behøver tre fixtures for at forblive ærlig: en side med -.25, +1.5 og 007.5, et objekt med et 400-cifret heltal og enhver rapport kørt under et komma-locale, hver valideret med en streng parser frem for at blive set efter. Object JSON, document JSON og analyserapporterne i PDF Library for Delphi deler samme talregler på tværs af Delphi, C++Builder og Free Pascal; den komplette funktionsliste ligger på PDF Library for Delphi-produktsiden