يعد الدمج والتقسيم عمليتي الصفحات اللتين يبدأ بهما الجميع أولاً، وهما يغطيان الكثير من الجوانب. لكنهما لا يغطيان كل شيء. هناك عائلة منفصلة من العمل تقوم بإعادة ترتيب الصفحات بدلاً من نقل ملفات كاملة: مثل وضع أربعة شرائح في ورقة واحدة لتوزيعها، أو سحب صفحة من نهاية المستند إلى مقدمته، أو سحب الصفحات 3 و 7 و 12 في مقتطف قصير دون لمس الباقي. يكشف PDFium عن ثلاث طرق لهذا الغرض تماماً، وتتصرف كل منها بشكل مختلف عن الدمج والتقسيم اللذين تعرفهما بالفعل. يستعرض هذا المقال ما تفعله، وأين تعيش نقاط الإخراج، وتفاصيل ملكية واحدة تسببت في انهيار في بيئة العمل.
والثلاثة هي ImportNPagesToOne لفرض N-up، و MovePages لإعادة الترتيب في المكان، و ImportPagesByIndex لاستخراج مجموعة فرعية. يكدس الدمج المستندات من النهاية إلى النهاية ويترك عدد الصفحات مساوياً لمجموع المدخلات. ويكتب التقسيم عدة ملفات مخرجات من مدخل واحد. تقع العمليات الثلاث هنا في المنتصف: إحداها تغير عدد صفحات المصدر التي تشترك في ورقة واحدة، والأخرى تغير الترتيب داخل مستند واحد، والأخيرة تنسخ حفنة مختارة من الصفحات إلى مستند آخر. معرفة أيها المناسب يغنيك عن إجبار عملية الدمج والحذف حيث يكفي استدعاء واحد.
ما يفعله فرض N-up بالفعل
الفرض (Imposition) هو مصطلح ما قبل الطباعة لترتيب عدة صفحات مصدر في ورقة واحدة أكبر بحيث تقرأ النتيجة المطبوعة والمطوية بالترتيب الصحيح. النسخة اليومية هي التوزيع الثنائي (2-up)، أو كتيب رباعي (4-up)، أو ورقة اتصال تناسب اثني عشر صورة مصغرة في الصفحة. يتعامل PDFium مع الهندسة من خلال استدعاء واحد:
function ImportNPagesToOne(
OutputWidth, OutputHeight: Single;
NumX, NumY : Cardinal): TPdf;
يصف NumX و NumY الشبكة. تضع القيمة 2, 1 صفحتين مصدر جنباً إلى جنب؛ وتجمع 2, 2 أربعة في تخطيط ربعي؛ وتبني 4, 3 ورقة اتصال من اثني عشر صفحة. يقرأ PDFium صفحات المصدر بالترتيب، ويقلص حجم كل منها لتناسب خليتها، ويملأ الشبكة من اليسار إلى اليمين، ومن الأعلى إلى الأسفل، ويبدأ ورقة إخراج جديدة كلما امتلأت الشبكة الحالية. لا يتم تعديل صفحات المصدر. ما تحصل عليه في المقابل هو مستند جديد صفحاته مركبة.
حجم الإخراج بالنقاط، وليس بالبكسل
كلا من OutputWidth و OutputHeight هما وحدتا مستخدم PDF، ووحدة مستخدم PDF هي نقطة واحدة، وهي واحد من اثنين وسبعين من البوصة. تعلن الوحدة عن الحجم المادي لورقة الإخراج، وليس لها علاقة ببكسلات الشاشة أو نقطة لكل بوصة للرسم (render DPI). هذا هو المكان الأكثر شيوعاً للخطأ في الفرض، لأن المطور المعتاد على الصور النقطية يبحث عن عدد البكسل وينتهي به الأمر بورقة بحجم طابع بريدي أو لوحة إعلانية.
الأرقام التي تستحق الحفظ هي حجما الصفحتين اللذان ستستخدمهما كثيراً. US Letter هو 612 في 792 نقطة، لأن 8.5 بوصة في 72 هي 612 و 11 بوصة في 72 هي 792. A4 هو تقريباً 595 في 842 نقطة، من أبعاده 210 في 297 مليمتر. يذكر رأس الربط نفسه القاعدة بوضوح، وهي أن الوحدة الواحدة هي جزء من اثنين وسبعين من البوصة، وتشحن الوحدة ثابتاً PointsPerInch يساوي 72 إذا كنت تفضل حساب الحجم من البوصات في الكود بدلاً من كتابة القيمة الحرفية.
const
LetterW = 612.0; // 8.5 in * 72
LetterH = 792.0; // 11 in * 72
var
Source, Composite: TPdf;
begin
Source := TPdf.Create(nil);
Composite := nil;
try
Source.FileName := 'slides.pdf';
Source.Active := True;
// Four source pages per Letter sheet, 2 by 2 grid.
Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
if Composite = nil then
raise Exception.Create('PDFium rejected the imposition arguments');
Composite.SaveAs('slides-4up.pdf');
finally
Composite.Free; // see the next section: this is mandatory
Source.Free;
end;
end;
المقبض المرتجع ملكك لتحريره
اقرأ التوقيع مرة أخرى. يرجع ImportNPagesToOne كائن TPdf، وليس قيمة منطقية (Boolean). قيمة الإرجاع هذه هي مقبض مستند جديد تماماً، مخصص بشكل منفصل عن المصدر، ويمتلكه المستدعي. يظل مصدر TPdf الذي استدعيت الطريقة عليه غير ملموس ولا يزال يمتلك مقبضه الخاص؛ والتركيب هو كائن ثانٍ مستقل. إذا تركت TPdf المرتجع يخرج عن النطاق دون تحريره، فستسرب مستند PDFium كاملاً.
الخطأ الأكثر خطورة يسير في الاتجاه الآخر. في الأساس، تطلب الطريقة من PDFium كائناً جديداً FPDF_DOCUMENT من خلال FPDF_ImportNPagesToOne، ثم تغلف هذا المقبض الخام داخل TPdf المرتجع بحيث يحكم عمر المغلف المقبض. من تلك النقطة فصاعداً، هناك مالك واحد بالضبط للمقبض، ومكان واحد بالضبط يجب إغلاقه فيه: عندما تقوم بـ Free للكائن المرتجع. مسار الخطأ المهمل الذي يحرر المغلف ويستدعي أيضاً FPDF_CloseDocument على المقبض الخام الذي التقطه يغلق نفس مستند PDFium مرتين. هذا تحرير مزدوج، وهو الخلل المحدد الذي أصاب مستدعياً هنا ذات مرة. القاعدة التي تمنع ذلك قصيرة. أغلق المستند في مسار واحد فقط، عن طريق تحرير TPdf الذي سلمته لك الطريقة، ولا تصل أبداً إلى ما وراء المغلف لإغلاق المقبض الذي تبناه بالفعل.
يخلص هذا إلى نتيجتين. أولاً، ترجع الطريقة nil عندما يرفض PDFium الوسائط، مثل الصفر في أي من محوري الشبكة أو فشل التخصيص، لذا فإن فحص nil يجب أن يكون قبل لمس النتيجة. ثانياً، قم بتهيئة متغير الإخراج إلى nil قبل try وحرره في finally، كما يفعل النموذج أعلاه، بحيث لا يؤدي أي فشل في المنتصف إلى تركك تحرر مرجعاً غير محدد أو تتخطى التحرير تماماً.
إعادة ترتيب الصفحات دون إعادة كتابتها
يبني الفرض مستنداً جديداً. وتغير إعادة الترتيب مستنداً واحداً في مكانه. يرفع MovePages مجموعة من الصفحات من مواقعها الحالية ويسقطها في وجهة ما، مع إزاحة كل شيء آخر حول الكتلة المنقولة بحيث يظل عدد الصفحات كما هو:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
الفهارس تبدأ من الصفر. يسرد PageIndices الصفحات التي سيتم نقلها، بالترتيب الذي يجب أن تنتهي به، و DestPageIndex هو الفهرس الذي تهبط عليه الصفحة الأولى المنقولة بعد استقرار النقل. ولأن PDFium يعيد تحديد موقع الصفحات بدلاً من نسخ محتواها وإعادة ضغطه، فإن العملية رخيصة وغير ضارة بالبيانات: تحتفظ كائنات الصفحة بتدفقاتها ومواردها ودقتها. هذا هو الاستدعاء الكامن وراء لوحة سحب وإعادة ترتيب الصفحات، حيث يسحب المستخدم صورة مصغرة إلى موضع جديد وتلتزم بالترتيب الجديد بنقلة واحدة. يرجع الاستدعاء False عندما يكون الفهرس خارج النطاق، لذا تحقق من صحة النتيجة بدلاً من افتراض حدوث إعادة الترتيب.
var
Doc: TPdf;
begin
Doc := TPdf.Create(nil);
try
Doc.FileName := 'report.pdf';
Doc.Active := True;
// Move the last page (index 4 in a 5-page file) to the very front.
if not Doc.MovePages([4], 0) then
raise Exception.Create('MovePages rejected the index');
Doc.SaveAs('report-reordered.pdf');
finally
Doc.Free;
end;
end;
سحب مجموعة فرعية بالفهرس
تنسخ العملية الثالثة مجموعة صريحة من الصفحات من مستند إلى آخر. يأخذ ImportPagesByIndex المستند المصدر ومصفوفة فهارس تبدأ من الصفر، ويدرج تلك الصفحات في الهدف عند موضع محدد:
function ImportPagesByIndex(
Source : TPdf;
const PageIndices: array of Integer;
InsertAt : Integer= 0): Boolean;
تستدعيها على المستند الهدف وتمرر المصدر كمعلمة أولى. يحدد PageIndices صفحات المصدر المراد سحبها، بالترتيب الذي تريده؛ InsertAt هو الموضع الذي يبدأ من الصفر في الهدف حيث تذهب الصفحة الأولى المستوردة، لذا يضعها 0 قبل الصفحة الأولى الموجودة ويلحقها بعدد صفحات الهدف الحالي. تستورد المصفوفة الفارغة كل صفحة، مما يجعل الاستدعاء نسخة كاملة عندما تحتاج إليها. يرجع False إذا كان أي فهرس خارج النطاق في المصدر.
هذا هو المكان الذي يهم فيه التباين مع التقسيم. يكتب التقسيم ملفات منفصلة، وتنتج عملية واحدة مخرجات متعددة على القرص. يفعل ImportPagesByIndex العكس تماماً: فهو يجمع مجموعة مختارة من الصفحات في مستند هدف واحد في الذاكرة، ثم تحفظه مرة واحدة. عندما تكون المهمة هي "أعطني الصفحات 3 و 7 و 12 كملف PDF قصير واحد"، فإن هذا هو الطريق المباشر، وهو يغلف FPDF_ImportPagesByIndex في الأسفل.
var
Source, Excerpt: TPdf;
begin
Source := TPdf.Create(nil);
Excerpt := TPdf.Create(nil);
try
Source.FileName := 'manual.pdf';
Source.Active := True;
Excerpt.CreateDocument; // start an empty target
// Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
raise Exception.Create('A requested page index is out of range');
Excerpt.SaveAs('manual-excerpt.pdf');
finally
Excerpt.Free;
Source.Free;
end;
end;
تجميع كل شيء معاً بنظافة
الشكل من البداية إلى النهاية هو نفسه في الثلاثة: افتح المصدر عن طريق تعيين FileName وتبديل Active إلى True، وقم بالعملية، واحفظ باستخدام SaveAs، وحرر ما تملكه. الفرع الوحيد الذي يحتاج إلى رعاية هو الاستدعاءات التي تخصص مستنداً جديداً. يقوم MovePages بتعديل المستند الذي تحتفظ به بالفعل، لذا هناك كائن واحد لتحريره. ويكتب ImportPagesByIndex في هدف قمت بإنشائه بنفسك، لذا تقوم بتحرير المصدر والهدف الذي فتحته. ImportNPagesToOne هو الاستثناء، لأن المستند الجديد هو قيمة إرجاع الطريقة وليس شيئاً قمت بإنشائه، ونسيان أنه مقبض منفصل يمتلكه المستدعي هو كيفية حدوث كل من التسرب والتحرير المزدوج. قم بتهيئة النتيجة إلى nil، وتحقق منها بعد الاستدعاء، وحررها في مسار واحد.
إذا كان العمل الفعلي لديك هو دمج ملفات كاملة بدلاً من إعادة ترتيب الصفحات، فراجع دمج ملفات PDF متعددة في مستند واحد. وإذا كان العكس، أي تقسيم مستند واحد إلى ملفات متعددة، فراجع تقسيم مستندات PDF إلى ملفات متعددة. تشحن طرق الفرض وإعادة الترتيب الموصوفة هنا كجزء من مكون PDFium لـ Delphi و C++Builder، جنباً إلى جنب مع واجهات برمجة التطبيقات للتحميل والرسم والتحرير المغطاة في مكان آخر في هذه المدونة.