Technical Article

شرح بيانات PDF الوصفية و outline و annotation

إذا جردت أوصاف الصفحات، بقيت لديك طبقة رقيقة من البنية لا يطبعها أحد، لكن كل قارئ وفهرس ونظام أرشفة يعتمد عليها. كائن الصفحة لا يعرف شيئًا عن الفصل الذي ينتمي إليه، أو المؤلف الذي كتبه، أو الحاشية التي تشير إلى مكان آخر. هذه المعرفة تعيش مستوى أعلى، في ثلاث بنيات متصلة بكتالوج المستند: metadata streams و outline tree و annotation arrays لكل صفحة. وما يجمعها سمة تجعل الخطأ فيها سهلًا. لا تحمل أي منها علامات مرئية على الصفحة، لذلك قد يبدو الملف سليم العرض تمامًا ومع ذلك يكون بلا bookmarks، أو يناقض حقل المؤلف الخاص به، أو يوجّه رابطًا إلى كائن صفحة لم يعد موجودًا

هذه هي الطبقة التي يعرضها PDF library بوصفها document properties و bookmark APIs واستدعاءات link أو annotation، وهي أيضًا الطبقة التي يقرأها search crawler ليقرر موضوع مستندك. أما نموذج الكائنات في الأسفل، فمغطى في الشرح العملي لبنية مستند PDF، وهنا يقتصر التركيز على ما يتدلّى من catalog

ترتبط البنيات الثلاث كلها عند catalog. والربط الكامل الذي يجمعها يبدو هكذا

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
   /Outlines 3 0 R
   /Names << /EmbeddedFiles 4 0 R >>
   /Metadata 5 0 R
>>
endobj

أربع إدخالات، وأربع نظم فرعية مستقلة. /Pages هو المستند المرئي؛ /Outlines هي شجرة bookmarks؛ /Metadata تشير إلى XMP stream؛ /Names تصل إلى document-wide name dictionary، الذي يحمل من بين أمور أخرى مرفقات الملفات المضمنة. وكل واحد منها اختياري، والقارئ الذي لا يجد أيا منها ما يزال يعرض الصفحات. وهذه الاختيارية هي بالضبط سبب تعفن طبقة التنقل أولًا عندما تُحرر الملفات بأدوات تفهم الصفحات فقط

مخزنا metadata يختلفان

يحمل PDF بيانات المستند الوصفية في مكانين في الوقت نفسه، وتبدأ المشكلة عندما يقولان شيئين مختلفين. الآلية الأصلية هي document information dictionary، المشار إليه عبر /Info في trailer: مجموعة مسطحة من أزواج المفتاح والقيمة لـ /Title و /Author و /Subject و /Keywords و /Creator و /Producer والتاريخين. وهي بسيطة ويقرأها كل عارض. أما PDF 2.0 فيجعل معظمها مهجورًا لصالح الآلية الثانية، وهي XMP metadata stream

XMP هو مستند XML مستقل بذاته، مكتوب بصيغة RDF، ويُخزن كـ stream يصل إليه catalog عبر /Metadata وموسوم بـ /Type /Metadata /Subtype /XML. وعلى خلاف Info dictionary المدفون داخل بنية كائنات PDF، صُممت حزمة XMP لكي تُستخرج وتُحلل وحدها بواسطة أدوات لا تعرف شيئًا عن PDF. وفيما يلي حزمة نموذجية

5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:xmp="http://ns.adobe.com/xap/1.0/"
        xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
      <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
      <dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
      <xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
      <xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
      <pdf:Producer>losLab PDF Library</pdf:Producer>
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj

