Technical Article

تحميل ملفات PDF ذات المراجع الهجينة من Word وExcel في Delphi

افتح ملف PDF أنتجه برنامج Microsoft Word أو Excel، وتصفح صفحاته، ولن يبدو أي شيء غير عادي. قم بتحميله في برنامج Delphi، واقرأ عدد الصفحات، وسيكون الرقم صحيحاً. ثم أعد حفظه مع تفعيل التشفير وتفشل المهمة مع EListError، أو يفتح الإخراج مع تحذير بوجود مرجع تقاطعي تالف. لم يكن الملف تالفاً أبداً. إنه ملف ذو مرجع هجين، والنية الأساسية التي تسمح لبرنامج عرض عمره خمسة عشر عاماً بفتحه هي نفسها التي تهزم برنامج التحميل الذي يتوقف عن القراءة مبكراً جداً.

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

ما يكتبه Word وExcel بالفعل

تصف المواصفة ISO 32000-1 تخطيط المرجع الهجين في §7.5.8.4. يقوم التطبيق الذي يريد ميزات PDF 1.5 مثل تدفقات الكائنات، مع السماح لبرنامج قراءة PDF 1.4 بفتح الملف، بكتابة معلومات المرجع التقاطعي مرتين. هناك جدول مرجعي تقاطعي كلاسيكي، وهو صفوف ASCII ذات العرض الثابت التي أنهت كل ملف PDF حتى الإصدار 1.4، وهناك تدفق مرجعي تقاطعي يفهرس الباقي. يحمل الجزء الأخير من القسم الكلاسيكي إدخال /XRefStm الذي قيمته هي إزاحة البايت لهذا التدفق.

تقسيم العمل متعمد. الكائنات التي يجب أن يصل إليها القارئ القديم، ومن بينها الفهرس وشجرة الصفحات، يمكن الوصول إليها من الجدول الكلاسيكي. الكائنات التي تم دمجها في تدفقات الكائنات المضغوطة يتم تمييزها كخالية في الجدول الكلاسيكي، مع إدخال من النوع f، بحيث يتخطاها قارئ 1.4 مباشرة ولا يتعثر أبداً في بنية لا يمكنه تحليلها. تقع مواقعها الحقيقية فقط في دفق المراجع التقاطعية. إن علامة مثل هذا الملف هي ذيله: قسم كلاسيكي قصير، غالباً لا يتعدى xref متبوعاً برأس قسم فرعي 0 0، والذي يشير ذيله إلى /XRefStm حيث توجد بيانات الاسترداد الفعلية.

لماذا لا يثبت عدد الصفحات الصحيح شيئاً

نظراً لأن الفهرس وشجرة الصفحات يمكن الوصول إليهما من الجدول الكلاسيكي عن قصد، فإن برنامج التحميل الذي يقرأ هذا الجدول فقط يجد /Root، ويمر عبر شجرة الصفحات، ويبلغ عن العدد الصحيح للصفحات. كل ما يحتاجه القارئ القديم موجود، لذا يبدو الملف سليماً. الكائنات المفقودة هي تلك المعبأة في تدفقات الكائنات: قواميس حقول AcroForm، وعناصر بنية ملفات PDF ذات العلامات (tagged-PDF)، والذيل الطويل من القواميس الصغيرة التي لم تكن بحاجة أبداً لأن تكون مرئية لبرنامج عرض قديم.

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

المصيدة هي كاشف يرى xref ويتوقف

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

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf');  // count is correct
    // inspect or edit the loaded document here
    Pdf.SaveLoadedDocument('Invoice_secured.pdf');     // walks every object
  finally
    Pdf.Free;
  end;
end;

مع وجود كاشف الخروج المبكر، يبدو التحميل جيداً وإعادة الحفظ هي المكان الذي تعلن فيه الكائنات الغائبة عن نفسها. لا يكمن الحل في قراءة المزيد من البايتات في البداية؛ بل في التعرف على الذيل الهجين وتتبع /XRefStm قبل تحديد أن الملف قد اكتمل.

ترتيب الدمج غير قابل للتفاوض

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

