PDFlibPas, losLab PDF Developer Library for Delphi, skriver hvert tal, det sætter ind i en content stream, med punktum som decimaltegn og ingen exponent, uanset hvad Windows' regionale indstillinger siger. Siden v3.539.26 formaterer AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path-output og recoloring deres operander gennem PLDoubleToStrConst, og siden v3.539.33 bruger parserne, der læser tallene tilbage, PLTryStrToFloatInvariant i stedet for systemets locale. På en tysk, fransk eller brasiliansk maskine giver samme kode nu de samme bytes som på en amerikansk, hvilket er den eneste adfærd, et filformat kan tolerate
Hvorfor ødelægger et locale med komma-decimaler en PDF uden en fejl?
Et locale med komma-decimaler ødelægger en PDF i stilhed, fordi kommaet ikke er et taltegn i PDF-syntaks, så skaden læses som gyldige tokens med forkert betydning. Før fixet var PLFloatToStr intet andet end et bart FloatToStr-kald, og FloatToStr følger FormatSettings.DecimalSeparator. Med komma som separator skrev AddPageMatrix(0.5, 0.5, 0, 0) 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 tillader cifre, ét punktum og et indledende fortegn i et tal og intet andet, så en content-parser læser den linje som tallet 0 efterfulgt af en ukendt token ,5, og cm-operatoren ender med de forkerte operander. Intet rejser en exception, intet logger. Siden renderes blot med en transformationsmatrix, der er drevet, og at arbejde sig baglæns fra et fejlplaceret tegn til en locale-indstilling er en elendig eftermiddag
Den anden defekt gemmer sig bag den første. FloatToStr bruger ffGeneral-formatet, som skifter til exponent-notation, så snart størrelsesordenen falder under 1E-4, så et lille offset kom ud som 1E-5. Samme §7.3.3 fastslår, at PDF ikke understøtter exponent-formen, hvilket betyder, at selv en maskine med amerikansk locale kunne skrive en ugyldig operand, hvis værdien bare var lille nok. Regressionstesterne for denne udgivelse låser begge fejlformer fast: de skifter separatoren til komma, kalder API'et og skanner det resulterende content for enhver token, der indeholder et komma eller en exponent
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
OldSeparator: Char;
begin
Lib := TPDFlib.Create;
try
Lib.SetPageDimensions(300, 200);
Lib.DrawBox(10, 10, 20, 20, 1);
OldSeparator := FormatSettings.DecimalSeparator;
try
FormatSettings.DecimalSeparator := ','; // simulér en de-DE-desktop
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 og senere skriver: 0.5 0 0 0.25 0.00001 12.75 cm
// ældre builds skrev: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
To slags tal, to familier af helpers
Fixet i PDFlibPas er en streng opdeling: tal, der vises til mennesker, må følge locale, og tal, der skrives til en maskine, gør det aldrig. PLFloatToStr og PLStrToFloat bliver i PDFlibExtra.pas til brugerrettet tekst, og deres deklaration bærer nu en kommentar, der siger netop det. Alt, der ender som PDF-syntaks, går gennem PLDoubleToStrConst med et fast antal decimaler valgt til opgaven: seks til matricer, fire til koordinater og TJ-justeringer, tre til farver og FDF-rektangler. Auditten for v3.539.26 rørte flere kaldsteder, end den oprindelige fejlrapport antydede:
AddPageMatrix,ScalePageogDeskewPage, som alle sætter encmforan det eksisterende sideindhold- Sideelement-byggerne, der udsender
Tm-nulstillinger,TJ-fremskridt ogcm-transformationer - Glyph-placeringsmatricer og outline-punkter i text-to-path-konverteren
- Den sorte fyldboks, som
RedactRegionsætter foran,/Rect-værdierne i FDF-eksport og de operander, recoloring skriver
PLDoubleToStrConst er en håndbygget formatter snarere end en wrapper omkring FloatToStrF, og tre af dens egenskaber betyder noget her. Den skriver altid et punktum og fjerner trailing-nuller, så 0.5 forbliver 0.5 frem for 0.500000. Den skriver aldrig en exponent for endeligt input. Og en værdi forskellig fra nul, der er mindre end den ønskede præcision, beholder sine signifikante cifre i stedet for at kollapse til nul, så PLDoubleToStrConst(1E-9, 6) returnerer 0.000000001; kun værdier under cirka 5E-16 bliver til 0. Den sidste regel findes, fordi at runde en lille skaleringsfaktor til nul forvandler en gyldig matrix til en singulær, hvilket er en værre fejl end den, der fikses
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Maskinoutput: punktum-decimal, ingen exponent, trailing-nuller fjernet
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // beholder 4 signifikante cifre
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Maskininput: blid fejl i stedet for EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // content-tal bruger aldrig et komma
end;
Hvorfor er parsesiden farligere end skrivesiden?
Parsesiden er farligere, fordi en locale-bundet parser ikke producerer et forkert tal, den kaster en exception. PLStrToFloat kalder StrToFloat, som rejser EConvertError, når teksten ikke matcher systemets separator. På et system med komma-decimaler betød det, at RecolorPage brød sammen i det øjeblik, den mødte en almindelig 0.5 g-operator, så alle virkelige sider fejlede, ikke bare exotiske. RenderPageRegionToFile afviste sit eget dokumenterede clip-format "10.5,20.5,50.5,40.5", og SVG-længdeattributter, SVG-eksportfarver, annoterings-vertexlister og output intent solidity-værdier blev enten afvist eller stiltiende erstattet med defaults. Et bibliotek, der virker perfekt på udviklermaskinen og fejler hos den første kunde i München, er præcis den slags kode, som ligesom tilfældene i artiklen om Delphi-kode, der virker ved et tilfælde, kun ser korrekt ud på grund af, hvor den blev testet
v3.539.33 klassificerede hvert StrToFloat- og TryStrToFloat-kald efter, hvor dets input kommer fra. Content stream-operander, SVG-attributter, painter-farvestrenge og kommaseparerede clip- og vertexlister har alle en fast punktum-syntaks, så de går nu gennem PLTryStrToFloatInvariant, som trimmer teksten, parser den med PLInvariantFormatSettings og returnerer False ved tomt, misdannet eller ikke-endeligt input i stedet for at kaste en exception. En kommasepareret liste rummer ikke plads til kompromis, fordi et komma ikke kan være både listeskilletegn og decimaltegn. Samme gennemløb rettede også en out-of-bounds-skrivning: RenderPageRegionToFile gemte tidligere en femte clip-værdi forbi sin fire-element-buffer. For recoloring-pipelinen, der er beskrevet i guiden til at konvertere en PDF til ét farverum, er det praktiske resultat, at RecolorPage og RecolorDocument ikke længere aborterer på et system med komma-decimaler. Regelværdier, som en caller taster ind i CheckDocumentPolicy, er det eneste parse-tilfælde, der i stedet bruger den tolerante helper, og grunden forklares i næste afsnit
Hvad sker der, hvis du kun retter den ene ende af en round-trip?
At rette kun den ene ende af en locale-round-trip ødelægger kode, der virkede før, hvilket er grunden til, at ændringen af strukturattributterne i v3.539.32 flyttede writer og reader sammen. SetStructElem*-wrapperne videreformidler tal som strenge: SetStructElemBBox formaterer fire værdier til én streng, gemmer den gennem AddTagAttribute, og /A-writeren parser senere den streng for at beslutte, om den bliver et tal, et array eller et navn. Begge ender brugte systemets locale, så på et system med komma-decimaler var round-trip'en selv-konsistent. Fejlen viste sig kun, når en caller fulgte dokumentationen og gav "0.5" til AddTagAttribute: readeren kunne ikke parse den og udsendte PDF-navnet /0.5. PDF/VCR-pladsholderen havde det spejlvendte problem, fordi biblioteket genererede GTS_BBox med punktum og derefter validerede den med locale før gemning
At kun skifte writeren til punktum ville have været værre end ingenting at gøre, da hver SetStructElem*-værdi så ville strande i den locale-bundne reader og degradere til et navn. Så writerne bruger nu PLDoubleToStrConst(v, 6), og readeren bruger den nye PLTryStrToFloatLenient, som prøver punktum-formen først og falder tilbage til systemets locale. En caller med komma-locale, der gav "1,25" tidligere, får stadig tallet 1.25. Kompromisset er bevidst og dokumenteret: på et tysk system blev "1.500" tidligere et navn, fordi StrToFloat afviser tusindtalsseparatorer, og det læses nu som 1.5, mens bogstavelige NAN- og INF-strenge ikke længere accepteres som tal
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // caller med komma-decimal
Lib := TPDFlib.Create;
try
Lib.BeginTag('Figure', 'Sales chart', '');
Lib.SetStructElemBBox(10.5, 20.25, 200.5, 100.75); // /BBox [ 10.5 20.25 200.5 100.75 ]
Lib.AddTagAttribute('Layout', 'SpaceAfter', '0.5'); // /SpaceAfter 0.5, før /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // stadig /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Hvor NaN og uendelig bliver standset
AddPageMatrix, ScalePage og RedactRegion afviser nu NaN og uendelige argumenter på forhånd og returnerer 0, for intet PDF-tal kan repræsentere dem. ScalePage afviste allerede faktorer på nul eller mindre, men NaN passerer en <= 0-test, så en NaN-skalering rejste tidligere hele vejen til formatteren. I v3.539.26 kaldte den formatter stadig Round på NaN, hvilket rejser EInvalidOp på Win32, hvor x87-enheden ikke maskerer ugyldige operationer; v3.539.31 fik PLDoubleToStrConst til at skrive 0 for NaN som en sidste forsvarslinje, men et nul i en matrix er en singulær transformation, så API-niveau-tjekket er stadig det egentlige fix. To grænser bliver med vilje stående. Metafile state-strenge skrives og læses med locale inden for én proces og forlader den aldrig, så de blev ladt i fred. Og en test, der formaterer 1E-5 gennem page element-stien, skal læse content, før laget omskrives, for genudsendelse af operander ved dokumentpræcision gør med rette den værdi til 0
Hvis din applikation sendes til kunder uden for punktum-decimal-verdenen, er den sikreste vane den, PDFlibPas' test-suite nu bruger: kør de PDF-producerende stier én gang med FormatSettings.DecimalSeparator sat til komma, og skan outputtet for kommaer og exponenter. Artiklen om at bevare parsede decimalers præcision dækker den anden halvdel af samme historie, hvordan tal læst fra en eksisterende fil beholder deres eksakte tekst ved gemning. Downloads, den fulde API-reference og trial-builden ligger på PDFlibPas Delphi PDF library-produktsiden