PDFlibPas، یعنی losLab PDF Developer Library، متن اعشاری دقیقی را که برای هر عدد حقیقی در سند parse کرده نگه میدارد و هر وقت آن مقدار هرگز تغییر نکرده باشد همان متن را موبهمو بازمینویسد. از v3.539.19 به بعد تنظیم SetPrecision فقط بر عددهایی حاکم است که خود کتابخانه میسازد یا ویرایش میکند، پس یک بارگذاری-و-ذخیرهٔ معمولی دیگر یک /Gamma در CalRGB با مقدار 2.22221 را به 2.2222 گرد نمیکند و رنگ صفحهای را که کسی دستش نزده جابهجا نمیکند. این تغییر در کد کوچک است و در آنچه دربارهٔ parserها میگوید بزرگ: مقداری که decode میکنی و literalی که بیرون میدهی دو چیز متفاوتاند و یک رفتوبرگشت Double یک تبدیل همانمانندی نیست
چرا یک ذخیره که چیزی را عوض نکرده بود رنگ صفحه را جابهجا کرد؟
چون پارامترهای فضای رنگ داشتند دوباره قالببندی میشدند، نه خود تصویر. فایلی که این را رو کرد یک سند اداری 35-صفحهای در کورپوس رگرسیون محلی است با یک تصویر هدر که در هر صفحه تکرار میشود. بارگذاری و بلافاصله ذخیره کردنش streamهای تصویری تولید کرد که byte-به-byte با ورودی یکسان بودند و مقایسهٔ hash مربوط به stream سند را تغییرنکرده گزارش داد. مقایسهٔ رندرشده اما مخالف بود: هر 35 صفحه در هدر اختلاف پیکسلی نشان میدادند و جای دیگری نه
تصویر هدر از یک فضای رنگ CalRGB رسم میشود که ISO 32000-1 §8.6.5.3 آن را با یک /WhitePoint، یک آرایهٔ اختیاری سه-عنصری /Gamma و یک /Matrix اختیاری نه-عنصری تعریف میکند. این آرایهها در dictionary فضای رنگ شیءهای عددی خالصاند. TPDFNumeric هر یک را فقط بهصورت یک Double ذخیره میکرد و بس، و TPDFNumeric.Output آن Double را از PDFPrecNum عبور میداد که پیشفرضش چهار رقم اعشار است. پس /Gamma از 2.22221 به 2.2222 میرفت، یک درایهٔ matrix از 0.71519 به 0.7152، و renderer با کمال وفاداری رنگهای کمی متفاوتی از کالیبراسیون کمی متفاوت تولید میکرد. byteهای تصویر بیگناه بودند؛ عددهای دور و برشان نه. بخش ناراحتکننده این است که چقدر نامرئی بود. مقایسهٔ byteهای stream decodeشده نمیتواند ببیندش، چون عددها در یک dictionary زندگی میکنند نه در یک stream. مقایسهٔ payloadهای پیوست هم نمیتواند ببیندش. حتی diff بازنگری که در مقالهٔ سطح تغییر توصیف شده روی بدنهٔ نرمالشدهٔ شیء اثر انگشت میگیرد، پس هر دو بازنگری به یک مقدار hash میشوند و diff آن دو را یکسان گزارش میکند. تنها رندر کردن بود که گرفتارش کرد، و همین است که کورپوس پایهای را وامیدارد هر صفحه را رندر کند نه اینکه فقط به بررسیهای ساختاری اعتماد کند
مقداری که parse کردی همان literalی نیست که باید بنویسی
یک عدد حقیقی در PDF یک رشتهٔ اعشاری است و ISO 32000-1 §7.3.3 صریح میگوید فقط یک رشتهٔ اعشاری است: نه نماد radix و نه فرم exponent. بعد Annex C دقتی را فهرست میکند که از یک پیادهسازی انتظار میرود رعایت کند، تقریباً پنج رقم معنادار اعشاری در بخش کسری. پیشفرض دقت خروجی برابر چهار از قبل زیر آن است و نزدیک صفر بدتر هم میشود: PLDoubleToStr مقدار را مقیاس میدهد، به عدد صحیح گرد میکند و اگر حاصل صفر باشد 0 بیرون میدهد؛ پس یک درایهٔ matrix با مقدار -0.000012345 یک رقم از دست نمیدهد، کاملاً ناپدید میشود
بالا بردن پیشفرض فقط پرتگاه را جابهجا میکند. fix این است که دست از این تظاهر برداریم که Double همان عدد است. وقتی tokenizer در TPDFStructure.Decode یک real استاندارد را تشخیص میدهد — یعنی توکنی که یک نقطهٔ اعشار دارد و هیچ نشانگر exponent ندارد — متن مبدأ را در فیلد جدید FOriginalText در کنار مقدار تبدیلشده ذخیره میکند. بعد Output آن متن را ترجیح میدهد و فقط وقتی چیزی برای ترجیح دادن نباشد به قالببندی برمیگردد
// Lib/PDFlibStruct.pas — کل fix در سمت خروجی
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;
دو مرز عمدیاند. عددهای صحیح حفظ نمیشوند، چون قالببندی عدد صحیح از قبل بیاتلاف است. فرمهای exponent مثل 6.02E23 به خاطر تولیدکنندههای خراب در ورودی تحمل میشوند اما در خروجی حفظ نمیشوند، چون نوشتن دوبارهشان یک نحو را که §7.3.3 ممنوع کرده ماندگار میکند؛ آنها مثل هر عدد تولیدشدهٔ دیگری از formatter میگذرند. tokenizer هم پیش از ذخیرهٔ متن، همان ترمیم کمینهٔ همیشگیاش را اعمال میکند، پس یک literal با نقطهٔ ابتدایی مثل .5 بهصورت 0.5 نگه داشته میشود و یک literal با نقطهٔ انتهایی مثل 5. بهصورت 5.0. هر دو برای هر خوانندهای همان عددند و خیلی فراگیرتر پذیرفته میشوند
SetPrecision پس از v3.539.19 چه چیزی را تضمین میکند؟
TPDFlib.SetPrecision حالا تعداد ارقام اعشار عددهایی را کنترل میکند که خودِ کتابخانه تولید میکند: مقدارهایی که از طریق painter رسم میشوند، عددهایی که از یک Double ساخته میشوند مثل از طریق NewNumeric، و هر مقدار parseشدهای که بعداً با SetTo ویرایش شده باشد. حواست باشد که متنی که از طریق object API decode میشود، مثلاً یک literal که به SetObjectFromString داده شده، از همان tokenizer میگذرد و همانطور حفظ میشود. یک اعشار parseشده که هرگز ویرایش نشده، بیتوجه به این تنظیم دقت ورودیاش را نگه میدارد و عوض کردن تنظیم بعد از بارگذاری بهصورت عقبگرد به آن دست نمیزند. مدخل مرجع SetPrecision هم در همان انتشار بهروزرسانی شد تا دقیقاً همین را بگوید، چون متن قبلی طوری بود که نشان میداد این تنظیم به هر عددی در فایل اعمال میشود
پاک شدن در SetTo رخ میدهد، نه با استخراج از پرچم Changed، و همین تفاوت مهم است. خط لولهٔ ذخیره پرچم Changed را روی objectها بهمحض نوشته شدن بازنشانی میکند، پس بررسیای به شکل «متن اصلی را بیرون بده مگر اینکه تغییر کرده باشد» شروع میکرد به بیرون دادن متن کهنه برای مقداری که در همان سشن ویرایش، ذخیره و دوباره ویرایش شده بود. گره زدن متن اصلی به خود انتساب باعث میشود این دو نتوانند با هم اختلاف پیدا کنند. تست رگرسیون هر یک از این رفتارها را با مقدارهای همان فایل اصلی پین میکند
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;
چرا مدل محتوا باز هم عددها را نرمال میکند؟
چون TPDFContentProgram به operandهای عددی canonical قول داده و آن قول از متن موبهمو داخل یک stream محتوا میارزد. مدل محتوای قابلویرایش، همانی که ردیاب حالت گرافیکی رویش ساخته شده، وجود دارد تا NormalizeContentStreams و بهینهساز و Emit از ورودی دلخواه خروجی پایدار و قابلمقایسه بسازند. اگر یک operand parseشده متن اصلیاش را به داخل مدل میبرد، یک توالی عملگر مثل 0.50000 0 0 RG متفاوت از 0.5 0 0 RG بیرون میآمد و هر مقایسهٔ پاییندستی با عادتهای قالببندی تولیدکننده رانش پیدا میکرد
پس مدل متن اصلی را در دو نقطهٔ ورودیاش حذف میکند. NormalizeContentNumbers روی هر operand اجرا میشود وقتی parser آن را push میکند و دوباره داخل SetOperand وقتی منبع تأمینشدهٔ caller decode میشود، و در آرایهها و dictionaryها بازگشتی پیش میرود تا dash patternها و آرایههای TJ و dictionaryهای property مربوط به محتوای علامتخورده را هم پوشش بدهد. صدا زدن SetTo(AsDouble) روی هر عدد کافی است، چون دقیقاً همان عملیاتی است که متن را پاک میکند. دادهٔ خام تصویر inline دستنخورده رها میشود، همانطور که همیشه بود
// Lib/PDFlibContentModel.pas — مدل محتوا قراردادش را نگه میدارد
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;
پس قاعدهٔ عملی برای callerها ساده است. یک LoadFromFile ساده و بعد SaveToFile streamهای محتوای دستنخورده و عددهای dictionary دستنخورده را همانطور که بودند رها میکند. صفحهای که از NormalizeContentStreams بگذرد، یا هر ویرایشی که از طریق مدل محتوا انجام شود، بهطور طراحیشده canonical بیرون میآید و بقیهٔ سند هنوز حفظ میشود. اینها دو درخواست متفاوتاند و حالا دو کار متفاوت میکنند
چه چیزی هزینه دارد و تضمین کجا تمام میشود
هر TPDFNumeric حالا یک ارجاع AnsiString اضافه حمل میکند و هر اعشار parseشده متن مبدأش را تا پایان عمر همان شیء زنده نگه میدارد. روی سندی با میلیونها عدد حقیقی این حافظهٔ واقعی است و جایاش در هر اندازهگیری اسناد بزرگ است، نه اینکه با دست رد شود. تضمین همچنین به سند خودِ آن عدد محدود است: کپی کردن objectها میان سندها یا بازسازی مقدارها از طریق object API عددهای جدید تولید میکند که مثل هر عدد جدید دیگری از دقت خروجی پیروی میکنند. ارزش دارد دقیق باشیم دربارهٔ آنچه این انتشار ادعا میکند و نمیکند. یک بارگذاری-و-ذخیرهٔ سند دستنخورده حالا همان عددهای کالیبراسیونی را حفظ میکند که renderer واقعاً مصرف میکند، که همان خصیصهای است که کورپوس پایهای بررسی میکند. ادعا نمیکند که خروجی byte-به-byte یکسان است، چون آن هم به شمارهگذاری objectها و فشردهسازی stream و شناسهٔ trailer بستگی دارد که در مقالهٔ شناسهٔ قطعی PDF بحث شده. و کاری نمیکند که diff اثر انگشت اختلافهای گرد کردن را در فایلهای تولیدشدهٔ نرمافزارهای دیگر ببیند، چون آنها هنوز بدنهٔ نرمالشده را hash میکنند. این درس خیلی فراتر از CalRGB تعمیم مییابد: وقتی یک parser فقط مقدار تبدیلشده را نگه میدارد، هر ذخیرهای یک ویرایش است و تنها راه متوجه شدن، نگاه کردن به نتیجهٔ رندر است. برخورد با عددها و معناشناسی SetPrecision روی صفحهٔ محصول losLab PDF Developer Library مستند شده است