مقال تقني

حفظ إصدار PDF دقيق في Delphi: توافق PDFiumPas

يحفظ PDFiumPas، غلاف Delphi وC++Builder حول محرك PDFium من Google، مستندًا بإصدار PDF دقيق من 1.3 حتى 1.7 عبر معامل PdfVersion الخاص بطريقة TPdf.SaveAs. يعيد استدعاء FPDF_SaveWithVersion الخاص بـPDFium نفسه كتابة ترويسة %PDF-M.m فقط، دون التحقق مما إذا كان محتوى المستند الفعلي قانونيًا عند ذلك الإصدار. يغلق PDFiumPas تلك الفجوة بتمريرة امتثال بعد الحفظ تجتاز سلسلة مراجعات جدول المراجع المتقاطع النشطة وتتحقق من إعلانات مستوى امتداد Adobe قبل أن يغادر الملف الطريقة

يهم ذلك التمييز أكثر ما يهم في إنتاج الطباعة، حيث يسمّي ملف تعريف PDF/X إصدار PDF دقيقًا وترفض أداة فحص مسبق أو RIP أي شيء يختلف بصمت مع ترويسته الخاصة، سيناريو مشروح من جانب المخرجات في التحقق من مستندات PDF/X الجاهزة للطباعة عبر PDFiumPas. تعرض SaveAs الهدف كتعداد TPdfVersion، من pv13 حتى pv17 إلى جانب قيم pv10 إلى pv12 الأقدم، بالإضافة إلى TSaveOption مستقل لإعادة الكتابة التزايدية أو الكاملة. مرّر PdfVersion وسيقوم PDFiumPas بمهمتين في استدعاء واحد: يطلب من PDFium ختم الترويسة المطلوبة، ثم يعيد قراءة البايتات المكتوبة حديثًا ويرفض إعادة ملف لا يمكن لمحتواه النشط أن يوجد قانونيًا عند ذلك الإصدار

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

لماذا يكون آخر تعريف كائن في الملف الشيء الخاطئ للثقة به؟

الكائن المادي الأخير برقم معين في ملف PDF ليس بالضرورة الكائن الذي سيحله قارئ متوافق لذلك الرقم اليوم. ملف PDF مرّ بعدة تحديثات تزايدية لا يملك رسم كائنات واحدًا، بل تاريخًا منها متراكبة داخل ملف واحد، وكل دورة إلحاق يمكنها تحرير كائن، أو إعادة تعريفه تحت رقم جيل جديد، أو ترك جسمه المادي القديم جالسًا بين علامتي endobj دون أي إدخال مرجع متقاطع يشير إليه بعد الآن

اصطدم PDFiumPas بنمط الفشل هذا بالضبط قبل أن يتتبع مراجعات xref صراحة: تعليق توضيحي للتنقيح (Redact) يتيم بسبب إعادة كتابة لاحقة لكائن صفحة، أو قاموس /MarkInfo متروك موجودًا ماديًا دون أي إدخال xref يشير إليه، كان لا يزال بالإمكان أن يظهر في مسح بايتات وأن يُفعّل فحص ميزة إصدار لم يعد ينطبق على المستند الذي كان قارئ ليفتحه فعليًا. اتجاه الفشل كان رفضًا خاطئًا، لا قبولًا خاطئًا: ملف تجاوز حقًا ميزة في مراجعته الحالية كان لا يزال بالإمكان حظره من الحفظ بإصدار أقل بسبب محتوى لم يعد بإمكان أحد الوصول إليه

كيف يحدد PDFiumPas أي تعريفات كائنات نشطة فعليًا؟

يحل PDFiumPas مجموعة الكائنات النشطة بالطريقة نفسها التي يحلها بها قارئ متوافق، باجتياز سلسلة المرجع المتقاطع بدلًا من مسح البايتات بحثًا عن ترويسات كائنات. يبدأ المُحلِّل عند آخر إزاحة startxref في الملف ويتبع كل رابط /Prev إلى الخلف عبر مراجعات أقدم، محلّلًا جداول مرجع متقاطع كلاسيكية، وتدفقات هجينة مرتبطة بـ/XRefStm، وتدفقات مرجع متقاطع خالصة على طول الطريق. يعمل الاجتياز من الأحدث إلى الأقدم ويُثبّت كل رقم كائن أول مرة يُرى، بحيث يُظلِّل إدخال حر في مراجعة لاحقة بشكل صحيح جسم كائن مكتوب في مراجعة أقدم، وتفوز إعادة تعريف تحت إزاحة أو جيل جديد دائمًا على ما استبدلته

