PDFlibPas, de losLab PDF Developer Library for Delphi, schrijft elk getal dat hij in een content stream zet met een punt als decimale scheidingsteken en zonder exponent, wat de Windows regionale instellingen ook zeggen. Sinds v3.539.26 formatteren AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path-uitvoer en recoloring hun operanden via PLDoubleToStrConst, en sinds v3.539.33 gebruiken de parsers die die getallen teruglezen PLTryStrToFloatInvariant in plaats van de systeem-locale. Op een Duitse, Franse of Braziliaanse machine produceert dezelfde code nu dezelfde bytes als op een Amerikaanse, en dat is het enige gedrag dat een bestandsformaat kan verdragen
Waarom corrumpeert een komma-decimale locale een PDF zonder foutmelding?
Een komma-decimale locale corrumpeert een PDF stilletjes, want de komma is geen getalteken in PDF-syntaxis, dus de schade leest als geldige tokens met de verkeerde betekenis. Vóór de fix was PLFloatToStr niets meer dan een kale FloatToStr-aanroep, en FloatToStr volgt FormatSettings.DecimalSeparator. Met een komma als scheidingsteken schreef AddPageMatrix(0.5, 0.5, 0, 0) dus 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 staat in een getal cijfers, één punt en een eventueel voorloopteken toe en niets anders, dus een content parser leest die regel als het getal 0 gevolgd door een onbekend token ,5, en de cm-operator eindigt met de verkeerde operanden. Niets gooit een exceptie, niets logt. De pagina rendert gewoon met een transformatiematrix die gedrift is, en van een misplaatste tekening terugwerken naar een locale-instelling is een misèrele middag
Het tweede defect schuilt achter het eerste. FloatToStr gebruikt het ffGeneral-formaat, dat omschakelt naar exponentnotatie zodra de grootte-orde onder 1E-4 zakt, dus een piepkleine offset kwam eruit als 1E-5. Dezelfde §7.3.3 stelt dat PDF de exponentvorm niet ondersteunt, wat betekent dat zelfs een machine met US-locale een ongeldige operand kon schrijven zodra de waarde klein genoeg was. De regression tests van deze release zetten beide faalvormen vast: ze zetten het scheidingsteken op een komma, roepen de API aan en scannen de resulterende content op elk token dat een komma of een exponent bevat
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 := ','; // simuleer een de-DE bureaublad
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 en nieuwer schrijven: 0.5 0 0 0.25 0.00001 12.75 cm
// oudere builds schreven: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Twee soorten getallen, twee families helpers
De fix in PDFlibPas is een strikte splitsing: getallen die aan mensen getoond worden mogen de locale volgen, en getallen die voor een machine geschreven worden doen dat nooit. PLFloatToStr en PLStrToFloat blijven in PDFlibExtra.pas staan voor tekst gericht op gebruikers, en hun declaratie draagt nu een comment die precies dat zegt. Alles wat uiteindelijk PDF-syntaxis wordt, gaat door PLDoubleToStrConst met een vast aantal decimalen dat per taak gekozen is: zes voor matrices, vier voor coördinaten en TJ-aanpassingen, drie voor kleuren en FDF-rechthoeken. De audit voor v3.539.26 raakte meer call sites dan het oorspronkelijke bugrapport deed vermoeden:
AddPageMatrix,ScalePageenDeskewPage, die allemaal eencmvóór de bestaande paginacontent plakken- De pagina-elementbouwers die
Tm-resets,TJ-stapjes encm-transformaties uitzenden - Glyph-plaatsingsmatrices en outline-punten in de text-to-path-converter
- Het zwarte invulvak dat
RedactRegionvooraan plakt, de/Rect-waarden in de FDF-export en de operanden die recoloring schrijft
PLDoubleToStrConst is een met de hand gebouwde formatter in plaats van een wrapper om FloatToStrF, en drie eigenschappen ervan zijn hier relevant. Hij schrijft altijd een punt en haalt afsluitende nullen weg, dus 0.5 blijft 0.5 in plaats van 0.500000. Hij schrijft nooit een exponent voor eindige invoer. En een niet-nul waarde die kleiner is dan de gevraagde precisie behoudt zijn significante cijfers in plaats van in te klappen tot nul, dus PLDoubleToStrConst(1E-9, 6) geeft 0.000000001 terug; alleen waarden onder ruwweg 5E-16 worden 0. Die laatste regel bestaat omdat het afronden van een piepkleine schaalfactor op nul een geldige matrix in een singuliere verandert, en dat is een ergere bug dan degene die hier gerepareerd wordt
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Machineuitvoer: punt-decimaal, geen exponent, afsluitende nullen verwijderd
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // behoudt 4 significante cijfers
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Machineinvoer: zachte fout in plaats van EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // contentnummers gebruiken nooit een komma
end;
Waarom is de parseerkant gevaarlijker dan de schrijfkant?
De parseerkant is gevaarlijker omdat een locale-gebonden parser geen verkeerd getal produceert, maar gooit. PLStrToFloat roept StrToFloat aan, die een EConvertError gooit als de tekst niet matcht met het systeemscheidingsteken. Op een systeem met komma-decimals betekende dat RecolorPage afbrak op het moment dat hij een gewone 0.5 g-operator tegenkwam, dus elke pagina uit de praktijk faalde, niet alleen exotische. RenderPageRegionToFile weigerde zijn eigen gedocumenteerde clip-formaat "10.5,20.5,50.5,40.5", en SVG-lengteattributen, SVG-exportkleuren, annotatievertexlijsten en output intent-dichtheidswaarden werden geweigerd of stilletjes door standaardwaarden vervangen. Een library die perfect werkt op de machine van de ontwikkelaar en faalt bij de eerste klant in München is precies het soort code dat, zoals de gevallen in het artikel over Delphi-code die bij toeval werkt, alleen correct lijkt vanwege waar hij getest is
v3.539.33 classificeerde elke StrToFloat- en TryStrToFloat-aanroep naar waar zijn invoer vandaan komt. Content stream-operanden, SVG-attributen, painter-kleurstrings en komma-gescheiden clip- en vertexlijsten hebben allemaal een vaste punt-syntaxis, dus die gaan nu door PLTryStrToFloatInvariant, die de tekst trimt, parst met PLInvariantFormatSettings en False teruggeeft bij lege, misvormde of niet-eindige invoer in plaats van te gooien. Een komma-gescheiden lijst laat geen ruimte voor compromissen, want een komma kan niet zowel lijstscheider als decimaalteken zijn. Dezelfde passage repareerde ook een out-of-bounds write: RenderPageRegionToFile bewaarde vroeger een vijfde clip-waarde voorbij zijn buffer van vier elementen. Voor de recoloring-pijplijn uit de gids voor het omzetten van een PDF naar één kleurruimte is het praktische resultaat dat RecolorPage en RecolorDocument op een systeem met komma-decimals niet meer afbreken. Regelwaarden die een aanroeper in CheckDocumentPolicy intikt zijn het ene parseergeval dat in plaats daarvan de soepele helper gebruikt, om de reden die de volgende paragraaf uitlegt
Wat gebeurt er als u maar één kant van een round-trip repareert?
Alleen één kant van een locale-round-trip repareren breekt code die eerst werkte, en daarom verplaatste de structure attribute-wijziging in v3.539.32 de schrijver en de lezer samen. De SetStructElem*-wrappers geven getallen door als strings: SetStructElemBBox formatteert vier waarden in één string, bewaart die via AddTagAttribute, en de /A-schrijver parst die string later om te beslissen of hij een getal, een array of een naam wordt. Beide kanten gebruikten de systeem-locale, dus op een systeem met komma-decimals was de round-trip zelfconsistent. De bug verscheen pas toen een aanroeper de documentatie volgde en "0.5" aan AddTagAttribute gaf: de lezer kon hem niet parsen en zond de PDF-naam /0.5 uit. De PDF/VCR-placeholder had het spiegelbeeldprobleem, want de library genereerde GTS_BBox met een punt en valideerde hem daarna met de locale vóór het opslaan
Alleen de schrijver op een punt zetten was erger geweest dan niets doen, want elke SetStructElem*-waarde zou dan door de locale-gebonden lezer vallen en tot een naam degraderen. Daarom gebruiken de schrijvers nu PLDoubleToStrConst(v, 6), en de lezer gebruikt de nieuwe PLTryStrToFloatLenient, die eerst de puntvorm probeert en terugvalt op de systeem-locale. Een aanroeper met komma-locale die vroeger "1,25" doorgaf, krijgt nog steeds het getal 1.25. De afweging is bewust en gedocumenteerd: op een Duits systeem werd "1.500" vroeger een naam omdat StrToFloat scheidingstekens voor duizendtallen weigert, en hij leest nu als 1.5, terwijl letterlijke NAN- en INF-strings niet langer als getallen geaccepteerd worden
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // aanroeper met komma-decimals
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, vroeger /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // nog steeds /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Waar NaN en oneindig worden tegengehouden
AddPageMatrix, ScalePage en RedactRegion wijzen NaN- en oneindige argumenten nu vooraf af en geven 0 terug, want geen enkel PDF-getal kan ze representeren. ScalePage weigerde factoren van nul of minder al, maar NaN passeert een <= 0-test, dus een NaN-schaal reisde vroeger door tot de formatter. In v3.539.26 riep die formatter nog Round aan op NaN, wat op Win32 een EInvalidOp gooit omdat de x87-eenheid ongeldige bewerkingen niet maskeert; v3.539.31 liet PLDoubleToStrConst 0 schrijven voor NaN als laatste verdedigingslinie, maar een nul in een matrix is een singuliere transformatie, dus de controle op API-niveau blijft de echte fix. Twee grenzen blijven bewust staan. Metafile state strings worden binnen één proces met de locale geschreven en gelezen en verlaten het nooit, dus die zijn met rust gelaten. En een test die 1E-5 via het pagina-elementpad formatteert, moet de content lezen vóórdat de laag herschreven wordt, want operanden op documentprecisie opnieuw uitzenden maakt van die waarde legitiem 0
Als uw applicatie naar klanten buiten de punt-decimale wereld gaat, is de veiligste gewoonte die welke de PDFlibPas-testsuite nu gebruikt: draai de PDF-producerende paden één keer met FormatSettings.DecimalSeparator op een komma en scan de uitvoer op komma's en exponenten. Het artikel over het behouden van geparsde decimale precisie behandelt de andere helft van hetzelfde verhaal, hoe getallen die uit een bestaand bestand gelezen zijn hun exacte tekst houden bij het opslaan. Downloads, de volledige API-referentie en de trial-build staan op de productpagina van de PDFlibPas Delphi PDF library