مقال تقني

فحص نسخة PDF المسبق في Delphi: قواعد Measure RL مقابل GEO

يفحص PDFlibPas‏ (PDF Library for Delphi) كل كائن مقابل جدول قواعد نسخ PDF قبل أن يكتب ملفاً، وحتى قريب كان ذلك الفحص المسبق لنسخة PDF يخلط قواميس القياس CAD العادية بالقواميس المكانية. رسم CAD من صفحة واحدة حُمّل بنجاح، ثم أعاد SaveToFile القيمة 0 بـ LastErrorCode تساوي 602 وطالب بـ 1.7 ExtensionLevel 3. القواعد المصححة تعامل قواميس /Measure المستقيمة الخطوط (/Subtype /RL) بوصفها PDF 1.6 عادية وتحتفظ ببوابة الامتداد لعلامات المكانية الحقيقية

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

كيف يعمل الفحص المسبق لنسخة PDF عند الحفظ في PDFlibPas؟

بوابة الحفظ PrepareAndCheckSaveVersion تقارن كل كائن غير مباشر مقابل PDFFeatureRules وتفشل عند أول قاعدة تطابق وتحتاج أكثر مما يجيزه الهدف. الهدف نسخة المستند (أو النسخة المقفلة بـ LockSaveVersion) زائد مستوى امتداد Adobe المعلن تحت /Extensions /ADBE. يحمل كل سجل TPDFFeatureRule ‏MinVersion و MinExtensionLevel و MatchKind مثل fmkDictKey أو fmkDictSubtype وسلسلة Match واسم Feature مقروءاً للبشر ونداءً اختيارياً. تسجل AddRule قاعدة نسخ عادية؛ و AddExtensionRule تثبّت MinVersion عند 17 دائماً وتضيف مستوى امتداد فوقه، فقاعدة امتداد لا تُستوفى أبداً إلا بـ PDF 1.7 زائد مدخل /Extensions الصحيح. حين تنطلق البوابة يُحتفظ للمرسل بالنسخة المطلوبة واسم الخاصية، وتكشفهما مفاتيح GetInformation‏ 311 و 312 و 313

الفحص المسبق للنسخة عند الحفظ في PDFlibPas: تقارن PrepareAndCheckSaveVersion كل كائن مقابل سجلات PDFFeatureRules تحمل MinVersion ومستوى امتداد ونوع مطابقة، وتثبت AddExtensionRule المتطلب عند PDF 1.7 زائد مستوى امتداد، وأول تطابق لا يستطيع الهدف استيفاءه يوقف الحفظ بخطأ 602
مفاتيح GetInformation‏ 311 و 312 و 313 تحول الرفض إلى تشخيص، تبلّغ النسخة المطلوبة والخاصية التي أطلقتها والهدف المقفل، فيستطيع المستدعي إصلاح الملف أو القاعدة بدل التخمين
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.LoadFromFile('floor-plan.pdf', '') <> 1 then
      raise Exception.Create('load failed');
    if Pdf.SaveToFile('floor-plan-out.pdf') <> 1 then
      if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
        // 311: النسخة المطلوبة، 312: الخاصية التي أطلقتها،
        // 313: النسخة المقفل عليها هدف الحفظ ('' حين لا قفل)
        Writeln('Needs ', Pdf.GetInformation(311),
          ' for ', Pdf.GetInformation(312),
          ', locked at [', Pdf.GetInformation(313), ']');
  finally
    Pdf.Free;
  end;
end;

لماذا فشل رسم CAD عادي بخطأ 602؟

كان جدول القواعد يحوي AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil)، التي تنطلق على أي قاموس بمجرد امتلاكه مفتاح /Measure، وكل مشهد قياس يملك واحداً. تحمل مصفوفة /VP للصفحة قواميس المشاهد، ويشير كل مشهد إلى قاموس قياسه عبر /Measure، وكانت مطابقة وجود المفتاح تتوقف هناك دون النظر إلى ما هو قاموس القياس فعلاً. أمكن حينها لمسح الخاصيات عند التحميل أن يرفع رقم نسخة المستند إلى 1.7، لكنه لا يكتب أبداً إعلان /Extensions بالنيابة عن ملف مدخل، فرأت بوابة الحفظ PDF 1.7 بمستوى امتداد 0 وبلّغت 1.7 ExtensionLevel 3. ورفض اختلاق إعلان امتداد مقصود: المكتبة لا تروّج ملف مدخل بصمت لتغطية قاعدة خاطئة

