تمنحك PDFium Component طريقة واحدة لتقسيم PDF ألا وهي: ImportPages. وأي شيء آخر، سواء كنت تقوم بعزل صفحة مفردة، أو تقطيع المستند بناءً على حدود عشوائية، أو باتباع بنية الإشارات المرجعية الخاصة بالمستند، فهو مجرد طرق مختلفة لتحديد أرقام الصفحات التي ستنتقل إلى كل ملف ناتج. وتبقى الآليات كما هي. وفهم ذلك مبكرًا يوفر عليك الكثير من المسارات الخاطئة
كيف تعمل حلقة التقسيم
يبقى النمط متماثلًا بغض النظر عن كيفية تقسيم المستند المصدر. أنشئ نسخة TPdf جديدة، واستدعِ CreateDocument لتهيئة ملف PDF فارغ في الذاكرة، ثم قم باستيراد الصفحات التي تريدها عبر ImportPages، واحفظ النتيجة، ثم قم بإعادة تعيين علامة Active إلى False قبل التكرار التالي. هذه الخطوة الأخيرة هي التي تفوت الكثيرين: يبدأ CreateDocument دائمًا مستندًا جديدًا، ولكن إذا كانت Active لا تزال True عند التشغيل مرة أخرى، فسيتم تجاهل المستند الموجود في الذاكرة ضمنيًا، لذا فإن إعادة التعيين أولاً تحافظ على الحالة نظيفة ومحددة بشكل جيد. وتتم إعادة استخدام النسخة الخارجية TPdf عبر جميع التكرارات، مما يبقي ضغط تخصيص الذاكرة منخفضًا في المهام الكبيرة
إليك كيف يبدو التقسيم صفحة بصفحة مجردًا إلى أساسياته:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
PdfOut.ImportPages(Source, IntToStr(I), 1);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
PdfOut.SaveAs(OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
إن المعلمة Range المرسلة إلى ImportPages تملك نفس تنسيق السلسلة الذي يستخدمه PDFium داخليًا: قائمة بأرقام الصفحات مفصولة بفواصل أو نطاقات مفصولة بواصلات، جميعها تعتمد على الفهرس 1. النطاق '3' يستورد الصفحة 3. النطاق '1-5' يستورد الصفحات 1 حتى 5 بالترتيب. النطاق '2,5,8' يستورد تلك الصفحات الثلاث. بينما تكون المعلمة الثالثة هي موضع الإدراج המعتمد على 1 في المستند الوجهة؛ ويمرر 1 دائمًا الصفحات المستوردة في بداية ملف يكون لولاه فارغًا، وهذا ما تريده هنا
التقسيم عن طريق نطاقات الصفحات
عندما يقدم المستدعي قائمة مثل 1-12,13-24,25-36، فإنك تقوم بتحليلها إلى أزواج بداية ونهاية ثم تشغيل الحلقة نفسها، منشئًا سلسلة النطاق من كل زوج:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeList[I], 1);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
التحقق من الصحة قبل الوصول إلى ImportPages يُعد أمرًا بالغ الأهمية هنا. يعيد ImportPages قيمة False عندما يتجاوز رقم صفحة في سلسلة النطاق الحد Source.PageCount، لكنه لا يطلق استثناءً (exception) ولا يُنتج ملف إخراج جزئي يمكنك اكتشافه من خلال اسمه فقط. تحقق من القيمة المعادة لـ SaveAs وقم بتسجيل حالات الفشل بشكل منفصل؛ إذ أن النطاق الذي يُنتج ملف إخراج فارغ لا يبدو خاطئًا بوضوح حتى يقوم شخص ما بفتحه
التقسيم عند حدود الإشارات المرجعية
يستخدم النهج الثالث بنية المستند الخاصة بدلاً من القوائم المقدمة خارجيًا. تحمل كل إشارة مرجعية في المستوى الأعلى رقم صفحة كهدف؛ ويمتد القسم الذي تحدده من تلك الصفحة إلى ما قبل صفحة الإشارة المرجعية التالية، أو إلى نهاية المستند للإدخال الأخير
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
PdfOut.ImportPages(Source, RangeStr, 1);
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
PdfOut.SaveAs(OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
المستند الذي لا يحتوي على إشارات مرجعية لا يعتبر حالة خطأ تستدعي إظهارها للمستخدم؛ بل يعني ببساطة أن وضع التقسيم هذا لا يملك شيئًا ليعمل انطلاقاً منه. يتعامل حارس Length(Bm) = 0 مع ذلك بصمت. ما يستحق الإظهار حقًا هو عندما يكون رقم صفحة الإشارة المرجعية خارج نطاق المستند، والذي يحدث في الملفات ذات البنية التالفة حيث لم يُحدّث المخطط أبدًا بعد حذف صفحات. فحص الحدود على StartPage و EndPage يتجاوز تلك الإدخالات بدلاً من تمرير نطاق غير صالح إلى ImportPages
تسمية ملفات الإخراج وإعادة تعيين Active
تحتاج سلامة أسماء الملفات المشتقة من الإشارات المرجعية إلى اهتمام صريح. فقد تحتوي عناوين الإشارات المرجعية على محارف صالحة في سلسلة PDF ولكنها غير صالحة في مسار نظام الملفات. على أقل تقدير، استبدل الشرطة المائلة، والشرطة المائلة العكسية، والنقطتين الرأسيتين قبل بناء مسار الإخراج. وفي نظام التشغيل Windows، تُحظر كذلك الرموز *، ?، "، <، >، و |؛ ويمكن لحلقة بسيطة على مجموعة ثابتة تغطية هذه الرموز دون استدعاء التعبيرات النمطية (regex)
يستحق السطر Active := False في نهاية كل تكرار التأكيد لأنه المطلب الوحيد غير البديهي في هذا النمط. لا يقوم CreateDocument بإغلاق كل ما هو مفتوح ضمنيًا. فإذا استمرت Active على وضع True عند تشغيل CreateDocument مرة أخرى، سيقوم PDFium بتجاهل المستند الحالي والبدء في مستند جديد دون الإبلاغ عن خطأ، ولكن هذا السلوك محدد حسب التطبيق في الحالات الاستثنائية ويكون القصد أكثر وضوحًا عندما تعيد التعيين بشكل صريح. اعتبره بمثابة الزوج المرتبط بـ try/finally: حيث يقوم كتلة finally بتحرير الكائن الخارجي؛ في حين تقوم Active := False بإعادة تعيين حالة المستند الداخلية بين تكرارات الحلقة
يظل استخدام الذاكرة مستقرًا عبر مهمة تقسيم كبيرة مع هذا النهج لأنك لا تحتفظ أبدًا بأكثر من مستند إخراج واحد في الذاكرة في الوقت نفسه. يظل المستند المصدر مفتوحًا وفي وضع القراءة فقط طوال الوقت؛ وتقوم ImportPages بنسخ بيانات الصفحة إلى المستند الجديد دون إجراء أي تعديل على المصدر. إذا كان المصدر مشفرًا، افتحه بكلمة المرور الخاصة به قبل الحلقة، وستكون الصفحات المنسوخة في كل ملف إخراج غير مشفرة، وهو غالبًا السلوك الصحيح للمخرجات المقسمة الموزعة على مستلمين مختلفين
شيء إضافي يخص SaveAs: تعيد هذه الدالة قيمة منطقية Boolean. قد يتسبب وجود دليل إخراج غير متوفر، أو مسار يحمل محارف يرفضها نظام التشغيل، أو أن القرص الصلب ممتلئ، جميعًا في إرجاع SaveAs لـ False دون إصدار أي استثناء. في مهمة مجمّعة تقوم بتقسيم مستند من 200 صفحة إلى 200 ملف من صفحة واحدة، من السهل التغاضي عن الفشل الصامت في الصفحة 147. ولذلك، تحقق من القيمة المعادة في كل استدعاء وأحصِ مرات النجاح مقارنة بالإجمالي المتوقع عند انتهاء الحلقة
أساليب ImportPages و CreateDocument المعروضة هنا جزء من PDFium Component لبيئات Delphi و C++Builder