مقال تقني

Tm الهوية في تدفقات محتوى PDF: إزالة peephole آمنة

تزيل PDF Library for Delphi معامل مصفوفة النص الهوية، 1 0 0 1 0 0 Tm، أثناء تحسين تدفق المحتوى peephole وقت الحفظ فقط عندما تكون مصفوفة النص ومصفوفة سطر النص هوية أصلاً: مباشرة بعد BT، أو مباشرة بعد Tm هوية سابقة. أما cm الهوية فما زالت تُسقط دائماً، لأن cm تضرب في CTM بينما Tm تستبدل مصفوفتي النص كلتيهما قطعاً. ومنذ v3.539.28 تبقى كل Tm هوية أخرى في التدفق

العيب الذي يصلحه هذا من النوع الهادئ. مولد تقارير يطلق BT (Invoice) Tj 1 0 0 1 0 0 Tm (Total) Tj ET، معتمداً على Tm الهوية لتعيد السلسلة الثانية إلى أصل فضاء النص قبل أن يطبق منطق تموضعه الخاص. كان المُحسِّن الأقدم يرى ستة أعداد تهجئ المصفوفة الهوية، فيقرر أن المعامل لا يمكن أن يغيّر شيئاً، ويمحوه. لم يفشل شيء، ولم يسجل شيء تحذيراً، والصفحة المحفوظة رسمت «Total» مباشرة بعد «Invoice» على خط الأساس نفسه، وهي بالضبط فئة العيوب التي لا ينتبه إليها أحد حتى يطبع عميلٌ ملف PDF

لماذا ليست 1 0 0 1 0 0 Tm دائماً no-op؟