ثلاث تفاصيل في تلك الكتلة هي التي تحدد ما إذا كانت البيانات الوصفية ستصمد أمام الأدوات الفعلية أم لا. تعليمات المعالجة xpacket ليست زينة: فهي تؤطر الحزمة بحيث يستطيع المستخرج العثور عليها داخل تدفق أكبر من البايتات، والكاتب الذي يهمل الإغلاق <?xpacket end="w"?> ينتج ملفًا يفتح بلا مشكلة، لكنه يربك المدققين الصارمين. وتهم أنواع خصائص البيانات أيضًا. dc:title بديل لغوي ملفوف داخل rdf:Alt، بينما dc:creator قائمة مرتبة وتستخدم rdf:Seq؛ وتمرير أي منهما كعقدة نصية عادية هو الخطأ الأكثر شيوعًا في XMP، ويُتسامح معه في معظم العارضات حتى تواجه العارض الذي لا يفعل ذلك. بادئات مساحات الأسماء أمر متعارف عليه، لكن URI التي ترتبط بها هي المعيار الفعلي: المحلل يعتمد على URI لا على البادئة

القاعدة الحاسمة مع المخزنين هي أن يتفقا. إذا قال /Info إن المؤلف شخص، وقال dc:creator اسمًا آخر، فأنت قد شحنت مستندًا يجيب عن السؤال نفسه بطريقتين، والجواب الذي ينتصر يعتمد على الحقل الذي تقرؤه الأداة المستهلكة. عادةً يكتب لك library الحقلين معًا، لكن ما إن تعدل أحدهما يدويًا أو تدمج ملفات من مولدات مختلفة حتى ينفصلا. عامل Info dictionary بوصفه توافقًا مع الأنظمة القديمة، وXMP بوصفه مصدر الحقيقة، وأعد توليدهما من مجموعة قيم واحدة بدل ترقيعهما كل على حدة. وفي PDF/A يصبح هذا مطلب امتثال: ISO 19005 يفرض XMP ويحظر أي خاصية في Info تتعارض مع نظيرتها في XMP

شجرة outline خلف لوحة bookmarks

ما يعرضه القارئ على أنه لوحة bookmarks هو، داخل الملف، شجرة مضاعفة الربط من القواميس تسمى document outline. يشير catalog إلى root outline dictionary عبر /Outlines؛ ويشير الجذر إلى أول وآخر العناصر في المستوى الأعلى؛ وكل عنصر مربوط بجيرانه وبالأصل الخاص به. لا توجد array واحدة للإشارات المرجعية في أي مكان. تُعاد بناء البنية كلها باتباع المراجع، ولهذا السبب بالضبط يمكن لرابط واحد مكسور أن يجعل فرعًا كاملًا يختفي من اللوحة من دون أي خطأ

8 0 obj                                    % the outline root
<< /Type /Outlines /Count 4 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj                                    % top-level: a chapter
<< /Title (Chapter 1: Results)
   /Parent 8 0 R /Count 2
   /First 12 0 R /Last 15 0 R >>
endobj
12 0 obj                                   % first child
<< /Title (Introduction)
   /Parent 9 0 R /Next 15 0 R
   /Dest [3 0 R /XYZ 72 720 0] >>
endobj
15 0 obj                                   % second child, last sibling
<< /Title (Methodology)
   /Parent 9 0 R /Prev 12 0 R
   /Dest [3 0 R /Fit] >>
endobj

عندما تقرأ الروابط تصبح الثوابت واضحة. كل عنصر يشير إلى /Parent الخاص به. الأشقاء يشكلون سلسلة عبر /Prev و /Next، مع حذف الأول لـ /Prev وحذف الأخير لـ /Next. ويذكر الأصل أول أبنائه وآخرهم عبر /First و /Last، ولا يمكن الوصول إلى الأبناء الواقعين بينهما إلا بمتابعة سلسلة الأشقاء. خطأ واحد يكفي لإخفاق صامت: /Next قديم يقطع فصلًا، وأصل لا ينهي /Last فيه السلسلة يترك العناصر يتيمة، والقارئ يعرض ما يستطيع الوصول إليه فقط

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

