دقيقتان لنسخ ثلاث صفحات من ملف PDF من 40 صفحة ليست مشكلة ضبط أداء. إنها إشارة إلى أن مسار API الخاطئ يُستخدم. عندما رأيت هذا الزمن لأول مرة في نموذج نسخ الصفحات الخاص بـ HotPDF Component، كان حدسي أن أفحص بنية المستند أولاً ثم الشفرة. اتضح أن هذا الترتيب مهم
ما كان بطيئًا فعليًا
كان ملف PDF المعني مستندًا مرجعيًا من 40 صفحة مع شجرة صفحات غير بسيطة، أي عدة عقد /Pages وسيطة بدل مصفوفة مسطحة واحدة. كانت الشفرة النموذجية الأصلية تستدعي LoadFromFile، ثم تبني مستندًا جديدًا بـ BeginDoc، ثم تدور على أرقام الصفحات المحددة، وفي كل تكرار تعيد تحميل المستند المصدر من القرص لاستخراج صفحة. هذا يعني كلفة التحليل الكاملة مضروبة في عدد الصفحات المطلوبة. ملف بحجم 12 ميغابايت اصطدم بالقرص ست مرات لاستخراج ثلاث صفحات لأن أحدًا لم يسأل هل يجب أن يبقى الملف مفتوحًا عبر التكرارات
العامل الثاني لم يكن ظاهرًا في الشفرة: إن LoadFromFile في HotPDF يحل جدول المراجع المتقاطعة بالكامل ويفك ضغط كل تدفق كائن عند التحميل. هذا سلوك صحيح لمستند أنت على وشك تعديله، لكنه عمل أكثر مما تحتاجه إذا كنت تريد فقط عدد الصفحات ومجموعة فرعية منها. وللوصول للهيكل بوضع القراءة فقط، فإن DAOpenFileReadOnly يتجنب تسلسل شجرة الكائنات كاملة، وهذا مهم مع الملفات المضغوطة التي تحتوي على موارد صور كبيرة
ولا واحد من هذين خطأ في المكتبة. كلاهما نتيجة اختيار استدعاء API صمم لمهمة واحدة ثم استخدامه لمهمة أخرى
استخدام InsertPagesFromDocument لاستخراج الصفحات
المسار الصحيح لنسخ نطاق من الصفحات من مستند HotPDF إلى آخر هو InsertPagesFromDocument بعد LoadFromFile على المستند المصدر. تحمل المصدر مرة واحدة، ثم تحمل الوجهة أو تنشئها مرة واحدة، ثم تنقل الصفحات وتحفظ النتيجة. ويبقى المصدر في الذاكرة طوال عمليات إدراج الصفحات:
procedure ExtractPages(const SourceFile, DestFile: string;
const PageRange: string);
var
Source, Dest: THotPDF;
begin
Source := THotPDF.Create(nil);
Dest := THotPDF.Create(nil);
try
// Load source once: full parse happens here and only here
Source.LoadFromFile(SourceFile);
// Build a minimal destination document
Dest.FileName := DestFile;
Dest.BeginDoc;
// Copy the requested range; '1-3' inserts pages 1 through 3
// starting at position 1 in the destination
Dest.InsertPagesFromDocument(Source, PageRange, 1);
Dest.EndDoc;
finally
Source.Free;
Dest.Free;
end;
end;
يقبل المعامل PageRange الصيغة نفسها التي يستخدمها المثال سطر الأوامر: قائمة مفصولة بفواصل لأرقام الصفحات أو للنطاقات مثل '1-3' أو '1,5,7-9'. الصفحات هنا تبدأ من 1. ينسخ InsertPagesFromDocument تدفقات المحتوى وقواميس الموارد وهندسة الصفحة من دون لمس البيانات الوصفية أو الإشارات المرجعية أو مرفقات الملفات المضمنة ما لم تكن تلك العناصر مشارًا إليها من الصفحات المنسوخة. لاستخراج ثلاث صفحات من مستند من 40 صفحة، هذه مجموعة عمل صغيرة
الزمن على الملف نفسه بحجم 12 ميغابايت الذي كان يستغرق دقيقتين سابقًا أصبح أقل من 1.5 ثانية بهذا النمط. ومعظم هذا الزمن هو استدعاء LoadFromFile الوحيد. بنية المستند تصبح غير مهمة بعد حل جدول الكائنات للمرة الأولى
عندما يكون LoadFromFile أكثر مما تحتاجه: واجهة Direct File API
إذا كنت تحتاج فقط إلى عد الصفحات أو فحص معلومات المستند أو نسخ ملف من دون لمس محتواه، فإن Direct File API تتجنب التحليل الكامل تمامًا. يربط DAOpenFileReadOnly جدول المراجع المتقاطعة من دون فك ضغط تدفقات الكائنات، لذلك يصبح عدد الصفحات O(xref size) بدلًا من O(file size):
procedure InspectPDF(const FileName: string);
var
Pdf: THotPDF;
Handle, PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Handle := Pdf.DAOpenFileReadOnly(FileName, '');
if Handle <= 0 then
Exit;
try
PageCount := Pdf.DAGetPageCount(Handle);
Writeln('Pages: ', PageCount);
// DACopyFile is a byte-preserving copy, no re-serialization
Pdf.DACopyFile(FileName, 'archive-copy.pdf');
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
الاستثناء هنا أن DAOpenFileReadOnly يقبل معامل كلمة مرور لكنه يعود إلى تحليل كامل مع المدخلات المشفرة، لأن فك التشفير يتطلب أن تحل شجرة الكائنات قاموس التشفير. إذا كانت ملفاتك المصدرية مشفرة، ففكها أولًا باستخدام DecryptFile للحصول على نسخة غير مشفرة، ثم افتح تلك النسخة عبر Direct File API. وظيفة DecryptFile على مستوى الملف تأخذ مسار إعادة كتابة مباشرًا بـ AES-256 للتشفير القياسي، وهي أسرع من LoadFromFile متبوعًا بـ SaveLoadedDocument للملفات الكبيرة لأنها لا تبني نموذج الكائنات الكامل في الذاكرة
الذاكرة أثناء المعالجة على دفعات كبيرة
المهام الدُفعية التي تعالج عشرات الملفات في حلقة تبدو صحيحة لكنها تراكم الذاكرة بهذا النمط: إنشاء THotPDF داخل الحلقة، استدعاء LoadFromFile، تنفيذ العمل، ثم استدعاء Free. هذا صحيح بنيويًا. المشكلة تظهر عندما تخصص العملية الداخلية كائنات مؤقتة، تلتقط الاستثناءات، وتترك تلك الكائنات حيّة في مسارات الخطأ. مدير الذاكرة في Delphi لا يضغط الذاكرة، لذلك فإن مئة تسريب في مسارات الخطأ عبر تشغيل دفعي يمكن أن ترفع الاستهلاك إلى درجة تبطئ التخصيص لبقية العمل
والحل ليس غريبًا. كل THotPDF وكل TStream أو TBitmap وسيط يشارك في عمل PDF يجب أن يكون داخل كتلة try/finally حيث يكون Free آخر تعليمة. اضبط المؤشرات المحلية على nil قبل try حتى تستطيع كتلة finally استخدام if Assigned(x) then x.Free بأمان إذا فشلت التهيئة في منتصف الطريق. هذه هي ملكية Delphi القياسية، وهي القصة الكاملة لهذه الفئة من المشكلات
وهناك نقطة أخرى يجب فحصها في سياقات الدُفعات: إن AddImage يسجل الصور في قائمة داخلية تبقى طوال عمر مثيل THotPDF. إذا أعدت استخدام مثيل واحد عبر مستندات كثيرة باستدعاء LoadFromFile مرارًا، فإن تسجيلات الصور من المستندات السابقة تبقى في القائمة. إما أن تنشئ مثيلًا جديدًا لكل مستند أو تستدعي مسار تنظيف قائمة الصور بين المستندات
القياس قبل تغيير أي شيء
قبل اللجوء إلى أي من هذه الأنماط، قس الزمن. يغلّف TStopwatch في Delphi من System.Diagnostics دالة QueryPerformanceCounter وهو دقيق بما يكفي لقياس زمن تشغيل عمليات I/O للملفات. لفّ LoadFromFile وحده وانظر كم يستحوذ من الزمن. إذا كان 90% من الزمن الكلي، فالحل هو Direct File API أو تقليل عدد مرات تحليل الملف نفسه. وإذا كان أقل من 20% فالعنق موجود في مكان آخر وأنت تطارد الشيء الخطأ
اتضح أن الاستخراج الذي استغرق دقيقتين في بداية هذا المقال كان كله بسبب نمط إعادة التحميل المتكرر. بنية المستند لم تضف شيئًا؛ فحتى شجرة صفحات مسطحة كانت ستعمل بالطريقة نفسها. الانتقال إلى استدعاء واحد LoadFromFile يتبعه استدعاء واحد لـ InsertPagesFromDocument خفّض الزمن إلى 1.3 ثانية على العتاد نفسه من دون لمس أي شيء آخر
واجهة معالجة الصفحات المعروضة هنا جزء من HotPDF Component لـ Delphi و C++Builder