مقاله فنی

افزودن نشانک‌ها به فایل‌های PDF موجود در دلفی با HotPDF

کامپوننت HotPDF نشانک‌ها (bookmarks) را از طریق AddLoadedOutline به یک PDF موجود در دلفی اضافه می‌کند، که ورودی‌های ساختار درختی (outline) را در هر سطح از لانه‌گزینی در یک سند بارگذاری‌شده ایجاد می‌کند، و مقصدهای نام‌گذاری‌شده را از طریق ResolveLoadedNamedDestination و AddLoadedNamedDestination می‌خواند و می‌نویسد. فایل را بارگذاری کنید، درخت را بسازید، SaveLoadedDocument را فراخوانی کنید و اکنون نوار کناری خواننده که قبلاً خالی بود، حاوی یک جدول محتوای کاربردی است

سناریویی که انگیزه انجام تمام این کارها است، به طرز ناامیدکننده‌ای رایج است. شما ده‌ها قرارداد را در یک بسته بازبینی ادغام می‌کنید، یا سه دفترچه راهنمای محصول را در یک فایل قابل توزیع واحد بخیه می‌زنید، و خروجی از نظر ساختاری خوب است: هر صفحه وجود دارد و هر فونت دست‌نخورده است. سپس شخصی آن را در Acrobat باز می‌کند و پنل نشانک‌ها خالی است. یک سند ۴۰۰ صفحه‌ای بدون ناوبری از نظر فنی کامل اما در عمل غیرقابل استفاده است و تا نسخه 2.347.0، کامپوننت HotPDF می‌توانست نشانک‌ها را در اسناد بارگذاری‌شده بخواند، تغییر دهد و حذف کند اما هرگز نمی‌توانست آنها را ایجاد کند. این شکاف اکنون برطرف شده است و این مقاله به بررسی بخش‌های جدید API و یک نکته از اسرار PDF، یعنی کلید /Count می‌پردازد، که اگر با آن به عنوان یک شمارنده ورودی ساده رفتار کنید، شما را به دردسر خواهد انداخت

ساختار درختی outline در واقع چیست

یک ساختار outline در PDF، یک درخت دوطرفه پیوندی از دیکشنری‌ها است و نه یک لیست مسطح، و این ساختار، هر پارامتر در API را توضیح می‌دهد. استاندارد ISO 32000-1 §12.3.3 آن را تعریف می‌کند: کاتالوگ سند به یک ریشه /Outlines اشاره می‌کند، ریشه به اولین و آخرین فرزند خود از طریق /First و /Last اشاره می‌کند و فرزندان هم‌سطح از طریق /Next و /Prev زنجیر می‌شوند در حالی که هر ورودی از طریق /Parent به بالا اشاره می‌کند. هر ورودی حامل یک رشته /Title و معمولاً یک /Dest است که مشخص می‌کند کلیک روی آن، خواننده را باید به کجا ببرد