يكتسب كل عنصر مكانه لأنه يشير إلى شيء ما. /Title هو ما تعرضه اللوحة؛ و /Dest هو المكان الذي تصل إليه النقرة. يمكن أن تكون الوجهة مضمنة داخل العنصر نفسه، كما في المثال أعلاه، أو اسمًا يُحل عبر document name dictionary، وهو الخيار الأفضل عندما تستهدف bookmarks و links كثيرة المواقع نفسها، لأنك تصلح الهدف المتحرك في مكان واحد. عادةً يخفي library هذه الشجرة خلف مقبض outline-root وطرائق تضيف العناصر الفرعية؛ وفي HotPDF يكشف المستند عن OutlineRoot من النوع THPDFDocOutlineObject، ويقوم بتمرير روابط /Prev و /Next و /Parent و /Count لك عند إلحاق العناصر. وهذا يستحق الاستفادة منه، لأن الصيانة اليدوية لهذه الثوابت أثناء التعديلات هي نقطة انهيار outline

الوجهات: نحو أين تتجه النقرة

كل من bookmarks وتعليقات link annotations يشير إلى وجهات، والوجهة أكثر من مجرد رقم صفحة. إنها array تسمي كائن صفحة ثم تحدد، عبر فعل في الخانة الثانية، كيف يجب على القارئ تأطيرها. وأكثرها شيوعًا وأكثرها إساءة استخدام هو /XYZ، بصيغة [page /XYZ left top zoom]. معاملاتُه الثلاثة مستقلة، ويمكن أن يكون أي منها null بمعنى "اترك هذا كما كان لدى القارئ". لذلك فإن [page /XYZ null null null] يقفز إلى الصفحة من دون المساس بموقع التمرير أو التكبير، وهو عادة ما تريده من رابط "اذهب إلى الصفحة". القيم كلها في default user space، وتقاس من الزاوية السفلية اليسرى مع ازدياد y إلى الأعلى، وهي نفس منظومة الإحداثيات التي يستخدمها محتوى الصفحة. أما المؤلفون القادمون من عقلية تخطيط الشاشة فيقيسون تلقائيًا من الأعلى ويرسلون القارئ إلى الطرف الخطأ من الصفحة

تستبدل عائلة /Fit التحديد الدقيق بالقدرة على الصمود. [page /Fit] يضبط الصفحة كلها داخل النافذة، و [page /FitH top] يطابق عرض الصفحة مع حافة علوية معطاة، و [page /FitR l b r t] يكبر مستطيلاً ليملأ العرض. وبما أن هذه الأنواع تحسب التحجيم من هندسة الصفحة لا من إحداثيات ثابتة، فإن وجهة /Fit تظل تتصرف بعقلانية بعد تغيير حجم الصفحة، بينما قد تترك وجهة /XYZ ذات التكبير المدمج القارئ يحدق في الهامش. بالنسبة إلى جدول المحتويات، فإن /FitH مع إحداثي أعلى القسم يصمد أفضل من /XYZ مع تكبير مُخمَّن

annotations: كل ما هو تفاعلي وليس محتوى الصفحة

annotation هو كائن يتراكب فوق الصفحة من دون أن يكون جزءًا من تدفق المحتوى الخاص بها. الروابط والملاحظات اللاصقة والتظليلات وعناصر النماذج وأيقونات المرفقات والطوابع كلها annotations، وتُسرد في array /Annots الخاصة بالصفحة التي تقع عليها. إزالة annotation من تلك array تزيله من الصفحة حتى لو بقي المحتوى الأساسي كما هو. وهذه هي الفكرة كلها: annotations طبقة تحرير منفصلة عن العلامات التي تعلوها

كل annotation يشترك في عمود فقري صغير. /Subtype يسمي النوع، و /Rect يعطي صندوقه المحيط بإحداثيات الصفحة، و /Contents يحمل نصًا يعمل أيضًا بوصفه الوصف القابل للوصول. annotation الخاص بالرابط يستحق الدراسة لأنه يأتي بصيغتين: وجهة خالصة، أو action

12 0 obj                                    % link to a destination
<< /Type /Annot /Subtype /Link
   /Rect [100 200 300 250]
   /Border [0 0 0]
   /Dest [5 0 R /XYZ null null null] >>
