PDF Library for Delphi عملگر ماتریس متن همانی، یعنی 1 0 0 1 0 0 Tm، را در بهینهسازی peephole مربوط به content stream هنگام ذخیره فقط وقتی حذف میکند که text matrix و text line matrix از قبل همانی باشند: درست بعد از BT، یا درست بعد از یک Tm همانیِ قبلی. یک cm همانی همچنان همیشه حذف میشود، چون cm در CTM ضرب میکند در حالی که Tm هر دو ماتریس متن را یکجا جایگزین میکند. از v3.539.28 هر Tm همانی دیگری در stream میماند
باگی که این تعمیر میکند از جنس بیصداست. یک گزارشساز BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET تولید میکند و به Tm همانی تکیه دارد که رشتهٔ دوم را قبل از اعمال منطق موقعیتدهی خودش به مبدأ text space برگرداند. بهینهساز قدیمی شش عددی را میدید که ماتریس همانی را املا میکنند، تصمیم میگرفت این عملگر به هیچ وجه نمیتواند چیزی را عوض کند و آن را حذف میکرد. هیچ چیزی شکست نمیخورد، هیچ چیزی هشدار لاگ نمیکرد، و صفحهٔ ذخیرهشده «Total» را بلافاصله بعد از «Invoice» روی همان خط پایه میکشید، که دقیقاً همان ردهٔ نقصی است که هیچکس تا وقتی مشتری PDF را چاپ نکرده متوجهش نمیشود
چرا 1 0 0 1 0 0 Tm همیشه no-op نیست؟
یک Tm همانی فقط وقتی no-op است که بیاید دو ماتریسی را جایگزین کند که از قبل همانیاند، و این خاصیتِ عملگرهای قبل از آن است، نه عملوندهای خودش. ISO 32000-1 §9.4.1 میگوید BT هر دو ماتریس متن (Tm) و ماتریس خط متن (Tlm) را به همانی مقداردهی اولیه میکند، و §9.4.2 Tm را اینطور تعریف میکند که هر دو را روی مقادیر دادهشده میگذارد، نه اینکه به آنها ضرب کند. آن را با cm (§8.4.4) مقایسه کن که در ماتریس تبدیل جاری از سمت راست ضرب میکند: ضرب در همانی هر CTM را دستنخورده میگذارد، پس 1 0 0 1 0 0 cm هر جا حذفش امن است. داخل یک text object اوضاع فرق میکند. Td و TD و T* و یک Tm غیرهمانی همه Tlm را جابهجا میکنند، و هر عملگر نمایش متن (Tj، TJ، '، ") Tm را به اندازهٔ عرض گلایفهایی که نقاشی کرده جلو میبرد. بعد از هر کدام از اینها، یک Tm همانی یک ریست واقعی به مبدأ است. اگر تا حالا موقعیتهای متن را دستی با ردیاب وضعیت CTM و ماتریس متن content stream دنبال کرده باشی، این همان تمایز بین ضرب کردن وضعیت و جایگزین کردنش است
اسکن رو-به-عقب چطور تصمیم میگیرد کدام Tm همانی را بیندازد
TPDFContentPeepholeOptimizer.RemoveIdentityMatrices حالا از هر Tm همانی رو-به-عقب راه میرود و فقط وقتی آن را میاندازد که اسکن اول به BT یا یک Tm همانی دیگر برسد. یک Tm همانیِ قبلی مهم است چه نگه داشته شده باشد چه خودش تازه برای حذف برنامهریزی شده باشد، چون به هر حال هر دو ماتریس را روی همانی گذاشته، دقیقاً مثل کاری که BT میکند. قاعده هر عملگری را که میتواند ببیند به یکی از دو گروه تقسیم میکند:
- ایست کن و Tm را نگه دار:
Td،TD،T*، یکTmغیرهمانی،Tj،TJ،'،"،ET، هر عملگری که parser نمیشناسد، یا شروع stream - رد شو و به اسکن ادامه بده: عملگرهایی که هرگز Tm یا Tlm را لمس نمیکنند، مثل
Tf،Tc، تنظیمکنندههای رنگ،gs، عملگرهای marked-content وcm
حالتهای محافظهکارانه عمدیاند. یک عملگر ناشناخته میتواند هر چیزی باشد، پس اسکن از عبور کردن از رویش برای استدلال خودداری میکند. ET text object را میبندد، پس یک Tm بعد از آن هیچ BT ای ندارد که برای مقادیر ماتریس ضامن باشد. اسکن همچنین هر بار روی یک content stream کار میکند، که برای صفحاتی که /Contents شان یک آرایه است مهم میشود: لایهای که وسط یک text object شروع میشود، بدون BT مخصوص خودش، Tm همانی اش را نگه میدارد حتی وقتی لایهٔ قبلی آن را زائد میکرد. این روی فایلهای عجیب چند بایت هزینه دارد و هرگز گلایفی را جابهجا نمیکند. اگر متن صفحه را در سطح دستورالعمل ویرایش میکنی، مثل آموزش گامبهگام نگاشت نویسه به بایت content، همان مدل TPDFContentProgram تجزیهشده است که بهینهساز بازنویسی میکند
uses
PDFlibContentModel, PDFlibContentOptimize;
function OptimizeSnippet(const Source: AnsiString): AnsiString;
var
Prog: TPDFContentProgram;
Optimizer: TPDFContentPeepholeOptimizer;
begin
Result := Source;
Prog := TPDFContentProgram.Create;
try
if not Prog.Parse(Source) then
Exit; // stream آسیبدیده: بایتها را دست نزن
Optimizer := TPDFContentPeepholeOptimizer.Create(Prog);
try
Optimizer.Run; // تعداد دستورالعملهای حذفشده را برمیگرداند
finally
Optimizer.Free;
end;
Result := Prog.Emit; // هر دستورالعمل یک خط
finally
Prog.Free;
end;
end;
// حذف شده: Tm درست بعد از BT، دومی از دو Tm همانی پشت سر هم
// OptimizeSnippet('BT /F1 12 Tf 1 0 0 1 0 0 Tm (hello) Tj ET')
// نگه داشته شده: Tm بعد از Td، بعد از Tj، بعد از یک Tm غیرهمانی، یا بیرون BT
// OptimizeSnippet('BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET')
هلپر را روی stream صورتحساب از ابتدای مقاله اجرا کن و Tm همانی جان سالم به در میبرد، چون اسکن رو-به-عقب قبل از رسیدن به BT به Tj میخورد. /F1 12 Tf و 2 Tc و 0 g را بین BT و Tm همانی بگذار و آن هنوز میرود، چون هیچکدام از اینها ماتریسهای متن را لمس نمیکنند. دنبالهای مثل BT 10 20 Td 1 0 0 1 0 0 Tm 1 0 0 1 0 0 Tm دقیقاً یک عملگر از دست میدهد: Tm همانی اول ماتریسی را که Td جابهجا کرده ریست میکند و فقط دومی زائد است
بهینهساز peephole واقعاً چه موقع اجرا میشود؟
بهینهساز فقط در فاز فشردهسازی اجرا میشود، داخل TPDFPageTree.Compress، و فقط روی content streamهایی که از قبل با Flate فشرده نشدهاند. TPDFlib.SetOptimizeContentStreams(1) پیشفرض است و همان کلید بهشکل فیلد OptimizeContentStreams از TPDFlibSaveOptions هم در دسترس است؛ هم CompressContent و هم CompressPage به آن توجه میکنند. stream ای که /Filter اش از قبل /FlateDecode است کلاً رد میشود، پس بارگذاری یک PDF فشردهٔ موجود و ذخیرهٔ دوبارهاش عملگرهایش را بازنویسی نمیکند. اگر stream در تجزیه شکست بخورد، بایتهای دیکود شدهٔ اصلی بدون تغییر فشرده میشوند. TPDFlib.NormalizeContentStreams محتوا را با فاصلهگذاری و اعداد کانونی تجزیه و دوباره تولید میکند اما هرگز بهینهساز را صدا نمیزند، که آن را به یک baseline مفید تبدیل میکند وقتی میخواهی ببینی قواعد peephole چقدر در تفاوت اندازه سهم دارند، کنار صرفههای بزرگتری که در بهینهسازی اندازه فایل PDF با font subsetting پوشش داده شده
var
Lib: TPDFlib;
Options: TPDFlibSaveOptions;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') <> 1 then
Exit;
// streamهای فشردهنشده از قواعد peephole میگذرند، بعد Flate
Lib.SetOptimizeContentStreams(1);
Lib.CompressContent;
Lib.SaveToFile('report-optimized.pdf');
// همان انتخاب از طریق save options همراه کتابخانه؛ False انصراف است
Options.CompressContent := True;
Options.CompressFonts := True;
Options.CompressImages := True;
Options.Linearize := False;
Options.KeepModDate := False;
Options.OptimizeContentStreams := False;
Options.GarbageCollect := False;
Options.PackObjectStreams := True;
Lib.SaveToFileOptions('report-plain.pdf', Options);
finally
Lib.Free;
end;
end;
تست رگرسیون قدیمی واقعاً چه چیزی را تضمین میکرد؟
تست رگرسیون قدیمی فقط یک شکل را تضمین میکرد: یک Tm همانی درست بعد از BT حذف میشود. Peephole_RemovesIdentityTextMatrix عبارت BT 1 0 0 1 0 0 Tm (hello) Tj ET را به بهینهساز میدهد و assertion میگذارد که هیچ Tm ای باقی نمیماند. یک انتشار قبلی قبلاً نوشته بود که انداختن یک Tm همانی وقتی Tlm همانی نیست ناامن است، و بعد همچنان همان رفتار را نگه داشته بود چون تست آن را «قفل» کرده بود. اگر با دقت دوباره بخوانی، تست هیچ چیزی دربارهٔ یک Tm همانی بعد از Td یا بعد از متن نمایشدادهشده نمیگوید؛ برداشتن پوشش یک نمونه بهعنوان قرارداد کل قاعده همان اشتباه واقعی بود. fix آن حالت اصلی را همچنان سبز نگه میدارد و شش حالت اضافه میکند که هم شکلهای قابلحذف و هم شکلهای نگهداشتهشده را ثابت میکنند، از جمله یک Tm بیرون از هر text object و یکی که بعد از ET میآید
موازنهاش وقتی نوشته شود بهراحتی قابل قبول است. تولیدکنندههایی که هر text object را بهشکل BT 1 0 0 1 0 0 Tm ... میپیچند هنوز همان عملگر زائدشان را حذفشده میگیرند، که تقریباً همهٔ صرفهجویی از آنجا میآمد. چیزی که بهینهساز از دست میدهد یک Tm همانیِ گاهبهگاه وسط یک text object است، چند بایت در هر صفحه قبل از اینکه حتی Flate آنها را ببیند، در ازای تضمینی که هدر ماژول صریح بیانش میکند: هر transform معادل خروجی است و هرگز صفحهٔ قابلمشاهده را عوض نمیکند. بهینهساز اندازهای که متن را جابهجا کند یک بهینهساز نیست، یک باگ رندر با نسبت فشردهسازی خوب است
parser مربوط به content stream، بهینهساز peephole و گزینههای فشردهسازی هنگام ذخیره که اینجا توصیف شدند همه در PDF Library for Delphi and C++Builder عرضه میشوند، که NormalizeContentStreams و CompressContent و TPDFlibSaveOptions را هم برای تنظیم نحوهٔ نوشتن هر سند در دسترس میگذارد