مقالة تقنية

تقسيم وتجميع الصفحات وقوالب العلامات المائية في ملفات PDF بدلفي

يقوم تقسيم وتجميع الصفحات بتركيب محتوى الصفحات الموجودة على قماش مشترك، وهو إجراء يختلف عن دمج الملفات. تنفذ مكتبة losLab PDF Library تجميع صفحات Form XObject وقوالب العلامات المائية القابلة للتجميع لدلفي: تقوم StitchPageOverlay، وStitchPagesSideBySide، وStitchPagesVertically بوضع الصفحات الملتقطة بأي مقياس مع دقة متجهة كاملة، وتقوم الخاصية TPDFlibCompositeWatermark بتكديس العلامات المائية النصية والصورية عبر نطاق صفحات في استدعاء واحد. إذا كان ما تحتاجه بالفعل هو إلحاق صفحات كاملة من عدة مستندات في ملف واحد، فهذا ينتمي لعائلة الدمج المغطاة في مقالة الدمج السريع لـ PDF على مستوى البايت؛ وتدور هذه المقالة حول دمج محتوى الصفحات داخل مساحة إحداثيات واحدة

إن الطلبات التي تؤدي إلى هذا المسار عادية ومتكررة. تريد مدرسة فرض صفحتين من الامتحان على ورقة أفقية واحدة لتقليل استهلاك الورق إلى النصف. يريد مراجع الترجمة رؤية الصفحة الإنجليزية وترجمتها جنباً إلى جنب في صفحة واحدة، مع إمكانية تمريرهما معاً. ويريد الفريق القانوني ختم كل مسودة بثلاث طبقات في نفس الوقت: شعار في الزاوية، وكلمة CONFIDENTIAL بشكل قطري في المنتصف، وسطر تذييل صغير، في الصفحات من 1 إلى 3 فقط. تمثل هذه الحالات الثلاث نفس المشكلة الأساسية، وهي وضع محتوى صفحة منسق بالفعل في مكان آخر بحجم آخر دون تقليل جودته إلى صورة نقطية (bitmap)

لماذا تفشل عملية دمج تدفقات محتوى PDF؟

يفشل التجميع البسيط، الذي ينسخ تدفق /Contents للصفحة المصدر إلى الصفحة المستهدفة، لثلاثة أسباب هيكلية. أولاً، تتصادم الإحداثيات: يفترض كلا تدفقي المحتوى أن أصل الإحداثيات الخاص بهما يقع في أسفل يسار MediaBox الخاص بكل منهما، وبالتالي يهبط المحتوى الملصق فوق المحتوى الموجود في المكان الخطأ. ثانياً، تتصادم أسماء الموارد: تحلل كل صفحة معاملات مثل /F1 Tf و/Im0 Do مقابل قاموس /Resources الخاص بها، وبشكل روتيني تربط صفحتان نفس الاسم بخطوط أو صور مختلفة، مما يجعل إحداهما تعرض بصمت بمورد خاطئ. ثالثاً، لا تمنحك نسخة التدفق الخام أي تكبير على الإطلاق؛ وأقصى ما يمكنك فعله هو الإزاحة (translate)

تحل Form XObjects المشكلات الثلاثة دفعة واحدة، ولهذا السبب تبني مكتبة losLab PDF Library تجميعها عليها. يحدد معيار ISO 32000-1 §8.10 كائن Form XObject كتدفق محتوى مستقل بذاته وله قاموس /Resources ومساحة إحداثيات خاصة به، ويتم وضعه في الصفحة عن طريق استدعاء اسمه تحت أي مصفوفة تحويل حالية نشطة. يقوم CapturePage بلف صفحة كاملة موجودة بالفعل، بمحتواها ومواردها، في كائن XObject من نوع /Subtype /Form، ثم تقوم وظائف التجميع بوضعه باستخدام مصفوفة cm: التكبير، والإزاحة، والتدوير، حسب الرغبة. ونظراً لأن المصدر يظل متجهاً (vector), يظل النص نصاً وتظل المسارات مسارات عند أي مستوى تكبير، وهي نفس الخاصية التي تجعل الرسومات المتجهة وأنماط التظليل مستقلة عن الدقة. لا يتم نقطة أي شيء، ويعني قاموس الموارد المعزول أن الصفحة الملتقطة لا يمكنها منازعة الصفحة المستهدفة على الاسم /F1

