PDFlibPas, losLab PDF Developer Library за Delphi, записва всяко число, което слага в content stream, с десетична точка и без експонента, каквото и да твърдят регионалните настройки на Windows. От v3.539.26 AddPageMatrix, ScalePage, DeskewPage, RedactRegion, text-to-path изходът и преоцветяването форматират операнди чрез PLDoubleToStrConst, а от v3.539.33 парсерите, които четат тези числа обратно, ползват PLTryStrToFloatInvariant вместо системния локал. На немска, френска или бразилска машина същият код вече дава същите байтове като на американска — единственото поведение, което файлов формат може да търпи
Защо локал със запетая разваля PDF без грешка?
Локал със запетая разваля PDF тихо, защото запетаята не е число-символ в PDF синтаксиса, така че щетата се чете като валидни токени с грешно значение. Преди поправката PLFloatToStr беше нищо повече от гол FloatToStr call, а FloatToStr следва FormatSettings.DecimalSeparator. При разделител запетая AddPageMatrix(0.5, 0.5, 0, 0) записваше 0,5 0 0 0,5 0 0 cm. ISO 32000-1 §7.3.3 позволява цифри, една точка и водещ знак в число и нищо друго, така че content парсер чете този ред като число 0, следвано от непознат токен ,5, а операторът cm свършва с грешни операнди. Нищо не вдига, нищо не логва. Страницата просто се рендерира с трансформационна матрица, която се е изплъзнала, а обръщането назад от изместен чертеж към настройка на локала е мъчителен следобед
Вторият дефект се крие зад първия. FloatToStr ползва формат ffGeneral, който превключва на експоненциален запис, щом големината падне под 1E-4, така че малко отместване излизаше като 1E-5. Същият §7.3.3 казва, че PDF не поддържа експонентната форма, тоест дори машина с американски локал можеше да запише невалиден операнд при достатъчно малка стойност. Регресионните тестове на този release заковат и двете форми на отказа: обръщат разделителя на запетая, викат API-то и сканират получения content за всеки токен, съдържащ запетая или експонента
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 := ','; // симулирай de-DE десктоп
Lib.AddPageMatrix(0.5, 0.25, 1E-5, 12.75);
// v3.539.26 и по-нови пишат: 0.5 0 0 0.25 0.00001 12.75 cm
// по-стари build-ове писаха: 0,5 0 0 0,25 1E-5 12,75 cm
Writeln(Lib.GetPageContentToString);
finally
FormatSettings.DecimalSeparator := OldSeparator;
end;
finally
Lib.Free;
end;
end;
Два вида числа, две семейства helper-и
Поправката в PDFlibPas е стриктно разделяне: числа, показвани на хора, могат да следват локала, а числа, писани за машина, никога. PLFloatToStr и PLStrToFloat остават в PDFlibExtra.pas за текст към потребителя, а декларацията им вече носи коментар, казващ точно това. Всичко, което става PDF синтаксис, минава през PLDoubleToStrConst с фиксиран брой десетични места, избрани за работата: шест за матрици, четири за координати и TJ отмествания, три за цветове и FDF правоъгълници. Одитът за v3.539.26 пипна повече call site-ове, отколкото предполагаше оригиналният доклад за бъга:
AddPageMatrix,ScalePageиDeskewPage, които всички залепятcmпред съществуващото съдържание на страницата- Строителите на page елементи, които излъчват
Tmнулирания,TJотмествания иcmтрансформации - Матрици за поставяне на глифи и контурни точки в text-to-path конвертора
- Черната запълваща кутия, която
RedactRegionзалепя отпред,/Rectстойностите в FDF експорта и операндите, които преоцветяването записва
PLDoubleToStrConst е ръчно изкован форматор, а не обвивка около FloatToStrF, и три негови свойства тук имат значение. Винаги записва точка и отрязва крайните нули, така че 0.5 си остава 0.5, а не 0.500000. Никога не записва експонента за краен вход. И ненулева стойност, по-малка от исканата прецизност, пази значещите си цифри, вместо да се срине в нула, така че PLDoubleToStrConst(1E-9, 6) връща 0.000000001; само стойности под около 5E-16 стават 0. Това последно правило съществува, защото закръглянето на малък мащабен фактор до нула превръща валидна матрица в сингулярна — по-лош бъг от поправяния
uses
PDFlibExtra;
procedure CheckNumberHelpers;
var
V: Double;
begin
// Machine изход: десетична точка, без експонента, крайни нули отрязани
Assert(PLDoubleToStrConst(1E-5, 6) = '0.00001');
Assert(PLDoubleToStrConst(-0.5, 6) = '-0.5');
Assert(PLDoubleToStrConst(0.000012346, 4) = '0.00001235'); // пази 4 значещи цифри
Assert(PLDoubleToStrConst(12345.25, 4) = '12345.25');
// Machine вход: мек отказ вместо EConvertError
Assert(PLTryStrToFloatInvariant('0.5', V) and (V = 0.5));
Assert(not PLTryStrToFloatInvariant('0,5', V)); // content числа никога не ползват запетая
end;
Защо страната на парсване е по-опасна от страната на запис?
Страната на парсване е по-опасна, защото парсер, вързан за локала, не произвежда грешно число, а хвърля. PLStrToFloat вика StrToFloat, който вдига EConvertError, когато текстът не пасва на системния разделител. На система със запетая това значеше, че RecolorPage се откажеше в момента, в който срещне обикновен оператор 0.5 g, така че всяка реална страница се проваляше, не само екзотичните. RenderPageRegionToFile отхвърляше собствения си документиран clip формат "10.5,20.5,50.5,40.5", а SVG дължините като атрибути, SVG експортните цветове, списъците с върхове на анотации и стойностите за плътност на output intent бяха или отказвани, или тихо заменяни с подразбиращи се. Библиотека, работеща съвършено на машината на разработчика и проваляща се на първия клиент в Мюнхен, е точно видът код, който — като случаите в статията за Delphi код, работещ по случайност — изглежда правилен само заради мястото, където е тестван
v3.539.33 класифицира всяко извикване на StrToFloat и TryStrToFloat по произхода на входа му. Операнди на content stream, SVG атрибути, цветни низове на painter и списъци с клипове и върхове, разделени със запетая, всички имат фиксиран синтаксис с точка, така че сега минават през PLTryStrToFloatInvariant, който отрязва интервалите, парсва с PLInvariantFormatSettings и връща False за празен, malformed или некраен вход, вместо да вдига. Списък, разделен със запетая, не оставя място за компромис, защото запетаята не може да е едновременно разделител на списъка и десетична запетая. Същият проход поправи и запис извън границите: RenderPageRegionToFile пазеше пета clip стойност след четириелементния си буфер. За pipeline-а за преоцветяване, описан в ръководството за превръщане на PDF в едно color space, практическият резултат е, че RecolorPage и RecolorDocument вече не се отказват на система със запетая. Стойностите на правила, които извикващът въвежда в CheckDocumentPolicy, са единственият случай на парсване с мекия helper — по причината, която следващият раздел обяснява
Какво става, ако поправите само единия край на round trip?
Поправката само на единия край на locale round trip чупи код, който си работеше, затова промяната на структурните атрибути в v3.539.32 премести writer-а и reader-а заедно. SetStructElem* обвивките предават числа като низове: SetStructElemBBox форматира четири стойности в един низ, съхранява го чрез AddTagAttribute, а writer-ът на /A после парсва този низ, за да реши дали става число, масив или име. И двата края ползваха системния локал, така че на система със запетая round trip-ът беше самосъгласуван. Бъгът се показа само когато извикващ следваше документацията и подаваше "0.5" на AddTagAttribute: reader-ът не можеше да го парсне и излъчваше PDF името /0.5. PDF/VCR placeholder-ът имаше огледалния проблем, защото библиотеката генерираше GTS_BBox с точка, после го валидираше с локала преди запис
Смяната само на writer-а към точка би била по-лоша от бездействие, защото всяка SetStructElem* стойност би провалила locale-вързания reader и би се изродила в име. Затова writer-ите вече ползват PLDoubleToStrConst(v, 6), а reader-ът — новия PLTryStrToFloatLenient, който пробва първо формата с точка и се връща към системния локал. Извикващ със запетая, който е подавал "1,25" в миналото, и досега получава числото 1.25. Компромисът е нарочен и документиран: на немска система "1.500" преди ставаше име, защото StrToFloat отхвърля разделители за хилядите, а сега се чете като 1.5, докато literal низовете NAN и INF вече не се приемат за числа
uses
System.SysUtils, PDFlibrary;
var
Lib: TPDFlib;
begin
FormatSettings.DecimalSeparator := ','; // извикващ със запетая за десетична
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, преди беше /0.5
Lib.AddTagAttribute('Layout', 'StartIndent', '1,25'); // пак /StartIndent 1.25
Lib.DrawText(20, 20, 'figure');
Lib.EndTag;
Lib.SaveToFile('tagged.pdf');
finally
Lib.Free;
end;
end;
Къде се спират NaN и безкрайността
AddPageMatrix, ScalePage и RedactRegion вече отхвърлят NaN и безкрайни аргументи отпред и връщат 0, защото нищо PDF число не може да ги представи. ScalePage вече отказваше фактори нула или по-малки, но NaN минава тест <= 0, така че NaN мащаб пътуваше чак до форматора. В v3.539.26 този форматор още викаше Round върху NaN, което вдига EInvalidOp на Win32, където x87 единицата не маскира невалидни операции; v3.539.31 накара PLDoubleToStrConst да записва 0 за NaN като последна защита, но нула в матрица е сингулярна трансформация, така че проверката на ниво API си остава истинската поправка. Две граници стоят нарочно. Metafile state низовете се записват и четат с локала вътре в един процес и никога не го напускат, така че бяха оставени на мира. И тест, който форматира 1E-5 през пътя на page елементите, трябва да прочете content-а, преди слоят да е пренаписан, защото преизлъчването на операнди при прецизност на документа законно превръща тази стойност в 0
Ако приложението ви пътува към клиенти извън света с десетична точка, най-безопасният навик е този, който тестовият набор на PDFlibPas вече ползва: минете PDF-произвеждащите пътища веднъж с FormatSettings.DecimalSeparator зададен на запетая и сканирайте изхода за запетаи и експоненти. Статията за запазване на парсната десетична прецизност покрива другата половина от същата история — как числата, прочетени от съществуващ файл, пазят точния си текст при запис. Изтегляния, пълната API референция и trial build-ът са на продуктовата страница PDFlibPas Delphi PDF library