مقال تقني

التعامل مع ملفات PDF ذات المرجع الهجين (Hybrid-Reference PDFs) من تطبيقات Office في Delphi

قم بتصدير مستند من Microsoft Word أو Excel باستخدام خيار الحفظ كملف PDF (Save as PDF) ويكون الملف على القرص، في أغلب الأحيان، ملفًا ذو مرجع هجين (hybrid-reference file). إنه يحمل معلومات المرجع التبادلي (cross-reference information) الخاصة به مرتين: مرة كجدول ذي عرض ثابت كلاسيكي (classic fixed-width table) ينهي كل ملف PDF حتى الإصدار 1.4، ومرة أخرى كتيار مرجعي تبادلي مضغوط (compressed cross-reference stream) تعتمد عليه معظم المستندات بالفعل. يربط مفتاح ذيل (trailer key) واحد، /XRefStm، العرضين (views) معًا، وما إذا كانت الأداة ترى المستند بأكمله يعود إلى ما إذا كانت تتبع ذلك المفتاح

ينظر هذا المقال إلى الملفات الهجينة من الجانب المستهلك (consuming side): كيف تبدو البايتات الموجودة في نهاية الملف، وكيف يتباعد العرضان تحت التحرير، وكيف يمكن لمسار (pipeline) Delphi اكتشاف وتوجيه المدخلات الهجينة. كيفية قيام المحمل (loader) بدمج العروض، ولماذا لا يكون الترتيب قابلاً للتفاوض، هو موضوع مقالنا حول HotPDF حول تحميل الملفات ذات المرجع الهجين؛ يتعلق هذا المقال بالتعرف على التخطيط (layout) في المقام الأول

لماذا تكتب عمليات تصدير Office الفهرس (index) مرتين

قدم PDF 1.5 ميزتين غيرتا شكل الملف: تيارات المرجع التبادلي (cross-reference streams)، والتي تخزن فهرس الكائن (object index) كبيانات ثنائية مضغوطة بدلاً من جدول نص عادي (plaintext table)، وتيارات الكائنات (object streams)، والتي تعبئ العديد من الكائنات الصغيرة في حاوية واحدة مضغوطة بتنسيق Flate. ينتج الكاتب الذي يستخدمها ملفات أصغر، لكن قارئ PDF 1.4 لا يمكنه فتح النتيجة، لأن الهياكل (structures) التي يعتمد عليها، وهي الكلمة الأساسية xref وقاموس trailer، قد اختفت

تحدد ISO 32000-1 §7.5.8.4 الحل الوسط. يكتب الملف ذو المرجع الهجين كليهما: جدول مرجعي تبادلي كلاسيكي يعالج الكائنات التي يجب أن يصل إليها القارئ القديم، الكتالوج وشجرة الصفحة من بينها، وتيار مرجعي تبادلي يفهرس كل شيء آخر. يتم تمييز الكائنات المطوية (folded) في تيارات الكائنات كـ free في الجدول الكلاسيكي، لذلك يتخطاها قارئ 1.4 دون شكوى؛ توجد مواقعها الحقيقية في التيار (stream) فقط. يحمل الذيل (trailer) الكلاسيكي بعد ذلك مفتاح /XRefStm الذي يحتفظ بإزاحة البايت (byte offset) لهذا التيار. لا يقرأ العارض (viewer) القديم المفتاح أبدًا ويعرض الملف من عرض الجدول. يتبعه العارض الحديث ويرى المستند الكامل. أصدر Word و Excel هذا التخطيط (layout) بالضبط لسنوات، وهذا هو السبب في أن الملفات الهجينة ليست حالة زاوية غريبة (exotic corner case) ولكنها جزء كبير مما تتلقاه مسارات الأعمال (business pipelines)

كيف يبدو ذيل (tail) الملف الهجين

التخطيط هو الأسهل للفهم من البايتات. إليك ذيل ملف هجين صغير، تم اختصار الإزاحات (offsets)؛ في تصدير Office حقيقي، عادةً ما تكون قيمة /XRefStm عبارة عن إزاحة كبيرة بالقرب من نهاية الملف. ترتيب القراءة هو المشي الذي يبدأ من الذيل (tail-first walk) والموصوف في نظرتنا العامة على بنية ملف PDF: ابحث عن %%EOF، اقرأ startxref، انتقل إلى الجدول

