Teknisk artikkel

Locale-nøytrale PDF-numre for komma-desimal-Delphi-apper

PDFlibPas, losLabs PDF Developer Library for Delphi, skriver hvert tall den putter i en content stream med punktum som desimalseparator og uten eksponent, uansett hva de regionale Windows-innstillingene sier. Siden v3.539.26 formaterer AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path-output og rekolorering operander gjennom PLDoubleToStrConst, og siden v3.539.33 bruker parserne som leser tallene tilbake PLTryStrToFloatInvariant i stedet for systemlocelen. På en tysk, fransk eller brasiliansk maskin produserer samme kode nå de samme bytene som på en amerikansk, og det er den eneste oppførselen et filformat kan tolerere

Hvorfor korrumperer en komma-desimal-locale en PDF uten en feil?

En komma-desimal-locale korrumperer en PDF i stillhet fordi kommaet ikke er et talltegn i PDF-syntaks, så skaden leses som gyldige token med feil mening. Før fiksen var PLFloatToStr ikke mer enn et bart FloatToStr-kall, 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 tillater siffer, ett punktum og et ledende fortegn i et tall og ingenting annet, så en content-parser leser den linjen som tallet 0 fulgt av en ukjent token ,5, og cm-operatoren ender opp med feil operander. Ingenting reises, ingenting logges. Siden renderes rett og slett med en transformasjonsmatrise som har drevet, og å jobbe seg baklengs fra en feilplassert tegning til en locale-innstilling er en elendig ettermiddag

Den andre defekten gjemmer seg bak den første. FloatToStr bruker ffGeneral-formatet, som bytter til eksponentnotasjon så snart størrelsen synker under 1E-4, så en liten offset kom ut som 1E-5. Samme §7.3.3 slår fast at PDF ikke støtter eksponentformen, noe som betyr at selv en maskin med amerikansk locale kunne skrive en ugyldig operand gitt en liten nok verdi. Regresjonstestene for denne utgivelsen fester begge feilformene: de flipper separatoren til komma, kaller API-et og skanner det resulterende innholdet for enhver token som inneholder et komma eller en eksponent

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 := ',';   // simuler et de-DE-skrivebord
      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
      // eldre bygg 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;
PDFlibPas AddPageMatrix på et komma-desimal-skrivebord skrev 0,5 0 0 0,25 1E-5 12,75 cm før fiksen, noe en PDF-parser leser som tallet 0 pluss ukjente token, slik at cm står igjen med feil operander og siden stille transformeres, mens PLDoubleToStrConst skriver gyldige punktumdesimaler
Kommaet er ikke et talltegn i PDF-syntaks, så skaden leses som gyldige token med feil mening — og ffGeneral-eksponenter som 1E-5 var ugyldige på alle loceler, ikke bare komma-ene

To slags tall, to familier av hjelpere

Fiksen i PDFlibPas er en streng deling: tall vist til mennesker kan følge locelen, og tall skrevet for en maskin gjør det aldri. PLFloatToStr og PLStrToFloat blir i PDFlibExtra.pas for brukervendt tekst, og deklarasjonen deres bærer nå en kommentar som sier nøyaktig det. Alt som ender som PDF-syntaks går gjennom PLDoubleToStrConst med et fast antall desimaler valgt for jobben: seks for matriser, fire for koordinater og TJ-justeringer, tre for farger og FDF-rektangler. Revisjonen for v3.539.26 rørte flere kallsteder enn den opprinnelige feilrapporten antydet:

  • AddPageMatrix, ScalePage og DeskewPage, som alle prepender en cm på eksisterende sideinnhold
  • Sideelementbyggerne som sender ut Tm-tilbakestillinger, TJ-fremrykk og cm-transformer
  • Glyfplasseringsmatriser og omrisspunkter i text-to-path-konverteren
  • Den svarte fyllboksen som RedactRegion prepender, /Rect-verdiene i FDF-eksporten og operandene rekoloreringen skriver

