مقال تقني

تكرار Form XObject: اكتشاف الدورات في PDFlibPas بـDelphi

يحل PDFlibPas استدعاءات Form XObject التكرارية في تدفقات محتوى PDF بـDelphi عبر تتبع سلسلة الاستدعاء النشطة، لا مجموعة زيارة عامة، بحيث يمكن لـTPDFlib.EnumPageContentStatesEx اجتياز النموذج نفسه المُستدعى عدة مرات على صفحة واحدة دون الخلط بين إعادة استخدام مشروعة ودورة. Form XObject لختم في قالب فاتورة هو الحالة النموذجية: يُستدعى الكائن نفسه من الترويسة، والتذييل، وطبقة علامة مائية على صفحة واحدة، ولا تكون سوى سلسلة استدعاء تلتف مرة أخرى على نفسها دورة حقيقية

يعرّف ISO 32000-1 §8.10 Form XObject بأنه تدفق محتوى قائم بذاته تستدعيه صفحة، أو نموذج آخر، بعامل Do، كاملًا بنظام إحداثيات خاص به في /Matrix، وحد قص في نظام الإحداثيات ذاك في /BBox، واختياريًا قاموس موارد خاص به. لا شيء في المعيار يحدد عدد مرات استدعاء نموذج واحد أو مدى عمق استدعاء النماذج لبعضها البعض، بحيث يتعين على مُحلِّل متوافق قبول إعادة استخدام مشروعة وتداخل مشروع بينما يدافع عن نفسه ضد الترتيب الواحد الذي يحظره المعيار فعليًا: نموذج يستدعي تدفق محتواه نفسه، مباشرة أو انتقاليًا. يبلّغ PDFlibPas عن ذلك التمييز عبر قيم TPDFlibContentFormTraversalStatus المرفقة بكل لقطة Do، وأبرزها ftsEnumerated لنزول ناجح وftsCycle للحالة الوحيدة التي هي فعليًا حلقة

لماذا لا تُطلق إعادة استخدام Form XObject نفسه دورة كاذبة؟

مرجع Form XObject مكرر ليس بحد ذاته دليلًا على شيء خاطئ. يسمح ISO 32000-1 باستدعاء كائن النموذج نفسه من أي عدد من الأماكن في تدفق محتوى يريده المؤلف، وهذا بالضبط كيف يُعاد استخدام ختم شعار، أو قالب ترويسة رسمية، أو تذييل رقم صفحة عبر صفحة دون تكرار تدفق محتواه عدة مرات. الحارس الساذج ضد التكرار الجامح هو مجموعة زيارة واحدة مفتاحها رقم الكائن: أول مرة يرى فيها مُجتاز كائن النموذج 12، يُعلِّم 12 كمرئي ويرفض دخوله مرة أخرى في أي مكان آخر من الشجرة. ينكسر ذلك النهج بمجرد أن يظهر الختم نفسه في زاويتين غير مرتبطتين من صفحة واحدة، لأن الاستدعاء الثاني، المشروع كليًا، يصل بعد أن يكون رقم الكائن مُعلَّمًا كمرئي بالفعل ويُرفَض كما لو كان حلقة

يتجنب PDFlibPas تلك الإيجابية الكاذبة بحصر اكتشاف الدورة في سلسلة الاستدعاء الحالية بدلًا من المستند بأكمله. تدفع EnumPageContentStatesEx تدفق النموذج المحلول إلى سلسلة الاستدعاء النشطة مباشرة قبل النزول إليه، ثم تُخرج ذلك الإدخال نفسه مرة أخرى بمجرد عودة النزول، ناجحًا كان أم لا. لا يبدأ استدعاء شقيق للتدفق نفسه إلا بعد أن يكون الأول قد أُخرج بالفعل، بحيث تكون سلسلة الاستدعاء خالية من ذلك التدفق بحلول الوقت الذي يتحقق فيه استدعاء الشقيق منه، ويجتازه المُجتاز تمامًا كما لو كان أي نموذج آخر. تبدو دورة حقيقية مختلفة على السلسلة نفسها: يستدعي النموذج A النموذج B، ويظل B مفتوحًا على السلسلة عندما يستدعي محتواه الخاص مرة أخرى في A، ولا يزال A جالسًا على السلسلة من الاستدعاء الخارجي الذي لم يَعُد بعد — هذا هو الشكل الوحيد الذي تبلّغ عنه ftsCycle، تدفق نموذج لا يزال مفتوحًا في مكان ما أبكر على سلسلة الاستدعاء الحالية، لا مجرد موجود في مكان آخر من الصفحة

ما مدى العمق الذي يمكن أن يصل إليه تكرار Form XObject قبل أن يوقفه PDFlibPas؟

