يجتاز HotPDF كل عامل في دفق محتوى صفحة ويسلّم كل واحد منها إلى كودك عبر VisitLoadedPageContentOperators. ولكل عامل تختار إجراءً: الاحتفاظ به، أو حذفه، أو استبداله ببايتاتك الخاصة، أو إيقاف الاجتياز، أو رفض التحرير بأكمله. وعندما يُحتفظ بكل عامل، يكون الدفق المُعاد كتابته مطابقًا بايتًا لبايت للأصل، وهذه هي الخاصية التي تجعل تشغيل الآلية آمنًا على مستندات لم تُنشئها أنت
وتحرير دفقات المحتوى هو حيث تخطئ أدوات PDF عادة. فالدفق تسلسل من العوامل فوق آلة مكدس، لا شجرة مستند، والنهج الساذج المتمثل في تشغيل تعبير نمطي على البايتات المفكوكة ينكسر بمجرد أن تحتوي سلسلة نصية حرفية شيئًا يشبه عاملاً، أو تحتوي صورة مضمّنة بيانات ثنائية عشوائية. أما المُجتاز الذي رمّز الدفق مسبقًا فلا يعاني من هذه المشكلة
ما الذي يمكنك فعله فعليًا بالوصول على مستوى العامل؟
ثلاث فئات من المهام. التدقيق: عدّ عدد المرات التي تضبط فيها الصفحة لونًا خارج مجموعة معروفة، أو إيجاد نص مرسوم بخط غير مضمّن. الإزالة: حذف علامة مائية رُسمت كمحتوى موسوم، أو إزالة استدعاء صورة محدد. والتوحيد القياسي: استبدال صيغة عامل بأخرى مكافئة يتعامل معها مستهلك لاحق بشكل أفضل
وتشترك هذه المهام جميعًا في متطلب واحد: يجب أن تكون قادرًا على ترك 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;
لماذا يكون العرض مُستعارًا لا قابلاً للتغيير؟
لا يستقبل رد النداء أبدًا شجرة نحو قابلة للتعديل. بل يستقبل عرضًا يمكنه جلب بايتات العملية الحالية أو مُعامِل محدد عند الطلب، وهذا كل شيء. ويمر كل تغيير عبر حقلي الإجراء والاستبدال بدلاً من ذلك
وهذا التوسط ليس شكليًا. فالتعديل المباشر للشجرة كان سيسمح لمُجتاز بتجاوز ميزانية الاستبدال وفحص التحريرات المتداخلة، والتحريرات المتداخلة في نموذج طبقات فوقية بنطاقات بايتات تنتج مخرجات خاطئة بصمت بدلاً من معطوبة بوضوح. وتوجيه التغييرات عبر حقل الإجراء يعني أن كل تحرير يُتحقق منه بالطريقة نفسها بصرف النظر عما قصده المُجتاز
والقراءة عند الطلب تُبقي الحالة الشائعة رخيصة أيضًا. فتدقيق يعدّ فقط أسماء العوامل لا يُجسِّد بايتات المُعامِلات إطلاقًا، وهذا مهم في دفق محتوى بمئات آلاف العمليات
حقل الحالة يصف المدخلات، دائمًا
يحمل كل حدث عمق حالة الرسوميات، وعمق كائن النص، وعمق المحتوى الموسوم، وعمق قسم التوافق كما كانت عليه في المدخلات الأصلية مباشرة قبل تنفيذ ذلك العامل. وهذا صحيح حتى لو حذف مُجتازك أو استبدل عوامل سابقة
وربط الحالة بالمدخلات الأصلية بدلاً من المخرجات المُحررة قرار مقصود له عائد عملي: تشغيل التدقيق نفسه مرتين على المستند نفسه ينتج نتائج متطابقة، وتدقيق يعمل جنبًا إلى جنب مع تحريرات لا يُزحزح إطار مرجعيته الخاص. وإذا احتجت إلى معرفة العمق في المخرجات، فأنت تعرف ما حذفته، فيمكنك حسابه؛ أما العكس، أي إعادة بناء مواضع مدخلات ثابتة من مخرجات متحركة، فغير ممكن
الحذف والاستبدال، بالقواعد التي تحميك
الاستبدال هو بايتات دفق محتوى خام، وأنت المسؤول عن صحة صياغتها. والضمانات المحيطة بها هي ميزانية إجمالي بايتات الاستبدال، وشرط عدم التداخل، والتحقق الاختياري من المخرجات الذي يعيد تحليل النتيجة قبل قبولها:
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