مقال تقني

معالجة ملفات PDF الكبيرة في Delphi باستخدام واجهة برمجة تطبيقات HotPDF Direct File

يجب أن يكون حساب الصفحات في أرشيف ممسوح ضوئياً (scanned archive) بحجم 1.4 جيجابايت رخيصاً. قم باستدعاء LoadFromFile على ذلك الملف وسيتوقف عن كونه رخيصاً: يحلل HotPDF بيانات المراجع المتقاطعة (cross-reference) ويبني كائناً في الذاكرة لكل كائن من الكائنات غير المباشرة للمستند التي يبلغ عددها مئات الآلاف، ويصطدم عامل 32 بت بسقف مساحة العنوان 2 جيجابايت في مكان ما في منتصف هذا التحليل. العملية التي أردتها، وهي عدد الصفحات، لم تكن بحاجة أبداً إلى أي من هذه الكائنات. كانت بحاجة إلى شجرة الصفحات ولا شيء غير ذلك. تلك الفجوة، بين ما تطلبه الوظيفة وما يقدمه التحميل الكامل، هي السبب الكامل وراء وجود واجهة برمجة تطبيقات Direct File

تمنح واجهة برمجة تطبيقات Direct File (Direct File API) وصولاً على مستوى الملف لـ Delphi و C++Builder إلى PDF: أعداد الصفحات، والنسخ، وفك التشفير، والإلحاقات التزايدية (incremental appends)، كل ذلك يقرأ من القرص ما يحتاجونه فعلياً بدلاً من إعادة بناء نموذج المستند بأكمله في ذاكرة الوصول العشوائي (RAM). المهارة هي مطابقة كل وظيفة بأخف طبقة (tier) يمكنها الإجابة عليها. احصل على هذا التطابق بشكل صحيح وستحتفظ الخدمة بذاكرة مسطحة عبر أي حجم إدخال. افهمها بشكل خاطئ وسيأخذ أول ملف كبير الحجم العامل إلى أسفل

ما يكلفك إياه التحميل الكامل

LoadFromFile ليس هو العدو. إنه يكسب ذاكرته: بمجرد وجود الشجرة في ذاكرة الوصول العشوائي (RAM)، يكون لديك وصول عشوائي (random access) إلى كل صفحة وكل كائن، وهذا بالضبط ما يتطلبه InsertPagesFromDocument و MovePage وإعادة التسلسل من خلال SaveLoadedDocument. لا يوجد اختصار لإعادة الهيكلة الحقيقية؛ يجب عليك الاحتفاظ بالمستند لإعادة ترتيبه

تبدأ المشكلة عندما لا تكون أحجام الإدخال تحت سيطرتك. تتجاهل تحميلات العملاء، ومخرجات الماسح الضوئي، وأرشيفات من عقد مضى كل ما افترضه مجموعة الاختبار (test corpus) الخاصة بك. قم بتحميل كل إدخال دون قيد أو شرط وسيتم تحديد سقف الذاكرة الخاص بك من خلال أكبر ملف منفرد سيرسله أي شخص على الإطلاق. يتتبع وقت التحليل عدد الكائنات، وتستقر الذاكرة المقيمة عند أضعاف حجم الملف بعد حساب هياكل الكائنات والتيارات التي تم فك تشفيرها، لذلك يمكن أن يعني جيجابايت على القرص عدة جيجابايتات مقيمة (resident)

إعادة التحويل البرمجي (Recompiling) لـ 64 بت ترفع سقف مساحة العنوان ولكنها تترك الفاتورة سليمة. لا يزال العامل يحرق ثواني من وحدة المعالجة المركزية (CPU) ومضاعفات للملف في ذاكرة الوصول العشوائي (RAM) للإجابة على سؤال كان يمكن لهيكل الملف نفسه الإجابة عليه في أجزاء من الألف من الثانية. في ظل التزامن (concurrency) تصبح الرياضيات معادية: تشترك أربعة أحمال كبيرة تعمل في وقت واحد في ميزانية ذاكرة واحدة، وتنخفض الإنتاجية بشكل كبير على وجه التحديد عندما يكون الطابور في أعمق نقطة ولا يمكنك تحمل ذلك على الأقل

قراءة ملف من خلال مقبض (handle)

تفتح طبقة القراءة فقط (read-only tier) ملفاً كمقبض (handle)، وتجيب على الأسئلة الهيكلية حوله، وتغلقه. لا توجد شجرة كائنات، ولا تقديم للصفحة، ولا ذاكرة تنمو مع الإدخال

