تستدعي AddText لختم سطر على صفحة PDF بواسطة PDFiumPas، ثم تستدعي فورًا FindFirst للتأكد من وقوع الختم، ويعود البحث فارغًا. النص موجود على الصفحة — يُظهره Acrobat — لكن مكوّن TPdf الخاص بـPDFiumPas يحتفظ ببنية FPDF_TEXTPAGE مخزَّنة مؤقتًا منفصلة، مُحلَّلة مرة واحدة من تدفق محتوى الصفحة، ولا يحدّث تعديل تلك البنية بأثر رجعي من تلقاء نفسه. استعلم عنها قبل تحديثها وستقرأ الصفحة تمامًا كما بدت قبل تغييرك، لا بعده
لماذا يعيد PDFium نصًا قديمًا فور التحرير؟
يغلّف PDFiumPas محرك رسم PDFium من Google لـDelphi وC++Builder، وتصل استدعاءاته الخاصة بالنص والتحرير إلى نظامين فرعيين مختلفين داخل ذلك المحرك. تنتمي FPDF_TEXTPAGE إلى جانب القراءة: تجتاز FPDFText_LoadPage تدفق محتوى الصفحة مرة واحدة وتبني صفحة النص — رموز الحروف، والمواضع، وقياسات الخط، وحدود الكلمات — ويحتفظ PDFiumPas بتلك البنية مخزَّنة مؤقتًا طالما بقيت الصفحة محمَّلة. تعمل استدعاءات التحرير مثل FPDFPage_InsertObject أو FPDFPage_GenerateContent على تمثيل مختلف كليًا، رسم كائنات الصفحة وتدفق المحتوى، ولا يدفع PDFium تلك التغييرات إلى صفحة نص مفتوحة بالفعل من تلقاء نفسه. إعادة بنائها عند كل تعديل كانت ستجعل التحرير الدفعي بطيئًا بشكل غير مقبول، لذا يقايض التصميم تلك التكلفة بقاعدة بدلًا من ذلك — أيًّا كان من يحمل المقبض يغلقه بعد تعديل يغيّر المحتوى، والقراءة التالية تبني واحدة جديدة
داخل ذاكرة التخزين المؤقت للنص في TPdf: FTextPage وLoadTextPage وUnloadTextPage
تتابع TPdf المقبض المخزَّن مؤقتًا في حقل خاص واحد، FTextPage، وتغلّف دورة حياته في طريقتين. تتحقق LoadTextPage مما إذا كانت FTextPage تساوي nil، وفقط في تلك الحالة، تستدعي FPDFText_LoadPage مقابل الصفحة الحالية؛ إذا كان مقبض موجودًا بالفعل، تعيد LoadTextPage استخدامه دون السؤال عمّا إذا تغيّرت الصفحة منذ بنائه. UnloadTextPage هي النصف الآخر: تغلق المقبض الأصلي بـFPDFText_ClosePage، وتضبط FTextPage مرة أخرى إلى nil، وتُسقط أيضًا قائمة روابط الويب المخزَّنة مؤقتًا وأي جلسة بحث قيد التنفيذ، بما أن كليهما اشتُقّ من صفحة النص نفسها ويصبحان قديمين للسبب نفسه
سلوك إعادة الاستخدام دون فحص في LoadTextPage هو بالضبط سبب أهمية الترتيب. يصبّ كل استعلام نص على TPdf — Text، وFindFirst، وGetWebLinks — عبر LoadTextPage أولًا، بحيث طالما أن FTextPage لا تزال تحمل المقبض السابق للتعديل، لا تملك أي من تلك الاستدعاءات أي طريقة لمعرفة أن تغييرًا حدث. لم يكن التنقل بين الصفحات المخاطرة هنا أبدًا: UnloadPage، التي تعمل عند تبديل الصفحات، وإعادة التحميل، وإغلاق المستند، كانت تغلق صفحة النص دائمًا مع الصفحة نفسها. السؤال المفتوح كان دائمًا حول التعديلات المُطبَّقة على الصفحة التي لا تزال جالسًا عليها
أي طرائق PDFiumPas تُحدِّث ذاكرة التخزين المؤقت تلقائيًا؟
تستدعي كل طريقة من طرائق تحرير الصفحة الخاصة بـTPdf نفسها — AddText، وSetText، وSetTextPositions، وAddPath، وRemoveObject، وInsertFormObjectFromXObject — UnloadTextPage قبل استدعاء UpdatePage (دالة FPDFPage_GenerateContent في PDFium) لتسلسل التغيير في تدفق المحتوى. استدعِ أيًّا من هذه وسيعيد أقرب استدعاء تالٍ لـText أو FindFirst أو GetWebLinks بناء صفحة النص من المحتوى كما يقف الآن، دون أي استدعاء إضافي مطلوب من جانبك
var
Pdf: TPdf;
Index: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
// AddText already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
Index := Pdf.FindFirst('Reviewed by J. Alvarez');
if Index >= 0 then
ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
finally
Pdf.Free;
end;
end;
النمط الذي لا يزال ينكسر: تخزين مقبض TextPage الخام مؤقتًا
تعرض TPdf المقبض الحي عبر خاصية للقراءة فقط TextPage، للحالة النادرة التي تحتاج فيها استدعاء دالة FPDFText_* لم يغلّفها PDFiumPas. تلك البوابة الاحتياطية هي أيضًا المكان الوحيد الذي لا يمكن للإبطال التلقائي المساعدة فيه: بمجرد نسخ قيمة FPDF_TEXTPAGE من الخاصية إلى متغير محلي، لا يملك PDFiumPas أي طريقة لمعرفة أنك لا تزال تحملها، ولا طريقة لتحديث نسختك عندما تعمل UnloadTextPage في مكان آخر من شيفرتك
var
Pdf: TPdf;
RawHandle: FPDF_TEXTPAGE;
StaleCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
RawHandle := Pdf.TextPage; // FPDFText_LoadPage handle, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
StaleCount := FPDFText_CountChars(RawHandle);
finally
Pdf.Free;
end;
end;
استخدام مقبض بعد أن عملت عليه FPDFText_ClosePage سلوك غير مُعرَّف في PDFium نفسه، لا اصطلاح PDFiumPas يمكنك اختيار تجاهله — قد يعيد آخر بيانات معروفة، أو لا يعيد شيئًا، أو يعطّل العملية، وأي من هذه يحدث على بناء معين ليس شيئًا يجب أن تعتمد عليه شيفرة التطبيق. القاعدة الآمنة ضيقة: اقرأ Pdf.TextPage حديثًا، مباشرة قبل استدعاء FPDFText_* الذي يحتاجها، ولا تحمل أبدًا نسخة عبر جملة قد تحرر الصفحة
اجمع تعديلاتك في دفعة، ثم استعلم مرة واحدة
لا شيء من هذا يعني أن كل استدعاء AddText أو RemoveObject يحتاج استعلام نص دفاعيًا فورًا بعده للتحقق من النتيجة. تدفع كل طريقة تحرير بالفعل تكلفة إغلاق صفحة النص مرة واحدة؛ الاستعلام بعد كل تعديل منفرد داخل حلقة يدفع تلك التكلفة مرة أخرى دون فائدة، بما أن FPDFText_LoadPage تجتاز تدفق المحتوى بأكمله مرة أخرى في كل مرة تعمل فيها
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
if Pdf.FindFirst('DRAFT') < 0 then
ShowMessage('Watermark cleared');
finally
Pdf.Free;
end;
end;
ينطبق منطق التجميع الدفعي نفسه على حالة البحث تحديدًا. تُكمل FindNext وFindPrevious جلسة بدأتها FindFirst، وتُفكَّك تلك الجلسة بواسطة UnloadTextPage جنبًا إلى جنب مع كل شيء آخر، بحيث يؤدي استدعاء FindNext مرة أخرى بعد تعديل — بدلًا من استدعاء FindFirst مرة أخرى — إلى إطلاق استثناء بدلًا من استئناف بحث بصمت مقابل محتوى لم يعد موجودًا. عامل أي تعديل كحد صارم لكل من محتوى النص وموضع البحث، ودع FindFirst جديدة واحدة على الجانب الآخر من تعديلاتك تلتقط البحث مرة أخرى
أين يلائم هذا العمل مع الاستخراج والتعليقات التوضيحية
استخراج النص العادي — قراءة نص صفحة دون تغيير أي شيء — لا يصطدم أبدًا بأي من هذا، لأن لا شيء يبطل مقبضًا لم يلمسه أي تعديل. لمعرفة كيفية عمل Text ومستطيلات الحروف وحدود الكلمات على صفحة غير مُعدَّلة، تغطي المقالة المرافقة حول استخراج النص عبر PDFiumPas ذلك المجال دون دورة حياة ذاكرة التخزين المؤقت لصفحة النص التي تضيفها هذه المقالة فوقها
تهم دورة حياة ذاكرة التخزين المؤقت أكثر ما تهم في سير العمل الذي يحرر ثم يتصرف فورًا على النتيجة: ختم تصحيح والبحث عنه، أو تنقيح فقرة والتأكد من اختفائها، أو تحديد موضع عبارة لترسية تعليق توضيحي مباشرة بعد إدراج نص بالقرب منها. تلك الحالة الأخيرة تستحق الإشارة إليها بمفردها — يُوضَع تعليق توضيحي لتظليل نص بنقاط رباعية من مستطيلات حروف مقروءة من صفحة النص، بحيث ينتهي تعليق توضيحي مبني من إحداثيات التُقطت قبل تعديل إلى تظليل النقطة الخاطئة بمجرد وقوع التعديل
واجهتا التحرير والنص الخاصتان بـTPdf جزء من مكوّن PDFium لـDelphi وC++Builder، وتحمل صفحة المنتج مرجع الطريقة الكامل لأسطح التحرير والاستخراج والبحث الموصوفة هنا