PLDoubleToStrConst er en håndrullet formatter i stedet for en wrapper rundt FloatToStrF, og tre av egenskapene betyr noe her. Den skriver alltid et punktum og stripper etterfølgende nuller, så 0.5 forblir 0.5 i stedet for 0.500000. Den skriver aldri en eksponent for endelig input. Og en ikke-null verdi mindre enn ønsket presisjon beholder sine signifikante siffer i stedet for å kollapse til null, så PLDoubleToStrConst(1E-9, 6) returnerer 0.000000001; bare verdier under omtrent 5E-16 blir 0. Den siste regelen finnes fordi avrunding av en liten skaleringsfaktor til null gjør en gyldig matrise singulær, noe som er en verre bug enn den som fikses

PDFlibPas deler tallformatering i to: PLFloatToStr og PLStrToFloat forblir localebundet for brukervendt tekst, mens PLDoubleToStrConst og PLTryStrToFloatInvariant formaterer alt som blir PDF-syntaks med punktum, ingen eksponent og en fast presisjon per jobb på seks, fire eller tre desimaler
Den invariante formatteren er håndrullet med vilje: den stripper etterfølgende nuller, skriver aldri en eksponent og beholder de signifikante sifrene til bittelike verdier, for avrunding av en skaleringsfaktor til null ville gjort en gyldig matrise singulær
uses
  PDFlibExtra;

procedure CheckNumberHelpers;
var
  V: Double;
begin
  // Maskin-output: punktumdesimal, ingen eksponent, etterfølgende nuller strippet
  Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
  Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
  Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235');  // beholder 4 signifikante siffer
  Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');

  // Maskin-input: myk feil i stedet for EConvertError
  Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
  Assert(not PLTryStrToFloatInvariant('0,5', V));   // content-numre bruker aldri komma
end;

Hvorfor er parsesiden farligere enn skrivesiden?

Parsesiden er farligere fordi en localebundet parser ikke produserer et feil tall, den kaster. PLStrToFloat kaller StrToFloat, som reiser EConvertError når teksten ikke samsvarer med systemseparatoren. På et komma-desimal-system betydde det at RecolorPage avbrøt i det øyeblikket den traff en helt vanlig 0.5 g-operator, så hver ekte side feilet, ikke bare eksotiske. RenderPageRegionToFile avviste sitt eget dokumenterte clip-format "10.5,20.5,50.5,40.5", og SVG-lengdeattributter, SVG-eksportfarger, annotasjonstopunkt-lister og output intent-soliditetsverdier ble enten nektet eller stille erstattet med standardverdier. Et bibliotek som fungerer perfekt på utviklermaskinen og feiler på den første kunden i München, er nøyaktig den typen kode som, som tilfellene i artikkelen om Delphi-kode som fungerer ved et uhell, bare virker riktig på grunn av hvor den ble testet

v3.539.33 klassifiserte hvert StrToFloat- og TryStrToFloat-kall etter hvor inputen kommer fra. Content stream-operandene, SVG-attributtene, painter-fargestrengene og de kommadelte clip- og topunkt-listene har alle fast punktumsyntaks, så de går nå gjennom PLTryStrToFloatInvariant, som trimmer teksten, parser den med PLInvariantFormatSettings og returnerer False for tom, misdannet eller ikke-endelig input i stedet for å reise. En kommadelt liste lar ikke rom for kompromiss, for et komma kan ikke være både listedelikator og desimaltegn. Samme gjennomgang fikset også en out-of-bounds-skriving: RenderPageRegionToFile lagret tidligere en femte clip-verdi forbi sin fire-elements buffer. For rekoloreringspipelinen beskrevet i guiden til å konvertere en PDF til ett fargerom er det praktiske resultatet at RecolorPage og RecolorDocument ikke lenger avbryter på et komma-desimal-system. Regelverdier en kaller skriver inn i CheckDocumentPolicy er det eneste parseringstilfellet som bruker den mildere hjelperen i stedet, av grunnen neste avsnitt forklarer

Hva skjer hvis du fikser bare én ende av en rundtur?