تحصل أعضاء تدفق الكائنات على فحص إضافي لا يمكن لبحث إزاحة بسيط توفيره بمفرده، آلية مشروحة بتفصيل أكبر في التحقق من تدفقات الكائنات والمرجع المتقاطع عبر PDFiumPas. يجب تأكيد أن كائنًا مضغوطًا مستردًا من /ObjStm نشط والده في الاجتياز نفسه، ويجب أن يتفق فهرسه مع موضع العضو نفسه داخل ترويسة ذلك التدفق قبل أن يعامله PDFiumPas كمحتوى حي. يصف القسم 7.5.8.4 من ISO 32000-1 حتى حالة مرجع هجين حيث يُعلِّم جدول توافق كلاسيكي كائنًا حرًا بينما يعرّف إدخال /XRefStm الخاص بالذيل (trailer) الكائن نفسه في آن واحد ككائن مضغوط في مكان آخر؛ يدمج PDFiumPas تدفق المرجع المتقاطع التكميلي في المراجعة نفسها قبل تطبيق الإدخالات الكلاسيكية، بحيث يفوز التعريف المضغوط بالطريقة التي يقصدها المعيار

مستويات امتداد Adobe: البوابة فوق رقم الإصدار

ترويسة %PDF-1.7 لا تعد إلا بمجموعة الميزات التي وحّدها ISO 32000-1 في 2008، بينما شُحنت عدة قدرات يعتمد عليها منتجو PDF اليوم لاحقًا كملحقات خاصة بـAdobe موضوعة فوق رقم الإصدار نفسه. سجّلت Adobe كل ملحق كزوج BaseVersion وExtensionLevel مسجَّل في قاموس /Extensions الخاص بدليل المستند تحت بادئة مطوّر، ADBE لملحقات Adobe الخاصة، بحيث يمكن لقارئ التمييز بين ملف PDF 1.7 عادي وآخر ينفّذ أيضًا مستوى امتداد مُرقَّمًا. حفظ عند pv17 دون ذلك الإعلان ليس خطأً بحد ذاته؛ لا يصبح كذلك إلا في اللحظة التي يعتمد فيها المحتوى النشط فعليًا على ميزة يُفترض أن يغطيها الإعلان

أي ميزات إصدار عالٍ تُفعّل بوابة الإصدار الصريح؟

يتحقق PDFiumPas من قائمة محددة مبنية على المعيار بدلًا من التخمين من رقم الإصدار وحده. تتطلب قواميس الصور التي تحمل إدخال /SMaskInData صريحًا أو قيمة /BitsPerComponent تساوي 16 كلاهما PDF 1.5، مع اتباع حالة الستة عشر بت لقواعد مكوّن الصورة في مرجع PDF 1.5 القسم 4.8 مباشرة. تتطلب التعليقات التوضيحية RichMedia وأفعال RichMediaExecute /BaseVersion /1.7 مع /ExtensionLevel 3 أو أعلى. تتطلب تدفقات PRC ثلاثية الأبعاد، المحددة بقاموس يحمل كلًا من /Type /3D و/Subtype /PRC، الإصدار الأساسي نفسه لكن فقط /ExtensionLevel 1. تتطلب قواميس القياس الجغرافي المكاني والتعليقات التوضيحية للإسقاط /BaseVersion /1.7 مع /ExtensionLevel 3، ملحق Adobe نفسه الذي يعتمد عليه RichMedia

يحمل الفحص الجغرافي المكاني تفصيلًا في قراءة المعيار يستحق المعرفة إذا بنيت يومًا منطق بوابة إصدار خاصًا بك فوق PDFiumPas. يُعلِّم الجدول 254 من ISO 32000-1 إدخال /Type الخاص بقاموس القياس كاختياري، مشيرًا فقط إلى أنه "إذا وُجد، يجب أن يكون Measure"، بينما يجعل الجدول 311 /Type إلزاميًا لقاموس تدفق ثلاثي الأبعاد الذي يعيش فيه محتوى PRC. مخرجات GeoPDF الحقيقية من أدوات رسم الخرائط تحذف روتينيًا /Type على قاموس القياس وتكتب فقط /Subtype /GEO، لذا فإن كاشف PDFiumPas الجغرافي المكاني يطابق على /Subtype وحده بدلًا من اشتراط كلا المفتاحين بالطريقة التي يمكن لكاشفه لـPRC ثلاثي الأبعاد فعلها بأمان. اشتراط /Type على كلا القاموسين كان سيسمح لمحتوى GeoPDF متوافق بالإفلات من البوابة دون اكتشاف، منتهيًا في ملف PDF 1.7 عادي دون إعلان مستوى امتداد يدعمه

