مقال تقني

التحرير الحقيقي لملفات PDF في Delphi: الحذف والتطهير

التحرير (Redaction) الحقيقي لملفات PDF في Delphi يعني حذف البايتات الأساسية، وليس الطلاء فوقها؛ وترسم مكتبة losLab PDF Library هذا الخط بواجهتي برمجة تطبيقات: حيث تقوم RedactRegion بتحرير تدفق محتوى الصفحة بحيث تختفي النصوص والرسومات المتجهية والصور التي تمت إزالتها ماديًا، بينما تقوم SanitizeDocument بمسح البيانات التعريفية والبرامج النصية والإجراءات التي قد تختبئ فيها المعلومات المحررة أيضًا؛ وإذا أخطأت في أي من النصفين، فستقوم بشحن ملف يبدو خاضعًا للرقابة على الشاشة ولكنه يعيد تسليم السر لأي شخص ينسخ النص أو يفتح خصائص المستند

إن وضع الفشل هذا مسألة معروفة للعامة؛ فقد أصدرت الصحف والمحاكم والوكالات الحكومية جميعها ملفات PDF تحتوي على مستطيل أسود موضوع فوق اسم أو رقم، فقط ليقوم القارئ بتحديد النص الموجود تحته ونسخه ولصق السلسلة النصية التي يُفترض أنها خاضعة للرقابة في عرض واضح؛ حيث كان المستطيل عبارة عن رسمة فقط، بينما كان النص لا يزال هناك، يقبع في تدفق المحتوى على بعد طبقة واحدة أسفل الطلاء، سليمًا تمامًا وقابلاً للتحديد بالكامل؛ وتعامل مكتبة losLab PDF Library، محرك الـ PDF لـ Delphi و C++Builder والمعروض من خلال الفئة TPDFlib، هذا التمييز باعتباره الهدف الكامل من التحرير وليس مجرد فكرة لاحقة

لماذا لا يعتبر رسم مربع أسود فوق نص PDF تحريرًا حقيقيًا؟

تعرض مكتبة losLab PDF Library الدالة MaskRect للحالات التي يكون فيها التغطية هو ما تريده حقًا: علامة مائية، تعتيم بصري، أو ختم إثبات؛ وتقوم MaskRect(Page, Left, Top, Width, Height, Red, Green, Blue) بإضافة مستطيل إلى تدفق محتوى الصفحة وتلوينه باستخدام معامل بناء المسار re متبوعًا بمعامل التعبئة f (المواصفة ISO 32000-1 الفقرة 8.5)؛ وتبقى كل بايت كانت موجودة بالفعل على الصفحة — كحروف الاسم الذي قمت بتغطيته، أو الضربات المتجهية، أو الصورة المدمجة — كما هي تمامًا، في مكانها السابق بتعليمات واحدة في نفس التدفق؛ ويرسم عارض متوافق المربع الأسود أخيرًا، بحيث يفوز بصريًا، والتغطية لا تغير شيئًا تحته

ويمكن لأي شخص استرداد المحتوى الأصلي بثلاث طرق: تحديد النص ونسخه مباشرة من خلال المربع المعتم، أو تشغيل مستخرج نصوص على الصفحة، أو حذف مستطيل التغطية من تدفق المحتوى؛ وتعد الدالة MaskRect صادقة بشأن هذا في اسمها نفسه — فهي تقنع (masks) ولا تزيل؛ والنتيجة هي ملصق فوق كلمة لا يزال بإمكانك قراءتها عن طريق إزالة الملصق

كيف تقوم RedactRegion بحذف المحتوى على مستوى التعليمات

تعمل الدالة RedactRegion(Page, Left, Top, Width, Height, Options) أسفل الطلاء بطبقة واحدة؛ حيث تقوم مكتبة losLab PDF Library بتحليل كل طبقة محتوى للصفحة إلى TPDFContentProgram، وتمر عبر قائمة التعليمات باستخدام محلل تتبع CTM (مثل FindInstructionsInRect في نموذج المحتوى)، وتحدد كل معامل عرض نص، ورسم مسار، ومعامل صورة Do تتقاطع مساحته المرسومة مع مستطيلك؛ وتتم إزالة هذه التعليمات باستخدام TPDFContentProgram.Delete، وتتم إعادة كتابة الطبقة المحررة باستخدام WritePageContentLayer؛ ولا تنزلق البايتات خلف غطاء — بل تغادر التدفق نهائيًا؛ وإذا تم تشغيل مستخرج نصوص بعد ذلك فلن يجد شيئًا، لأنه لا يوجد شيء للبحث عنه