Å fikse bare én ende av en locale-rundtur knekker kode som pleide å fungere, og det er derfor strukturattributtendringen i v3.539.32 flyttet skriveren og leseren sammen. SetStructElem*-wrapperne videreformidler tall som strenger: SetStructElemBBox formaterer fire verdier til én streng, lagrer den gjennom AddTagAttribute, og /A-skriveren parser den strengen senere for å avgjøre om den blir et tall, en array eller et navn. Begge ender brukte systemlocelen, så på et komma-desimal-system var rundturen selvkonsistent. Bug-en viste seg bare når en kaller fulgte dokumentasjonen og sendte "0.5" til AddTagAttribute: leseren klarte ikke å parse den og sendte ut PDF-navnet /0.5. PDF/VCR-plassholderen hadde speilbildeproblemet, for biblioteket genererte GTS_BBox med punktum og validerte den så med locelen før lagring

Å endre bare skriveren til punktum ville vært verre enn å gjøre ingenting, siden hver SetStructElem*-verdi da ville feilet hos den localebundne leseren og degradert til et navn. Så skriverne bruker nå PLDoubleToStrConst(v, 6), og leseren bruker den nye PLTryStrToFloatLenient, som prøver punktumformen først og faller tilbake til systemlocelen. En komma-locale-kaller som sendte "1,25" i fortiden, får fortsatt tallet 1.25. Avveiningen er bevisst og dokumentert: på et tysk system ble "1.500" et navn fordi StrToFloat avviser tusenseparatorer, og den leses nå som 1.5, mens bokstavelige NAN- og INF-strenger ikke lenger aksepteres som tall

PDFlibPas SetStructElemBBox og sine søsken videreformidler tall som strenger gjennom AddTagAttribute, og /A-skriveren parser strengene tilbake, så v3.539.32 flyttet begge ender sammen: PLDoubleToStrConst skriver med punktum og PLTryStrToFloatLenient leser punktum først med locale-fallback, slik at komma-locale 1,25 fortsatt leses som 1.25
Å fikse bare skriveren ville ha degradert hvert strukturert element-attributt til et PDF-navn, og det er derfor en rundtur flytter begge ender sammen eller ingen av dem
uses
  System.SysUtils, PDFlibrary;

var
  Lib: TPDFlib;
begin
  FormatSettings.DecimalSeparator := ',';   // komma-desimal-kaller
  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'); // fortsatt /StartIndent 1.25
    Lib.DrawText(20, 20, 'figure');
    Lib.EndTag;
    Lib.SaveToFile('tagged.pdf');
  finally
    Lib.Free;
  end;
end;

Hvor NaN og uendelighet stoppes

AddPageMatrix, ScalePage og RedactRegion avviser nå NaN og uendelige argumenter på forhånd og returnerer 0, for ingen PDF-nummer kan representere dem. ScalePage nektet allerede faktorer på null eller mindre, men NaN passerer en <= 0-test, så en NaN-skala reiste tidligere hele veien til formatteren. I v3.539.26 kalte den formatteren fortsatt Round på NaN, noe som reiser EInvalidOp på Win32 der x87-enheten ikke maskerer ugyldige operasjoner; v3.539.31 fikk PLDoubleToStrConst til å skrive 0 for NaN som en siste forsvarslinje, men en null i en matrise er en singulær transform, så sjekken på API-nivå er fortsatt den ekte fiksen. To grenser blir med vilje stående. Metafile-tilstandsstrenger skrives og leses med locelen inne i én prosess og forlater den aldri, så de ble latt i fred. Og en test som formaterer 1E-5 gjennom sideelementstien må lese innholdet før laget omskrives, for å gjenutsende operander ved dokumentpresisjon gjør rettmessig at den verdien blir 0

Hvis applikasjonen din sender til kunder utenfor punktum-desimal-verden, er den tryggeste vanen den PDFlibPas-testsuiten nå bruker: kjør de PDF-produserende stiene én gang med FormatSettings.DecimalSeparator satt til komma og skann outputen for kommaer og eksponenter. Artikkelen om å bevare parset desimalpresisjon dekker den andre halvdelen av samme historie, hvordan tall lest fra en eksisterende fil beholder sin nøyaktige tekst ved lagring. Nedlastinger, hele API-referansen og prøvebygget ligger på PDFlibPas Delphi PDF library produktsiden