endobj
13 0 obj                                    % link that runs an action
<< /Type /Annot /Subtype /Link
   /Rect [50 50 200 100]
   /Border [0 0 0]
   /A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj

/Rect هو hotspot؛ فالنقر داخله ينقل القارئ إلى الوجهة، مع إعادة استخدام القواعد نفسها التي يستخدمها outline. أما /Border [0 0 0] فيقوم بعمل حقيقي، إذ يمنع المستطيل الافتراضي القبيح الذي ترسمه العارضات حول الروابط. والصيغة الثانية تستبدل /Dest العاري بـ action /A، ويختار /S داخله السلوك: /GoTo داخل هذا الملف، /GoToR لملف آخر، /URI لعنوان ويب، /Launch لتشغيل برنامج خارجي. والأخيرة تستحق الشك. فـ /Launch الذي يشغل ملفًا تنفيذيًا هو السلوك الذي يجعل PDF ناقلًا للبرمجيات الخبيثة، لذلك تمنعه العارضات المطابقة أو تطلب الإذن بصوت عال، ويفشل الرابط عند معظم القراء. تمسك بـ /URI و /GoTo واترك /Launch وشأنه

تضيف annotations العلامة مثل التظليلات والملاحظات اللاصقة، وكذلك annotations الشكل مثل /Square، تعقيدًا صغيرًا: مظهرها على الشاشة لا يُستنتج من نوعها. يعرض القارئ نسخته الخاصة ما لم تثبت المظهر باستخدام appearance stream، وهو الإدخال /AP الذي يشير إلى form XObject يحمل أوامر الرسم. إذا تجاهلته فقد يبدو التظليل نفسه مختلفًا في قارئين، أو قبل وبعد مرور الملف عبر editor. وأي شيء يكون شكله الدقيق جزءًا من المستند يجب أن تزوده بـ /AP. وبالمناسبة، مرفقات الملفات تعيد استخدام الآلية نفسها: stream ملف مضمّن و file specification dictionary، وتظهر إما كـ annotation من النوع /FileAttachment أو عبر tree الأسماء /EmbeddedFiles تحت /Names الخاص بـ catalog

أين تنهار هذه الطبقة، وكيف تلتقط ذلك

الفشل المتكرر عبر كل هذا هو المرجع المعلّق. تتوقف الإشارات المرجعية عن الظهور عندما لا يحتوي catalog على إدخال /Outlines أو عندما تنكسر سلسلة الأشقاء في منتصف الشجرة؛ وتتجاهل العارضات البيانات الوصفية عندما تفتقر XMP stream إلى العلامة /Type /Metadata /Subtype /XML أو عندما تكون حاوية xpacket مشوهة. وفي كل حالة يكون محتوى الصفحة سليمًا، لذلك يبدو الفتح العابر صحيحًا، ولا يظهر العيب إلا في اللوحة التي لم يراجعها أحد

عادتان رخيصتان تلتقطان معظم ذلك. افتح الملف النهائي في عارض حقيقي وانقر عبر لوحة bookmarks ومجموعة من الروابط، وهذا يختبر graph المراجع بالطريقة التي سيفعلها القارئ. ثم أعد قراءة البيانات الوصفية بأداة منفصلة وتحقق من أن Info dictionary و XMP متفقان، وهو الخلاف الوحيد الذي لا تكشفه أي كمية من النقر. عندما تولد هذه الطبقة عبر library تتولى bookkeeping الروابط، فلن تنفتح معظم هذه الأفخاخ أصلًا. إن مكوّن HotPDF لـ Delphi و C++Builder يعرّض بنى outline و annotation و metadata عبر document-level APIs، لذلك تصف أنت تسلسل bookmarks والروابط ويقوم هو بتمرير المراجع. أما نموذج الكائنات الذي ترتبط به هذه البنى، فيغطيه الشرح التقني لبنية ملف PDF بما فيه catalog و cross-reference table اللذان يعتمدان عليهما