% ... body objects, including object streams and, at byte 116,
% the cross-reference stream (a stream object with /Type /XRef) ...

xref                    % classic section: what startxref points at
0 4
0000000000 65535 f      % slot 0: head of the free list, always present
0000000017 00000 n      % object 1: the catalog, visible to any reader
0000000000 65535 f      % object 2: marked free -- lives in an object stream
0000000000 65535 f      % object 3: same; only the stream view locates it
trailer
<<
  /Size 4
  /Root 1 0 R
  /XRefStm 116          % byte offset of the cross-reference stream
>>
startxref
7164                    % byte offset of the 'xref' keyword above
%%EOF

تحمل تفصيلتان في هذه النسخة (dump) الآلية بأكملها. أولاً، يشير startxref إلى القسم الكلاسيكي، عن قصد: هذا هو العنوان الذي يجب أن يهبط عليه قارئ قديم. لا يمكن الوصول إلى تيار المرجع التبادلي إلا من خلال مفتاح /XRefStm داخل قاموس الذيل، لذلك فإن المحلل (parser) الذي لا يبحث أبدًا عن هذا المفتاح لا يعلم أبدًا بوجود التيار. ثانيًا، الكائنات 2 و 3 هي أكاذيب من النوع الحميد (benign kind). يعلن الجدول الكلاسيكي أنها حرة (free)، لكنها كائنات حقيقية توجد داخل حاوية مضغوطة؛ العلامة الحرة (free marking) هي ما يمنع قارئ 1.4 من التعثر فوق إدخالات لا يمكنه استخدامها. يستنتج المستهلك الذي يثق في العرض الكلاسيكي وحده أن معظم هذا المستند غير موجود

كيف يتباعد العرضان (views)

الملف الهجين الخارج حديثًا من Word متسق داخليًا: يصف كلا العرضين المستند نفسه، كل منهما ضمن نطاقه المعلن. تبدأ المشكلة عندما يتم تحرير الملف بواسطة أداة تفهم واحدًا فقط من العروض. ضع في اعتبارك أداة ختم (stamping utility) تلحق (appends) تحديثًا تزايديًا بنمط كلاسيكي (classic-style incremental update): كائنات جديدة، وقسم xref جديد، وسلسلة /Prev للقسم السابق، وذيل جديد. إذا أسقط هذا الذيل مفتاح /XRefStm، فسيصبح عرض التيار (stream view) يتيمًا؛ إذا قام بنسخ القيمة القديمة للأمام، فإن عرض التيار لا يزال يصف المستند كما كان قبل التحرير. في كلتا الحالتين، يختلف الفهران (indexes) الآن حول ما يحتويه الملف

يحتوي الملف الناتج على بصمة فشل (failure signature) مميزة: الكائنات المرئية في عرض واحد مفقودة أو قديمة في العرض الآخر. يجد القارئ الذي يحلل من خلال عرض التيار إصدار ما قبل التحرير من كائن محدث، أو لا يجد أي إدخال على الإطلاق لكائن ملحق (appended one). يرى القارئ على عرض الجدول التعديل ولكنه يفقد مسار الكائنات المضغوطة التي يحددها التيار وحده. من الناحية العملية، يظهر هذا كحقول نموذج (form fields) تنجو في عارض واحد وتتلاشى في آخر، والتعليقات التوضيحية (annotations) التي يبدو أن مسار الختم (stamping pass) قد حذفها، أو عمليات البحث التي تهبط على الكائن الخطأ تمامًا

ما يجعل هذه الملفات باهظة الثمن (expensive) لتصحيح أخطائها هو أن Adobe Acrobat يفتحها عادةً دون شكوى: عندما يختلف الفهرس (index) عن البايتات، فإنه يعيد بناء بيانات المرجع التبادلي بهدوء عن طريق مسح رؤوس الكائنات (object headers)، لذا فإن كل من أنتج الملف المعطل لا يرى أي خطأ. يظهر الفشل لاحقًا، عندما يصل الملف إلى مستهلك صارم (strict consumer)، أو مدقق فحص مسبق (preflight validator)، أو خدمة توقيع (signing service)، أو وظيفة استيعاب أرشيفية (archival ingest job)، والتي تثق في البنية المعلنة وتبلغ عن الكائنات المفقودة أو عدم تطابق المرجع التبادلي. تبدأ كل تذكرة إزالة التزامن الهجينة (hybrid desynchronization ticket) تقريبًا بـ "تفتح بشكل جيد في Acrobat"