Tm الهوية بلا عمل فقط حين تستبدل مصفوفتين تحملان الهوية أصلاً، وهذه خاصية للمعاملات التي قبلها لا لمعاملاتها هي. تقول 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 محذوفة بأمان في أي مكان. داخل كائن نص الصورة مختلفة. ‏Td و TD و T* و Tm غير الهوية كلها تحرك Tlm، وكل معامل إظهار نص (Tj و TJ و ' و ") يقدّم Tm بعرض المحارف التي رسمها. بعد أي منها، Tm الهوية إعادة ضبط حقيقية إلى الأصل. وإن كنت تتبعت مواضع النص يدوياً يوماً مع متتبع حالة CTM ومصفوفة النص لتدفق المحتوى فهذا هو التمييز نفسه بين تضمين الحالة واستبدالها

يعامل PDFlibPas ‏1 0 0 1 0 0 cm و 1 0 0 1 0 0 Tm بمعاملة مختلفة: ‏cm تضرب من اليمين في CTM فتنعدم في أي مكان، بينما 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، وأي معامل لا يعرفه المحلل، أو بداية التدفق
  • تخطو فوقه وتواصل المسح: معاملات لا تلمس Tm أو Tlm أبداً، مثل Tf و Tc ومضبطات الألوان و gs ومعاملات المحتوى الموسوم و cm
يتجول PDFlibPas ‏RemoveIdentityMatrices خلفاً من كل Tm هوية: ‏Tf و Tc ومضبطات الألوان و gs و cm تُخطى، بينما توقف Td و TD و T* و Tm غير الهوية و Tj و TJ ومعاملاً مجهولاً أو ET المسح وتُبقي الـ Tm، وتشهد BT بصحة إسقاطها
و Tm هوية سابقة توقف المسح أيضاً، لأنها أبقيت أو مجدولة للحذف فقد تركت المصفوفتين على الهوية — وفي الحالين لا يحرك المُحسِّن محرفاً أبداً

الحالات المحافظة متعمدة. المعامل المجهول يمكن أن يكون أي شيء، فيرفض المسح أن يستنتج ما بعده. و ET تُغلق كائن النص، فلا BT تشهد لقيم المصفوفة لـ Tm تليها. ويعمل المسح على تدفق محتوى واحد في كل مرة، وهذا مهم للصفحات التي /Contents فيها مصفوفة: طبقة تبدأ في وسط كائن نص، بلا BT خاصة بها، تبقي Tm هويتها حتى لو كانت الطبقة السابقة ستجعلها زائدة عن الحاجة. ذلك يكلف بايتات قليلة على ملفات غريبة ولا يحرك محرفاً أبداً. وإن كنت تحرر نص الصفحة على مستوى التعليمة، كما في جولة تعيين المحارف إلى بايتات المحتوى، فنموذج 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; // تدفق تالف: اترك البايتات وشأنها
    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')

شغّل المساعدة على تدفق الفاتورة من المقدمة تبقى Tm الهوية حية، لأن المسح الخلفي يصطدم بـ Tj قبل أن يبلغ BT. ضع /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، وفقط على تدفقات المحتوى غير المضغوطة بـ Flate أصلاً. ‏TPDFlib.SetOptimizeContentStreams(1) هي الافتراضية، والمفتاح نفسه متاح بحقل OptimizeContentStreams في TPDFlibSaveOptions؛ ويحترمه CompressContent و CompressPage كلاهما. والتدفق الذي /Filter فيه أصلاً /FlateDecode يُتخطى كلياً، فتحميل PDF مضغوط قائم وحفظه من جديد لا يعيد كتابة معاملاته. وإذا أخفق التدفق في التحليل ضغطت البايتات المفكوك الأصلية دون تغيير. ‏TPDFlib.NormalizeContentStreams تحلل المحتوى وتعيد إطلاقه بتباعد وأعداد قياسية لكنها لا تستدعي المُحسِّن أبداً، وهذا يجعلها مرجعاً مفيداً حين تريد أن ترى كم يساهم من فرق حجم قواعد peephole، إلى جانب المكاسب الأكبر المغطاة في تحسين حجم ملف PDF بتجزئة الخطوط

يشغل PDFlibPas مُحسِّن peephole داخل تمريرة الضغط وقت الحفظ فقط: يحترم TPDFPageTree.Compress ‏SetOptimizeContentStreams، والتدفق المفلتر أصلاً بـ /FlateDecode يُتخطى كلياً، والتدفق غير القابل للتحليل يُضغط ببايتاته الأصلية دون تغيير، و NormalizeContentStreams لا تستدعي المُحسِّن إطلاقاً
التدفقات المضغوطة المتخطاة هي الجزء الهادئ: حمّل PDF قائماً واحفظه من جديد وتخرج معاملاته بلا مساس، لأن المُحسِّن لا يعيد كتابة إلا تدفقات فكّها أولاً
var
  Lib: TPDFlib;
  Options: TPDFlibSaveOptions;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('report.pdf', '') <> 1 then
      Exit;
    // التدفقات غير المضغوطة تمر بقواعد peephole ثم Flate
    Lib.SetOptimizeContentStreams(1);
    Lib.CompressContent;
    Lib.SaveToFile('report-optimized.pdf');

    // الخيار نفسه عبر خيارات الحفظ المجمعة؛ ‏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 على المُحسِّن ويجزم أنه لم يبق Tm. وإصدار أقدم كان قد لاحظ أصلاً أن إسقاط Tm هوية غير آمن حين لا تكون Tlm هوية، ثم أبقى السلوك على أي حال لأن الاختبار «أقفله». اقرأه بعناية من جديد لا يقول الاختبار شيئاً عن Tm هوية بعد Td أو بعد نص معروض؛ ومعاملة تغطية عينة واحدة بوصفها عقد القاعدة كلها كان الخطأ الفعلي. الإصلاح يبقي الحالة الأصلية ناجحة ويضيف ست حالات تثبت الصور القابلة للإزالة والمبقية معاً، بما فيها Tm خارج أي كائن نص وأخرى تلي ET

المقايضة سهلة القبول متى كُتبت على الورق. المولدات التي تغلف كل كائن نص بـ BT 1 0 0 1 0 0 Tm ... ما زالت تحصل على إزالة ذلك المعامل الزائد، ومن هناك جاءت تقريباً كل المدخرات. وما يتخلى عنه المُحسِّن هو Tm هوية عابرة في وسط كائن نص، حفنة بايتات لكل صفحة قبل أن تراها Flate أصلاً، مقابل ضمان ينص عليه رأس الوحدة بوضوح: كل تحويل يعادل مخرجاته ولا يغير الصفحة المرئية أبداً. مُحسِّن حجم يحرك النص ليس مُحسِّناً، إنه عيب عرض بنسب ضغط جيدة

محلل تدفق المحتوى ومُحسِّن peephole وخيارات الضغط وقت الحفظ الموصوفة هنا تأتي كلها في PDF Library for Delphi and C++Builder، التي تكشف أيضاً NormalizeContentStreams و CompressContent و TPDFlibSaveOptions لضبط كيفية كتابة كل مستند