Technical Article

طوابع الصفحات القابلة لإعادة الاستخدام عبر كائنات Form XObjects مع PDFium

يبدو وضع علامة مائية أو شعار على كل صفحة من صفحات المستند كأنه مهمة تستغرق خمس دقائق حتى تفتح النتيجة في فاحص حجم الملف. النهج الواضح هو المشي في الصفحات وبناء نفس نصوص كائنات الصور مجدداً على كل صفحة. هذا ينجح بصرياً، ولكنه مبذر بطريقة تتراكم. العلامة المائية المائلة "مسودة" المرسومة مباشرة على تقرير مكون من مائة صفحة تمثل مائة نسخة من نفس مسار وبيانات النص الموجودة في تدفقات المحتوى، ويحمل الملف المحفوظ كل واحدة منها.

كائن Form XObject هو الهيكل الذي يوفره PDF لتجنب هذا بالضبط. فهو يغلف قطعة من المحتوى القابل لإعادة الاستخدام، صفحة كاملة أو قالباً صغيراً، في كائن مسمى واحد يمكن رسمه عدة مرات في مواضع عديدة. يعيش المحتوى في الملف مرة واحدة. وكل صفحة تريد الطابع تحتفظ بتعليمات قصيرة تقول "ارسم XObject N هنا، مع هذا التحويل". وبذلك تضيف العلامة المائية لمائة صفحة كائناً واحداً للمحتوى في الملف بدلاً من مائة، وهذا هو الفرق بين مستند ينمو خطياً مع عدد صفحاته ومستند لا ينمو. العلامات المائية، وطوابع الشعارات، وقوالب أرقام الصفحات، والأختام هي جميعها نفس الشكل من المشاكل، وكائن Form XObject هو الأداة الصحيحة لكل منها.

لماذا يتفوق كائن مخزن واحد على مائة إعادة رسم

التوفير هيكلي، وليس تجميلياً. يتم عرض صفحة PDF عن طريق تنفيذ تدفق المحتوى الخاص بها، وهو تسلسل من مشغلي الرسم. عندما تعيد رسم طابع لكل صفحة، فإنك تلحق تسلسل المشغل الكامل لهذا الطابع بتدفق كل صفحة، وتتكرر البايتات بعدد الصفحات التي لديك. يقوم كائن Form XObject بنقل هؤلاء المشغلين إلى تدفق واحد مخزن مرة واحدة في المستند. المرجع الذي تحتفظ به الصفحة الفردية صغير: فهو يدفع مصفوفة تحويل، ويستدعي XObject، ويستعيد الحالة. ولم يعد عدد الصفحات يضاعف تكلفة العمل الفني.

هذا يهم أكثر عندما يكون الطابع ثقيلاً. الختم المتجه الذي يحتوي على مئات من أجزاء المسار، أو الصورة النقطية للشعار، يكون مكلفاً في التخزين. مخزناً مرة واحدة ويُشار إليه، يتم دفع ثمن الجزء الثقيل لمرة واحدة ويكون العبء الإضافي لكل صفحة عبارة عن بضع بايتات من الاستدعاء. النتيجة البصرية على الصفحة مطابقة لإعادة رسم مباشرة، وهذا هو الهدف. لا يمكن للقارئ معرفة الفرق، لكن حجم الملف يمكنه ذلك بالتأكيد.

التقاط صفحة إلى XObject

يبني PDFium الكائن القابل لإعادة الاستخدام من صفحة موجودة. المصدر هو صفحة في مستند ما قمت بفتحه، أو ملف PDF صغير من صفحة واحدة لا يحتوي على شيء سوى عملك الفني للعلامة المائية، أو صفحة معينة من ملف أكبر. يقوم CreateXObjectFromPage بالتقاط محتوى تلك الصفحة المصدر إلى مقبض قابل لإعادة الاستخدام ينتمي إلى المستند الوجهة، وهو المستند الذي تقوم بختمه.

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile('Report.pdf');
    Stamp.LoadFromFile('Watermark.pdf');   // one page of artwork

    // Capture page 0 of the stamp document into a reusable handle that
    // is owned by Dest. Source must be active; the index is zero-based.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... place it, then free it before closing Stamp (see below) ...

التوقيع هو CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. تعيد الطريقة nil عند الفشل بدلاً من إثارة استثناء، لذا فإن التحقق الصريح أعلاه ليس اختيارياً. المقبض الذي يعود هو TPdfXObject تمتلكه، وقيدا دورة الحياة المرتبطان به هما الجزء الذي يقع فيه الناس في هذا التمرين بأكمله، لذا حصلا على قسم خاص بهما أدناه.

وضع الطابع على صفحة

لا يفعل XObject الملتقط شيئاً بمفرده. لجعله يظهر، تقوم بإدراج نسخة منه في الصفحة الحالية للمستند باستخدام InsertFormObjectFromXObject. يعيد هذا الاستدعاء كائن الصفحة الأساسي، FPDF_PAGEOBJECT، والمقبض المعاد هو كيفية تحديد موضع التنسيب. بدون تحويل، يهبط الطابع عند الأصل في إحداثيات الصفحة المصدر نفسها، وهو ما نادراً ما تريده.

نظراً لأن InsertFormObjectFromXObject يدرج نسخة واحدة لكل استدعاء ويعيد كائن صفحة جديداً في كل مرة، يمكنك رسم نفس كائن XObject عدة مرات في صفحة واحدة بتحويلات مختلفة، ويظل المحتوى المخزن محسوباً لمرة واحدة في الملف. الشعار الموجود في الزاوية والعلامة المائية الخفيفة التي تغطي الصفحة بأكملها يمكن أن يأتيا من نفس الكائن الملتقط.

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
begin
  // The current page of Dest receives one copy of the XObject.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Position it: move 200 units right, 500 up, at 70% scale.
  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    FPDFPageObj_SetMatrix(PageObj, M.Handle);
  finally
    M.Free;
  end;
  // Dest.SaveLoadedDocument(...) when every page is done.
