يرسم أحدهم مربعًا أسود فوق اسم، ولا يسطّح شيئًا، ويرسل الملف، فيحدّد المراجع المستطيل ويلصق الاسم في رسالة بريد إلكتروني. يجيب PDFiumPas عن ذلك بالتنقيح على مستوى المشغّلات: تحذف SaveAsRedacted فقط قياسات Unicode scalars التي تمسّ صناديق حروفها مستطيل تنقيح، وتعيد بناء الناجية من الخط الأصلي والحجم والمصفوفة ونمط العرض واللون، وتقتُصّ المسارات والصور المحاذية للمحاور بدلًا من إسقاطها كاملة
لماذا لا يكون المستطيل المرسوم تنقيحًا
عملية رسم تُضاف فوق تيار المحتوى لا تخفي شيئًا، لأن مشغّلات إظهار النص تحتها لا تزال في التيار ولا تزال تُعيّن نقاط الرموز. تُعرّف ISO 32000-1 §9.4 الكائن النصي بوصفه تسلسلًا من مشغّلات التموضع والإظهار داخل BT وET؛ والمستطيل المملوء المرسوم بعده هو ببساطة مشغّل آخر في التيار نفسه. الاستخراج يجتاز المشغّلات لا البكسلات، لذا يعود النص المغطّى سليمًا. التنقيح الحقيقي يجب أن يزيل المعامل، لا أن يحجب الإخراج
التنفيذ الآمن البديهي وحشي: اعثر على كل كائن صفحة يتقاطع صندوقه المحيط مع مستطيل تنقيح واحذف الكائن كله. هذا ما فعلته إصدارات PDFiumPas السابقة، وهو صحيح لكنه مكلف. قد تحمل Tj واحدة صف جدول كامل، لذا فإن تعتيم رقم حساب واحد كان يأخذ معه التاريخ والوصف والمبلغ. تعبئة مستطيلة اتفق أن تكون شريط جدول بعرض الصفحة كاملًا كانت تختفي عبر الصفحة كلها. شعار فاتورة كان يختفي لأن التنقيح قصّ ركنًا واحدًا منه. ينقل الإصدار 3.101.0 القرار مستوى واحدًا إلى أسفل، من كائن الصفحة إلى المعامل
ماذا يحذف التنقيح على مستوى المشغّلات فعلًا؟
يحذف PDFiumPas قياسات Unicode scalars لا الكائنات النصية. أثناء SaveAsRedacted يبني المكوّن تعيينًا من الحروف إلى كائنات الصفحة انطلاقًا من صفحة النص المحمّلة، ثم يقرأ لكل حرف يملكه الكائن قيد الاختبار صندوق الحرف ويقاطع ذلك الصندوق مع كل مستطيل تنقيح. الحروف التي تمسّ مستطيلًا تُعلَّم للإزالة؛ والباقي يُعلَّم بوصفه ناجيًا. إذا لم يتقاطع شيء، يُترَك الكائن وشأنه تمامًا. إذا تقاطعت كل الحروف، يُزال الكائن كاملًا، كما كان من قبل. الحالة المختلطة وحدها هي التي تُشغّل التقسيم
تُعاد بعدها إصدار كل ناجية بوصفها كائنًا نصيًا خاصًا بها مبنيًا من مقبض الخط الأصلي، وحجم الخط الأصلي، ومصفوفة النص لكل حرف، ونمط عرض النص الأصلي، وحالة التعبئة والحد للكائن الأصل بما في ذلك عرض الحد ووصل الخطوط ونهاية الخط ومصفوفة الشرطات. إعادة استخدام مقبض الخط بدلًا من حلّ مقبض جديد هو ما يُبقي الحروف الرسومية متطابقة قياسيًا، وإعادة استخدام مصفوفة كل حرف هو ما يُبقي تقارب الحروف وتباعد الكلمات في موضعها دون إعادة تشغيل التخطيط. الثمن هو عدد الكائنات: الحرف الواحد المُبقى يصبح كائنًا نصيًا واحدًا، ولهذا يوجد TPdfRedactionOptions.MaxSplitObjects بوصفه سقفًا صلبًا للشظايا المولَّدة
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // الملف يحمل بالفعل تعليقات /Redact التوضيحية
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // التقسيم على مستوى المشغّلات (الافتراضي)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // سقف الشظايا المولَّدة
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // افشل بإحكام، لا تُرسل الملف
finally
Pdf.Free;
end;
end;
المستطيلات تُقتصّ، الهندسة المدوّرة لا
لا تُقسَّم المسارات إلا عندما يستطيع PDFiumPas إثبات أن المسار مستطيل محاذٍ للمحاور. الإثبات ضيّق عمدًا: يجب أن تكون كلتا قيمتي القص في مصفوفة الكائن أقل من 0.0001، وأن يتكون المسار من أربعة إلى ستة مقاطع تبدأ بـ MOVETO وتستمر بـ LINETO فقط، وأن تحط النقاط المحوّلة على أركان حدود الكائن الأربعة جميعها ضمن تفاوت قدره 0.01. المسار الذي يجتاز ذلك الفحص يُختزَل بطرح مستطيلات متتالٍ، حيث ينحت كل مستطيل تنقيح مجموعة الناجين إلى شرائح يسار ويمين وأسفل وفوق، وكل شريحة ناتجة تُعاد إنشاؤها بوضع التعبئة الأصلي وعلم الحد وحالة الطلاء. المنحنيات والمثلثات والأشكال المقصوصة وأي شيء مدوّر تفشل في الفحص ويُزال الكائن كاملًا
تتبع الصور ISO 32000-1 §8.9، حيث تحتل عينات الصورة مربع الوحدة المحوّل عبر مصفوفة التحويل الحالية. يعكس PDFiumPas ذلك التحويل ليعيد كل شظية ناجية في فضاء الصفحة إلى إحداثيات صورة مُطبَّعة، ويحصرها في مجال الوحدة، ثم يحوّلها إلى مؤشرات بكسل بالتقريب نحو الداخل: الحافتان اليسرى والعليا تمران عبر Ceil، واليمنى والسفلى عبر Floor. ذلك الاتجاه مهم. التقريب نحو الخارج سيتيح لعمود جزئي من بكسلات المصدر من الجانب المنقَّح البقاء عند حافة الشظية. تُحوَّل حدود البكسل الصحيحة بعدها عائدًا إلى إحداثيات مُطبَّعة وتُستخدم لاشتقاق مصفوفة الشظية، بحيث تحط خريطة البكسلات المقتصّة تمامًا على حد البكسل الذي قُصّت عنده. القص نفسه هو نسخ صفوف مدرك للخطوة عبر صيغ Gray وBGR وBGRx وBGRA. كما في المسارات، الصورة المدوّرة أو المائلة، أو التي تحتوي مصفوفتها على قيمة قياس منحلّة، تُزال بالكامل
// بعد استدعاء SaveAsRedacted ناجح
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// تعذّر تقسيم أي شيء: كل كائن متقاطع أُسقط كاملًا
LogWholeObjectFallback(SourcePdf);
لماذا يفشل PDFiumPas بإحكام عند الحروف غير المعيّنة؟
لأن الحرف الرسومي الذي لا يملك قياس Unicode scalar قابلًا لإعادة الإنتاج لا يمكن إعادة بنائه بأمانة. إعادة بناء ناجية تعني استدعاء API ضبط النص بسلسلة، وذلك يتطلب نقطة رمز مستقرة لكل حرف مُبقى. الخطوط الرمزية المجمّعة جزئيًا ذات بيانات ToUnicode المعطوبة أو الغائبة قد تُسفر عن تعيين فارغ، وإعادة الترميز بالتخمين ستُنتج إخراجًا يبدو صحيحًا على الشاشة بينما يحمل حرفًا مختلفًا تحته. يرفض PDFiumPas: فحص الحروف المُبقاة يُطلق استثناء، ويُلتقَط الاستثناء داخل SaveAsRedacted، ويعود TPdfRedactionReport.Succeeded بقيمة False مع الرسالة في ErrorMessage، وتُرجع الدالة False. القاعدة نفسها تنطبق على ميزانية التقسيم، التي تُطلق استثناء بدلًا من اقتطاع مجموعة الشظايا بصمت. عندما يحتوي مستند على خطوط لا تثق بها وتريد السلوك القديم الحتمي، اضبط Options.PreservePartialObjects := False فيختفي كل كائن متقاطع كاملًا
تشذيب الموارد عبر النطاقات المشتركة
تقسيم الكائنات يخلّف كائنات يتيمة، وتشذيبها ليس ببساطة مقارنة قاموس /Resources على مستوى الصفحة. تتيح ISO 32000-1 §7.8.3 أن يُشار إلى قاموس الموارد نفسه من عدة صفحات، ومن كائنات Form XObject، ومن الأنماط، ومن تيارات مظهر التعليقات التوضيحية في آن واحد. حذف اسم خط لأن صفحة واحدة توقفت عن استخدامه سيكسر صفحة أخرى لا تزال تستخدمه. لذلك يعمل PruneUnusedPdfResources لكل نطاق: يحلّ /Contents سواء كان مصفوفة مباشرة، أو مرجعًا غير مباشر إلى مصفوفة، أو تيارًا واحدًا، ثم يجمع استخدام الموارد من المشغّلات التي تسمّي الموارد فعلًا — Tf للخطوط، وDo لكائنات XObject، وgs لحالة الرسومات، وCS وcs وSCN وscn لفضاءات الألوان والأنماط، وsh للتظليلات، وBDC وDP لخصائص المحتوى المعلّم، بالإضافة إلى مدخل /CS للصور المضمّنة. عندما يتشارك عدة نطاقات في قاموس واحد، تُوحَّد مجموعات الأسماء المستخدمة لكل فئة قبل إزالة أي شيء
لا تُسقَط إلا الأسماء المؤكَّد عدم الإشارة إليها عبر كل نطاق يشير إلى القاموس. النطاق الذي لا يمكن تحليله بثقة يُترَك دون مساس، وهو الاتجاه المتحفّظ: الملف غير المشذوب أكبر فحسب، أما المشذوب خطأً فهو تالف. تُكتب القواميس الناجية عائدًا بوصفها تحديثًا تزايديًا متفرقًا يحمل أرقام الجيل الدقيقة، ثم تكتسح إعادة كتابة قائمة على قابلية البلوغ الكائنات التي أصبحت غير قابلة للبلوغ بمجرد اختفاء الأسماء. يُبلّغ TPdfResourcePruneReport عن ScannedScopeCount وUpdatedScopeCount وRemovedNameCount وRemovedObjectCount وعدادات البايتات وعلم Succeeded. يشغّل SaveAsRedacted هذه الخطوة تلقائيًا على الناتج المُعقَّم، لذا فإن مسار التنقيح يتضمنها أصلًا، لكن الدالة مُصدَّرة على مستوى التيارات لخطوط المعالجة التي تريدها وحدها
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// يبقى AllowSignedDocument على False: إعادة الكتابة التزايدية
// ستُبطل نطاقات البايتات التي يغطيها التوقيع
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
دمجه في خط معالجة المستندات
لا يعدّل مسار التنقيح المستند الذي حمّلته أبدًا. تلتقط SaveAsRedacted لقطة معزولة، وتطبّق عليها تعليقات /Redact التوضيحية، وتنزع المرفقات، وتجري تمريرة التعقيم التي تزيل إجراء الفتح وإجراءات الكتالوج وأشجار الأسماء والملفات المرتبطة وAcroForm والبيانات الوصفية، وتشذّب الموارد، وعندها فقط تكتب تيار الإخراج. إعادة فتح ذلك الناتج بوصفه مستندًا مستقلًا وإعادة استخراج النص هي خطوة التحقق الجديرة بالبقاء في مجموعة اختباراتك، لأنها الفحص الوحيد الذي يجيب عن السؤال الأصلي — هل يستطيع قارئ الحصول على السلسلة بعد. نتيجة واحدة عليك التخطيط لها: التقسيم يستبدل كائنات الصفحة، لذا فإن أي مقبض FPDF_PAGEOBJECT كنت تمسك به يصبح ميتًا بعدها، وهو فخ العمر نفسه الموضح في مقابض كائنات الصفحة القديمة بعد التحويل
قطعتان مجاورتان تُكمّلان سير العمل. تحديد أين تذهب مستطيلات التنقيح يبدأ عادة من الهندسة المستخرجة، ونموذج الكتل وترتيب القراءة في كتل النص المنظمة وترتيب القراءة مصدر أفضل للمربعات المرشحة من سلسلات الحروف الخام. تقديم النتيجة إلى مراجع ينتمي إلى قواعد التحصين في بناء معاينة PDF آمنة، حيث يبقى تعبئة النماذج وJavaScript معطّلين افتراضيًا. تغطي معًا الحلقة التي تحتاجها معظم سير عمل الامتثال: التحديد، والتنقيح على مستوى المشغّلات، والتحقق بإعادة الفتح، والمعاينة بأمان. سطح API الكامل وتنزيل النسخة التجريبية وشروط الترخيص للمكوّن موجودة على صفحة منتج مكوّن PDFium Delphi