نفس الانضباط يحمي من قيام مراجعة أقدم بإحياء كائن محذوف. تتسلسل التحديثات التراكمية إلى الوراء من خلال /Prev، والإدخال الحر من النوع 0 هو حارس يشير إلى أن قسماً أحدث قد أحال رقم كائن إلى التقاعد. يجب ألا يُسمح لقسم لاحق وأقدم في السلسلة بالكتابة فوق هذا الحارس بموقع قديم. تعامل مع الظهور الأول باعتباره موثوقاً للعلامات الحرة وسيظل الكائن المحذوف محذوفاً؛ تعامل معه بإهمال وسيقوم تاريخ الملف نفسه بإحياء محتوى أزالته المراجعة الأخيرة.

ما يعنيه هذا في HotPDF

يقوم المحرك بحل ملفات المراجع الهجينة نيابة عنك، ويفعل ذلك في كل مسار يتعين عليه تحليل بيانات المراجع التقاطعية. قم بتحميل مستند باستخدام LoadFromFile أو LoadFromStream، وقم بإجراء تغييراتك، واستدعِ SaveLoadedDocument؛ أو قم بتشغيل عملية ذات خطوة واحدة مثل EncryptFile التي تقرأ مدخلاً وتكتب مخرجاً. وفي كلتا الحالتين، تقرأ عملية الاسترداد /XRefStm، وتدمج قسم التدفق قبل الإدخالات الكلاسيكية، وتحل الكائنات التي تعيش في التدفقات قبل أن يسردها الكاتب. مسار تشفير AES-256 هو المكان الذي ظهرت فيه المشكلة لأول مرة، لأن تشفير مستند يعيد كتابة كل كائن وبالتالي يتطلب تحديد موقع كل كائن بالفعل.

// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
  'owner-secret', '', aes256, [prPrint, prFillAnnotations]);

التفاصيل التي تستحق الاحتفاظ بها تقع قبل واجهة برمجة التطبيقات. الملفات التي تصل من Word و Excel و PowerPoint وقائمة طويلة من مسارات "حفظ كملف PDF" تكون هجينة بشكل روتيني، لذا فإن برنامج التحميل الذي تختبره فقط ضد مخرجات المولد الخاص بك قد لا يقابل أبداً مثل هذا الملف في الاختبار. قم بتغذية بيئة الاختبار الخاصة بك بمستندات تم تصديرها من تطبيقات Office حقيقية، وليس فقط بالملفات التي أنتجها الكود الخاص بك.

فحص ملف تشك فيه

عمليتا فحص تحسمان السؤال بسرعة. افتح الملف في عرض سداسي عشري (hex view) واقرأ البايتات بعد startxref النهائي؛ يظهر ملف هجين قسماً كلاسيكياً قصيراً يحتوي قاموس المقطورة الخاص به على /XRefStm. أو قارن عدد الكائنات الذي يبلغ عنه التحليل الكامل بأعلى رقم كائن يعلنه /Size في المقطورة. تعني الفجوة الكبيرة أن الكائنات تختبئ في تدفقات لم يفتحها برنامج التحميل، وهو نفس النقص الذي يتحول إلى فشل في وقت الحفظ لاحقاً.

تتم تغطية جانب الكاتب من هذه القصة، وكيفية إنتاج تدفقات الكائنات والمراجع التقاطعية المضغوطة في المقام الأول، في مقالنا حول تدفقات الكائنات والتحديثات التراكمية. عندما يكون الملف الهجين المعني كبيراً جداً أيضاً، فإن تقنيات التحميل في دليل واجهة برمجة تطبيقات الملف المباشر لمسارات عمل PDF الكبيرة تتيح لك فحصه دون قراءة الملف بأكمله في الذاكرة. يقترن كلاهما بشكل طبيعي عملية الاسترداد الموصوفة هنا، والتي يتم شحنها كجزء من مكون HotPDF لـ Delphi و C++Builder جنباً إلى جنب مع واجهات برمجة التطبيقات للتحميل والتحرير والتشفير والتوقيع المغطاة في مكان آخر في هذه المدونة.