اكتشاف ملف هجين (hybrid file) في Delphi الخالص (plain Delphi)

لا يتطلب تصنيف المدخلات مكتبة PDF. يمكن أن يظهر مفتاح /XRefStm فقط داخل قاموس ذيل (trailer dictionary) كلاسيكي، ويقع الذيل النشط (active trailer) ضمن الكيلوبايتات القليلة الأخيرة من الملف، لأن المواصفات تتطلب ظهور %%EOF بالقرب من النهاية المادية. قراءة نافذة ذيل محددة (bounded tail window) والبحث فيها يكفي للفرز (triage):

uses
  System.SysUtils, System.Classes, System.StrUtils, System.Math;

function IsHybridReferencePdf(const FileName: string): Boolean;
const
  TailWindow = 2048;
var
  Stream: TFileStream;
  Buf: TBytes;
  Tail: string;
  Len, TrailerPos, NextPos, KeyPos, StartXrefPos: Integer;
begin
  Result := False;
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    if Stream.Size < 48 then
      Exit;
    Len := Min(TailWindow, Integer(Stream.Size));
    SetLength(Buf, Len);
    Stream.Position := Stream.Size - Len;
    Stream.ReadBuffer(Buf[0], Len);
  finally
    Stream.Free;
  end;

  // Every keyword involved is 7-bit ASCII, so a byte-wise decode is safe
  Tail := TEncoding.ANSI.GetString(Buf);

  // Find the LAST 'trailer' keyword: with incremental updates,
  // the newest trailer is the one that governs the file
  TrailerPos := 0;
  NextPos := Pos('trailer', Tail);
  while NextPos > 0 do
  begin
    TrailerPos := NextPos;
    NextPos := PosEx('trailer', Tail, NextPos + 1);
  end;
  if TrailerPos = 0 then
    Exit;  // no classic trailer: a pure xref-stream file, not hybrid

  // A hybrid trailer carries /XRefStm between 'trailer' and 'startxref'
  KeyPos := PosEx('/XRefStm', Tail, TrailerPos);
  StartXrefPos := PosEx('startxref', Tail, TrailerPos);
  Result := (KeyPos > 0) and
    ((StartXrefPos = 0) or (KeyPos < StartXrefPos));
end;

تتوافق النتائج الثلاث مع التخطيطات الثلاثة. الملف الكلاسيكي فقط (classic-only file) به ذيل ولكن ليس به /XRefStm: خطأ (False). الملف الذي يلتزم تمامًا بتيارات المرجع التبادلي لا يحتوي على الكلمة الأساسية trailer على الإطلاق، ومفاتيح الذيل الخاصة به تعيش في قاموس التيار (stream dictionary): خطأ أيضًا، بشكل صحيح، لأن هذا الملف مضغوط، وليس هجينًا. التخطيط المزدوج الفهرس (double-indexed layout) هو الوحيد الذي يُرجع صحيح (True)

للاستخدام الإنتاجي (production use)، هناك إجراءان تقوية (hardenings) يستحقان الأسطر الإضافية. تحليل (Parse) العدد الصحيح بعد /XRefStm، والانتقال (seek) إلى تلك الإزاحة، والتأكد من أن كائن تيار مع /Type /XRef موجود بالفعل هناك؛ يمكن أن يحمل الملف المقتطع (truncated file) المفتاح بينما يختفي التيار، وهو ما ينتمي إلى فئة مختلفة (different bucket) عن الملف الهجين السليم. وعامل (treat) حجم النافذة (window size) كمعلمة (parameter): 2 كيلوبايت يغطي مخرجات Office العادية، لكن قاموس الذيل الكبير بشكل غير عادي يمكن أن يدفع الكلمة الأساسية خارج النطاق، وتوسيع النافذة يتغلب على إعلان الملف بأنه كلاسيكي عن طريق الصدفة

