مقال تقني

مقابض كائنات الصفحة في PDFium قديمة بعد التحويل في Delphi

عندما تُعيد FPDFPage_TransFormWithClip كتابة صفحة، كل مقبض FPDF_PAGEOBJECT تحمله بالفعل لا يزال يصف التحليل من قبل التحويل. يحلّ PDFium Component لـDelphi وC++Builder هذا داخل TransformPageContent، التي تُفرِّغ صفحة النص، وتُعيد توليد المحتوى، ثم تُعيد تحميل الصفحة بحيث ترى الاستعلامات اللاحقة الإحداثيات الجديدة

العرض هادئ. تُطبّق مقياسًا 0.9 لإضافة هامش طباعة، ثم تقرأ PageObjectInfo وتحصل على نفس الأرقام بالضبط التي حصلت عليها قبل الاستدعاء. لا استثناء، لا رمز خطأ، لا شيء في سجل. هذا فشل مختلف عن صفحة النص المُخزَّنة مؤقتًا الموصوفة في مقال صفحات النص القديمة بعد التحرير: هناك الذاكرة المؤقتة مقبض FPDF_TEXTPAGE واحد يمكنك إسقاطه وإعادة بنائه، هنا المشكلة كل مقبض كائن صفحة في متغيراتك الخاصة، بالإضافة إلى فئة من الوصولات (getters) تُبلغ عن الفشل عبر رمز إرجاع يتجاهله معظم المستدعين

لماذا تصبح حدود كائن الصفحة قديمة دون خطأ؟

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

تُعرِّف الفقرة §7.8.2 من ISO 32000-1 دفق المحتوى كتسلسل المُشغِّلات التي ترسم صفحة، وتُعرِّف الفقرة §8.3.3 كيف تُخطِّط مصفوفة التحويل الحالية مساحة المستخدم إلى مساحة الجهاز. تحويل على مستوى الصفحة يُعبَّر عنه بتغليف وإعادة كتابة تلك المُشغِّلات، لا بتعديل إحداثيات كل كائن في مكانه. لذا الإحداثيات التي يحملها الكائنون قد لا تتغيّر إطلاقًا؛ ما يتغيّر هو المصفوفة السارية عند رسمهما. أي مقبض حُلِّل تحت المصفوفة القديمة يُجيب عن أسئلة الهندسة تحت المصفوفة القديمة، ويُجيب دون شكوى

ما الذي تُعيد FPDFPage_TransFormWithClip كتابته فعليًا

تُعيد كتابة الصفحة، لا لقطاتك. تأخذ FPDFPage_TransFormWithClip مصفوفة FS_MATRIX ومستطيل تحديد حواف FS_RECTF وتُطبِّق كليهما على محتوى الصفحة بأكمله. إنها الاستدعاء الصحيح للهوامش، وتحجيم الفرز، وتطبيع صفحة غريبة الحجم مقابل صندوق هدف. إنها الاستدعاء الخاطئ إن كنت تتوقع أن تتبعها المقابض الموجودة، ويستحق التذكير أيضًا أنها تلمس محتوى الصفحة فقط: التعليقات التوضيحية طبقة منفصلة وتحتاج TransformPageAnnotations، التي تُمرِّر نفس معاملات المصفوفة الستة إلى FPDFPage_TransformAnnots

var
  Info: TPdfPageObjectInfo;
  Scale: FS_MATRIX;
  Clip: TPdfRectangle;
begin
  Pdf.PageNumber:= 1;
  Info:= Pdf.PageObjectInfo(0);           // snapshot taken before the transform

  Scale.a:= 0.9;   Scale.b:= 0.0;
  Scale.c:= 0.0;   Scale.d:= 0.9;
  Scale.e:= 29.7;  Scale.f:= 42.0;        // 5% margin, A4 in points
  Clip:= Pdf.GetPageBox(pbMedia);
  Pdf.TransformPageContent(Scale, Clip);

  // Info.Bounds still holds pre-transform geometry, and Info.Handle now
  // points into a page that TransformPageContent has already replaced
end;

ترتيب التحديث الذي تستخدمه TransformPageContent

أربع خطوات، بهذا الترتيب: إفراغ صفحة النص، التحويل، توليد المحتوى، إعادة تحميل الصفحة. تُشغّل TPdf.TransformPageContent بالضبط ذلك التسلسل. تستدعي CheckPageActive، وتنسخ المصفوفة وتحديد الحواف إلى أشكال سجلها الأصلية، وتستدعي UnloadTextPage، ثم FPDFPage_TransFormWithClip، ثم UpdatePage، وهي الغلاف حول FPDFPage_GenerateContent، وأخيرًا ReloadPage

كل خطوة تكسب موضعها. UnloadTextPage تأتي أولًا لأن FPDF_TEXTPAGE المُخزَّنة مؤقتًا تحمل صناديق أحرف محسوبة تحت المصفوفة القديمة، وتُسقط أيضًا قائمة روابط الويب المشتقة وأي جلسة بحث قيد التقدم بُنيت منها. FPDFPage_GenerateContent يجب أن تُشغَّل قبل إعادة التحميل، لأن التحويل يعيش في الصفحة داخل الذاكرة حتى يُسلسل مرة أخرى إلى دفق المحتوى، وإعادة تحميل بخلاف ذلك ستُعيد تحليل الدفق غير المُعدَّل. ReloadPage تُختتم بـFPDF_LoadPage مقابل فهرس الصفحة الحالي، وهو الشيء الوحيد الذي يُعطيك فعليًا رسم كائن جديدًا

// After the transform, re-enumerate. Do not reuse anything captured earlier.
var
  I: Integer;
  Info: TPdfPageObjectInfo;
