مقالة تقنية

إضافة إشارات مرجعية لملفات PDF الموجودة في دلفي باستخدام HotPDF

يضيف HotPDF إشارات مرجعية إلى ملف PDF موجود في دلفي من خلال AddLoadedOutline، والذي ينشئ مدخلات مخطط تفصيلي عند أي مستوى تداخل في مستند محمل، ويقرأ ويكتب الوجهات المسماة من خلال ResolveLoadedNamedDestination وAddLoadedNamedDestination. قم بتحميل الملف، وابنِ الشجرة، واستدعِ SaveLoadedDocument، وسيحمل الشريط الجانبي للقارئ الذي كان فارغاً جدول محتويات يعمل

إن السيناريو الذي يحفز كل هذا شائع بشكل محبط. تقوم بدمج اثني عشر عقداً في حزمة مراجعة واحدة، أو تجميع ثلاثة أدلة منتجات في ملف واحد قابل للتوزيع، والنتيجة ممتازة من الناحية الهيكلية: كل صفحة موجودة، وكل خط سليم. ثم يفتحه شخص ما في برنامج Acrobat وتكون لوحة الإشارات المرجعية فارغة. المستند المكون من 400 صفحة بدون تنقل مكتمل فنياً ولكنه غير قابل للاستخدام عملياً، وحتى الإصدار 2.347.0 كان بإمكان HotPDF قراءة الإشارات المرجعية وتعديلها وحذفها في المستندات المحملة ولكن لم يكن بإمكانه إنشاؤها أبداً. تم سد هذه الفجوة الآن، وتستعرض هذه المقالة مساحة واجهة برمجة التطبيقات الجديدة والجزء الغامض في PDF، وهو المفتاح /Count، الذي سيضرك إذا تعاملت معه كعداد إدخالات بسيط

ما هي شجرة المخطط التفصيلي في الواقع

المخطط التفصيلي لملف PDF هو شجرة قواميس مرتبطة بشكل مزدوج، وليس قائمة مسطحة، وهذا الهيكل يفسر كل معلمة في واجهة برمجة التطبيقات. يحددها معيار ISO 32000-1 §12.3.3: يشير كتالوج المستند إلى جذر /Outlines، ويشير الجذر إلى أطفاله الأوائل والأواخر عبر /First و/Last، وتتسلسل الأشقاء عبر /Next و/Prev بينما تشير كل إدخالة إلى الأعلى عبر /Parent. يحمل كل إدخال سلسلة /Title وعادةً /Dest يحدد المكان الذي يجب أن تأخذ القارئ إليه عند النقر عليها

تأتي الوجهات في نوعين، ويبقيها معيار ISO 32000-1 §12.3.2 منفصلة عمداً. الوجهة الصريحة هي مصفوفة مضمنة مثل [page /XYZ x y zoom]: مرجع مباشر لكائن الصفحة بالإضافة إلى مواصفات العرض. بينما تخزن الوجهة المسماة سلسلة اسم، ويعيش الهدف الفعلي في كتالوج المستند تحت شجرة الأسماء /Names /Dests، وهو مستوى واحد من التوجيه غير المباشر يتيح للعديد من الروابط مشاركة هدف واحد ويسمح للهدف بالتحرك دون لمس الروابط. يكتب HotPDF وجهات /XYZ صريحة عندما ينشئ إشارات مرجعية، ويمنحك استدعاءات منفصلة لقراءة شجرة الأسماء وتوسيعها

كيف تقوم بإضافة إشارات مرجعية إلى ملف PDF موجود في دلفي؟

استدعِ AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) بعد LoadFromFile: مرر ParentIndex = -1 لمدخل على المستوى الأعلى، أو الفهرس القائم على الصفر لمدخل مستوى أعلى موجود للتداخل تحته. تنشئ الوظيفة جذر /Outlines إذا لم يكن للمستند واحد من قبل، وتبني قاموس المدخلات، وتكتب مصفوفة /Dest [page /XYZ x y zoom] تشير إلى DestPageIndex القائم على الصفر، وتربط المدخل الجديد في سلسلة الأشقاء. وتعيد 0 عند النجاح و -1 عند الفشل، على سبيل المثال عندما يكون DestPageIndex خارج النطاق

هناك تفصيل واحد مهم للترتيب: يتم إدراج مدخل جديد في رأس سلسلة أطفال والده، كـ /First الجديد. لذلك إذا أضفت "Chapter 1" ثم "Chapter 2" كمدخلات مستوى أعلى، يظهر Chapter 2 فوق Chapter 1 in sidebar، وينزاح Chapter 1 إلى الفهرس 1 على المستوى الأعلى. النمط العملي هو إما إضافة المدخلات بترتيب قراءة عكسي، أو إضافة كل والد وتعبئة أطفاله على الفور بينما لا يزال يقع في الفهرس 0

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
    begin
      // Add chapters in reverse reading order: each new
      // top-level entry becomes /First (index 0).
      Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
      Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
      // Chapter 1 is now top-level index 0; nest sections
      // under it (children also prepend, so reverse order).
      Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
      Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
      Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

