مقال تقني

إعادة كتابة دفقات محتوى PDF بأمان في Delphi

يجتاز HotPDF كل عامل في دفق محتوى صفحة ويسلّم كل واحد منها إلى كودك عبر VisitLoadedPageContentOperators. ولكل عامل تختار إجراءً: الاحتفاظ به، أو حذفه، أو استبداله ببايتاتك الخاصة، أو إيقاف الاجتياز، أو رفض التحرير بأكمله. وعندما يُحتفظ بكل عامل، يكون الدفق المُعاد كتابته مطابقًا بايتًا لبايت للأصل، وهذه هي الخاصية التي تجعل تشغيل الآلية آمنًا على مستندات لم تُنشئها أنت

وتحرير دفقات المحتوى هو حيث تخطئ أدوات PDF عادة. فالدفق تسلسل من العوامل فوق آلة مكدس، لا شجرة مستند، والنهج الساذج المتمثل في تشغيل تعبير نمطي على البايتات المفكوكة ينكسر بمجرد أن تحتوي سلسلة نصية حرفية شيئًا يشبه عاملاً، أو تحتوي صورة مضمّنة بيانات ثنائية عشوائية. أما المُجتاز الذي رمّز الدفق مسبقًا فلا يعاني من هذه المشكلة

رسم تخطيطي لعوامل دفق محتوى PDF مثل BT وTf وTJ وهي تتدفق إلى بيانات حدث المُجتاز مع المرحلة واسم العامل وعدد المُعامِلات ونطاق البايتات وحالة التداخل، ثم تُعيد أحد خمسة إجراءات: الاحتفاظ أو الاستبدال أو الحذف أو التوقف أو الرفض
كل عامل تتم زيارته ينتج قرارًا واحدًا بالضبط، ومرور يحتفظ بكل شيء يكتب النطاقات غير الممسوسة كما هي لمخرجات مطابقة بايتًا لبايت

ما الذي يمكنك فعله فعليًا بالوصول على مستوى العامل؟

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

وتشترك هذه المهام جميعًا في متطلب واحد: يجب أن تكون قادرًا على ترك 99 بالمئة الأخرى من الدفق دون مساس. تُخزّن شجرة نحو HotPDF نسخة واحدة من المصدر بالإضافة إلى نطاقات مدمجة، وتكون التحريرات طبقات فوقية مرتّبة غير متداخلة. وكتابة النتيجة تنسخ النطاقات غير الممسوسة مباشرة وتصدر بايتات الاستبدال فقط حيث غيّرت شيئًا، بحيث لا يمكن لجولة دون تغيير أن تُدخل أي انحراف

عقد المُجتاز

نفّذ الواجهة أو عيّن الحدث، ثم استدعِ طريقة الاجتياز مع ضبط ApplyChanges على false للتدقيق أو true لإعادة الكتابة. وتحمل بيانات الحدث المرحلة، واسم العامل، وعدد مُعامِلاته، ونطاق بايتاته، وحالة التداخل الحالية، وعرضًا مُستعارًا لقراءة المُعامِلات:

uses
  HPDFDoc, HPDFContentStream;

procedure TAuditor.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
var
  Bytes: TBytes;
begin
  if EventData.Phase <> covpOperator then
    Exit;

  // عدّ عوامل عرض النص المرسومة داخل محتوى موسوم
  if (EventData.OperatorName = 'Tj') or
     (EventData.OperatorName = 'TJ') then
  begin
    Inc(FTextOperators);
    if EventData.State.MarkedContentDepth > 0 then
      Inc(FTextInsideMarkedContent);
  end;

  // اقرأ معاملاً فقط عند الحاجة إليه
  if EventData.OperatorName = 'Do' then
    if EventData.View.GetOperandBytes(0, Bytes) then
      NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));

  EventData.Action := coaKeep;
end;

