مقاله فنی

Tm همانی در content stream های PDF: حذف امن peephole

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 دنبال کرده باشی، این همان تمایز بین ضرب کردن وضعیت و جایگزین کردنش است

PDFlibPas با 1 0 0 1 0 0 cm و 1 0 0 1 0 0 Tm دو جور رفتار می‌کند: cm در CTM از سمت راست ضرب می‌کند و هر جا no-op است، در حالی که Tm هر دو Tm و Tlm را یکجا جایگزین می‌کند و هر Tj ماتریس Tm را به اندازهٔ عرضی که نقاشی کرده جلو می‌برد، پس یک Tm همانی بعد از متن نمایش‌داده‌شده یک ریست واقعی است
گزارش‌ساز به همان ریست تکیه داشت: حذف Tm همانی Total را بلافاصله بعد از Invoice روی همان خط پایه می‌کشید و در راه پرینتر مشتری هیچ چیزی نه شکست می‌خورد، نه لاگ می‌شد و نه هشدار می‌داد

اسکن رو-به-عقب چطور تصمیم می‌گیرد کدام 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
RemoveIdentityMatrices در PDFlibPas از هر Tm همانی رو-به-عقب راه می‌رود: Tf و Tc و تنظیم‌کننده‌های رنگ و gs و cm رد می‌شوند، در حالی که Td و TD و T* و یک Tm غیرهمانی و Tj و TJ یا یک عملگر ناشناخته یا ET اسکن را متوقف و Tm را نگه می‌دارند، و BT برای انداختنش ضامن می‌شود
یک Tm همانی قبلی هم اسکن را متوقف می‌کند، چون چه نگه داشته شود چه از قبل برای حذف برنامه‌ریزی شده باشد هر دو ماتریس را روی همانی گذاشته؛ به هر حال بهینه‌ساز هرگز یک گلایف را جابه‌جا نمی‌کند

حالت‌های محافظه‌کارانه عمدی‌اند. یک عملگر ناشناخته می‌تواند هر چیزی باشد، پس اسکن از عبور کردن از رویش برای استدلال خودداری می‌کند. 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 پوشش داده شده

PDFlibPas بهینه‌ساز peephole را فقط داخل فاز فشرده‌سازی هنگام ذخیره اجرا می‌کند: TPDFPageTree.Compress به SetOptimizeContentStreams توجه می‌کند، stream ای که از قبل با /FlateDecode فیلتر شده کلاً رد می‌شود، stream تجزیه‌ناپذیر با بایت‌های اصلی و بدون تغییرش فشرده می‌شود، و NormalizeContentStreams هرگز اصلاً بهینه‌ساز را صدا نمی‌زند
streamهای فشردهٔ رد شده بخش بی‌صدای ماجراست: یک PDF موجود را بارگذاری کن، دوباره ذخیره کن، و عملگرهایش دست‌نخورده بیرون می‌آیند چون بهینه‌ساز فقط streamهایی را بازنویسی می‌کند که اول خودش دیکود کرده
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 را هم برای تنظیم نحوهٔ نوشتن هر سند در دسترس می‌گذارد