يبدو ختم علامة مائية أو شعار على كل صفحة من المستند وكأنه مهمة تستغرق خمس دقائق حتى تفتح النتيجة في فاحص حجم الملف. النهج الواضح هو المرور على الصفحات، وبناء نفس كائنات النص أو الصورة مرة أخرى على كل منها. هذا يعمل بصريًا، ولكنه مهدر بطريقة تتضاعف. العلامة المائية القطرية "DRAFT" المرسومة مباشرة على تقرير من مائة صفحة هي مائة نسخة من نفس المسار وبيانات النص الموجودة في تدفقات المحتوى، والملف المحفوظ يحمل كل واحد منها
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(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // one page of artwork
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 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. تقوم الطريقة بإثارة استثناء إذا لم يكن المستند المصدر Active، وترجع nil بدلاً من الإثارة عندما لا يستطيع PDFium بناء الكائن، لذا فإن الفحص الصريح أعلاه ليس اختياريًا. المقبض الذي يعود هو TPdfXObject تمتلكه، وقيود العمر الافتراضي المرفقة به هي الجزء من هذا التمرين بأكمله الذي يوقع الناس في الخطأ، لذا فقد حصلوا على قسم خاص بهم أدناه
وضع الختم على صفحة
كائن XObject الملتقط لا يفعل شيئًا بمفرده. لجعله يظهر، تقوم بإدراج نسخة منه في الصفحة الحالية للمستند، تلك المحددة بواسطة خاصية PageNumber المستندة إلى 1، باستخدام InsertFormObjectFromXObject. يُرجع هذا الاستدعاء كائن الصفحة الأساسي، وهو FPDF_PAGEOBJECT، والمقبض الذي تم إرجاعه هو كيف يمكنك تحديد موضع الإدراج. بدون تحويل يهبط الختم عند نقطة الأصل في إحداثيات الصفحة المصدر نفسها، وهو نادرًا ما يكون المكان الذي تريده
نظرًا لأن InsertFormObjectFromXObject يدرج نسخة واحدة لكل استدعاء ويعيد كائن صفحة جديدًا في كل مرة، يمكنك رسم نفس XObject عدة مرات في صفحة واحدة في تحويلات مختلفة، ولا يزال المحتوى المخزن محسوبًا مرة واحدة في الملف. يمكن أن يأتي شعار الزاوية وعلامة مائية باهتة لكامل الصفحة من نفس الكائن الملتقط
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
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);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits to its content stream
// if not Dest.SaveAs(...) then ... when every page is done.
end;
تفصيلان من تفاصيل التدبير المنزلي يجعلان هذا آمنًا. أولاً، بمجرد الإدراج، ينتمي كائن الصفحة إلى الصفحة، وليس إلى XObject. إن تحرير XObject لاحقًا لا يبطل المواضع التي قمت بها بالفعل. هذا هو ما يتيح لترتيب إنشاء-وضع-تحرير الموضح أدناه العمل. ثانيًا، يؤدي الإدراج وتحديد المواضع فقط إلى تغيير قائمة كائنات الصفحة في الذاكرة؛ UpdatePage هو ما يقوم بتسلسل تلك القائمة مرة أخرى في تدفق محتوى الصفحة، لذا فإن الصفحة التي تقوم بتحريرها دون استدعائها تحفظ كما لو لم يتم وضع الختم أبدًا
قاعدة عمر المقبض التي تلدغ الناس
يحكم قيدان مقبض 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، من وحدة 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. 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. PageNumber is 1-based.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := 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
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
شكل كتل try يقوم بالعمل الحقيقي. يحرر finally الداخلي XObject قبل أن يتمكن التحكم من الوصول إلى finally الخارجي الذي يحرر Stamp، لذلك يتم تحرير المقبض دائمًا بينما لا يزال مصدره على قيد الحياة، حتى لو تم إطلاق استثناء في منتصف الحلقة. احصل على هذا التداخل بشكل صحيح وقاعدة العمر الافتراضي ستعتني بنفسها
الختم هو إحدى زوايا مجموعة أدوات أكبر لبناء وتحرير محتوى الصفحة. إذا كان الختم الخاص بك هو نفسه صورة بدلاً من صفحة ملتقطة، فإن تحويل الصور إلى مستندات PDF باستخدام PDFium يغطي إدخال تلك الصورة النقطية في مستند أولاً. وعندما يكون الشيء الذي تريد حمله بجانب الختم المرئي عبارة عن ملف بدلاً من حبر على الصفحة، فإن العمل مع مرفقات PDF في Delphi يوضح جانب الملف المضمن. كل هذا يتم شحنه مع PDFium Component لـ Delphi و C++Builder، إلى جانب واجهات برمجة تطبيقات العرض والتحرير والمستندات التي تمت تغطيتها في مكان آخر في هذه المدونة