مقال تقني

تحرير PDF يدويًا في محرر نصوص، ثم إصلاحه

يستطيع HotPDF حفظ مستند محمّل بصيغة تشخيصية مصممة ليقرأها ويحررها إنسان، ثم إصلاح الملف بعد أن تحرره. تكتب SaveLoadedDocumentDiagnostic المستند بترتيب قواميس حتمي، وكل كائن ككائن غير مباشر عادي، بلا دفقات كائنات، وبلا دفق مرجع تقاطعي، وبلا تخطيط للعرض التدريجي (linearisation). ثم تعيد RepairDiagnosticDocument بناء إزاحات البايتات، وأطوال الدفقات، وجدول المرجع التقاطعي، والذيل الذي أبطله تحريرك

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

ما الذي يجعل الحفظ التشخيصي مختلفًا؟

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

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

رسم تخطيطي يقارن بين حفظ PDF إنتاجي عادي وحفظ HotPDF التشخيصي الذي يكتب كائنات نص عادية مقروءة وجدول مرجع تقاطعي نصي صرف يمكن فتحه في أي محرر نصوص
الحفظ العادي يخفي الكائنات داخل دفقات مضغوطة، بينما يرتّب الكاتب التشخيصي كل كائن في كتلة مستقلة مع جدول مرجع تقاطعي نصي صرف. والمخرجات ملف يمكنك تصفّحه وتغييره في أي محرر
uses
  HPDFDoc;

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoice.pdf') <= 0 then
      Exit;
    // مقروء، كائن واحد لكل كتلة، لا شيء مخفي داخل دفقات كائنات
    Pdf.SaveLoadedDocumentDiagnostic('invoice-diagnostic.pdf');
  finally
    Pdf.Free;
  end;
end;

لماذا يُفسد التحرير الملف، وما الذي يجب إعادة بنائه؟

ثلاثة عناصر في ملف PDF تُرمّز مواضع أو أعداد البايتات. جدول المرجع التقاطعي يربط كل رقم كائن بإزاحة بايت. وقيمة startxref في نهاية الملف تشير إلى الجدول. وكل قاموس دفق يحمل /Length يذكر عدد البايتات التي يشغلها متن الدفق

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

رسم تخطيطي يوضح كيف يُزيح التحرير اليدوي إزاحات البايتات بحيث يصبح جدول المرجع التقاطعي ومؤشر startxref وأطوال الدفقات قديمة إلى أن تعيد RepairDiagnosticDocument بناء الثلاثة من التخطيط الفعلي
إضافة بايتات تُزيح كل إزاحة تليها، فتتوقف المواضع المسجّلة ومؤشر startxref وأطوال الدفقات المُعلَنة عن مطابقة الواقع. يعيد الإصلاح بناء كل واحد منها من واقع ما يحتويه الملف المُحرَّر الآن فعليًا
var
  Options: THPDFDiagnosticRepairOptions;
  Report: THPDFDiagnosticRepairReport;
begin
  Options := THPDFDiagnosticRepairOptions.Default;
  Options.VerifyOutput := True;         // تحميل النتيجة قبل استبدالها

  if THotPDF.RepairDiagnosticDocument('invoice-diagnostic.pdf',
       'invoice-fixed.pdf', Options, Report) then
    Writeln(Format('%d objects, %d streams, %d lengths corrected, ' +
      '%d xref rows rebuilt, %d pages verified',
      [Report.ObjectCount, Report.StreamCount,
       Report.RepairedStreamLengthCount, Report.RebuiltXRefEntryCount,
       Report.VerifiedPageCount]))
  else
    Writeln(Format('repair failed (%d): %s',
      [Ord(Report.Status), string(Report.Diagnostic)]));
end;

انسخ البايتات، لا تُعِد التسلسل

القرار التصميمي الذي يجعل هذه الأداة موثوقة هو أنها لا تُحمِّل المستند أبدًا وتكتبه من جديد. فهي تنسخ نطاقات البايتات للكائنات التي تحققت من صحتها، ولا تغيّر إلا قيم /Length الخاطئة، وتعيد بناء الجدول والذيل و startxref حولها

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

رسم تخطيطي يقارن بين نسخ الإصلاح في HotPDF لنطاقات البايتات التي تحقق منها حرفيًا ومسار التحميل وإعادة التسلسل الذي يوحّد الصياغة ويعيد كتابة هروبات السلاسل النصية ويعيد ترميز حمولات الدفقات
ينسخ الإصلاح النطاقات التي تحقق منها بايتًا بايتًا ويصحح فقط قيم /Length الخاطئة، بحيث يبقى الفارق اليدوي الذي أحدثته سليمًا. أما إعادة التحميل والتسلسل فكانت ستعيد كتابة الصياغة والهروبات والترميزات التي لم تلمسها أبدًا بصمت

القاعدتان اللتان تمنعان الكائنات الزائفة

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

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

وأطوال الدفقات تحتاج عناية مماثلة. فطول الحمولة الحقيقي يستثني فاصل نهاية السطر الذي يُدرجه الكاتب قبل endstream. فعندما يقع الطول المُعلَن بالضبط عند العلامة، يُقبل ذلك التصريح. وعندما لا يقع، يُجري المُصلِّح بحثًا محدودًا عن زوج endstream و endobj ويطرح فاصلاً واحدًا بالضبط، سواء كان CR أو LF أو CRLF. والخطأ بمقدار بايت واحد هنا ينتج ملفًا يُفتح ويرسم صفحة بها عامل زائد شارد في نهاية دفق محتواها

ما الذي يُسمح للذيل بالاحتفاظ به

الذيل المُعاد بناؤه ليس نسخة من القديم. فـ /Size و /Prev و /XRefStm جميعها تصف تخطيط الملف السابق ويُعاد حسابها أو تُحذف. ولا تبقى إلا المدخلات التي تحققت أهدافها من صحتها: /Info و /ID و /Encrypt. ويجب أن يُحلَّل /Root إلى كائن Catalog واحد بالضبط — لا صفر ولا عدة كائنات، وهذا بالضبط ما ينتجه غالبًا ملف مُجمَّع بلصق كتل كائنات

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

الميزانيات ورموز الفشل الصادقة

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

وتُبلّغ حالات الفشل عن حالة محددة بدلاً من قيمة منطقية: وسائط غير صالحة، مصدر أكبر من اللازم، ليس ملفًا تشخيصيًا، تجاوز حد الكائنات، كائن غير صالح، كائن مكرر، فهرس مفقود، ذيل غير صالح، خطأ في المخرجات، أو فشل التحقق. وكل حالة تقابل إصلاحًا مختلفًا. فكائن مكرر يعني أنك لصقت كتلة كائن مرتين. وفهرس مفقود يعني عادة أن الكائن الحامل لـ /Type /Catalog تلف. وفشل التحقق يعني أن البنية أصبحت متسقة الآن لكن شجرة الصفحات لا تُحمَّل، وهذا نموذجيًا نتيجة مصفوفة /Kids حُررت دون مواءمة مع /Count الخاص بها

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

الحفظ التشخيصي والإصلاح والفحص المسبق والمقارنة جميعها تعمل على نموذج المستند المحمّل نفسه في Delphi و C++Builder؛ وتجد قائمة الميزات الكاملة على صفحة مكوّن HotPDF لـ PDF في Delphi