PDF Library for Delphi (PDFlibPas) sender ut gyldig JSON for hvert PDF-nummer siden v3.539.31. GetObjectJSON omskriver token som ISO 32000-1 godtar men RFC 8259 avviser, slik som -.25, +1.5 og 007.5, til -0.25, 1.5 og 7.5 siffer for siffer; GetDocumentJSON og analysrapportene skriver null for NaN og Infinity; og PLDoubleToStr skriver 0 for NaN i stedet for å reise EInvalidOp midt i en eksport. Før fiksen kunne biblioteket produsere JSON som dets egen leser nektet å laste inn
Hvorfor ødelegger et gyldig PDF-nummer JSON?
Fordi de to grammatikkene er uenige om fire små detaljer, og en PDF-parser som respekterer kildeteksten vil bære de detaljene rett inn i outputen. ISO 32000-1 §7.3.3 lar et tall starte med et plusstegn, utelate heltallsdelen (.5), slutte på et bart punktum (4.) og bære ledende nuller (007.5). RFC 8259 §6 tillater ingenting av det: et valgfritt minus, en heltallsdel som enten er 0 eller starter med 1 til 9, og minst ett siffer etter ethvert desimaltegn. Produsenter står fritt til å skrive PDF-formene, og massevis av generatorer og håndredigerte filer gjør det
Lekkasjen kom fra en bevisst presisjonsfunksjon. Siden v3.539.19 returnerer TPDFNumeric.Output den nøyaktige teksten tokenizeren parset for reelle tall, noe som holder en kalibrert fargeverdi nøyaktig ved lagring, som beskrevet i bevaring av parset PDF-desimalpresisjon. Tokenizeren lapper allerede .5 til 0.5 og 4. til 4.0 på vei inn, og heltall reformateres fra sin verdi, så +3 kommer tilbake som 3. Det som overlever verbatim, er resten: et signert ledende punktum (-.25), et eksplisitt plusstegn på et reelt tall (+1.5) og ledende nuller (007.5). Den gamle objektskriveren la ved Output rett etter "value":, og TJSONParser.ParseNumber i bibliotekets egen leser stopper på hver av dem med «Invalid JSON number», så eksporten lyktes og re-importen feilet 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 en array skrevet som [-.25 +1.5 007.5]
JSON := Lib.GetObjectJSON(12, 0);
// v3.539.31 og senere: verdiene ankommer som -0.25, 1.5 og 7.5
// SetObjectJSON godtar ingen options, så send 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 beholder PDFNumberTextToJSON hvert siffer?
PDFNumberTextToJSON staver tokenen om i stedet for å regne den ut på nytt fra en Double. Funksjonen i PDFlibObjectJSON leser et valgfritt fortegn, samler siffer før og etter ett enkelt desimaltegn og anvender så bare redigeringene JSON krever: den slipper et plusstegn, stripper ledende nuller mens den beholder én, supplierer 0 når heltallsdelen er tom, slipper et bart etterfølgende punktum og setter minusen tilbake. En token som inneholder et annet tegn, eller ingen siffer i det hele tatt, faller tilbake til PLJSONNumber(Value, 10), som skriver null når verdien ikke er endelig
-.25blir-0.25, og+.5blir0.5+1.5blir1.5007.5blir7.5, mens0.75forblir som den er4.blir4hvis en slik token noensinne når skriveren2.22221og1.250000beholder hvert brøksiffer, etterfølgende nuller inkludert
Å formatere fra den lagrede Double ville vært kortere og feil, av samme grunn som presisjonsfiksen finnes: standard outputpresisjon er fire desimaler, og selv en fullpresisjonskonvertering kan legge binær støy til en desimalliteral. Å beholde sifrene betyr at SetObjectJSON og ImportObjectJSON, som gir hver JSON-talltekst til PDF-tokenizeren, gjenskaper nøyaktig samme verdi. Garantien dekker verdien, ikke bytene: etter en re-import blir -.25 lagret og skrevet ut som -0.25. Begge stavemåtene er like under §7.3.3, men en byte-nivå-diff vil flagge endringen, så ikke behandle en eksport-og-import-syklus som en no-op på et dokument hvis byte er dekket av en signatur
Hva skjer med et tall JSON ikke kan representere?
GetDocumentJSON skriver nå null for ethvert tall som er NaN eller uendelig, for RFC 8259 §6 har ingen syntaks for noen av dem. Infinity er lettere å produsere enn det høres ut som: PDF-tokenizeren akkumulerer siffer ved gjentatt multiplikasjon inn i en Double, som topper seg nær 1.8 × 10308, så en heltallsliteral litt over 300 siffer lang blir i stillhet +Inf. Ærlige filer inneholder aldri en slik literal; fuzzede og fiendtlige gjør det, og det er derfor de hører hjemme i samme testkorpus som tilfellene i herding av en Pascal-PDF-parser mot ondsinnede filer. Den gamle dokumentskriveren formaterte ikke-heltall med Str(D:0:6), og for +Inf skriver den teksten +Inf, som ingen JSON-konsument vil parse
null-en er bevisst tapfull. Konsumenter av GetDocumentJSON-output må akseptere null hvor som helst et tall kan dukke opp, og bør lese den som «en verdi var til stede men kan ikke representeres», ikke som en manglende nøkkel. Den opprinnelige literalen kan ikke gjenopprettes fra dokument-JSON-en, så en pipeline som bryr seg, bør logge objektet og behandle filen som mistenkelig i stedet for å erstatte med en standardverdi
Hvorfor kunne én enkelt NaN avbryte en SVG- eller JSON-eksport?
Fordi PLDoubleToStr, den invariante tallformatteren bak content streams, SVG, XML, CSV og mesteparten av JSON-en i biblioteket, skalerte inputen sin og kalte Round, og Round(NaN) reiser EInvalidOp på mål som Win32, der Delphi lar x87-unntaket for ugyldige operasjoner stå umaskert. Unntaket fykte etter at skriveren allerede hadde sendt ut deler av outputen sin, så én degenerert måling, en 0/0 i en metrikk eller en NaN sendt inn av en kaller, etterlot en avkortet fil. PLDoubleToStr returnerer nå 0 for NaN, og heltallsgrenen klemmer til ±9.2e18 som brøkgrenen, så Infinity kommer også ut som en endelig literal
Null er det riktige svaret for en content stream, der en nummerplass må holde et tall, og det feile svaret for en rapport, der 0 er en plausibel måling. JSON-skrivere som må holde forskjellen, bruker PLJSONNumber(Value, Decimals) fra PDFlibExtra, som skriver null for NaN eller Infinity og invariante siffer ellers. PLJSONNumber står nå bak GetSimilarImageDeduplicationReportJSON, GetAnnotationHitsJSON og strekkode-, deskew-, strukturert tekst- og PDF/VCR-rapportene; deskew-rapporten skrev tidligere 0 for en ikke-endelig vinkel og skriver nå null
uses
SysUtils, PDFlibTypes, PDFlibExtra;
function SkewReportJSON(Page: Integer; Angle, Confidence: Double): string;
var
B: PLStringBuilder;
begin
B := PLStringBuilder.Create(128);
try
// Formater hver Double til tekst først; PLJSONNumber skriver null
// for NaN eller Infinity og bruker alltid et desimaltegn
B.Append('{"page":').Append(Page)
.Append(',"angle":').Append(string(PLJSONNumber(Angle, 4)))
.Append(',"confidence":').Append(string(PLJSONNumber(Confidence, 4)))
.Append('}');
// Aldri B.Append(Angle): Double-overloaden følger brukerlocelen
Result := B.ToString;
finally
B.Free;
end;
end;
Hvor sniker brukerlocelen seg fortsatt inn i JSON?
Gjennom enhver formatter som spør de regionale innstillingene, og en full revisjon av maskinlesbar output fant nøyaktig én igjen: maxAcceptedMeanError i GetSimilarImageDeduplicationReportJSON, skrevet med PLFloatToStr, en tynn wrapper over FloatToStr. På et skrivebord med komma som desimalseparator inneholdt rapporten "maxAcceptedMeanError":1,5, som en JSON-parser leser som verdien 1 fulgt av en vill token. Feltet rapporterer den verste aksepterte pikselfeilen fra perseptuell bildededuplikering, og den går nå gjennom PLJSONNumber(Stats.MaxAcceptedMeanError, 6). En gjenværende felle er PLStringBuilder: på Delphi er den en ren alias av System.SysUtils.TStringBuilder, hvis Append(Double)-overload formaterer gjennom brukerlocelen, mens FPC-bygg bruker en bibliotekklasse i stedet, så en test på Free Pascal eller en en-US-maskin fanger den aldri
uses
System.SysUtils, System.JSON, PDFlibrary;
var
Lib: TPDFlib;
Report: WideString;
Parsed: TJSONValue;
begin
// Reproduser et tysk eller fransk skrivebord inne i testkjøringen
FormatSettings.DecimalSeparator := ',';
Lib := TPDFlib.Create;
try
// Bruk en fixture som virkelig inneholder nesten dupliserte bilder,
// ellers er gjennomsnittsfeilen 0 og bug-en forblir skjult
Lib.LoadFromFile('scanned-batch.pdf', '');
// Tørrkjøring med terskler 2, 2, 4: dokumentet endres 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 regresjonssuite for JSON-output trenger tre fixtures for å forbli ærlig: en side som bærer -.25, +1.5 og 007.5, et objekt som holder et 400-siffers heltall, og en vilkårlig rapport kjørt under en komma-locale, hver validert med en streng parser i stedet for å vurderes med øynene. Objekt-JSON, dokument-JSON og analysrapportene i PDF Library for Delphi deler de samme nummerreglene på tvers av Delphi, C++Builder og Free Pascal; den komplette funksjonslisten ligger på PDF Library for Delphi produktsiden