يمنحك PDFium Component طريقة واحدة لتقسيم PDF: ImportPages. وكل ما عدا ذلك، سواء كنت تعزل صفحة واحدة، أو تقطع عند حدود اعتباطية، أو تتبع بنية الإشارات المرجعية الخاصة بالمستند نفسه، ليس سوى طرق مختلفة لتقرير أرقام الصفحات التي تذهب إلى كل ملف خرج. أما الآليات فتبقى كما هي. وفهم ذلك مبكرًا يوفر عليك كثيرًا من المنعطفات الخاطئة
كيف تعمل حلقة التقسيم
النمط واحد بصرف النظر عن طريقة تقسيمك للمستند المصدر. أنشئ نسخة TPdf جديدة، واستدعِ CreateDocument عليها لتهيئة ملف PDF فارغ في الذاكرة، واستورد الصفحات التي تريدها بـ ImportPages، واحفظ النتيجة، ثم أعد ضبط Active إلى False قبل التكرار التالي. وتلك الخطوة الأخيرة هي التي يغفل عنها الناس: فـ CreateDocument لا يغلق ضمنيًا المستند الذي لا يزال في الذاكرة، ولذلك يجب أن تحفظ خرجك وتعيد ضبط Active := False صراحةً قبل استدعائه مرة أخرى؛ وإعادة الضبط أولًا تبقي الحالة نظيفة ومحددة جيدًا. ويُعاد استخدام نسخة 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 سلسلة أرقام صفحات تبدأ من 1؛ نقطة الإدراج 1 = الموضع الأول
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // أعد الضبط قبل 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;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
التحقق قبل الوصول إلى ImportPages مهم هنا. فـ ImportPages تعيد False عندما يتجاوز رقم صفحة في سلسلة النطاق Source.PageCount، لكنها لا تثير استثناءً ولا تنتج ملف خرج جزئيًا يمكنك اكتشافه بالاسم وحده. افحص القيمة التي تعيدها 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;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // تخطَّ قسمًا مشوّهًا بدلًا من كتابة ملف فارغ
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
المستند الذي لا يحوي إشارات مرجعية ليس حالة خطأ تستحق إظهارها للمستخدم على أنها كذلك؛ بل يعني فقط أن وضع التقسيم هذا لا يملك ما يعمل عليه. ويتولى الحارس Length(Bm) = 0 ذلك بصمت. أما ما يستحق الإظهار فهو أن يكون رقم صفحة إشارة مرجعية خارج نطاق المستند، وهو ما يحدث في الملفات المشوّهة التي لم يُحدَّث فيها المخطط التفصيلي بعد حذف صفحات. ويتخطى فحص الحدود على StartPage وEndPage تلك المدخلات بدلًا من تمرير نطاق مهمل إلى ImportPages
تسمية ملفات الخرج وإعادة ضبط Active
تحتاج سلامة أسماء الملفات المشتقة من الإشارات المرجعية إلى اهتمام صريح. فعناوين الإشارات المرجعية قد تحوي محارف صالحة في سلسلة PDF لكنها غير صالحة في مسار نظام ملفات. وكحد أدنى، استبدل الشرطة المائلة الأمامية والشرطة المائلة العكسية والنقطتين قبل بناء مسار الخرج. وعلى Windows، تُمنع أيضًا * و? و" و< و> و|؛ وحلقة بسيطة على مجموعة ثابتة تغطيها دون جلب تعبير نمطي
يستحق السطر Active := False في نهاية كل تكرار التأكيد لأنه المتطلب الوحيد غير البديهي في النمط. فـ CreateDocument لا يغلق ضمنيًا ما هو مفتوح. وإذا كانت Active لا تزال True عندما يعمل CreateDocument مرة أخرى، فإن المستند الذي لا يزال في الذاكرة لم يُغلق أو يُحفظ قط بشكل صحيح، ولا يمكنك الاعتماد على سلوك محدد جيدًا في تلك الحالة، لذا احفظ وأعد الضبط صراحةً قبل بدء المستند التالي. فكّر فيه على أنه قرين try/finally: فكتلة finally تحرر الكائن الخارجي؛ وActive := False يعيد ضبط حالة المستند الداخلي بين تكرارات الحلقة
يبقى استخدام الذاكرة عبر مهمة تقسيم كبيرة مستويًا بهذا النهج لأنك لا تحتفظ أبدًا بأكثر من مستند خرج واحد في الذاكرة في آن واحد. ويظل المستند المصدر مفتوحًا وللقراءة فقط طوال الوقت؛ فـ ImportPages تنسخ بيانات الصفحات إلى المستند الجديد دون تعديل المصدر. وإذا كان المصدر مشفرًا، فافتحه بكلمة مروره قبل الحلقة وستكون الصفحات المنسوخة في كل ملف خرج غير مشفرة، وهو عادةً السلوك الصحيح لخرج تقسيم يوزَّع على مستلمين مختلفين
أمر آخر بخصوص SaveAs: إنها تعيد Boolean. فمجلد خرج غير موجود، أو مسار يحوي محارف يرفضها نظام التشغيل، أو حالة امتلاء القرص، كلها تجعل SaveAs تعيد False دون إثارة استثناء. وفي مهمة دفعية تقسّم مستندًا من 200 صفحة إلى 200 ملف بصفحة واحدة، يسهل إغفال فشل صامت عند الصفحة 147. افحص القيمة المعادة في كل استدعاء وقارن عدد النجاحات بالإجمالي المتوقع عند انتهاء الحلقة
الطريقتان ImportPages وCreateDocument الموضحتان هنا جزء من PDFium Component لـ Delphi وC++Builder