هناك تفصيلان للتنفيذ يستحقان الاهتمام؛ حيث تأخذ RedactRegion مستطيلًا من أعلى اليسار في نقطة الأصل والوحدات الحالية وتحوله داخليًا إلى مساحة مستخدم أسفل اليسار التي يستخدمها تدفق المحتوى، بحيث تحدد المنطقة بالطريقة التي تراها بها على الصفحة؛ وعندما يغطي مستطيل عدة تعليمات، تقوم RedactRegion بحذفها بترتيب تنازلي للفهرس، لأن إزالة التعليمات ٤ قبل التعليمات ٧ سيؤدي إلى إزاحة كل فهرس لاحق وتخريب النطاق؛ وإذا قمت بتمرير Options = 0، تقوم مكتبة losLab PDF Library أيضا بوضع مربع أسود مصمت فوق المنطقة التي تم مسحها كعلامة مرئية — ولكن هذه العلامة هي تجميلية فقط، وتُطبق بعد أن يختفي المحتوى الحقيقي بالفعل

وفي الممارسة العملية، نادرًا ما تقوم بكتابة إحداثيات المستطيل بشكل ثابت؛ بل تقوم بالبحث في الصفحة عن السلسلة النصية الحساسة وتقرأ صندوقها المحيط (bounding box) — وهو نفس البحث في الصفحة وتعداد العناصر المغطى في تحديد موقع النص وعناصر الصفحة في Delphi — ثم تسلم هذا المستطيل إلى RedactRegion:

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('contract.pdf', '') = 1 then
    begin
      // حذف كل شيء مرسوم داخل صندوق ٢٢٠ × ١٤ نقطة في الصفحة ١.
      // الإحداثيات هي أعلى اليسار، بنقطة الأصل والوحدات الحالية.
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);   // Options=0 يضع علامة سوداء
      PDF.SaveToFile('contract-redacted.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

أين لا تزال المعلومات المحررة تختبئ في ملف PDF؟

التحرير الذي يتوقف عند الطبقة المرئية يترك أثرًا خلفه، لأن ملف PDF يخزن المعلومات في أماكن لا ينظر إليها القارئ أبدًا؛ وتصنف مكتبة losLab PDF Library التسريبات إلى قنوات تستحق الذكر؛ حيث يحمل قاموس معلومات المستند (Document Information Dictionary) ودفق بيانات XMP التعريفية بشكل روتيني سلاسل نصية للمؤلف (Author) أو المنتج (Producer) أو الكلمات المفتاحية (Keywords) التي تسمي الشخص أو الحالة التي قمت بإزالتها من المتن؛ ويمكن لجافا سكريبت على مستوى المستند و OpenAction الخاص بالكتالوج أن يتم تنفيذهما عند الفتح والوصول إلى بيانات اعتقدت أنها اختفت؛ ويحمل كل تعليق توضيحي إجراء /A اختياريًا، ويمكن للتعليق التوضيحي نفسه — ملاحظة لاصقة، حقل نموذج قديم، أو رابط — أن يقبع مباشرة فوق البقعة المحررة مع نسخته الخاصة من النص؛ ولا يتأثر أي من هذا بالتشفير؛ فالملف المشفر الذي يحتوي على حزمة XMP بنص عادي لا يزال يسرب عنوانه إلى أي مفهرس، وهي ثغرة تستحق تدقيق التشفير والأذونات بمفردها؛ ويجب أن يزور التطهير الدفاعي كل من هذه الأماكن المختبئة، وليس فقط الصفحة التي كنت تنظر إليها

تنظيف المستند باستخدام SanitizeDocument

تجيب مكتبة losLab PDF Library على هذا الانتشار للتسريبات باستخدام SanitizeDocument(Flags)، وهو استدعاء تنظيم يقوم بتشغيل مجموعة من عمليات التجريد المستقلة المحددة بواسطة قناع البت (bitmask) ويرجع عدد العمليات التي طبقها؛ وتكون العلامات (flags) قابلة للجمع: حيث يقوم SANITIZE_JAVASCRIPT بإزالة كل حزمة جافا سكريبت على مستوى المستند وأي نص برمجى لـ OpenAction؛ ويقوم SANITIZE_ACTIONS بتجريد قاموس إجراء /A من كل تعليق توضيحي، من خلال TPDFAnnots.RemoveAnnotAction الجديد، ويمسح OpenAction الخاص بالكتالوج؛ ويقوم SANITIZE_METADATA بمسح الكلمات المفتاحية والمنشئ والمنتج والمؤلف والموضوع في كل من قاموس معلومات المستند (المواصفة ISO 32000-1 الفقرة 14.3.3) ودفق بيانات XMP التعريفية (الفقرة 14.3.2)؛ ويقوم SANITIZE_ANNOTATIONS بحذف كل تعليق توضيحي على مستوى المستند بأكمله؛ ويقوم SANITIZE_OPEN_ACTION بإزالة إدخال /OpenAction للكتالوج؛ ويعد SANITIZE_ALL هو الاتحاد الثنائي لجميع العناصر الخمسة

وقوانين الفهرس داخل عمليات التجريد هذه هي بالضبط نوع الأشياء التي تعطل الحلقة المصنوعة يدويًا بصمت، ولهذا السبب توجد SanitizeDocument كاستدعاء واحد؛ حيث يبدأ GlobalJavaScriptPackageName من الصفر، وتقوم RemoveGlobalJavaScript بتقليص القائمة أثناء سيرها، لذا فإن تجريد جافا سكريبت يقرأ دائمًا فهرس الحزمة 0 ويزيل حتى يصل العدد إلى الصفر؛ وبالمقابل، يبدأ DeleteAnnotation من 1؛ ويقدم AnnotationCount تقريرًا عن الصفحة المحددة حاليًا، لذا فإن عمليات تجريد التعليقات التوضيحية تحدد كل صفحة بدورها قبل العد؛ وتحصل مكتبة losLab PDF Library على هذه الحدود بشكل صحيح داخليًا، لذا يمكنك تمرير قناع علامة واحدة وقراءة عدد مرتجع واحد بدلاً من إعادة إنتاج خمس قواعد تكرار مختلفة

var
  Applied: Integer;
begin
  // تجريد البيانات التعريفية والبرامج النصية والإجراءات مع الاحتفاظ بالتعليقات التوضيحية:
  Applied := PDF.SanitizeDocument(SANITIZE_METADATA or SANITIZE_JAVASCRIPT or
    SANITIZE_ACTIONS or SANITIZE_OPEN_ACTION);

  // أو مسح كل قناة مخفية، بما في ذلك التعليقات التوضيحية:
  Applied := PDF.SanitizeDocument(SANITIZE_ALL);
end;

سير عمل التحرير والتطهير الكامل

اجمع الاثنين معًا لتصبح العملية الكاملة قصيرة؛ حيث تقوم مكتبة losLab PDF Library بإزالة المحتوى المرئي باستخدام RedactRegion، صفحة تلو الأخرى، ثم تمسح كل قناة مخفية باستخدام SanitizeDocument(SANITIZE_ALL) قبل كتابة الملف؛ ونظرًا لأن التحرير يتم تشغيله دائمًا تقريبًا على مستندات وصلت من خارج نطاق سيطرتك، فمن المفيد تحميلها من خلال مسار محصن — وهو نفس الموقف الدفاعي الموضح في تحليل ملفات PDF غير الموثوقة بأمان — بحيث يفشل الإدخال المشوه بشكل نظيف بدلاً من الفشل في منتصف عملية التحرير

var
  PDF: TPDFlib;
  Applied: Integer;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile('incoming.pdf', '') = 1 then
    begin
      PDF.RedactRegion(1, 90, 705, 220, 14, 0);       // حذف الخط الحساس
      Applied := PDF.SanitizeDocument(SANITIZE_ALL);   // مسح البيانات التعريفية، وجافا سكريبت، والإجراءات، والتعليقات التوضيحية
      PDF.SaveToFile('published.pdf');
    end;
  finally
    PDF.Free;
  end;
end;

أين يتوقف تحرير تدفق المحتوى

تعمل RedactRegion على تدفق المحتوى، وهو بالضبط المكان الذي يعيش فيه النص الرقمي الأصلي وحيث لا يتواجد النص الممسوح ضوئيًا؛ فعندما تكون الصفحة عبارة عن صورة ممسوحة ضوئيًا، تكون الكلمات عبارة عن بكسلات داخل كائن صورة XObject، وليست معاملات نصية، ولا يمكن لمكتبة losLab PDF Library نحت اسم من منتصف مسح رسومي (raster) بنفس الطريقة التي تحذف بها حرفًا؛ وتعامله مع الصور متحفظ عن قصد: حيث تتم إزالة أي صورة يتقاطع موضعها مع المستطيل بأكملها، لذا فإن صندوق التحرير في صفحة ممسوحة ضوئيًا يأخذ المسح بأكمله معه بدلاً من جزء منه؛ وتحرير منطقة داخل صورة يعني تنقيطها وطلائها وإعادة دمجها على مستوى البكسل، وهي عملية مختلفة عن الحذف من تدفق المحتوى; وينطبق نفس الحذر على طبقة نص OCR فوق صفحة ممسوحة ضوئيًا: حيث تزيل RedactRegion معاملات النص التي يمكنها رؤيتها، لكن الصورة تحتها تبقى دون مساس ما لم يتقاطع كائن الصورة XObject الخاص بها مع الصندوق أيضًا

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

الانضباط الذي يختم كل سير عمل للتحرير هو التحقق، وليس الثقة؛ فقم بتحرير المنطقة، وتطهير المستند، وحفظه في ملف جديد، وإعادة فتحه، ومحاولة استرداد كل من النص الذي أزلته والبيانات التعريفية التي مسحتها؛ وعندما يعود كلاهما فارغين، يكون التحرير قد نجح؛ وتشحن واجهتا برمجة التطبيقات RedactRegion و SanitizeDocument الموضحتان هنا مع مكتبة losLab PDF Library لـ Delphi و C++Builder، إلى جانب مرجع نموذج المحتوى والتطهير الكامل