مقصدها به دو شکل ارائه می‌شوند و استاندارد ISO 32000-1 §12.3.2 آنها را به طور تعمدی مجزا نگه می‌دارد. یک مقصد صریح (explicit destination) یک آرایه درون‌خطی مانند [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 صفر‌محور اشاره دارد، و ورودی جدید را به زنجیره فرزندان هم‌سطح متصل می‌کند. این تابع در صورت موفقیت ۰ و در صورت شکست -۱ را برمی‌گرداند، به عنوان مثال زمانی که DestPageIndex خارج از محدوده باشد

یک جزئیات اتصال برای ترتیب اهمیت دارد: یک ورودی جدید در ابتدای زنجیره فرزندان والد خود، به عنوان /First جدید درج می‌شود. بنابراین اگر "فصل ۱" و سپس "فصل ۲" را به عنوان ورودی‌های سطح بالا اضافه کنید، فصل ۲ در بالای فصل ۱ در نوار کناری ظاهر می‌شود و فصل ۱ به شاخص سطح بالای ۱ منتقل می‌شود. الگوی عملی این است که ورودی‌ها را به ترتیب معکوس خواندن اضافه کنید، یا اینکه هر والد را اضافه کرده و بلافاصله فرزندانش را در حالی که هنوز در شاخص ۰ قرار دارد پر کنید

پارامترهای X، Y و Zoom مستقیماً روی مقصد /XYZ نگاشت می‌شوند و همه به طور پیش‌فرض ۰ هستند. مشخصات PDF مبدأ مختصات را در گوشه پایین سمت چپ قرار می‌دهد، بنابراین Y = 792 بالای یک صفحه با اندازه Letter ایالات متحده را هدف قرار می‌دهد و طبق استاندارد ISO 32000-1 مقدار صفر در یک اسلات /XYZ به نمایشگر می‌گوید که تنظیمات فعلی خود را حفظ کند، که این دقیقاً همان چیزی است که در تقریباً هر مورد برای زوم می‌خواهید. همه چیز روی گراف شیء بارگذاری شده در حافظه اتفاق می‌افتد؛ هیچ چیز دیسک را لمس نمی‌کند تا زمانی که SaveLoadedDocument اجرا شود، همان الگوی ویرایش و سپس ذخیره که API اسناد بارگذاری شده برای بازنویسی عنوان، نویسنده و متادیتای XMP در فایل‌های PDF بارگذاری شده استفاده می‌کند

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;

هدف‌گذاری مجدد نشانک‌ها: پرش‌های صفحه در برابر اکشن‌های 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 انتظار دارد که یک ورودی outline از طریق یکی از این دو مکانیسم و نه هر دو ناوبری کند. بنابراین متد SetLoadedOutlineURI را برای ورودی‌هایی که مقصد صفحه‌ای ندارند رزرو کنید، مانند یک برگ با عنوان "صفحه اصلی محصول" یا "گزارش مشکل" در انتهای درخت، به جای سربرگ فصل تبدیل شده. اکشن URI نوشته شده در اینجا همان ساختار /S /URI است که برای یادداشت‌های لینک در محتوای صفحه استفاده می‌شود که در راهنمای ایجاد هایپرلینک در اسناد PDF با HotPDF با جزئیات بیشتر پوشش داده شده است

مقصدهای نام‌گذاری‌شده در یک PDF بارگذاری‌شده چگونه کار می‌کنند؟

مقصدهای نام‌گذاری‌شده، مکانیسم لنگر پایدار استاندارد ISO 32000-1 §12.3.2.3 هستند: به جای قرار دادن یک مرجع صفحه در هر لینک، سند یک نقشه نام به مقصد را در درخت نام /Names /Dests کاتالوگ نگه می‌دارد و لینک‌ها به ورودی‌ها با نام اشاره می‌کنند. متد ResolveLoadedNamedDestination(Name) یک نام را در آن درخت جستجو می‌کند و شاخص صفحه صفر‌محور را که به آن اشاره دارد، یا در صورت نبود نام مقدار -۱ را برمی‌گرداند. حل‌کننده هر دو شکل ذخیره‌سازی را که مشخصات برای مقدار نگاشت شده اجازه می‌دهد مدیریت می‌کند: یک آرایه مقصد خالی مانند [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)، که یک جستجوی راحت است که عنوان نشانک را می‌گیرد و صفحه مقصد آن را برمی‌گرداند، سطح بالای درخت outline را اسکن می‌کند، بنابراین از آن برای تأیید ورودی‌های سطح فصل که به تازگی ایجاد کرده‌اید استفاده کنید و نه برای برگ‌های عمیق لانه‌گزینی شده

تله Count/: فرزندان را می‌شمارد و نه ورودی‌ها را

کلید /Count در ریشه outline به معنای آنچه اکثر توسعه‌دهندگان فرض می‌کنند نیست و فرض اشتباه باعث ایجاد شکست در تست‌ها می‌شود که شبیه خراب شدن داده‌ها به نظر می‌رسد. استاندارد ISO 32000-1 §12.3.3 کلید /Count را به عنوان تعداد کل فرزندان مرئی در تمام سطوح و نه تعداد ورودی‌های سطح بالا تعریف می‌کند. در یک ورودی مجزا، علامت نیز حامل معنا است: یک مقدار مثبت به این معنی است که ورودی باز است و این تعداد فرزند در نوار کناری نشان داده می‌شود، در حالی که یک مقدار منفی به این معنی است که ورودی جمع شده است و فرزندان |N| در داخل آن پنهان هستند

این موضوع از مجموعه رگرسیون خود HotPDF در زمان ساخت APIهای نشانک اسناد بارگذاری‌شده بیرون آمد. یک سند آزمایشی حاوی سه نشانک سطح بالا بود که یکی از آنها دارای یک فرزند بود؛ پس از حذف یک ورودی سطح بالا، متد GetLoadedOutlineCount که مقدار ریشه /Count را می‌خواند، به جای عدد انتظار رفته ۲، مقدار ۱ را گزارش داد. هیچ چیز خراب نشده بود: عدد ۳ اولیه در ابتدا هرگز "سه ورودی سطح بالا" نبود و محاسبه مجدد صحیح باید در زنجیره سطح بالا حرکت کرده و هر گره را به همراه /Count مثبت آن جمع کند و از فرزندان گره‌های جمع‌شده صرف‌نظر کند. درس برای کد شما مستقیم است: هرگز پس از ویرایش‌های ساختاری، تفاوت‌های /Count را فرض نکنید، زیرا این مقدار به طور غیرخطی با فرزندان مرئی تغییر می‌کند. در عوض، ساختار را بر اساس محتوا تأیید کنید

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

جایگاه این ویژگی در مجموعه ابزار اسناد بارگذاری‌شده

ایجاد نشانک و مقصد نام‌گذاری‌شده، تصویری را تکمیل می‌کند که API اسناد بارگذاری شده مدتی است در حال پر کردن آن است: همان چرخه بارگذاری-ویرایش-ذخیره که متادیتای سند را بازنویسی می‌کند، یادداشت‌های لینک را اضافه می‌کند و یادداشت‌ها را از طریق وارد و خارج کردن XFDF انتقال دوطرفه می‌دهد. این الگو یکنواخت است: فراخوانی LoadFromFile، یک سری فراخوانی‌های *Loaded* در برابر گراف شیء در حافظه و سپس یک SaveLoadedDocument، که ادغام ساخت outline را در خط لوله موجود ادغام یا نشان‌گذاری به عنوان یک مرحله دیگر قبل از ذخیره ساده می‌کند

APIهای مورد بحث در اینجا، یعنی AddLoadedOutline، SetLoadedOutlineDestination، SetLoadedOutlineURI، ResolveLoadedNamedDestination، AddLoadedNamedDestination و GetLoadedBookmarkPageIndex در کامپوننت استاندارد HotPDF Component برای دلفی و C++Builder بدون هیچ وابستگی خارجی و بدون نیاز به زمان اجرای نمایشگر جداگانه ارائه می‌شوند