هل يخفّض PDFiumPas تلقائيًا الميزات غير المدعومة؟

ليس كقدرة عامة، وافتراض العكس هو الخطأ الذي يجب تجنبه هنا. تصبّ SaveAs الإصدار الهدف عبر روتين داخلي، ValidatePdfVersionCompliance، وعندما يجد ذلك الروتين ميزة لا يستطيع الإصدار الهدف أو إعلان مستوى امتداده دعمها، تُطلق SaveAs استثناءً يحمل نص خطأ الروتين بدلًا من كتابة الملف؛ يحصل المستدعي على سبب دقيق مُسمّى بالميزة، لا مستندًا مُعاد كتابته بصمت أبدًا. المكان الوحيد الذي يعيد فيه PDFiumPas كتابة المحتوى تلقائيًا هو هدف PDF 1.3، حيث يزيل افتراضيات الشفافية المحايدة سمانتيكيًا /BM /Normal و/CA 1 و/ca 1 التي يكتبها PDFium دائمًا في قواميس ExtGState بصرف النظر عن الإصدار الهدف، لأن تلك القيم المحددة لا تحمل أي معنى بصري وPDF 1.3 يسبق تلك المفاتيح كليًا

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

لا تزال الشفافية الحقيقية غير الافتراضية وأقنعة الصور الناعمة (soft masks) تفشل كليًا عند هدف PDF 1.3، لأن إزالتها كانت ستغيّر شكل الصفحة فعليًا، ولن يتخذ PDFiumPas ذلك القرار نيابة عنك. حدّان ذوا صلة يستحقان التخطيط لهما قبل أن يدخل إصدار دقيق خط أنابيب دفعي. لا تحمل مخرجات الإصدار الصريح أبدًا قاموس /Encrypt؛ يفشل الحفظ فورًا إذا كان المصدر محميًا، ما يصادف أن يتماشى مع ملفات تعريف PDF/X وPDF/A التي تحظر التشفير أصلًا، لكنه يعني أن فك التشفير خطوة منفصلة في سير عملك بدلًا من شيء تفعله SaveAs عنك. لا يملك PDFiumPas أيضًا طريقة عامة لكتابة إعلان /Extensions /ADBE على دليل، لذا فإن ملفًا مصدريًا يحتوي محتوى RichMedia، أو PRC ثلاثي الأبعاد، أو جغرافيًا مكانيًا لكنه يفتقر ذلك الإعلان لن يجتاز البوابة بصرف النظر عن PdfVersion الذي تطلبه؛ يجب أن يكون الإعلان موجودًا بالفعل في المصدر، عادة لأن أداة التأليف كتبته، أو يجب أن تخرج الميزة قبل الحفظ. خاصية TPdf.PdfVersion للقراءة فقط تستحق التحقق منها قبل محاولة حفظ بإصدار دقيق حتى، بما أنها تحل الإصدار الفعّال الواعي بالدليل نفسه، ترويسة أو تجاوز /Version، أيًّا كان الحالي، الذي يعتمد عليه مُتحقق وقت الحفظ نفسه

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

عامل استثناء SaveAs عند هدف إصدار دقيق كتقرير فحص مسبق لا كخلل: تسمّي الرسالة البند الدقيق الذي ينتهكه المستند المصدري، وهي بالضبط المعلومة التي يحتاجها متجر طباعة أو خط أنابيب أرشيف قبل أن يذهب ملف إلى أبعد من ذلك. مسار الحفظ بإصدار صريح، ومُحلِّل مراجعة xref النشطة، وفحوصات مستوى امتداد Adobe الموصوفة هنا تُشحن كجزء من مكوّن PDFiumPas القياسي لـDelphi وC++Builder؛ تحمل صفحة المنتج مرجع TPdf.SaveAs الكامل إلى جانب بقية واجهة التوافق والنماذج البرمجية