تطابق المعلمات X، وY، وZoom مباشرة وجهة /XYZ وتفتقر جميعها إلى 0 افتراضياً. يضع PDF أصل الإحداثيات في الزاوية اليسرى السفلية، لذا فإن Y = 792 يستهدف الجزء العلوي من صفحة بحجم US Letter، ووفقاً لمعيار ISO 32000-1 فإن القيمة الصفرية في خانة /XYZ تخبر العارض بالاحتفاظ بإعداده الحالي، وهو بالضبط ما تريده للتكبير في كل الحالات تقريباً. يحدث كل شيء على رسم الكائن المحمل في الذاكرة؛ ولا يلمس أي شيء القرص حتى يتم تشغيل SaveLoadedDocument، وهو نفس نموذج التعديل ثم الحفظ الذي تستخدمه واجهة برمجة تطبيقات المستندات المحملة لـ إعادة كتابة العنوان والمؤلف وبيانات XMP الميتا على ملفات PDF المحملة

إعادة توجيه الإشارات المرجعية: قفزات الصفحات مقابل إجراءات URI

يمكن إعادة ربط الإشارات المرجعية الحالية دون إعادة إنشائها، ويمنح HotPDF الحالتين استدعاءين متميزين لأن هياكل PDF الكامنة تحتها مختلفة حقاً. يستبدل SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) الـ /Dest لمدخل المستوى الأعلى عند Index بمصفوفة /XYZ جديدة، وهي الأداة للفشل الكلاسيكي بعد الدمج حيث تشير كل إشارة مرجعية إلى أرقام صفحات الملف الأصلي قبل الدمج. بينما يقوم SetLoadedOutlineURI(Index, URI) بشيء مختلف هيكلياً: فهو يربط قاموس إجراءات /A مع /S /URI، مما يحول الإشارة المرجعية إلى رابط ويب بدلاً من قفزة داخل المستند

// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);

// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');

Pdf.SaveLoadedDocument('retargeted.pdf');

حافظ على وضوح الآليتين عند تصميم الشجرة. الـ /Dest هو ملاحة بحتة داخل المستند؛ بينما يغادر إجراء URI المستند تماماً، ويتوقع معيار ISO 32000-1 أن ينتقل مدخل المخطط التفصيلي من خلال آلية واحدة أو أخرى، وليس كلتيهما. لذا، احتفظ بـ SetLoadedOutlineURI للمدخلات التي لا تحمل بالفعل وجهة صفحة، مثل ورقة "الصفحة الرئيسية للمنتج" أو "الإبلاغ عن مشكلة" في نهاية الشجرة بدلاً من عنوان فصل محول. إجراء URI المكتوب هنا هو نفس هيكل /S /URI المستخدم للتعليقات التوضيحية للروابط على محتوى الصفحة، وهو مغطى بمزيد من العمق في دليل إنشاء الارتباطات التشعبية في مستندات PDF مع HotPDF

كيف تعمل الوجهات المسماة في ملف PDF محمل؟

الوجهات المسماة هي آلية الارتساء الثابتة لمعيار ISO 32000-1 §12.3.2.3: بدلاً من تضمين مرجع صفحة في كل رابط، يحتفظ المستند بخريطة من الاسم إلى الوجهة في شجرة الأسماء /Names /Dests في الكتالوج، وتشير الروابط إلى المدخلات بالاسم. يبحث ResolveLoadedNamedDestination(Name) عن اسم في تلك الشجرة ويعيد فهرس الصفحة القائم على الصفر الذي يشير إليه، أو -1 إذا كان الاسم غائباً. يتعامن المحلل مع كلا شكلي التخزين اللذين تسمح بهما المواصفات للقيمة المطابقة: مصفوفة وجهة مجردة مثل [page /XYZ x y zoom] وشكل القاموس الذي يلف المصفوفة تحت مفتاح /D

تذهب AddLoadedNamedDestination(Name, DestPageIndex, X, Y) في الاتجاه الآخر: فهي تسجل مرساة جديدة في شجرة /Names /Dests، وتنشئ الشجرة نفسها عندما لا يحتوي المستند على أي منها، وتعيد True عند النجاح. يغطي الزوج معاً تدفق العمل حيث يجب أن يحترم المستند المدمج المراسي التي كتبتها الأدوات في المراحل السابقة، وينشر مراسي جديدة لتستهدفها الروابط اللاحقة