كيف تقوم بجمع صفحتي PDF في صفحة واحدة؟

تعتبر الوظيفة StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) هي الإجابة المباشرة: فهي تلتقط كلا الصفحتين المصدر، وتنشئ صفحة جديدة أوسع، وتضع اللقطتين يساراً ويميناً مع وجود مسافة فاصلة بينهما، بحيث يتم تكبير كل منهما لتناسب TargetHeight. تعيد الوظيفة رقم الصفحة الجديدة (يبدأ من 1) أو 0 عند الفشل، ويتم التقاط وإخفاء كلتا الصفحتين المصدر كجزء من العملية. حالة الامتحان بصفحتين لكل ورقة هي وظيفة تتكون من ثلاثة أسطر

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

بالنسبة للتجميع الرأسي، تقوم الوظيفة StitchPagesVertically(PageRanges, Gap, TargetWidth) بتكديس نطاق صفحات كامل في صفحة واحدة طويلة تشبه الملصق. تقبل المعلمة PageRanges بناء جملة النطاق القياسي مثل '1-3,5'. يتم تكبير كل صفحة مصدر بحيث يطابق عرضها TargetWidth، لذا فإن مساهمة الارتفاع لكل صفحة هي ارتفاعها الخاص مضروباً في عامل التكبير الخاص بها، وارتفاع صفحة الإخراج هو مجموع تلك الارتفاعات بالإضافة إلى فجوة Gap واحدة بين كل زوج. هذا هو الشكل الذي تريده لمستندات المراجعة ذات التمرير المستمر أو الإيصالات الطويلة المجمعة من مصادر بحجم رسالة (letter)

كيف تقوم بتراكب صفحة فوق أخرى؟

ترسم الوظيفة StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) صفحة موجودة بالفعل فوق صفحة موجودة أخرى، بدلاً من إنشاء صفحة جديدة. يتم التقاط الصفحة المصدر كـ Form XObject ورسمها فوق المستهدف عند المستطيل الذي تحدده، مع Opacity من 0 إلى 1 (1 معتم). العرض والارتفاع معلمتان حرتان، لذا يتمدد الموضع إذا اختلفت النسبة بينهما عن نسبة العرض إلى الارتفاع الخاصة بالصفحة المصدر؛ مرر زوجاً متناسباً عندما تريد صورة مصغرة غير مشوهة للمصدر. الاستخدام النموذجي هو مستند يتم الاحتفاظ بكتلة اعتماده كصفحة مستقلة وختمها على الغلاف

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

ماذا يحدث لأرقام الصفحات بعد الالتقاط؟

تستهلك CapturePage الصفحة المصدر: حيث يتم نقل محتواها إلى كائن XObject ويتم إخفاء الصفحة التي أصبحت فارغة الآن، لذا فإن كل صفحة مرقمة أعلى من المصدر تنزاح فوراً إلى الأسفل بمقدار واحد. هذه هي الدلالة الأكثر أهمية في واجهة برمجة تطبيقات التجميع بالكامل. تتعامل مكتبة losLab PDF Library مع العمليات الحسابية داخل كل استدعاء تجميع، وتقوم StitchPageOverlay بضبط فهرس الهدف عندما يقع الهدف أعلى من المصدر، وتقوم StitchPagesSideBySide بالتقاط الصفحة ذات الرقم الأعلى أولاً بحيث يظل فهرس الالتقاط الثاني صالحاً، ولكن الكود المحيط بك يجب أن يأخذ في الاعتبار هذا الانزياح بين الاستدعاءات. هناك قاعدتان تحافظان على صحة كود التجميع المتعدد: قم بتسجيل أي أحجام صفحات أو أرقام صفحات ستحتاج إليها قبل الالتقاط الأول، لأنها تتغير بعد ذلك. وعندما تلتقط عدة صفحات بنفسك في حلقة، كرر بترتيب صفحات تنازلي، بحيث لا يؤدي إخفاء صفحة لاحقة أبداً إلى إزعاج فهرس صفحة سابقة لم تلتقطها بعد

