الدمج والتقسيم هما عمليتان للصفحات يلجأ إليهما الجميع في البداية، وتغطيان الكثير من المهام. لكنها لا تغطي كل شيء. هناك عائلة منفصلة من العمل تعيد ترتيب الصفحات بدلاً من نقل ملفات كاملة: وضع أربع شرائح على ورقة واحدة كنشرة، سحب صفحة من خلف المستند إلى المقدمة، أو سحب الصفحات 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 هي نقطة واحدة، وهي واحد على اثنين وسبعين من البوصة. تعلن الوحدة عن الحجم المادي لورقة الإخراج، وليس لها أي علاقة ببكسلات الشاشة أو نقطة في البوصة (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 مرتين. هذا تحرير مزدوج (double-free)، وهو الخلل المحدد الذي أصاب متصلًا هنا مرة واحدة. القاعدة التي تمنع ذلك قصيرة. أغلق المستند في مسار واحد فقط، عن طريق تحرير TPdf الذي سلمته لك الطريقة، ولا تتجاوز أبدًا الغلاف لإغلاق المقبض الذي اعتمده بالفعل
ينتج عن ذلك نتيجتان. أولاً، تُرجع الطريقة nil عندما يرفض PDFium الوسيطات، مثل الصفر على أي من محاور الشبكة أو فشل التخصيص، لذا فإن التحقق من nil ينتمي إلى ما قبل لمس النتيجة. ثانيًا، قم بتهيئة متغير الإخراج الخاص بك إلى nil قبل try وحرره في finally، كما تفعل العينة أعلاه، بحيث لا يمكن أن يتركك الفشل في منتصف الطريق تقوم بتحرير مرجع غير محدد أو تخطي التحرير تمامًا
إعادة ترتيب الصفحات بدون إعادة كتابتها
يبني الفرض مستندًا جديدًا. تعمل إعادة الترتيب على تغيير مستند واحد في مكانه. ترفع MovePages مجموعة من الصفحات من مواضعها الحالية وتسقطها في وجهة، وتزيح كل شيء آخر حول الكتلة المنقولة بحيث يظل عدد الصفحات كما هو:
function MovePages(
const PageIndices: array of Integer;
DestPageIndex : Integer): Boolean;
الفهارس مبنية على الصفر. تُسرد PageIndices الصفحات التي سيتم نقلها، بالترتيب الذي يجب أن تنتهي إليه، و DestPageIndex هو الفهرس الذي تهبط عليه أول صفحة منقولة بعد استقرار النقل. نظرًا لأن PDFium يعيد تحديد موقع الصفحات بدلاً من نسخ محتواها وإعادة ضغطه، فإن العملية رخيصة وغير قابلة للفقد (lossless): تحتفظ كائنات الصفحة بتدفقاتها ومواردها ودقتها. هذه هي الدعوة وراء لوحة صفحات السحب وإعادة الترتيب، حيث يسحب المستخدم صورة مصغرة إلى فتحة جديدة وتقوم بتأكيد الترتيب الجديد بحركة واحدة. تُرجع 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، جنبًا إلى جنب مع تحميل، وتصيير، وتحرير API التي تمت تغطيتها في مكان آخر على هذه المدونة