المواصفة صريحة في حالة المستقيم الخطوط. وصلت قواميس القياس في PDF 1.6، ويعطي §12.9 من ISO 32000-1 ‏/Subtype افتراضياً RL، نظام إحداثيات مستقيم الخطوط يصفه مجموعة مدخلاته الخاصة: نسبة المقياس، وتنسيقا رقمي X و Y، والمسافة والمساحة. أما القياس المكاني فهو الإضافة اللاحقة من Adobe Extension Level 3 فوق PDF 1.7، تعرّف بـ /Subtype /GEO وتحمل مصفوفات نقاط جغرافية وقواميس أنظمة إحداثيات ووحدات عرض، وهي البنى التي تجولها قراءة مشاهد GeoPDF ومصفوفات GPTS و LPTS في Delphi. وكلا القاموسين متعلق بالمفتاح /Measure نفسه، فأي قاعدة تتوقف عند المفتاح لا يمكن أن تكون صحيحة للاثنين. معلومة التمييز تجلس مستوى واحداً أسفل، في قاموس القياس ذاته

مفتاح /Measure واحد وقاموسان في PDFlibPas: القياس المستقيم الخطوط، بـ /RL أو بحذف النوع الفرعي، يحتاج PDF 1.6 فقط، بينما يحتاج القاموس المكاني 1.7 ExtensionLevel 3، فيقرر CB_GeospatialDictionary بالمحتوى حيث عجزت قاعدة المفتاح القديمة عن التفريق
الفحص المسبق الذي يعترض ملفاً سليماً أسوأ من بطيء، لأن المستدعي يتلقى تشخيصاً موثوق المظهر عن خاصية لم يحوها المستند قط، ولهذا نزل فحص التمييز مستوى واحداً

ما الذي ما زال يفرضه مجموعة القواعد المصححة؟

يحذف الإصلاح قاعدة المفتاح غير المشروطة ويبقي البوابات التي تصف متطلبات نسخ حقيقية. صفحة تحمل /VP أو /UserUnit ما زالت تحتاج PDF 1.6 عبر CB_PagePDF16Entries، ومفتاح /PtData ما زال يحتاج مستوى امتداد 3، و CB_GeospatialDictionary تقرر هل قاموس القياس مكاني بمحتواه لا بالمفتاح الذي وصل إليه

// المحذوف: كل قاموس بمفتاح /Measure عُدّ مكانياً
// AddExtensionRule(3, fmkDictKey, 'Measure', '/Measure geospatial dictionary', Nil);

AddRule(16, fmkCustom, '', 'Page PDF 1.6 entry /UserUnit /VP', CB_PagePDF16Entries);
AddExtensionRule(3, fmkDictKey, 'PtData', '/PtData geospatial dictionary', Nil);
AddExtensionRule(3, fmkCustom, '', 'geospatial measure dictionary', CB_GeospatialDictionary);

function CB_GeospatialDictionary(Obj: TPDFObject; const Ctx: TPDFRuleContext): Boolean;
var
  Dict: TPDFDictionary;
begin
  Result := False;
  if not (Obj is TPDFDictionary) then
    Exit;
  Dict := TPDFDictionary(Obj);
  Result := (Dict.StringValue('Subtype') = 'GEO') or
    (Dict.FindIndexByKeyName('GCS') >= 0) or (Dict.FindIndexByKeyName('DCS') >= 0) or
    (Dict.FindIndexByKeyName('GPTS') >= 0) or (Dict.FindIndexByKeyName('LPTS') >= 0) or
    (Dict.FindIndexByKeyName('PDU') >= 0);
end;

يثبت انحدار Delphi و FPC المشترك ذلك الحد من الجانبين. مشهد قاموس قياسه يحذف /Subtype وآخر يتهجى /RL كلاهما يجتاز عند PDF 1.6، والصفحة نفسها ما زالت مرفوضة عند PDF 1.5، وكشف الخاصيات لم يعد يبلّغ عن امتداد له. وإضافة مصفوفة /GPTS تعيد الحكم إلى 1.7 ExtensionLevel 3، وهو يجتاز متى أُعلن مستوى الامتداد، وقاموس /Subtype /GEO مجرد يُرفض دونه. والنداء الرجعي متحفظ بالتصميم: قاموس مستقيم الخطوط يحمل كذلك مفتاح /GCS أو /PDU شارداً يعامل مكانياً، لأن تلك المفاتيح لا معنى لها في نموذج RL

‏LockSaveVersion هي حيث يصير هذا التغيير مرئياً للمستدعين. تقبل TPDFlib.LockSaveVersion من '1.0' إلى '1.7'، وتعيد 0 لغير ذلك، وتثبّت نسخة المستند، وتمنع استدعاءات جانب الكاتب من رفعها بصمت، ومع ذلك تجري بوابة الحفظ مقابل القيمة المقفلة. وبالقواعد المصححة يحفظ ملف CAD مقفل عند 1.6 بنظافة. وملف GeoPDF حقيقي مقفل عند 1.6 ما زال يأخذ 602، وهي الجواب الصحيح، واستدعاءات التأليف المكانية مثل SetMeasureDictCoordinateSystem تعلن مستوى الامتداد 3 بنفسها حين تبني ذلك المحتوى عبر الواجهة

if Pdf.LockSaveVersion('1.6') <> 1 then
  raise Exception.Create('unsupported version string');