var
  Pdf: THotPDF;
  Handle, PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Handle := Pdf.DAOpenFileReadOnly('archive-2026-06.pdf', '');
    if Handle > 0 then
    try
      PageCount := Pdf.DAGetPageCount(Handle);
      RouteByPageCount('archive-2026-06.pdf', PageCount);
    finally
      Pdf.DACloseFile(Handle);
    end;
  finally
    Pdf.Free;
  end;
end;

ثلاث عادات تحافظ على هذه الطبقة صادقة. أولاً، تحقق من القيمة المرتجعة (return value). يعني المقبض غير الموجب (non-positive handle) أن الفتح فشل، وإطلاق DAGetPageCount على مقبض ميت هو نوع الخطأ الذي يظل مخفياً حتى اليوم الذي يرسل فيه العميل ملفاً مشوهاً (malformed). ثانياً، قم بإقران كل فتح ناجح بـ DACloseFile داخل كتلة finally؛ الخدمة التي تسرب المقابض لا تتعطل، إنها تتعفن فقط، وهو أسوأ. ثالثاً، احترم ما تفعله معلمة كلمة المرور فعلياً. يقبل DAOpenFileReadOnly واحدة، ولكن بالنسبة للمدخلات المشفرة، فإنه يسقط بهدوء إلى تحليل كامل لقراءة عدد الصفحات، لذلك يتبخر ضمان الذاكرة المسطحة (flat-memory guarantee). قم بتوجيه الملفات المحمية من خلال DecryptFile أولاً وستبقى بقية خط الأنابيب (pipeline) رخيصة

يتضاعف نفس المسبار (probe) كبوابة فرز (triage gate). تظهر الملفات بتسمية خاطئة، أو تم تحميلها جزئياً، أو تمت إعادة تسميتها من تنسيق آخر تماماً، ويرفض فحص DAOpenFileReadOnly كل ذلك عند الباب الأمامي في أجزاء من الألف من الثانية، مع تثبيت الخطأ على الملف المخالف. البديل هو السماح لملف غير هام بالركوب عميقاً في عامل طابور وتفجيره هناك، حيث يمكن أن يكلف فك تشابك الإدخال الذي تسبب فيه فترة ما بعد الظهر

نسخ وفك تشفير وتشفير ملفات بأكملها

تنقل الطبقة الثانية الملفات الكاملة وتحولها دون الكشف عن محتوياتها الداخلية على الإطلاق. هذه هي الاستدعاءات التي تعتمد عليها خطوط أنابيب الإدخال بشكل أكبر

// Structural copy: validate-and-move without parsing the object tree
Status := Pdf.DACopyFile('incoming\statement.pdf', 'verified\statement.pdf');
LogDirectFileStatus('copy', Status);

// Decrypt while copying: the Direct File route into protected inputs
Status := Pdf.DecryptFile('incoming\protected.pdf',
  'verified\plain.pdf', 'batch-password');
LogDirectFileStatus('decrypt-copy', Status);

// Encrypt while copying: protect an output without a full load
Status := Pdf.EncryptFile('verified\statement.pdf',
  'outbound\statement.pdf', 'owner-secret', '', aes256, [prPrint]);
LogDirectFileStatus('encrypt-copy', Status);

كل استدعاء يكسب مكانه. DACopyFile هي النسخة التي تم التحقق من صحتها من دليل العزل (quarantine directory) إلى التخزين المدار (managed storage): يفتح ويفهرس هيكل PDF أثناء سيره، لذا يفشل الإدخال المقطوع أو غير PDF هنا بدلاً من ثلاث مراحل في المصب (downstream). يكتب DecryptFile نسخة مفككة التشفير على طول مسار إعادة كتابة AES-256 مباشر يتخطى شجرة الكائنات كلما سمح الإدخال بذلك، وهو نظير الملف الكبير لتدفق فك التشفير التحميل وإعادة الحفظ الذي تم تناوله في مقال تشفير AES-256. يشغل EncryptFile نفس الحركة في الاتجاه المعاكس، مطبقاً حماية بكلمة مرور أثناء نسخ على مستوى الملف بمعلمات نوع المفتاح والإذن التي يستخدمها المسار داخل الذاكرة بالفعل

إلحاق التغييرات بدلاً من إعادة الكتابة