end;

تفصيل ملكية واحد يجعل التنظيف آمناً. بمجرد إدراجه، ينتمي كائن الصفحة إلى الصفحة، وليس إلى XObject. لا يؤدي تحرير كائن XObject لاحقاً إلى إبطال المواضع التي حددتها بالفعل. هذا هو ما يسمح بترتيب الإنشاء والموضع والتحرير الموصوف أدناه بالعمل بشكل صحيح.

قاعدة عمر المقبض التي تؤذي المبرمجين

يحكم مقبض XObject قيدان، ويؤدي تجاهل أي منهما إلى حدوث فشل يبدو غير مرتبط بسببه. أولاً، يجب أن يكون المستند المصدر نشطاً في اللحظة التي تستدعي فيها CreateXObjectFromPage. تقرأ عملية الالتقاط محتوى الصفحة المصدر من المستند المصدر النشط، لذلك يجب أن يكون هذا المستند وصفحته مفتوحين وصالحين عند بناء المقبض. ثانياً، وهذا هو الذي يفاجئ الناس، يجب تحرير المقبض قبل إغلاق الصفحة المصدر، وعملياً قبل إغلاق المستند المصدر الذي أتى منه أو تحريره.

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

المصفوفة، وما تعنيه أرقامها الستة

التنسيب هو تحويل تآلفي ثنائي الأبعاد، وهو نفس التحويل الذي يستخدمه PDF في كل مكان لتحديد موضع المحتوى (ISO 32000-1، القسم 8.3.4). إنه ستة أرقام، مكتوبة a, b, c, d, e, f، ويكشف عنها PDFium كسجل FS_MATRIX. إنهم يعينون نقطة من مساحة الكائن الخاصة إلى مساحة الصفحة:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)

يمكنك ملء تلك القيم الست يدوياً، ولكن كتابتها يدوياً هي المكان الذي يخطئ فيه التدوير، لأن التدوير يخلط كل من a, b, c, d معاً. يركب غلاف TPdfMatrix العمليات الشائعة نيابة عنك ويضربها بالتتابع أثناء تقدمك، لذا تتسلسل عمليات Translate وScale وRotate بالترتيب الذي تستدعيها به. العلامة المائية المائلة هي تدوير يتبعه نقل لإعادة توسيطها، والشعار الموجود في الزاوية هو قياس يتبعه نقل. عندما تكون المصفوفة جاهزة، مرر قيمتها الخام إلى FPDFPageObj_SetMatrix(PageObj, M.Handle)، حيث M.Handle هي FS_MATRIX الأساسية. تتوفر الدالة منخفضة المستوى FPDFPageObj_Transform، التي تأخذ القيم الست مباشرة كأعداد مزدوجة الدقة (double)، عندما تفضل تمرير الأرقام بدلاً من بناء غلاف.

ختم كل صفحة، بالترتيب الصحيح

يجمع النمط الكامل الأجزاء معاً بالترتيب الذي تتطلبه قاعدة دورة الحياة. افتح كلا المستندين، والتقط الطابع مرة واحدة، وامش في الصفحات المستهدفة لتحديد كل واحدة بدورها وإدراج نسخة وتحديد موضعها، ثم حرر XObject، ثم احفظ، ودع المستند المصدر يغلق آخراً.

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  i: Integer;
begin
  Dest := TPdf.Create;
  Stamp := TPdf.Create;
  try
    Dest.LoadFromFile(ASource);
    Stamp.LoadFromFile(AStamp);

    // 1. Capture the artwork once. Stamp is active here.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Place a copy on every page of Dest.
      for i := 0 to Dest.PageCount - 1 do
      begin
        Dest.CurrentPageIndex := i;          // make page i current
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // diagonal watermark
          M.Translate(150, 100);             // nudge into position
          FPDFPageObj_SetMatrix(PageObj, M.Handle);
        finally
          M.Free;
        end;
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    Dest.SaveLoadedDocument(AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

شكل كتل try هو ما يقوم بالعمل الحقيقي. تحرر كتلة finally الداخلية كائن XObject قبل أن يتمكن التحكم من الوصول إلى كتلة finally الخارجية التي تحرر Stamp، وبالتالي يتم تحرير المقبض دائماً بينما لا يزال مصدره حياً، حتى لو حدث استثناء في منتصف الحلقة التكرارية. (استخدم أي محدد صفحة حالية يكشفه بناؤك، فجسم الحلقة هو نفسه في كلتا الحالتين.)

الختم هو ركن واحد من مجموعة أدوات أكبر لبناء محتوى الصفحة وتحريره. إذا كان طابعك عبارة عن صورة وليس صفحة ملتقطة، فإن تحويل الصور إلى مستندات PDF باستخدام PDFium يغطي إدخال تلك الصورة النقطية في مستند أولاً. وعندما يكون الشيء الذي تريد حمله بجانب الطابع المرئي عبارة عن ملف وليس حبراً على الصفحة، فإن العمل مع مرفقات PDF في Delphi يوضح جانب الملف المضمن. كل هذا يشحن مع مكون PDFium لـ Delphi وC++Builder، إلى جانب واجهات برمجة تطبيقات المعاينة والتحرير والمستندات المغطاة في مكان آخر من هذه المدونة.