PDFlibPas, losLab PDF Developer Library, пази точния десетичен текст, който е парсвал за всяко реално число в документ, и изписва този текст дословно, стига стойността да не е била модифицирана. От v3.539.19 нататък настройката SetPrecision управлява само числата, които библиотеката създава или редактира, така че обикновен load-and-save вече не закръгля CalRGB /Gamma от 2.22221 на 2.2222 и не мести цветовете на страница, която никой не е пипал. Промяната е малка по код и голяма по това, което казва за парсерите: стойността, която декодирате, и literal-ът, който изписвате, са две различни неща, а round trip през Double не е идентично преобразуване
Защо запис, който не променя нищо, измести цветовете на страницата?
Защото се преформатираха параметрите на цветовото пространство, а не изображението. Файлът, извадил това наяве, е офис документ на 35 страници в локалния регресионен корпус, с изображение в header-а, преизползвано на всяка страница. Зареждането му и записът му веднага обратно дадоха image stream-ове, байт по байт идентични с входа, а сравнение по stream хешове докладва документа непроменен. Рендирано сравнение се противопостави: всяка от 35-те страници показваше пикселни разлики в header-а, и никъде другаде
Изображението в header-а се рисува през CalRGB цветово пространство, което ISO 32000-1 §8.6.5.3 дефинира чрез /WhitePoint, незадължителен триелементен масив /Gamma и незадължителен деветелементен масив /Matrix. Тези масиви са обикновени числови обекти в речника на цветовото пространство. TPDFNumeric пазеше всеки като Double и нищо друго, а TPDFNumeric.Output форматираше този Double през PDFPrecNum, който по подразбиране е четири знака след десетичната запетая. Така /Gamma премина от 2.22221 на 2.2222, матричен елемент — от 0.71519 на 0.7152, и renderer-ът вярно изведе леко различни цветове от леко различна калибровка. Байтовете на изображението бяха невинни; числата около тях — не. Некомфортната част е колко невидимо беше всичко това. Сравнение на декодирани stream байтове не го вижда, защото числата живеят в речник, а не в stream. Сравнение на attachment payload-и не го вижда. Дори revision diff-ът, описан в статията за modification levels, фингърпринтива нормализирано тяло на обект, така че и двете ревизии хешират до една стойност и diff-ът ги докладва идентични. Само рендирането го хвана — затова corpus базовата линия рендира всяка страница, вместо да вярва само на структурните проверки
Стойността, която сте парснали, не е literal-ът, който трябва да изпишете
Реално число в PDF е десетичен стринг, и ISO 32000-1 §7.3.3 е изричен, че е само десетичен стринг: без radix нотация, без експоненциална форма. Annex C после изброява точността, която се очаква имплементация да уважава — приблизително пет значещи десетични цифри в дробната част. Точност на изхода по подразбиране от четири вече е под това, а около нулата става по-зле: PLDoubleToStr скалира стойността, закръгля до цяло число и изписва 0, когато резултатът е нула, така че матричен елемент от -0.000012345 не губи цифра — изчезва изцяло
Повдигането на стойността по подразбиране само би преместило стръма. Поправката е да спрете да се преструвате, че Double е числото. Когато токенизаторът в TPDFStructure.Decode разпознае стандартно реално число — тоест токенът съдържа десетична точка и няма експоненциален маркер — той пази изходния текст в новото поле FOriginalText редом с превърнатата стойност. Output тогава предпочита този текст и се връща към форматиране само когато няма какво да предпочете
// Lib/PDFlibStruct.pas — цялата поправка от страната на изхода
Function TPDFNumeric.Output: AnsiString;
Begin
If FOriginalText<> '' Then
Result:= FOriginalText
Else
Result:= PLDoubleToStr(FValue, Owner.PDFPrecNum);
End;
Procedure TPDFNumeric.SetTo(Const Value: Double);
Begin
FOriginalText:= ''; // редактирано число е ново число
FValue:= Value;
FChanged:= True;
End;
Две граници са нарочни. Целите числа не се опазват, защото целочисленото форматиране вече е беззагубно. Експоненциални форми като 6.02E23 се толерират на входа заради развалени producer-и, но не се опазват на изхода, тъй като записването им обратно би увековечило синтаксис, който §7.3.3 забранява; те минават през форматера като всяко число, генерирано от библиотеката. Токенизаторът приложи и обичайния си минимален ремонт преди да запази текста, така че literal с водеща точка като .5 се пази като 0.5, а literal със завършваща точка като 5. — като 5.0. И двете са същото число за всеки читател и са много по-широко приемани
Какво гарантира SetPrecision след v3.539.19?
TPDFlib.SetPrecision сега контролира десетичните знаци на числата, които самата библиотека произвежда: стойности, рисувани през painter-а, числа, създадени от Double — например през NewNumeric — и всяка парсната стойност, която отсам е била редактирана с SetTo. Обърнете внимание, че текст, декодиран през object API — например literal, подаден на SetObjectFromString — минава през същия токенизатор и се пази по същия начин. Парснато десетично число, никога не модифицирано, пази входната си точност независимо от настройката, а смяната на настройката след зареждането не го пипа ретроактивно. Референтният запис на SetPrecision беше обновен в същото издание да каже точно това, защото старата формулировка подсказваше, че настройката важи за всяко число във файла
Изчистването става в SetTo, вместо да се извежда от флага Changed, и тази разлика има значение. Записващият pipeline рестартира Changed върху обектите, щом бъдат изписани, така че проверка от вида „изпиши оригиналния текст, освен ако не е променено" би започнала да изписва остарял текст за стойност, редактирана, записана и пак редактирана в същата сесия. Завързването на оригиналния текст със самото присвояване прави невъзможно двете да се разминат. Регресионният тест заземява всяко от тези поведения със стойностите от оригиналния файл
uses
PDFlibStruct;
var
Structure: TPDFStructure;
Values: TPDFArray;
Number: TPDFNumeric;
begin
Structure := TPDFStructure.Create;
try
Structure.PDFPrecNum := 4;
Values := TPDFArray(Structure.Decode('[2.22221 0.71519 -0.000012345 1 0.12567]'));
// Нетрогнатият вход оцелява дословно, включително стойността, която
// четирипозиционното форматиране би свлякло до 0
Assert(Values.Output = '[ 2.22221 0.71519 -0.000012345 1 0.12567 ]');
// Редакция изхвърля оригиналния текст и следва PDFPrecNum
Number := TPDFNumeric(Values.Item[0]);
Number.SetTo(0.123456);
Assert(Number.Output = '0.1235');
Assert(Structure.NewNumeric(0.123456).Output = '0.1235');
// Понижаването на точността после не достига нетрогнатия вход
Structure.PDFPrecNum := 2;
Assert(TPDFNumeric(Values.Item[1]).Output = '0.71519');
finally
Structure.Free;
end;
end;
Защо content моделът все още нормализира числа?
Защото TPDFContentProgram обещава канонични числови операнди, и това обещание струва повече от дословен текст в content stream. Редактируемият content модел — същият, върху който е строен tracker-ът на graphics state — съществува, за да дават NormalizeContentStreams, оптимизаторът и Emit стабилен, сравним изход от произволен вход. Ако парснат операнд носеше оригиналния си текст в модела, операторна последователност като 0.50000 0 0 RG би излязла различно от 0.5 0 0 RG, а всяко сравнение надолу по веригата би се носило по форматиращите навици на producer-а
Затова моделът обелва оригиналния текст на двата си входа. NormalizeContentNumbers върви върху всеки операнд, докато парсерът го бута, и отново вътре в SetOperand, когато се декодира подаден от извикващия източник, и се рекурсира през масиви и речници, така че dash патерните, масивите TJ и property речниците на marked content са покрити. Извикването на SetTo(AsDouble) върху всяко число е достатъчно, тъй като точно това е операцията, която изчиства текста. Суровите inline image данни остават на мира, както винаги
// Lib/PDFlibContentModel.pas — content моделът пази своя договор
Procedure NormalizeContentNumbers(Obj: TPDFObject);
Var
K: Integer;
Begin
If Obj is TPDFNumeric Then
TPDFNumeric(Obj).SetTo(TPDFNumeric(Obj).AsDouble)
Else If Obj is TPDFArray Then
For K:= 0 To TPDFArray(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFArray(Obj).Item[K])
Else If Obj is TPDFDictionary Then
For K:= 0 To TPDFDictionary(Obj).Count- 1 Do
NormalizeContentNumbers(TPDFDictionary(Obj).Entry[K].Value);
End;
Практическото правило за извикващите затова е просто. Обикновен LoadFromFile, последван от SaveToFile, оставя нетрогнати content stream-ове и нетрогнати речникови числа както са били. Страница, минала през NormalizeContentStreams, или всяка редакция, направена през content модела, излиза канонична по конструкция, а останалата част от документа пак се пази. Това са две различни заявки, и сега правят две различни неща
Какво струва и докъде стига гаранцията
Всеки TPDFNumeric сега носи една допълнителна справка към AnsiString, и всяко парснато десетично число държи изходния си текст жив за времето на живота на обекта. На документ с милиони реални числа това е реална памет, и принадлежи на всяко измерване с големи документи, вместо да се маха с ръка. Гаранцията е обвързана и с собствения документ на числото: копиране на обекти между документи или възстановяване на стойности през object API дава нови числа, които следват точността на изхода като всяко друго ново число. Си струва да бъдем точни за това, което изданието твърди и не твърди. Load-and-save на нетрогнат документ сега пази калибровъчните числа, които renderer-ът действително консумира — свойството, което corpus базовата линия проверява. Не твърди байт-идентичен изход, който зависи още от номерирането на обектите, компресията на stream-овете и trailer идентификатора, обсъдени в статията за детерминистичния PDF ID. И не кара fingerprint diff-ът да вижда разлики при закръгляне във файлове, произведени от друг софтуер, тъй като те пак хешират нормализираното тяло. Урокът се обобщава добре отвъд CalRGB: когато парсер пази само превърнатата стойност, всеки запис е редакция, и единственият начин да забележите е да погледнете рендираното. Числовата обработка и семантиките на SetPrecision са документирани на продуктовата страница на losLab PDF Developer Library