التحديث التزايدي (Incremental update)، المحدد في ISO 32000-1 §7.5.6، هو الطبقة الثالثة. تظل البايتات الأصلية في مكانها على القرص، ويتم إلحاق أي كائنات جديدة أو معدلة بعدها، متبوعة بقسم مراجع متقاطعة (cross-reference) جديد يرتبط بالأصل. بالنسبة لأرشيف بحجم 900 ميجابايت يحتاج إلى إضافة صفحة واحدة، فإن تكلفة الكتابة هي الدلتا (delta)، وليست الملف بأكمله

// Append an audit page to a large archive without rewriting it
Pdf.BeginIncrementalUpdate('archive-2026-06.pdf');
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Processed by intake service 2026-06-11');
Pdf.SaveIncrementalUpdate('archive-2026-06-stamped.pdf');  // original bytes + delta

نقطتان من الانضباط تهمان هنا. يجب أن يشير BeginIncrementalUpdate إلى الملف الأصلي، حيث ترتبط بيانات المراجع المتقاطعة الملحقة بإزاحات البايت داخله. والنموذج هو للإلحاق فقط (append-only) حسب التصميم: كل حفظ تزايدي ينمي الملف، ولا يقلصه أبداً. سوف ينتفخ المستند الذي يتم ختمه ليلياً بلا حدود حتى يؤدي إعادة التسلسل الدوري، بتحميله وكتابته مرة أخرى من خلال SaveLoadedDocument، إلى ضغطه. طبيعة الإلحاق فقط هذه هي ما يجعل التحديث التزايدي الطريقة الآمنة الوحيدة للمس مستند موقع رقمياً، وهو قيد تم فحصه في مقال التوقيعات الرقمية و PAdES. تحصل آلية المراجع المتقاطعة الأساسية على معاملتها الخاصة في مقال تيارات الكائنات والتحديثات التزايدية

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

مطابقة الطبقة مع العملية

منطق الاختيار قصير بما يكفي للاحتفاظ به في رأسك، ومن المفيد ترميزه كقرار توجيه صريح في الجزء العلوي من خط الأنابيب بدلاً من ترك كل وظيفة ترتجل مسارها الخاص. العملية التي تحتاجها تحدد الطبقة:

  • العد أو الفحص أو التصنيف (Count, inspect, or classify) يفتح مقبضاً: DAOpenFileReadOnly، DAGetPageCount، DACloseFile
  • نقل أو فك تشفير أو تشفير ملف بأكمله (Moving, decrypting, or encrypting a whole file) يبقى على مستوى الملف مع DACopyFile أو DecryptFile أو EncryptFile
  • إعادة هيكلة الصفحات أو دمج المستندات (Restructuring pages or merging documents) يحتاج إلى التحميل الكامل: LoadFromFile، ثم InsertPagesFromDocument أو MovePage، ثم SaveLoadedDocument
  • إضافة دلتا صغيرة إلى ملف ضخم أو موقع (Adding a small delta to a huge or signed file) يستدعي BeginIncrementalUpdate ويحفظ

تبلي خطوط الأنابيب المختلطة بلاءً حسناً لوضع عتبة حجم (size threshold) أمام مسار التحميل الكامل. أرسل أي شيء يتجاوز بضع مئات من الميجابايت عبر طبقات Direct File، واحتفظ بالتحميل الكامل لإعادة الهيكلة الحقيقية على عامل 64 بت بميزانية ذاكرة حقيقية. تحول العتبة تعطل نفاد الذاكرة (out-of-memory crash) إلى قرار توجيه يمكنك رؤيته وضبطه

مهما كانت الطبقة التي تتعامل مع وظيفة، اكتب إخراجها إلى اسم مؤقت وأعد تسميته في مكانه فقط بمجرد التحقق من صحة النتيجة. يبدو الملف نصف المكتوب الموجود تحت الاسم النهائي تماماً مثل ملف جيد للمرحلة التالية من خط الأنابيب، وتجعل استدعاءات Direct File الفحص رخيصاً: تأكيد الإخراج هو مسبار مقبض (handle probe) من سطر واحد

يتم شحن واجهة برمجة تطبيقات Direct File كجزء من مكون HotPDF لـ Delphi و C++Builder. تربط صفحة المنتج مرجع الوظيفة الكامل، بما في ذلك استدعاءات التحديث التزايدي الموضحة هنا