كيف تقوم بتطبيق علامة مائية متعددة الطبقات على نطاق صفحات؟

تحول طبقة قالب العلامة المائية في PDFlibWatermark.pas مبدأ "حدد مرة واحدة، وطبق على صفحات متعددة" إلى كائنات. يحمل TPDFlibWatermarkBase ما تشترك فيه جميع العلامات المائية: Position ذو تسع نقاط (من wpTopLeft إلى wpBottomRight، بالإضافة إلى wpCustom الذي يستخدم إحداثيات X/Y الخام)، وAngle بالدرجات، وOpacity من 0 إلى 1، وسلسلة PageRange حيث يعني الفراغ كل الصفحات. يضيف TPDFlibTextWatermark نصاً، ومعرف خط (يفتقر إلى Helvetica افتراضياً)، وحجماً، ولون RGB؛ بينما يضيف TPDFlibImageWatermark معرف صورة من صورة تمت إضافتها مسبقاً بالإضافة إلى عرض وارتفاع الموضع. يحتفظ TPDFlibCompositeWatermark بقائمة من العلامات المائية الفرعية ويطبقها بشكل متكرر، وتعتبر TPDFlib.ApplyWatermark هي نقطة الدخول ذات الاستدعاء الواحد التي تحترم نطاق صفحات القالب وتعيد عدد الصفحات المختومة. يبدو الختم القانوني المكون من ثلاث طبقات كالتالي

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

هناك تفصيلان في التجميع يستحقان التوضيح. وراثة النطاق هي أحادية الاتجاه وغير مدمرة: فالابن ذو الـ PageRange الفارغ يرث مؤقتاً نطاق المركب أثناء Apply، بينما يحتفظ الابن الذي يحدد نطاقه الخاص بنطاقه، لذا يمكنك تثبيت التذييل في كل صفحة بينما يغطي الختم القطري قسم المسودة فقط. وتتبع الملكية المركب: تقوم TPDFlibCompositeWatermark.Destroy بتحرير أبنائها، لذا تقوم بتحرير المركب مرة واحدة ولا شيء غير ذلك، كما في المثال أعلاه

حدود يجدر معرفتها

ينطبق حدان صادقان، وكلاهما يتبع كيفية عمل الآلية. أولاً، يلف الالتقاط تدفق محتوى الصفحة ومواردها فقط. تعيش التعليقات التوضيحية والروابط وحقول النموذج والتعليقات ككائنات منفصلة في قاموس الصفحة، وليس في تدفق المحتوى، لذا فهي لا تنتقل إلى كائن XObject؛ ويحمل التخطيط المجمع محتوى الصفحة المرئي ولكن ليس الطبقة التفاعلية لمصادرها. ثانياً، تحديد موضع النص ذو التسع نقاط هو تقريب: فبدون محرك تخطيط كامل لا يمكن للمكتبة معرفة العرض المعروض الدقيق لسلسلة نصية، لذا فهي تقدر مربع النص بـ 40% من عرض الصفحة عند تحليل مواضع مثل wpCenter. هذا دقيق بما يكفي لتوسيط تسمية قصيرة، ولكن لوضع دقيق بالبكسل للسلاسل النصية الطويلة، انتقل إلى wpCustom واحسب الإحداثيات بنفسك

التكبير ليس قيداً عملياً هنا، نظراً لأن الالتقاط والتجميع يعالجان مراجع الكائنات بدلاً من إعادة ترميز محتوى الصفحة؛ وإذا كانت مصادرك مئات الميجابايت، فإن نفس الانضباط الموضح في دليل دمج وتقسيم الملفات الكبيرة مع الوصول المباشر ينطبق قبل التجميع. تشحن وظائف التجميع وفئات قالب العلامة المائية الموضحة هنا في losLab PDF Library الحالية لدلفي، وC# وVB.NET، إلى جانب العناصر الأولية ذات المستوى الأدنى CapturePage وDrawCapturedPage وDrawCapturedPageMatrix عندما تحتاج إلى تخطيط لا تغطيه استدعاءات التجميع الثلاثة المبنية مسبقاً