مقال تقني

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

كانت المهمة عادية: خدمة Delphi ليلية تأخذ أرشيفات رهن عقاري ممسوحة ضوئيا، وتحسب الصفحات، وتوجه كل ملف إلى صف المعالجة الصحيح. عملت بهدوء لأشهر، إلى أن وصل أرشيف بحجم 1.4 GB. يفسر LoadFromFile بيانات cross-reference ويمثل كائنا لكل واحد من مئات آلاف الكائنات غير المباشرة في الملف، وفي خدمة 32-bit دفعت تلك الشجرة working set فوق سقف فضاء العناوين البالغ 2 GB في منتصف التفسير. لم يكن الحل خادما أكبر. كانت المهمة تسأل سؤالا واحدا فقط: كم صفحة؟ وإجابة ذلك لم تكن تتطلب تحميل المستند أصلا

يوجد HotPDF Direct File API لهذا الصنف من العمل تحديدا: عمليات PDF على مستوى الملف من Delphi وC++Builder تقرأ ما تحتاجه من القرص بدلا من تمثيل نموذج المستند كاملا. معرفة الطبقة التي تنتمي إليها عملية معينة في API هي الفرق بين خدمة ذات استخدام ذاكرة ثابت وخدمة تسقط عند أول إدخال كبير جدا

ما الذي يمنحه التحميل الكامل، وما كلفته

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

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

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

فحص قائم على handles

تفتح طبقة القراءة فقط الملف كمقبض، وتجيب عن أسئلة بنيوية، ثم تغلقه: لا شجرة كائنات، ولا رسم صفحات، ولا ذاكرة متناسبة مع الحجم

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;

ثلاث قواعد تبقي هذه الطبقة موثوقة. افحص handle؛ قيمة غير موجبة تعني أن الفتح فشل، واستدعاء DAGetPageCount على handle ميت هو نوع الخطأ الذي لا يظهر إلا على الملف المشوه الذي يرسله عميل. اربط كل فتح ناجح بـ DACloseFile داخل كتلة finally، لأن خدمة تسرّب handles تتدهور ببطء بدلا من أن تفشل بوضوح. واعرف كلفة معامل كلمة المرور: يقبل DAOpenFileReadOnly كلمة مرور، لكنه في المدخلات المشفرة يعود داخليا إلى تفسير كامل للإجابة عن سؤال عدد الصفحات، ولذلك فإن خاصية الذاكرة الثابتة لا تصمد إلا للملفات غير المشفرة، ويجب أن تمر المدخلات المحمية عبر DecryptFile أولا

توفر طبقة handle أيضا بوابة فرز رخيصة. تصل الملفات من العملاء mislabeled، أو مقطوعة بسبب تحميل فاشل، أو معاد تسميتها من تنسيقات أخرى؛ يرفض فحص DAOpenFileReadOnly هذه الحالات في أجزاء من الثانية عند الباب الأمامي، مع خطأ واضح مربوط بالملف الصحيح، بدلا من تركها تفشل عميقا داخل عامل صف حيث تكلف diagnosis ظهيرة كاملة

عمليات الملف الكامل: نسخ، فك تشفير، تشفير

الطبقة الثانية تحول ملفات كاملة دون كشف داخلياتها، وهي عماد خطوط أنابيب الاستقبال

// 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 إلى تخزين مُدار؛ يفتح بنية PDF ويفهرسها أثناء السير، لذلك يفشل إدخال مقطوع أو غير PDF هنا بدلا من ثلاث مراحل لاحقا. ينتج DecryptFile نسخة مفكوكة التشفير، آخذا مسار إعادة كتابة مباشر لـ AES-256 يتجنب بناء شجرة الكائنات متى سمح الإدخال بذلك، وهو النظير الخاص بالملفات الكبيرة لمسار التحميل وإعادة الحفظ لفك التشفير المشروح في مقالة تشفير AES-256. أما EncryptFile فهو الصورة المعاكسة، إذ يطبق حماية كلمة المرور أثناء نسخ على مستوى الملف بالمعاملات نفسها الخاصة بنوع المفتاح والأذونات التي يستخدمها المسار داخل الذاكرة

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

التحديث التزايدي، المعرّف في ISO 32000-1 §7.5.6، هو الطبقة الثالثة: تبقى البايتات الأصلية على القرص دون لمس، وتضاف الكائنات المعدلة أو الجديدة بعدها مع قسم cross-reference يربط إلى الأصل. لأرشيف 900 MB يحتاج صفحة مضافة واحدة، كلفة الكتابة هي 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 إلى الملف الأصلي، لأن بيانات cross-reference الملحقة ترتبط بإزاحات داخله. والنموذج append-only بالتصميم؛ كل حفظ تزايدي يجعل الملف أكبر، لا أصغر، ولذلك فإن مستندا يُختم كل ليلة ينمو بلا حد حتى إعادة تسلسل دورية، أي تحميل المستند وكتابته مرة أخرى عبر SaveLoadedDocument، لضغطه. خاصية append-only هي أيضا ما يجعل التحديث التزايدي الطريقة الآمنة الوحيدة لتعديل المستندات الموقعة رقميا، وهو قيد تفحصه مقالة التوقيعات الرقمية وPAdES؛ أما آليات cross-reference الكامنة فمغطاة في مقالة object streams والتحديثات التزايدية

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

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

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

  • العد والفحص والتصنيف — افتح handle: DAOpenFileReadOnly وDAGetPageCount وDACloseFile
  • نقل ملفات كاملة أو فك تشفيرها أو تشفيرها — استدعاءات مستوى الملف: DACopyFile وDecryptFile وEncryptFile
  • إعادة بناء الصفحات أو دمج المستندات — تحميل كامل: LoadFromFile، ثم InsertPagesFromDocument أو MovePage، ثم SaveLoadedDocument
  • إضافة delta صغير إلى ملف ضخم أو موقّعBeginIncrementalUpdate ثم الحفظ

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

أيا كانت الطبقة التي تعالج المهمة، مرر مخرجاتها عبر اسم مؤقت وأعد تسميته إلى المكان النهائي فقط بعد أن تتحقق النتيجة؛ فالملف نصف المكتوب تحت الاسم النهائي لا يمكن تمييزه عن ملف جيد لدى المرحلة التالية من خط الأنابيب. تجعل استدعاءات Direct File ذلك التحقق رخيصا، لأن تأكيد الناتج نفسه فحص handle بسطر واحد

أسئلة شائعة: ملفات PDF كبيرة في خدمات Delphi

كيف أحصل على عدد صفحات PDF دون تحميل الملف كله؟

DAOpenFileReadOnly مع DAGetPageCount كما في مثال الفحص أعلاه؛ يبقى استخدام الذاكرة ثابتا بغض النظر عن حجم الملف

لماذا يكبر ملف PDF بعد كل حفظ؟

التحديثات التزايدية تضيف append بالتصميم؛ لا يُزال شيء أبدا. اضغط دوريا بتحميل كامل وإعادة حفظ، أي LoadFromFile ثم SaveLoadedDocument، عندما لا تكون المراجعات المتراكمة مطلوبة

هل يفتح Direct File API ملفات PDF المشفرة؟

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

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