PDFlibPas, losLab PDF Developer Library for Delphi, skriver vartenda tal den lägger i en innehållsström med punkt som decimaltecken och utan exponent, oavsett vad Windows regionala inställningar säger. Sedan v3.539.26 formaterar AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-till-path-utdata och omfärgning sina operander genom PLDoubleToStrConst, och sedan v3.539.33 använder parsarna som läser talen tillbaka PLTryStrToFloatInvariant i stället för systemlokalen. På en tysk, fransk eller brasiliansk maskin ger samma kod nu samma byte som på en amerikansk, vilket är det enda beteende ett filformat kan tolerera
Varför förstör en komma-decimal-locale en PDF utan felmeddelande?
En komma-decimal-locale förstör en PDF tyst för att kommat inte är ett taltecken i PDF-syntax, så skadan framstår som giltiga token med fel betydelse. Före fixen var PLFloatToStr inget mer än ett naket FloatToStr-anrop, och FloatToStr följer FormatSettings.DecimalSeparator. Med kommatecken skrev AddPageMatrix(0.5, 0.5, 0, 0) 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 tillåter siffror, en period och ett inledande tecken i ett tal och inget annat, så en innehållsparsare läser den raden som talet 0 följt av en okänd token ,5, och cm-operatorn hamnar med fel operander. Inget kastas, inget loggas. Sidan renderas helt enkelt med en transformationsmatris som drivit ur, och att arbeta baklänges från en felplacerad ritning till en locale-inställning är en eländig eftermiddag
Den andra defekten gömmer sig bakom den första. FloatToStr använder formatet ffGeneral, som växlar till exponentnotation så snart storleken understiger 1E-4, så en liten offset blev 1E-5. Samma §7.3.3 säger att PDF inte stöder exponentformen, vilket betyder att även en maskin med US-locale kunde skriva en ogiltig operand givet ett tillräckligt litet värde. Regressionstesterna för denna release låser båda feltyperna: de växlar separatorn till komma, anropar API:et och söker igenom det resulterande innehållet efter varje token som innehåller ett 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 := ','; // simulera ett de-DE-skrivbord
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 och senare skriver: 0.5 0 0 0.25 0.00001 12.75 cm
// äldre byggen 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;
Två slags tal, två familjer av hjälpare
Fixen i PDFlibPas är en strikt delning: tal som visas för människor får följa lokalens, och tal som skrivs för en maskin gör det aldrig. PLFloatToStr och PLStrToFloat stannar kvar i PDFlibExtra.pas för användarriktad text, och deras deklaration bär nu en kommentar som säger exakt det. Allt som slutar som PDF-syntax går genom PLDoubleToStrConst med ett fast antal decimaler valt för jobbet: sex för matriser, fyra för koordinater och TJ-justeringar, tre för färger och FDF-rektanglar. Revisionen för v3.539.26 rörde fler anropsställen än den ursprungliga buggrapporten antydde:
AddPageMatrix,ScalePageochDeskewPage, som alla lägger encmförst i det befintliga sidinnehållet- Sid-elementbyggarna som sänder ut
Tm-återställningar,TJ-framflyttningar ochcm-transformationer - Glyphplaceringsmatriser och konturpunkter i text-till-path-konverteraren
- Den svarta fyllnadsrutan som
RedactRegionlägger först,/Rect-värdena i FDF-exporten och operanderna som omfärgningen skriver
PLDoubleToStrConst är en handskriven formaterare i stället för en wrapper runt FloatToStrF, och tre av dess egenskaper spelar roll här. Den skriver alltid en period och klipper avslutande nollor, så 0.5 förblir 0.5 i stället för 0.500000. Den skriver aldrig exponent för ändlig indata. Och ett värde skilt från noll som är mindre än den begärda precisionen behåller sina signifikanta siffror i stället för att kollapsa till noll, så PLDoubleToStrConst(1E-9, 6) returnerar 0.000000001; bara värden under ungefär 5E-16 blir 0. Den sista regeln finns för att avrundning av en liten skalfaktor till noll förvandlar en giltig matris till en singulär, vilket är en värre bugg än den som fixas
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Maskinutdata: punkt-decimal, ingen exponent, avslutande nollor borttagna
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // behåller 4 signifikanta siffror
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Maskinindata: mjukt misslyckande i stället för EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // innehållstal använder aldrig komma
end;
Varför är parsidan farligare än skrivsidan?
Parsidan är farligare för att en locale-bunden parser inte producerar ett felaktigt tal, den kastar. PLStrToFloat anropar StrToFloat, som kastar EConvertError när texten inte matchar systemets separator. På ett system med komma-decimaler innebar det att RecolorPage avbröts i ögonblicket den mötte en vanlig 0.5 g-operator, så varje sida ur verkligheten misslyckades, inte bara exotiska. RenderPageRegionToFile avvisade sitt eget dokumenterade clip-format "10.5,20.5,50.5,40.5", och SVG-längdattribut, SVG-exportfärger, annoteringarnas vertexlistor och output intent-soliditetsvärden avvisades eller ersattes tyst med standardvärden. Ett bibliotek som fungerar perfekt på utvecklarens maskin och fallerar hos den första kunden i München är precis den sorts kod som, liksom fallen i artikeln om Delphi-kod som fungerar av en slump, bara ser korrekt ut på grund av var den testades
v3.539.33 klassificerade varje StrToFloat- och TryStrToFloat-anrop efter var dess indata kommer ifrån. Innehållsström-operander, SVG-attribut, painter-färgsträngar och komma-separerade clip- och vertexlistor har alla en fast punkt-syntax, så de går nu genom PLTryStrToFloatInvariant, som trimmar texten, parsar den med PLInvariantFormatSettings och returnerar False för tom, felformad eller icke-ändlig indata i stället för att kasta. En komma-separerad lista lämnar inte utrymme för kompromiss, eftersom ett komma inte kan vara både listavgränsare och decimaltecken. Samma pass fixade också en skrivning utanför gränserna: RenderPageRegionToFile brukade lagra ett femte clip-värde förbi sin fyraelementsbuffert. För omfärgningspipelinen som beskrivs i guiden till att konvertera en PDF till en färgrymd är det praktiska resultatet att RecolorPage och RecolorDocument inte längre avbryts på ett system med komma-decimaler. Regelvärden som en anropare skriver in i CheckDocumentPolicy är det enda parsningfall som i stället använder den toleranta hjälparen, av det skäl nästa avsnitt förklarar
Vad händer om du fixar bara ena änden av en roundtrip?
Att fixa bara ena änden av en locale-roundtrip bryter kod som tidigare fungerade, vilket är därför strukturattributändringen i v3.539.32 flyttade skrivaren och läsaren tillsammans. SetStructElem*-wrapparna vidarebefordrar tal som strängar: SetStructElemBBox formaterar fyra värden till en sträng, lagrar den genom AddTagAttribute, och /A-skrivaren parsar senare den strängen för att avgöra om den blir ett tal, en array eller ett namn. Båda ändarna använde systemlokalen, så på ett system med komma-decimaler var roundtripsen självkonsistent. Buggen dök bara upp när en anropare följde dokumentationen och skickade "0.5" till AddTagAttribute: läsaren kunde inte parsa den och sände ut PDF-namnet /0.5. PDF/VCR-platshållaren hade spegelbildsproblemet, eftersom biblioteket genererade GTS_BBox med punkt och sedan validerade den med localen före sparande
Att bara byta skrivaren till punkt hade varit värre än att inte göra något, eftersom varje SetStructElem*-värde då hade fallerat den locale-bundna läsaren och degraderat till ett namn. Så skrivarna använder nu PLDoubleToStrConst(v, 6), och läsaren använder den nya PLTryStrToFloatLenient, som provar punktformen först och faller tillbaka till systemlokalen. En anropare med komma-locale som skickade "1,25" förr får fortfarande talet 1.25. Avvägningen är medveten och dokumenterad: på ett tyskt system blev "1.500" tidigare ett namn eftersom StrToFloat avvisar tusentalsseparatorer, och det läses nu som 1.5, medan de literala NAN- och INF-strängarna inte längre accepteras som tal
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // anropare 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, var /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // fortfarande /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Där NaN och oändlighet stoppas
AddPageMatrix, ScalePage och RedactRegion avvisar nu NaN och oändliga argument i förväg och returnerar 0, för inget PDF-tal kan representera dem. ScalePage vägrade redan faktorer på noll eller mindre, men NaN passerar ett <= 0-test, så en NaN-skala brukade färdas hela vägen till formateraren. I v3.539.26 anropade den formateraren fortfarande Round på NaN, vilket kastar EInvalidOp på Win32 där x87-enheten inte maskerar ogiltiga operationer; v3.539.31 lät PLDoubleToStrConst skriva 0 för NaN som en sista försvarslinje, men en nolla i en matris är en singulär transform, så kontrollen på API-nivå är fortfarande den verkliga fixen. Två gränslinjer är kvarvarande med flit. Metafiltillståndssträngar skrivs och läses med localen inuti en process och lämnar den aldrig, så de lämnades orörda. Och ett test som formaterar 1E-5 genom sid-elementvägen måste läsa innehållet innan lagret skrivs om, eftersom återutsändning av operander i dokumentprecision legitimt gör det värdet till 0
Om din applikation levereras till kunder utanför punkt-decimal-världen är den säkraste vanan den som PDFlibPas-testsviten nu använder: kör de PDF-producerande vägarna en gång med FormatSettings.DecimalSeparator satt till komma och sök utdatan efter komman och exponenter. Artikeln om att bevara parsad decimalprecision täcker den andra halvan av samma historia, hur tal som lästs ur en befintlig fil behåller sin exakta text vid sparande. Nedladdningar, den fullständiga API-referensen och testversionen finns på produktsidan för PDFlibPas Delphi PDF library