if Pdf.SaveToFile('floor-plan-16.pdf') <> 1 then
begin
  if Pdf.LastErrorCode = PDFLIB_ERROR_VERSION_COMPLIANCE then
    // محتوى حقيقي فوق 1.6، مثل قاموس قياس GEO
    raise Exception.CreateFmt('Locked at 1.6 but %s needs %s',
      [string(Pdf.GetInformation(312)), string(Pdf.GetInformation(311))]);
end;
Pdf.UnlockSaveVersion;

لماذا كان مسح قواعد النسخة أبطأ مما يلزم؟

كان المسح ينسخ كل TPDFFeatureRule إلى سجل محلي قبل اختباره، ولأن السجل يحمل حقلين من AnsiString كانت كل نسخة تضبط عددي مرجع وتحرر القيم السابقة. يزور الفحص المسبق كل عقدة من كل شجرة كائنات، والعدديات من ذلك، فتضاعفت تلك الكلفة بعدد الكائنات في عدد القواعد، والقواعد التي لا تنطبق حتى على نسخة الهدف كانت تنسخ أولاً وتتخطى بعد ذلك. ولأن PDFFeatureRules تملأ مرة عند تهيئة الوحدة وتعامل كغير قابل للتغيير، تمرر v3.539.17 مدخلات الجدول مباشرة إلى MatchSingleRule و RuleExceedsTarget، التي تأخذ معاملات const Rule مرجعاً دون لمس السلاسل

تسريع مسح القواعد في PDFlibPas: كان الفحص المسبق ينسخ كل سجل TPDFFeatureRule قبل اختباره، مضبطاً عددي مرجع AnsiString عن كل كائن يُزار، بينما تقرأ معاملات const الآن الجدول غير القابل للتغيير في موضعه، فهبط وسيط جولة مطابقة القواعد من 0.711 s إلى 0.203 s
المكسب حقيقي لكنه ضيق: الحفظ الكامل يدفع كذلك كلفة كشف الخاصيات المؤجل وفك الكائنات والتسلسل، فالنسبة المقيسة تعود لمسار مطابقة القواعد لا إلى زمن الحفظ الكلي
// قبل: نسخة سجل مُدارة لكل قاعدة، عن كل كائن يُزار
Rule := PDFFeatureRules[X];
if not RuleExceedsTarget(Rule, TargetVersion, TargetExtensionLevel) then
  Continue;

// بعد: معاملات const تقرأ مدخل الجدول غير القابل للتغيير في موضعه
if not RuleExceedsTarget(PDFFeatureRules[X], TargetVersion, TargetExtensionLevel) then
  Continue;
if MatchSingleRule(Obj, Ctx, PDFFeatureRules[X]) then
begin
  RequiredVersion := RequiredVersionString(PDFFeatureRules[X]);
  FeatureName := PDFFeatureRules[X].Feature;
  Result := False;
  Exit;
end;

الأثر المقيس ضيق وينبغي نقله على هذا النحو. يفحص المقياس مصفوفة من 20,000 كائن عددي مقابل هدف PDF 1.4 عشر مرات في الجولة؛ وبنيته بـ FPC Win64 عند -O2، هبط وسيط خمس جولات من 0.711 s إلى 0.203 s، وتشغيل البنيتين بالترتيب المعاكس أعطى 0.459 s مقابل 0.150 s. وذلك مكسب بنحو 3x على مسار مطابقة القواعد وحده. الحفظ الحقيقي يدفع كذلك كلفة كشف الخاصيات المؤجل وفك الكائنات والتسلسل، فالنسبة لا تنتقل إلى زمن الحفظ الكلي. ترتيب القواعد والنداءات وعتبات النسخ وتشخيص أول فشل لم تتغير، ولم تُخزَّن أي قاعدة عبر الحفظات ولا تُخطَّ للوصول إلى ذلك

ماذا تفحص حين يفشل PDF محمل في الفحص المسبق للنسخة؟

اقرأ المفتاحين 311 و 312 قبل لمس النسخة. إن سمت الخاصية قاموساً مكانياً والملف يرسم قياسات مستقيمة الخطوط فقط، فتلك كانت هذه الإيجابية الزائفة، وبنية حالية تحفظ الملف دون تغيير. وإن كانت الخاصية حقيقية، فإما أن تعلن الامتداد وإما أن تقفل على نسخة تحوي المحتوى بصدق؛ رفع النسخة مجرد إسكات للبوابة يخفي سؤال هل يستطيع المستهلكون في اتجاه المصب قراءة ما تشحنه. والمبدأ نفسه من الفحوص المحدودة المدعومة بالدليل يقود الفحص المسبق بنمط المؤلف PDF/E-1 لمستندات الهندسة، حيث تلتقي رسومات CAD بمعيار مطابقة لا برقم نسخة

فحوص مطابقة النسخة وقواميس القياس والمكانية وقفل نسخة الحفظ كلها جزء من PDF Library for Delphi، صندوق أدوات PDFlibPas لمطوري Delphi و C++Builder و Lazarus