يحل اكتشاف الدورة وتحديد العمق مشكلتين مختلفتين، ويحافظ PDFlibPas عليهما كنتيجتي TPDFlibContentFormTraversalStatus مختلفتين لهذا السبب بالضبط. سلسلة من عشرين نموذجًا مميزًا، يستدعي كل منها التالي ولا يتكرر أي منها، ليست دورة بأي تعريف — لا يجد فحص السلسلة النشطة أبدًا تدفقًا مكررًا — لكن عشرين مستوى صادق من التداخل لا يزال يعني عشرين مستوى من التحليل، وتركيب المصفوفة، وحل الموارد يمكن لملف PDF مشوَّه أو عدائي دفعه أعلى بشكل تعسفي إذا لم يوقفه شيء آخر. تأخذ EnumPageContentStatesEx معامل MaxFormDepth لهذا السبب بالضبط وتُحدّ أيًّا كانت القيمة المُمرَّرة إلى حد أقصى 64، بصرف النظر عمّا يطلبه المستدعي. عمق صفر حالة خاصة تستحق المعرفة بمفردها: فهي تُعطِّل تكرار النموذج كليًا وتعيد إنتاج السلوك المسطّح، على مستوى الصفحة فقط، لطريقة EnumPageContentStates الأقدم، وهذا سبب إبلاغ كل لقطة Do في ذلك النمط عن ftsNotRequested بدلًا من محاولة أي شيء

var
  Lib: TPDFlib;
  States: array of TPDFlibContentGraphicsState;
  Count, I: Integer;
begin
  Lib:= TPDFlib.Create;
  try
    if Lib.LoadFromFile('invoice-batch.pdf', '')<> 1 then
      Exit;
    Lib.SelectPage(1);
    Count:= Lib.EnumPageContentStatesEx(True, 8, States);  // count only
    SetLength(States, Count);
    Lib.EnumPageContentStatesEx(True, 8, States);          // fill
    for I:= 0 to Count- 1 do
      if States[I].FormTraversalStatus= ftsCycle then
        LogSuspectForm(States[I].XObjectResource, States[I].ContentDepth);
  finally
    Lib.Free;
  end;
end;

متتبع فرعي واحد لكل استدعاء: عزل حالة الرسم

يحصل كل نزول إلى Form XObject على متتبع حالة رسم خاص به بدلًا من مشاركة ذلك الذي يجتاز الصفحة بالفعل، لأنه يتعين على تدفق محتوى نموذج ترك حالة الرسم تمامًا كما وجدها، ولا يستطيع PDFlibPas افتراض أن كل PDF يفتحه يحترم ذلك المتطلب فعليًا. يبدأ المتتبع الفرعي من لقطة لأيًّا كانت CTM، وحالة اللون، ومعاملات النص النشطة عند تعليمة Do المستدعية، ثم يعيد ضبط مكدس الحفظ-والاستعادة الخاص به وتتبع المسار الحالي إلى فارغ قبل تنفيذ تعليمة واحدة من النموذج. تبقى q غير متوازنة بلا Q مطابقة داخل نموذج مهمَل أو تالف، وهو ليس شيئًا نادرًا في ملفات PDF المُنتجة بأدوات أقدم، محتواة داخل متتبع ذلك الاستدعاء الواحد ولا تتسرب أبدًا إلى متتبع الصفحة أو إلى استدعاء شقيق للختم نفسه يجلس سطرًا لاحقًا في تدفق المحتوى

تركّب /Matrix الخاصة بالنموذج مع CTM الساري عند Do بالطريقة نفسها التي يفعلها عامل cm، مضروبة من اليسار مقابل التحويل الحالي بدلًا من الاستبدال به، ويعيد PDFlibPas عمدًا استخدام مسار الشيفرة الواحد ذلك بدلًا من صيانة صيغة ثانية، بما أن تنفيذين مستقلين لجبر المصفوفة نفسه بالضبط نوع التكرار الذي ينحرف بصمت بعد بضع جولات من تركيب التحجيم، والدوران، والقص. تقص /BBox بعد ذلك في فضاء إحداثيات النموذج الخاص به بعد أن تكون المصفوفة قد طُبِّقت بالفعل، وتُحوَّل كل الزوايا الأربع لذلك الصندوق فرديًا بدلًا من الزاويتين المتقابلتين فقط، بما أن نموذجًا مُدارًا أو مُشوَّهًا يمكنه من ناحية أخرى الإبلاغ عن صندوق حدود يفوّت محتوى حقيقيًا يجلس فيما كان زاوية قصوى قبل أن يحرك التحويل ذلك إلى مكان آخر. توسيع الحلقة من المثال السابق فوق مصفوفة States نفسها يقرأ تلك الحقول مباشرة

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Do')and (States[I].XObjectKind= cxkForm)and
    States[I].FormBBoxKnown then
    Writeln('Form ', States[I].XObjectResource, ' matrix ',
      States[I].FormMatrix.M11:0:3, ',', States[I].FormMatrix.M12:0:3,
      ' bbox ', States[I].FormBBoxLeft:0:1, '..', States[I].FormBBoxRight:0:1);

هل يتشارك نموذجان بالاسم نفسه للمورد الخط نفسه؟

