مقال تقني

أختام الصفحات القابلة لإعادة الاستخدام عبر Form XObjects مع PDFium

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

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

مخطط يقابل رسم معاملات علامة مائية على كل صفحة PDF بتخزينها مرة واحدة في Form XObject مع PDFium
إعادة رسم الختم على كل صفحة تكرر بايتاته عبر كل تيار محتوى، بينما يخزن Form XObject العمل الفني مرة واحدة ويدع كل صفحة تشير إليه

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

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

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

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

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

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // صفحة واحدة من الرسم الفني
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // التقط الصفحة 0 من مستند الختم إلى مقبض قابل لإعادة الاستخدام
    // تملكه Dest. يجب أن يكون المصدر Active؛ والفهرس مستند إلى 0.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... ضعه ثم حرره قبل إغلاق Stamp (انظر أدناه) ...

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

وضع الختم على صفحة

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

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

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
begin
  // الصفحة الحالية لـ Dest تستلم نسخة واحدة من XObject.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // ضعه: حرّك 200 وحدة إلى اليمين و500 إلى الأعلى، بمقياس 70%.
  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // ثبّت تعديلات هذه الصفحة في تدفق محتواها
  // if not Dest.SaveAs(...) then ... عندما تنتهي كل الصفحات.
end;

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

قاعدة عمر المقبض التي تلدغ الناس

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

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

مخطط دورة حياة مرتب لأختام صفحات PDFium يوضح الالتقاط والوضع وتحرير مقبض TPdfXObject وإغلاق مستند الختم أخيرًا
التقط الختم مرة واحدة، وضعه على كل صفحة، وحرر الـ XObject ما دام مستند الختم مفتوحاً، ثم احفظ وأغلق المصدر أخيراً

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

الموضع عبارة عن تحويل تآلفي ثنائي الأبعاد، وهو نفس التحويل الذي يستخدمه 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 : المقياس الأفقي والعمودي
// b, c : حدّا القص / الدوران
// e, f : الترجمة (المكان الذي تهبط فيه نقطة الأصل على الصفحة)

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

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

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

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 1. التقط الرسم الفني مرة واحدة. Stamp نشِط هنا.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. ضع نسخة على كل صفحة من Dest. PageNumber مستند إلى 1.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // اجعل الصفحة I هي الحالية
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // علامة مائية قطرية
          M.Translate(150, 100);             // تحريكه إلى موضعه
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // ثبّت تعديلات هذه الصفحة
      end;
    finally
      XObject.Free;                          // 3. حرّره قبل إغلاق Stamp
    end;

    // 4. اكتب النتيجة بينما لا يزال Dest مفتوحًا.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // المصدر يُغلق أخيرًا
    Dest.Free;
  end;
end;

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

تشريح FS_MATRIX يوضح المعاملات الأفينية الستة التي يستخدمها PDFium لقياس وتدوير وإزاحة Form XObject مختوم على الصفحة
ستة أعداد تحوّل إحداثيات الختم إلى فضاء الصفحة، ويركب TPdfMatrix القياس والتدوير والنقل بترتيب الاستدعاء ليرسي علامة مائية قطرية

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