begin
  Pdf.TransformPageContent(Scale, Clip);   // unload text page, transform,
                                           // generate content, reload page
  for I:= 0 to Pdf.ObjectCount- 1 do
  begin
    Info:= Pdf.PageObjectInfo(I);          // handle and bounds from the new parse
    if Info.Bounds.Right> PageWidth then
      Log('object '+ IntToStr(I)+ ' still overflows after scaling');
  end;
end;

تفصيلة واحدة في ReloadPage تستحق النسخ إن كتبت هذا التسلسل بنفسك يومًا. تُحمِّل الصفحة الجديدة أولًا وتُلزمها بالحقل بعد ذلك فقط، بحيث يترك تحميل صفحة فاشل الصفحة الأصلية الحالية وكل ذاكراتها المؤقتة المشتقة سليمة بدلًا من إسقاطك في حالة نصف مُفكَّكة. إعادة التحميل ليست مجانية — أنت تدفع ثمن إعادة تحليل كاملة للصفحة — لكنها تُدفَع مرة واحدة لكل تحويل، لا مرة واحدة لكل استعلام، ولا يوجد بديل صحيح أرخص

لا تحمل المقابض عبر إعادة التحميل

بعد إعادة التحميل، المقابض القديمة ليست قديمة فحسب، إنها متدلية. FPDF_PAGE السابقة أُغلقت، وقيم FPDF_PAGEOBJECT التي انتمت إليها مؤشرات إلى ذاكرة مُحرَّرة. تعرض TPdfPageObjectInfo المقبض الأصلي في حقل Handle الخاص بها، وهو مفيد حقًا لتمرير كائن مباشرةً إلى استدعاء منخفض المستوى، وخطر حقًا بنفس القدر للاحتفاظ به في حقل نموذج أو قائمة عبر عملية تُعيد تحميل الصفحة. عامل سجل لقطة كصالح فقط حتى الاستدعاء التالي الذي يُعيد توليد المحتوى، بنفس روح قواعد الملكية المُناقَشة في ملاحظات ABI وسلامة الذاكرة عند حدود PDFium

هل يمكن لوصول (getter) أن يفشل ويبدو مع ذلك بيانات صالحة؟

نعم، وهذا النصف الثاني من نفس المشكلة. FPDFPageObj_GetRotatedBounds وFPDFPageObj_GetIsActive وصولات بوسيط خارجي: تُعيد علامة نجاح int وتكتب الإجابة الحقيقية في وسيط مرجعي. كلاهما يمكن أن يُعيد FALSE لكائن أُنشئ لكن صفحته لم تُعَد تحليلها بعد. عندما يحدث ذلك يبقى الوسيط الخارجي دون مساس، وسجل Pascal مُهيَّأ بـDefault(TPdfPageObjectInfo) كله أصفار، لذا يرى المستدعي رباعي أضلاع بأربع نقاط عند الأصل وعلامة Active بقيمة False. استدعاء فاشل رُقِّي بصمت إلى بيانات تبدو معقولة

تُجيب TPdfPageObjectInfo على هذا بحراس صريحين. تحمل HasRotatedBounds نتيجة استدعاء FPDFPageObj_GetRotatedBounds، وتحمل HasActiveState نتيجة FPDFPageObj_GetIsActive، وتُكتب حقول الهندسة والحالة فقط عندما يكون الحارس المقابل True. نفس الشكل يتكرر عبر السجل للوصولات الأخرى ذات الوسيط الخارجي، لذا HasMatrix وHasFillColor وHasStrokeColor وHasStrokeWidth كلها تعني نفس الشيء: الاستدعاء الأصلي نجح والحقل المجاور ذو معنى

Info:= Pdf.PageObjectInfo(I);

if Info.HasRotatedBounds then
  // RotatedBounds is array [1..4] of TPdfPoint, in draw order
  UseQuad(Info.RotatedBounds[1], Info.RotatedBounds[2],
          Info.RotatedBounds[3], Info.RotatedBounds[4])
else
  // the native call failed; fall back to the axis-aligned rectangle
  UseRect(Info.Bounds);

if Info.HasActiveState and (not Info.Active) then
  SkipObject(I);         // genuinely inactive
// if HasActiveState is False, the object state is unknown, not inactive

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

أين لا يزال هذا يُوقِع

ثلاثة حدود صادقة. أولًا، التحديث لكل صفحة: حوّل الصفحة الثانية وأي مقابض تحملها للصفحة الأولى تبقى غير متأثرة، لكن الآن لديك صفحتان حُلِّلتا في وقتين مختلفين وعليك تذكر أي اللقطات جاءت من أيهما. ثانيًا، استقرار الفهرس غير مضمون عبر إعادة توليد محتوى — بعد إعادة التحميل، الفهرس 3 هو أيًا كان الفهرس 3 في التحليل الجديد، لذا أعِد تحديد هوية الكائنات بنوعها وهندستها بدلًا من افتراض أن المواضع ثبتت. ثالثًا، مستطيل التحديد في FPDFPage_TransFormWithClip يُطبَّق على محتوى الصفحة ولا يُغيّر حجم أي من صناديق الصفحة؛ إذا قلّصت المحتوى لإنشاء هامش، لا يزال MediaBox بنفس الحجم الذي كان عليه دائمًا، وسيُظهر عارض الورقة الأصلية بالرسم مُصغَّرًا داخلها. لا شيء من هذا غريب — إنه النتيجة العادية لواجهة C برمجية تُوزِّع مؤشرات إلى حالة مُحلَّلة وتترك العمر للمستدعي. الإصلاح هو الذي يعمل في كل مكان آخر: عرِّف بالضبط متى تنتهي صلاحية لقطة، وحدِّث عند تلك الحدود، ولا تدع أبدًا استدعاءً فاشلًا يتنكر كقيمة

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