توجيه الملفات الهجينة عبر مسار (pipeline) Delphi

يشتري لك الاكتشاف قرار توجيه (routing decision). بالنسبة للملفات التي تتم قراءتها أو عرضها أو التحقق من صحتها فقط، استخدم محمل (loader) يحل (resolves) كلا العرضين، ثم تحقق من السلوك بدلاً من البايتات. يحلل مكون PDFium سلسلة /XRefStm أثناء التحميل، لذلك فإن جدول الكائنات الذي يراه الكود الخاص بك هو الجدول المدمج (merged one)، وعمليات التحقق الموضحة في مقالنا حول التحقق من كائن وتيارات المرجع التبادلي تنطبق دون تغيير. إذا تعرض ملف هجين غير متزامن لتلف سيء بما يكفي لرفض التحميل، فإن المحرك يبلّغ عنه من خلال مجموعة الأخطاء (error set) الخاصة به، FPDF_ERR_SUCCESS و FPDF_ERR_UNKNOWN و FPDF_ERR_FILE و FPDF_ERR_FORMAT و FPDF_ERR_PASSWORD و FPDF_ERR_SECURITY و FPDF_ERR_PAGE، مع إنتاج التلف الهيكلي لـ FPDF_ERR_FORMAT. ومع ذلك، لا تعتمد على هذه الإشارة: فـ PDFium متساهل حسب التصميم ويعيد بناء معظم الملفات غير المتسقة بهدوء، لذا فإن التحميل الناجح يثبت أنه كان من الممكن استرداد الملف، وليس أن العرضين الخاصين به متفقان. التحقق المجدي من الاتساق هو مقارنة ما تجده مسيرة الكائنات الكاملة (full object walk) بما يعلنه /Size الخاص بالذيل

بالنسبة للملفات التي يعدلها مسارك (pipeline)، فإن السياسة الأكثر أمانًا هي التوقف عن جعلها هجينة (hybrid) على الإطلاق. التحميل المتبوع بحفظ كامل (full save) من خلال HotPDF يعيد كتابة المستند بمرجع تبادلي (cross-reference) واحد متسق ذاتيًا في شكل واحد: لا /XRefStm، لا عرض ثانٍ للخروج عن التزامن، كل كائن مملوك لإدخال فهرس (index entry) واحد بالضبط. هذا التطبيع (normalization) هو ما تريده قبل الاستيعاب الأرشيفي (archival ingest)، قبل خدمة RIP الصارمة في المراحل اللاحقة أو خدمة التوقيع، وبعد أي تعديل يتم تطبيقه على مدخل هجين. إنه يعمل لأن المحمل (loader) دمج العروض بشكل صحيح عند الدخول، وهي الآلية التي يمر بها مقال المرجع الهجين لـ HotPDF بالتفصيل

فئة الملفات الوحيدة التي يجب تركها كما هي هي المستندات الموقعة رقميًا. تنقل إعادة الكتابة الكاملة كل بايت، مما يبطل أي توقيع محسوب على النطاقات الأصلية. يجب أن يدخل تغيير الهجين الموقع كتحديث تزايدي سليم (proper incremental update) يحافظ على كلا العرضين؛ الملف الذي يحتاج للقراءة فقط يجب أن يمر دون أن يمس. التطبيع مخصص للملفات التي تملكها؛ الملفات الموقعة لا تقم إلا بالإضافة إليها (append to)

ملفات PDF ذات المرجع الهجين ليست مشوهة (malformed)؛ إنها جسر التوافق الخاص بالتنسيق، وستستمر تطبيقات Office في إنتاجها طالما أن أجهزة قراءة PDF 1.4 باقية في قاعدة التثبيت (install base). المسار الذي يمكنه اكتشاف مفتاح /XRefStm، والتحقق من صحة المستند المدمج باستخدام مكون PDFium، وإعادة إنشاء إخراج نظيف بفهرس واحد باستخدام مكون HotPDF، يتعامل معها على حقيقتها: مدخلات عادية مع علامة إرشادية إضافية (extra signpost) في الذيل