procedure TAuditor.Run(const FileName: string);
var
  Pdf: THotPDF;
  Stats: THPDFContentOperatorVisitStatistics;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) <= 0 then
      Exit;
    Pdf.OnContentOperator := HandleOperator;
    for I := 0 to Pdf.LoadedPageCount - 1 do
      Pdf.VisitLoadedPageContentOperators(I, False, Stats);   // تدقيق فقط
    Writeln(Format('%d operators visited, max q/Q depth %d',
      [Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
  finally
    Pdf.Free;
  end;
end;

لماذا يكون العرض مُستعارًا لا قابلاً للتغيير؟

لا يستقبل رد النداء أبدًا شجرة نحو قابلة للتعديل. بل يستقبل عرضًا يمكنه جلب بايتات العملية الحالية أو مُعامِل محدد عند الطلب، وهذا كل شيء. ويمر كل تغيير عبر حقلي الإجراء والاستبدال بدلاً من ذلك

وهذا التوسط ليس شكليًا. فالتعديل المباشر للشجرة كان سيسمح لمُجتاز بتجاوز ميزانية الاستبدال وفحص التحريرات المتداخلة، والتحريرات المتداخلة في نموذج طبقات فوقية بنطاقات بايتات تنتج مخرجات خاطئة بصمت بدلاً من معطوبة بوضوح. وتوجيه التغييرات عبر حقل الإجراء يعني أن كل تحرير يُتحقق منه بالطريقة نفسها بصرف النظر عما قصده المُجتاز

والقراءة عند الطلب تُبقي الحالة الشائعة رخيصة أيضًا. فتدقيق يعدّ فقط أسماء العوامل لا يُجسِّد بايتات المُعامِلات إطلاقًا، وهذا مهم في دفق محتوى بمئات آلاف العمليات

حقل الحالة يصف المدخلات، دائمًا

يحمل كل حدث عمق حالة الرسوميات، وعمق كائن النص، وعمق المحتوى الموسوم، وعمق قسم التوافق كما كانت عليه في المدخلات الأصلية مباشرة قبل تنفيذ ذلك العامل. وهذا صحيح حتى لو حذف مُجتازك أو استبدل عوامل سابقة

وربط الحالة بالمدخلات الأصلية بدلاً من المخرجات المُحررة قرار مقصود له عائد عملي: تشغيل التدقيق نفسه مرتين على المستند نفسه ينتج نتائج متطابقة، وتدقيق يعمل جنبًا إلى جنب مع تحريرات لا يُزحزح إطار مرجعيته الخاص. وإذا احتجت إلى معرفة العمق في المخرجات، فأنت تعرف ما حذفته، فيمكنك حسابه؛ أما العكس، أي إعادة بناء مواضع مدخلات ثابتة من مخرجات متحركة، فغير ممكن

الحذف والاستبدال، بالقواعد التي تحميك

الاستبدال هو بايتات دفق محتوى خام، وأنت المسؤول عن صحة صياغتها. والضمانات المحيطة بها هي ميزانية إجمالي بايتات الاستبدال، وشرط عدم التداخل، والتحقق الاختياري من المخرجات الذي يعيد تحليل النتيجة قبل قبولها:

ضمانات إعادة الكتابة في Delphi: مُجتاز يُعلن نية الإجراء والاستبدال تُوجَّه عبر ميزانية الاستبدال، ونطاق بايتات غير متداخل، وضمانات التحقق من المخرجات إلى الصفحة المحفوظة المُتحقق منها
كل تغيير يمر عبر حقل الإجراء إلى ضمانات الميزانية والتداخل وإعادة التحليل، بحيث يُلغي فشل التحقق العملية بدلاً من كتابة عوامل معطوبة
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
  var EventData: THPDFContentOperatorEventData);
begin
  EventData.Action := coaKeep;
  if EventData.Phase <> covpOperator then
    Exit;

  // أسقط العوامل التي ترسم نموذج العلامة المائية
  if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
    EventData.Action := coaDelete
  // وحّد لونًا شاردًا إلى الرمادي الرسمي للمؤسسة
  else if EventData.OperatorName = 'rg' then
  begin
    EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
    EventData.Action := coaReplace;
  end
  // ارفض التحرير بأكمله إذا صادفنا شيئًا لا نفهمه
  else if EventData.OperatorName = 'BI' then
    EventData.Action := coaReject;
end;

var
  Options: THPDFContentOperatorVisitOptions;
begin
  Options := THPDFContentOperatorVisitOptions.Default;
  Options.ValidateOutput := True;        // إعادة التحليل قبل القبول
  Options.MaxReplacementBytes := 64 * 1024;

  Pdf.OnContentOperator := HandleOperator;
  if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
    if Stats.Modified then
      Pdf.SaveLoadedDocument('cleaned.pdf');
end;

ومجموعة الإجراءات تتضمن مخرجين لسبب وجيه. coaStop ينهي الاجتياز ويحتفظ بما تقرر حتى الآن، وهذا صحيح عندما تكون قد وجدت ما كنت تبحث عنه. وcoaReject يتخلى عن الزيارة بأكملها ويترك الصفحة دون مساس، وهذا صحيح عندما تصادف بنية لا تغطيها قواعد إعادة الكتابة لديك. والصور المضمّنة هي المثال الكلاسيكي: فحمولتها الثنائية ليست شيئًا ينبغي لمُجتاز يوحّد الألوان قياسيًا أن يحرر حولها

قراءة الإحصاءات، وأين يتوقف الاجتياز

يميّز حقل الحالة بين الاكتمال، والتوقف، والرفض، والمدخلات غير الصالحة، وتجاوز حد، والمخرجات غير الصالحة، وخطأ في المُجتاز. وهذه مشكلات مختلفة: فالمخرجات غير الصالحة تعني أن بايتات الاستبدال الخاصة بك لم تُعِد التحليل، بينما المدخلات غير الصالحة تعني أن دفق المصدر كان مشوّهًا بالفعل ولم يُحاول أي تحرير

وتجعل العدادات التدقيق ذاتي التحقق. فمقارنة VisitedOperatorCount بـ KeptOperatorCount و DeletedOperatorCount و ReplacedOperatorCount يجب أن تتوافق مجموعها، وأرقام أقصى عمق تخبرك ما إذا كانت الصفحة متداخلة بعمق يستحق نظرة أقرب. ومقارنة SourceBytes بـ OutputBytes تعطيك أثر الحجم لتحريرك في رقم واحد

رسم تخطيطي لتسوية إحصاءات التدقيق حيث يجمع عدد العوامل المُحتفَظ بها والمحذوفة والمُستبدَلة إلى عدد العوامل المُزارة، إلى جانب سبع حالات ختامية من الاكتمال إلى خطأ المُجتاز، مع ملاحظة أن أعماق الحالة تصف المدخلات الأصلية بحيث تتكرر عمليات التدقيق بشكل متطابق
عدادات يجب أن تتوافق مجاميعها إضافة إلى حالة تسمي كل نهاية ممكنة تجعل التدقيق ذاتي التحقق على مستندات لم تُنشئها أنت

وللمهام الشائعة توجد مسارات أعلى مستوى لا تتطلب كتابة مُجتاز إطلاقًا. إزالة المحتوى نهائيًا، مع تنظيف الموارد الذي يحتاجه التنقيح الحقيقي، موضحة في التنقيح ودمج الصفحات على المستندات المحمّلة. واستبدال النص المرئي موضح في البحث والاستبدال في ملف PDF موجود. اِلجأ إلى مُجتاز العوامل عندما تكون قاعدتك بنيوية لا نصية، أو عندما تحتاج تدقيقًا لا يجيب عنه أي تقرير موجود

يعمل اجتياز دفقات المحتوى واستخراجها وإعادة كتابتها على نموذج المستند المحمّل نفسه في Delphi و C++Builder؛ وتجد قائمة الميزات الكاملة على صفحة مكوّن HotPDF لـ PDF في Delphi