كانت مهمة الليل التي تجمع حزم إغلاق الرهونات تعمل جيدا عامين، ثم بدأت تموت في الأسبوع الذي انتقل فيه مورّد المسح إلى 600 DPI. تجاوزت ملفات الأرشيف الفردية 1.8 GB، وبدأت خطوة التجميع — التي كانت تحمّل كل مستند كاملا قبل لمس صفحة واحدة — تستنفد مساحة عناوين العامل لمجرد عد الصفحات. لم يتغير المتطلب: افتح، عد، اختر النطاقات، اربط. ما تغير هو أن التحميل الكامل للشجرة يكف عن كونه افتراضا معقولا في مكان ما قرب علامة الغيغابايت. يعالج PDFlibPas، مكتبة PDF من losLab لـ Delphi و C++Builder، هذا تحديدا عبر طبقة Direct Access: عائلة من الدوال ذات البادئة DA مدعومة بقارئ streaming يمشي في جدول cross-reference في مكانه بدلا من تجسيد المستند.
أين تذهب الذاكرة في التحميل الكامل
تحميل PDF "بالطريقة العادية" يعني تحليل xref، وحل كل كائن غير مباشر إلى شجرة في الذاكرة، وفك object streams، وربط شجرة الصفحات والخطوط والتعليقات التوضيحية بكائنات تستطيع تعديلها. بالنسبة إلى سير عمل التحرير فهذا اختيار صحيح. أما في أعمال الدمج والتقسيم والفحص فهو في معظمه هدر: قد يحتوي أرشيف مسح مكون من 30,000 صفحة على ملايين الكائنات غير المباشرة، بينما تحتاج مهمة التقسيم إلى قراءة بضع مئات فقط — عقد الصفحات في النطاق المطلوب وما تشير إليه.
تعكس طبقة Direct Access النموذج. تقوم DAOpenFile و DAOpenFileReadOnly بتحليل trailer و xref — بضعة كيلوبايتات في نهاية الملف — وتعيد مقبض ملف. تُجلب الكائنات كسولا عندما يحتاجها استدعاء. النتيجة العملية هي أن فتح ملف متعدد الغيغابايت يستغرق تقريبا ما يستغرقه فتح ملف صغير، وأن الذاكرة تتبع ما تلمسه لا ما يحتويه الملف.
فحص ملف ضخم من دون تحميله
النمط أدناه مأخوذ من benchmark الملفات الكبيرة الخاص بالمكتبة: افتح للقراءة فقط، اطرح أسئلة، أغلق. لا توجد شجرة مستند في أي لحظة.
var
Lib: TPDFlib;
Handle, Pages: Integer;
begin
Lib := TPDFlib.Create;
try
Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
if Handle = 0 then
raise Exception.Create('Direct access open failed');
Pages := Lib.DAGetPageCount(Handle);
Writeln('pages : ', Pages);
Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
Lib.DACloseFile(Handle);
finally
Lib.Free;
end;
end;
وضع القراءة فقط يستحق التفضيل كلما أمكن: فهو يسمح لمرحلة الاستقبال بالعمل بينما تحتفظ عمليات أخرى بالملف، ويوثق النية — إذا استدعت مرحلة فحص دالة مغيرة بالخطأ، فستفشل مبكرا بدلا من إفساد الأرشيف.
PageRef مقبض كائن، لا رقم صفحة
أكثر خطأ شائع في DA API هو تمرير رقم صفحة حيث تتوقع الدالة PageRef. كل استدعاء DA تقريبا يعمل على صفحة — DAExtractPageText و DARenderPageToFile و DARotatePage و DACapturePage — يأخذ مقبض مرجع إلى كائن الصفحة، تحصل عليه بترجمة الرقم الذي يراه الإنسان عبر DAFindPage:
PageRef := Lib.DAFindPage(Handle, 250); // page number -> object handle
if PageRef <> 0 then
begin
Text := Lib.DAExtractPageText(Handle, PageRef, 0);
Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;
تمرير الرقم الخام 250 بدلا من ذلك لا يرفع خطأ — بل يخاطب أي كائن يقع خلف قيمة المقبض تلك، وهذا في يوم جيد يفشل بوضوح، وفي يوم سيئ يستخرج النص من الصفحة الخطأ إلى مستند يراه العميل. إذا لففت طبقة DA في كود خدمتك، فاجعل تجاوز الترجمة مستحيلا: اقبل أرقام الصفحات عند الحدود، واستدع DAFindPage فورا، ومرر المراجع فقط داخليا.
دمج مئات الملفات بقائمة مسماة
بالنسبة إلى ملفين، تكفي MergeFiles(First, Second, Output). أما تجميع الدفعات فيتوسع بشكل أفضل عبر قوائم الملفات: سجّل المدخلات تحت اسم قائمة، ثم ادمج القائمة في مرور واحد.
Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');
// Verify the result the cheap way: direct access again
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);
لعائلة الدمج ثلاثة متغيرات، والفرق ليس السرعة وحدها. تتخطى MergeFileListFast حفظ شجرة البنية؛ وتفرض MergeFileListStrict الوضع الصارم؛ والنسخة بلا لاحقة هي الافتراض المتوازن. القاعدة التشغيلية الناتجة: إذا كان أي مدخل Tagged PDF يجب أن تبقى بنية الوصول الخاصة به — أي شيء مُنتج لـ PDF/UA مثلا — فاستخدم المتغير الافتراضي أو Strict، لأن Fast سيسقط شجرة البنية بصمت. بالنسبة إلى أرشيفات المسح العادية بلا tagging، يكون Fast أداء مجانيا. قرر لكل pipeline، لا حسب مزاج المطور، وسجل المتغير المستخدم في سجل المهمة.
التقسيم من دون تحميل: استخراج النطاقات
يتبع التقسيم فلسفة عدم التحميل نفسها. تسحب ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) نطاق صفحات مباشرة من ملف إلى ملف — '1-500'، أو '501-1000'، أو اختيارات مفصولة بفواصل — من دون أن يتحول المصدر إلى شجرة مستند. عندما يكون المستند محملا بالفعل لأسباب أخرى، تنتج ExtractPageRanges مستندا جديدا في الذاكرة من المستند الحالي، وتسحب CopyPageRanges نطاقات من مستند محمل آخر عبر ID. بالنسبة إلى تقسيم كشوف فردية من تدفقات طباعة مجمعة، صيغة ملف إلى ملف هي ما يمنع ملف إدخال 4 GB من التضخم داخل RAM.
ملفات تكذب بشأن هندستها
تواجه خطوط الملفات الكبيرة ملفات تالفة بمعدل لا تراه خطوط الملفات الصغيرة، ببساطة لأن المدخلات تمر عبر أنظمة أكثر. يستحق شكلان من الفشل معالجة صريحة.
أولا، الرؤوس المزاحة. أحيانا تضيف بوابات البريد و print spoolers بايتات قبل PDF، فلا تعود علامة %PDF عند offset 0، وتصبح كل offsets في xref خاطئة بالمقدار نفسه. يكتشف قارئ streaming ذلك ويعرّضه — DAShiftedHeader على المستوى المسطح، و ShiftedHeader على TSmartPDFReader — ويعوّض أثناء القراءة. الحسابات المنزلية للـ offsets عادة لا تفعل ذلك، ولهذا يكون العَرَض الكلاسيكي: "يعمل على كل ملف نولده، ويفشل على ملفات العميل X".
ثانيا، جداول cross-reference المكسورة. تقوم DACopyFile(InputFileName, OutputFileName, PageCount) ببث الملف كله إلى نسخة جديدة مع إعادة بناء xref، وتعيد عدد الصفحات كمنتج جانبي. تشغيلها كمرحلة تطبيع أمام مستهلك لاحق حساس يحول فئة من فشل التحليل المتقطع إلى خطوة إصلاح واحدة متوقعة. وعندما تحتاج تعديلاتك نفسها إلى حفظ، تكتب DAAppendFile كـ incremental update — تضيف مراجعة جديدة بدلا من إعادة كتابة غيغابايتات، فيبقى زمن الحفظ متناسبا مع التغيير لا مع حجم الملف.
تفاصيل التسليم: linearization والتركيب
تكمل قدرتان متجاورتان خط الملفات الكبيرة. عندما يُقدّم الناتج المجمع عبر HTTP للعرض داخل المتصفح، تعيد LinearizeFile تنظيمه لبث byte-range بحيث تظهر الصفحة الأولى قبل تنزيل بقية حزمة 500 MB — وتستحق التنفيذ كمرحلة أخيرة، بعد كل الدمج، لأن أي تعديل لاحق يزيل linearization. وعندما تحتاج الحزم إلى تركيب لا مجرد ربط — صفحة غلاف مختومة خلف كل كشف، أو فرض صفحتين مصدر على ورقة إخراج واحدة — تحول DACapturePage أي صفحة إلى قالب قابل لإعادة الاستخدام تضعه DADrawCapturedPage على صفحة وجهة داخل مستطيل عشوائي، مع بقاء المصدر متعدد الغيغابايت خارج التحميل الكامل.
أسئلة شائعة عن الملفات الكبيرة
ما حجم الملف الذي يستطيع Direct Access التعامل معه؟ offsets من نوع Int64 في طبقة DA كلها، لذلك ليس حد الصيغة هو القيد؛ القرص المتاح وسقف xref الكلاسيكي ذي 10 أرقام هما القيدان. عمليا، أرشيفات مسح متعددة الغيغابايت أمر عادي؛ تبقى الذاكرة محدودة لأن الكائنات تُجلب عند الطلب.
هل يحافظ الدمج على bookmarks والروابط؟ مسار الدمج الافتراضي ينقل بنية المستند؛ أما Fast فيبادل حفظ شجرة البنية بالسرعة. تحقق بمدخلاتك الحقيقية: افتح الناتج، امش في outline، وافحص بعض الروابط الداخلية — اختبار بخمس دقائق أنهى كثيرا من سلاسل الدعم الطويلة.
هل أستطيع التحرير عبر Direct Access أم القراءة فقط؟ توجد منطقة وسطى مفيدة: عمليات مستوى الصفحة مثل DARotatePage و DAMovePage و DAHidePage وقراءات حقول النماذج تعمل على المقبض، وتثبتها DAAppendFile تزايديا. أما تحرير مستوى المحتوى فيبقى من اختصاص طبقة المستند الكاملة.
مقالات ذات صلة
إذا كان الناتج المدمج يجب أن يبقى قابلا للوصول، فخلفية شجرة البنية مشروحة في مقالة الوصول في Tagged PDF — فهي تشرح ما كان متغير Fast في الدمج سيتخلى عنه. ولإخراج المحتوى من النطاقات التي تقسمها، راجع دليل استخراج النصوص والصور والخطوط.
قائمة دوال Direct Access الكاملة تأتي مع المكتبة؛ الإصدارات وتنزيلات التجربة موجودة في صفحة منتج PDFlibPas.