var
  PageIdx: Integer;
begin
  // Honor an anchor another tool wrote into /Names /Dests.
  PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
  if PageIdx >= 0 then
    Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);

  // Publish a new anchor for other links to target.
  if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
    Pdf.SaveLoadedDocument('anchored.pdf');
end;

الحدود الصادقة: الوجهات التي يكتبها HotPDF، للإشارات المرجعية والمراسي المسماة على حد سواء، هي وجهات /XYZ. الأنواع الصريحة الأخرى التي تحددها المواصفات، مثل /Fit و/FitH و/FitB وغيرها، لا تنبعث من هذه الاستدعاءات، على الرغم من أن /XYZ مع تكبير صفري يغطي حالة الاستخدام المهيمنة "اذهب إلى هذه البقعة، واحتفظ بالتكبير". ويقوم GetLoadedBookmarkPageIndex(Title)، وهو البحث المريح الذي يأخذ عنوان الإشارة المرجعية ويعيد صفحة وجهتها، بمسح المستوى الأعلى لشجرة المخطط التفصيلي، لذا استخدمه للتحقق من المدخلات على مستوى الفصل التي أنشأتها للتو بدلاً من الأوراق المتداخلة بعمق

فخ /Count: يعدد الفروع وليس المدخلات

مفتاح /Count على جذر المخطط التفصيلي لا يعني ما يفترضه معظم المطورين, ويؤدي الافتراض الخاطئ إلى فشل اختبارات تبدو مثل تلف البيانات. يحدد معيار ISO 32000-1 §12.3.3 المفتاح /Count كالعدد الإجمالي لـ الفروع المرئية على جميع المستويات، وليس عدد مدخلات المستوى الأعلى. وتحمل الإشارة في المدخل الفردي معنى أيضاً: تعني القيمة الموجبة أن المدخل مفتوح ويظهر هذا العدد من الفروع في الشريط الجانبي، بينما تعني القيمة السالبة أن المدخل مطوي ويتم إخفاء |N| من الفروع داخله

ظهر هذا من مجموعة اختبارات التراجع الخاصة بـ HotPDF أثناء بناء واجهات برمجة تطبيقات الإشارات المرجعية للمستندات المحملة. كان مستند الاختبار يحتوي على ثلاث إشارات مرجعية على المستوى الأعلى، إحداها تحتوي على طفل؛ وبعد حذف إدخال مستوى أعلى واحد، أبلغ GetLoadedOutlineCount، الذي يقرأ جذر /Count، عن 1 بدلاً من 2 المتوقعة. لم يتلف شيء: لم يكن الرقم 3 الأولي "ثلاثة مدخلات مستوى أعلى" في المقام الأول، ويجب أن تسير إعادة الحساب الصحيحة في سلسلة المستوى الأعلى لتجميع كل عقدة بالإضافة إلى /Count الموجب الخاص بها، وتخطي فروع العقد المطوية. الدرس المباشر للكود الخاص بك هو: لا تؤكد أبداً على فروق /Count بعد التعديلات الهيكلية، لأن القيمة تتحرك بشكل غير خطي مع الفروع المرئية. تحقق من الهيكل بواسطة المحتوى بدلاً من ذلك

// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
  ShowMessage('Outline verified');

أين يتناسب هذا مع مجموعة أدوات المستندات المحملة

يكمل إنشاء الإشارات المرجعية والوجهات المسماة صورة كانت واجهة برمجة تطبيقات المستندات المحملة تملؤها منذ فترة: حيث تقوم نفس دورة التحميل والتعديل والحفظ بالفعل بإعادة كتابة بيانات تعريف المستند، وإضافة التعليقات التوضيحية للروابط، وإرسال واستقبال التعليقات التوضيحية من خلال استيراد وتصدير XFDF. النمط موحد: LoadFromFile، وسلسلة من استدعاءات *Loaded* مقابل رسم الكائن في الذاكرة، ثم SaveLoadedDocument واحدة، مما يسهل طي بناء المخطط التفصيلي في دمج موجود أو خط ختم كخطوة إضافية قبل الحفظ

واجهات برمجة التطبيقات التي تمت مناقشتها هنا، وهي AddLoadedOutline وSetLoadedOutlineDestination وSetLoadedOutlineURI وResolveLoadedNamedDestination وAddLoadedNamedDestination وGetLoadedBookmarkPageIndex، تشحن في مكون HotPDF القياسي لدلفي وC++Builder، دون أي تبعيات خارجية ودون الحاجة لبيئة تشغيل عارض منفصلة