لا. اسم مورد مثل /F1 لا يعني شيئًا إلا بالنسبة لقاموس الموارد النشط عند نقطة استخدامه، وكائنا Form XObject مختلفان حران في تعريف خطين مختلفين تمامًا تحت ذلك الاسم المطابق. يحل PDFlibPas هذا بتتبع نطاق مورد جنبًا إلى جنب مع كل اسم مورد: عندما يحمل نموذج قاموس /Resources خاصًا به، يصبح ذلك القاموس نطاق المورد الكامل لكل شيء بداخله، دون احتياطي لكل مفتاح إلى الصفحة أو قاموس المستدعي لأيًّا كان ما يصادف أن يغفله قاموس النموذج نفسه. لا يرث سوى نموذج بلا مفتاح /Resources على الإطلاق، وهو نمط لا يزال بعض مولّدات PDF الأقدم تنتجه، القاموس المستدعي كليًا، وذلك استثناء توافق متعمد لا قاعدة عامة تستحق الاعتماد عليها في مخرجات جديدة. هوية الخط في لقطة TPDFlibContentGraphicsState إذن هي زوج FontResource وFontResourceScope، لا الاسم وحده، مع توفر FontObjectNumber للتأكد بالضبط من أي كائن غير مباشر حُلّ إليه /F1 معين في ذلك النطاق تحديدًا

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

for I:= 0 to Count- 1 do
  if (States[I].OperatorName= 'Tj')and States[I].TextAdvanceResolved then
    RecordGlyphRun(States[I].FontResource, States[I].FontResourceScope,
      States[I].FontObjectNumber, States[I].ContentDepth);

قراءة FormTraversalStatus في خط أنابيبك الخاص

تحوّل FormTraversalStatus كل لقطة Do إلى تقرير تشخيصي صغير بحد ذاتها، وخط أنابيب يتجاهلها يُهدر بالضبط المعلومة التي كانت لتفسر استخراجًا غير مكتمل. تعني ftsNotApplicable أن التعليمة لم تكن قط استدعاء نموذج محلولًا أصلًا؛ وتعني ftsNotRequested أن التكرار كان مُطفأً لهذا الاستدعاء؛ وتعني ftsEnumerated أن النموذج حُلِّل واجتيز بنجاح؛ وتُعلِّم ftsDepthLimit وftsCycle الطريقتين اللتين يُقطَع بهما نزول عمدًا؛ وتغطي ftsMalformed كل شيء آخر أوقف الاجتياز — مرجع تدفق غير قابل للحل، أو /Matrix أو /BBox فشلت في التحليل، أو استثناء أُطلق أثناء تنفيذ محتوى النموذج نفسه. تهم تلك الحالة الأخيرة تشغيليًا، لأن اجتيازًا متداخلًا فاشلًا يتراجع عن أيًّا كانت المخرجات الجزئية التي أنتجها بالفعل لذلك الفرع، بحيث لا يحتاج مستدعٍ أبدًا للتخمين ما إذا كان نموذج فارغًا حقًا أو انفجر ببساطة بعد تعليمتين في تدفق محتواه

var
  Tally: array[TPDFlibContentFormTraversalStatus] of Integer;
  Status: TPDFlibContentFormTraversalStatus;
begin
  for Status:= Low(Tally) to High(Tally) do
    Tally[Status]:= 0;
  for I:= 0 to Count- 1 do
    Inc(Tally[States[I].FormTraversalStatus]);
  if (Tally[ftsCycle]> 0)or (Tally[ftsMalformed]> 0) then
    FlagForManualReview(SourceFileName, Tally[ftsCycle], Tally[ftsMalformed]);
end;

الحدود، والتكاليف، وأين يلائم هذا

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

حدّان يستحقان ضبط التوقعات حولهما قبل أن تدخل هذه الواجهة البرمجية خط أنابيب أكبر. سقف العمق البالغ 64 مستوى ليس مقبض ضبط لمستندات عميقة مشروعة، بما أن الفواتير، والكشوف، وقوالب التقارير الحقيقية لا تُداخل النماذج أساسًا أكثر من ثلاثة أو أربعة مستويات — مستند يصطدم فعليًا بـftsDepthLimit أكثر احتمالًا بكثير أن يكون مشوَّهًا أو عدائيًا من كونه مفصَّلًا بشكل غير عادي، ويستحق تسجيله كإشارة جودة بيانات بدلًا من إعادة المحاولة بصمت برقم أكبر. EnumPageContentStatesEx أيضًا واجهة تحليل من جانب القراءة: تبلّغ عمّا يفعله تدفق محتوى، لا عمّا إذا كان ينبغي أن يكون نموذج مرئيًا أصلًا، وهو سؤال منفصل تجيب عنه حالة رؤية مجموعة المحتوى الاختياري عندما يجلس ختم أو نموذج علامة مائية خلف طبقة قد يكون عارض قد أطفأها. اكتشاف الدورة عبر سلسلة الاستدعاء، والعزل لكل استدعاء، ونطاق الموارد يشكّلون معًا زاوية واحدة من سطح فحص تدفق المحتوى في مكوّن PDFlibPas لـDelphi وC++Builder