Elk reëel getal dat een PDF-bestand bereikt moet met een punt worden geschreven, wat het besturingssysteem ook als decimaalteken denkt te zijn. De HotPDF Delphi Component leidt ze allemaal door één invariante conversielaag in HPDFTypes, zodat een Duitse, Franse of Braziliaanse workstation 0.5 0 0 0.5 5.5 7.25 cm emitteert en niet de komma-variant die geen enkele conforme reader zal parsen
Dat klinkt als een fix van twee regels. Dat was het niet. Het defect was jarenlang gecamoufleerd door een side effect, dook op in drie subsystemen waar niemand aan denkt als getalopmaakcode, verdween onder de ene compiler en overleefde onder een andere, en de laatste laag ervan had niets met het decimaalteken te maken
Waarom corrumpeert een decimale-komma-locale een PDF-contentstream?
Omdat ISO 32000-1 §7.3.3 een numeriek object definieert als een optioneel teken, decimale cijfers en hooguit één punt. Een komma hoort niet bij die productie en is volgens §7.2.2 ook geen delimiter, dus de reader ziet niet twee operanden waar jij er één bedoelde. Hij ziet een token dat niet lexet, en de operator die het bevatte wordt weggegooid. Een enkele 0,5 in een graphics-state-matrix neemt het hele q ... Q-blok eromheen mee. Het falen heeft ook een kenmerkende vorm in het veld: het reproduceert nooit op de ontwikkelmachine, het arriveert als supportticket uit één land, en het leest als een renderbug in plaats van een syntaxisfout, want de meeste viewers gooien de misvormde operator stilzwijgend weg en renderen de rest van de pagina
// Hostproces dat met DecimalSeparator = ',' draait
Doc.LoadFromFile('invoice.pdf');
Doc.RedactLoadedRect(0, 10.25, 20.5, 40.75, 60.5, 0.5, 0.25, 0.125);
Doc.StitchLoadedPage(0, 1, 5.5, 7.25, 0.5);
// Wat de toegevoegde /Contents vroeger droeg:
// 0,5 0,25 0,125 rg 10,25 20,5 30,5 40 re f
// q 0,5 0 0 0,5 5,5 7,25 cm /StitchSrc Do Q
//
// ISO 32000-1 7.3.3 heeft geen productie voor "0,5", dus de
// redactierechthoek en de gestitche pagina verdwijnen beide zonder fout
Het side effect dat het defect jarenlang verborg
HotPDF schrijft de meeste creatiepad-getallen via _CutFloat, en vroege revisies van die functie deden iets stilletjes vijandigs: bij elke enkele aanroep kende het '.' toe aan de globale FormatSettings.DecimalSeparator van het hostproces. Dat corrumpeert de locale-state van de applicatie die het component host en het is niet thread-safe, maar de interessante consequentie is de derde. Zodra er content was gegenereerd was de processeparator een punt, dus elke kale FloatToStr en StrToFloat elders in de library werd toevallig correct. De create-then-edit-volgorde die bijna elk voorbeeld gebruikt slaagde daarom, en wat hij maskeerde was elk pad dat vóór de eerste _CutFloat-aanroep draait: de geladen-document-editors StitchLoadedPage en RedactLoadedRect, de tekst- en beeldstempels, samengestelde watermerken, barcode-tekenen via Format('%.2f'), de /DA-default-appearance-parser en het hele Type 1-fontsubsysteem
Type 1-parsing faalde op de lelijkste manier van allemaal. FontMatrix, BlueValues, stem-hints en de reële getallen binnen Type 2-charstrings zijn PostScript-reals die met punten worden geschreven, volgens de Adobe Type 1 Font Format-specificatie. 0.001 parsen onder een komma-locale gooit EConvertError, een omvattend try/except slikte het in, en de font-matrix bleef op nul staan. Elk glyph uit dat font klapte daarna in, wat eruitziet als een fontprobleem in plaats van een localeprobleem en je een middag ingebedde glyph-rendering debuggen instuurt
Eén conversielaag in plaats van verspreide patches
De regel die HotPDF nu volgt is bot: elk getal dat in of uit PDF-, PostScript-, JSON- of JavaScript-syntax wordt geschreven gaat door een expliciete invariante conversie, en geen enkele code ergens leunt op een globaal FormatSettings-side effect. HPDFTypes bezit de vier conversies, plus het settings-record dat ze delen en één fixed-precision-helper voor het barcode-pad
// Lib/HPDFTypes.pas - het enkele invariante conversie-oppervlak
function HPDFInvariantFormatSettings: TFormatSettings;
function HPDFFloatToInvariantStr(Value: Extended): string;
function HPDFTryInvariantStrToFloat(const S: string;
out Value: Extended): Boolean;
function HPDFInvariantStrToFloat(const S: string): Extended;
function HPDFInvariantStrToFloatDef(const S: string;
const Default: Extended): Extended;
function HPDFFormatInvariant2(Value: Extended): string;
Twee units droegen al een eigen privé-InvariantFormatSettings-functie, HPDFContentStream en HPDFTextExtraction, en beide werden teruggebracht tot een one-line forward in plaats van als parallelle implementaties achtergelaten. Het Type 1-subsysteem is de bewuste uitzondering: het draagt geen VCL-afhankelijkheid en refereert HPDFTypes niet, dus het houdt een lokale spiegel genaamd HPDFType1Core.Type1StrToFloat met identieke semantiek, op de theorie dat een gedupliceerde functie van tien regels een kleinere aansprakelijkheid is dan de graphics-laag in een fontparser slepen. Twee andere plekken werden bewust met rust gelaten. HPDFHTMLImport parseert CSS-lengtes door bewust de punt voor de locale-separator te wisselen en daarna met de locale te parsen, wat voor die input juist is, en HPDFType1CB.GlyphCommands samen met de FontBBox-strings is publieke API die callers met hun eigen StrToFloat teruglezen, waar produceren en consumeren in dezelfde locale zelfconsistent is en invariante output elke caller in een komma-regio zou breken
Wat garandeert HPDF_HAS_FORMAT_SETTINGS over compilers heen?
Het garandeert dat het invariante pad in elke ondersteunde toolchain werkelijk compileert, wat de voor de hand liggende versiegate niet deed. Delphi XE2 introduceerde TFormatSettings.Create en de FloatToStr / TryStrToFloat-overloads die een TFormatSettings nemen, dus het eerste instinct is om bij elke call-site {$IFDEF XE2+}, InvariantFormatSettings{$ENDIF} te schrijven. Dat instinct heeft een blinde vlek: HotPDF.inc definieert D2009+ onder Free Pascal maar definieert nooit XE2+, want XE2+ beantwoordt een vraag over Delphi-versienummers, terwijl Free Pascal 3.2.2 die overloads ongeacht dat meelevert. Elke van die gates degradeerde dus stilzwijgend naar de locale-afhankelijke tak in FPC-builds, precies op de platforms waar de oorspronkelijke rapporten vandaan kwamen. HPDF_HAS_FORMAT_SETTINGS is gedefinieerd in zowel het FPC-blok als het XE2+-blok van HotPDF.inc, en het is het enige symbool dat de getalhelpers testen
{$IFDEF HPDF_HAS_FORMAT_SETTINGS}
function HPDFInvariantFormatSettings: TFormatSettings;
begin
{$IFDEF FPC}
Result := DefaultFormatSettings; // geen TFormatSettings.Create in FPC
{$ELSE}
Result := TFormatSettings.Create;
{$ENDIF}
Result.DecimalSeparator := '.';
Result.ThousandSeparator := ',';
end;
{$ENDIF}
Binnen de functie overleeft één compiler-tak, want Free Pascal heeft geen TFormatSettings.Create en je moet vanuit DefaultFormatSettings beginnen. Alleen de twee getalseparators voeden de conversies, dus de datum- en valutavelden houden wat de huidige locale levert. Pre-XE2 Delphi heeft helemaal geen overloads, en daar vallen de helpers terug op converteren met de proceslocale en daarna het separator-teken in de resulterende string herschrijven, wat lelijk is maar nooit globale state aanraakt
De derde laag: exponenten die SVG toestaat en PDF niet
Het decimaalteken fixen is noodzakelijk en niet voldoende. W3C SVG 1.1 §4.2 laat een exponent toe in zijn getalproductie en XPS-markup doet hetzelfde, dus 1E-6 is onderweg een perfect valide coördinaat, terwijl ISO 32000-1 §7.3.3 helemaal geen exponent toestaat. Een waarde die invariante opmaak overleeft kan dus nog steeds als 1E-6 in een contentstream landen en worden afgewezen, en FloatToStrF in fixed format kan zelf naar exponentvorm terugvallen zodra de waarde het RTL-cijferbereik verlaat. HPDFSafeSVG.SVGFloat doet daarom na de invariante conversie zijn eigen werk: hij spoelt alles onder 1.0E-7 naar nul, en als er dan een E overleeft, stript hij de exponent en verplaatst de decimale punt met de hand, met nullen opgevuld aan de kant die de verschuiving nodig heeft. SVGFinite bewaakt de ingang door NaN en alles buiten ±1.0E30 af te wijzen voordat het de formatter bereikt, en SVGReadNumber met SVGSkipSeparators handelt de inkomende richting teken voor teken af in plaats van naar de RTL te defereren
// Lib/HPDFSafeSVG.pas - decimale notatie voor PDF-operanden
if Abs(Value) < 0.0000001 then
Value := 0;
S := HPDFFloatToInvariantStr(Value);
ExponentAt := Pos('E', UpperCase(S));
if ExponentAt > 0 then
begin
ExponentValue := StrToInt(Copy(S, ExponentAt + 1, MaxInt));
SetLength(S, ExponentAt - 1);
// ... de decimale punt met ExponentValue verschuiven, opvullen met '0'
// links of rechts, en dan het teken weer vastmaken
end;
Dat is dezelfde TSVGBuffer die SVG-pagina-export en de graphics-state-afhandeling ervan voedt, en dezelfde numerieke discipline geldt aan de importkant wanneer XPS- en OXPS-documenten naar PDF worden geconverteerd via HPDFCreateXPSDocumentHandler en het hdfXPS-lid van THPDFHandledDocumentFormat. Markup-formaten zijn gul over getalsyntax. De PDF-contentstream is dat niet, en de conversiegrens is waar dat verschil moet worden opgelost
Regressietests die de locale reproduceren in plaats van haar te veronderstellen
Een locale-bug die alleen op de machine van iemand anders verschijnt is het waard om te pinnen met tests die de locale zelf zetten, en HotPDF draagt er twee. LoadedEditsEmitInvariantNumbersUnderCommaLocale kent een komma toe aan FormatSettings.DecimalSeparator vóór de eerste edit, draait RedactLoadedRect, StampLoadedPageText, StitchLoadedPage en een samengestelde stempel, leest dan de paginacontent-bytes terug en assert zowel dat de exacte operator-strings aanwezig zijn als dat de buffer nergens een komma bevat. DocumentCreationEmitsInvariantNumbersAndKeepsHostLocale dekt de andere helft en de oorspronkelijke zonde: hij rijdt het creatiepad onder dezelfde komma-locale, inclusief DirectDrawBarcode met zijn vaste twee-decimale opmaak, en assert daarna dat de contentstream geen komma draagt en dat FormatSettings.DecimalSeparator nog steeds een komma is wanneer EndDoc terugkeert
Die tweede assertie is degene die het oorspronkelijke defect op de dag dat het werd geschreven zou hebben gevangen, want hij test wat de library met zijn host doet in plaats van alleen wat hij naar schijf schrijft. De les generaliseert voorbij PDF: elk formaat met een vaste lexicale grammatica, PostScript en JSON en JavaScript inbegrepen, heeft een expliciete invariante conversie aan de grens nodig. Naar een globale setting grijpen om het goede antwoord te krijgen werkt totdat iemand je code in een andere volgorde, op een andere thread of vanuit een andere compiler aanroept, en tegen die tijd ziet het falen eruit als alles behalve een opmaakprobleem
De hier beschreven getalopmaak-laag zit in de HotPDF Delphi Component voor Delphi en C++Builder, samen met de geladen-document-bewerking, barcode-, SVG-export- en XPS-conversiepaden die ervan afhangen