Artikel Teknis

Menambahkan Bookmark ke PDF yang Ada di Delphi dengan HotPDF

HotPDF menambahkan bookmark ke PDF yang ada di Delphi melalui AddLoadedOutline, yang membuat entri outline pada tingkat sarang (nesting) apa pun pada dokumen yang dimuat, dan membaca serta menulis tujuan bernama (named destinations) melalui ResolveLoadedNamedDestination dan AddLoadedNamedDestination. Muat berkas, bangun pohon outline, panggil SaveLoadedDocument, and bilah sisi pembaca yang dulunya kosong kini memiliki daftar isi yang berfungsi

Skenario yang melatarbelakangi semua ini sangatlah umum terjadi. Anda menggabungkan selusin kontrak menjadi satu paket tinjauan, atau menyatukan tiga manual produk menjadi satu berkas distribusi tunggal, dan hasilnya secara struktural baik-baik saja: setiap halaman ada, setiap font utuh. Namun, ketika seseorang membukanya di Acrobat, panel bookmark ternyata kosong. Dokumen sepanjang 400 halaman tanpa navigasi secara teknis memang lengkap tetapi secara praktis tidak dapat digunakan, dan hingga versi 2.347.0 HotPDF hanya dapat membaca, memodifikasi, dan menghapus bookmark pada dokumen yang dimuat tetapi tidak pernah bisa membuatnya. Kesenjangan tersebut kini telah ditutup, dan artikel ini membahas permukaan API baru dan satu bagian rahasia PDF, yaitu kunci /Count, yang akan menyulitkan Anda jika Anda menganggapnya sebagai penghitung entri biasa

Apa sebenarnya pohon outline itu

Outline PDF adalah pohon kamus yang terhubung ganda (doubly linked tree of dictionaries), bukan daftar datar, dan struktur itulah yang menjelaskan setiap parameter dalam API. ISO 32000-1 §12.3.3 mendefinisikannya: katalog dokumen menunjuk ke root /Outlines, root menunjuk ke anak pertama dan terakhirnya melalui /First dan /Last, dan saudara kandung berantai melalui /Next dan /Prev sementara setiap entri menunjuk kembali ke atas melalui /Parent. Setiap entri membawa string /Title dan, biasanya, sebuah /Dest yang menyatakan ke mana mengkliknya harus mengarahkan pembaca

Tujuan (destination) datang dalam dua rasa, dan ISO 32000-1 §12.3.2 sengaja memisahkannya

Tujuan (destination) datang dalam dua rasa, dan ISO 32000-1 §12.3.2 sengaja memisahkannya. Tujuan eksplisit adalah array inline seperti [page /XYZ x y zoom]: referensi langsung ke objek halaman ditambah spesifikasi tampilan. Sebaliknya, tujuan bernama menyimpan string nama, dan target sebenarnya hidup di katalog dokumen di bawah pohon nama /Names /Dests, satu tingkat tidak langsung yang memungkinkan banyak tautan berbagi satu target dan memungkinkan target berpindah tanpa menyentuh tautan tersebut. HotPDF menulis tujuan /XYZ eksplisit saat membuat bookmark, dan memberi Anda panggilan terpisah untuk membaca dan memperluas pohon nama

Bagaimana cara menambahkan bookmark ke PDF yang ada di Delphi?

Panggil AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) setelah LoadFromFile: berikan ParentIndex = -1 untuk entri tingkat atas, atau indeks berbasis nol dari entri tingkat atas yang sudah ada untuk bersarang di bawahnya. Fungsi ini membuat root /Outlines jika dokumen tidak pernah memilikinya, membangun kamus entri, menulis array /Dest [page /XYZ x y zoom] yang menunjuk ke DestPageIndex berbasis nol, dan menautkan entri baru ke dalam rantai saudara kandung. Fungsi ini mengembalikan nilai 0 jika berhasil dan -1 jika gagal, misalnya ketika DestPageIndex berada di luar rentang

Satu detail penautan penting untuk pengurutan: entri baru dimasukkan di kepala dari rantai anak induknya, sebagai /First yang baru. Jadi jika Anda menambahkan "Chapter 1" dan kemudian "Chapter 2" as entri tingkat atas, Chapter 2 muncul di atas Chapter 1 di bilah sisi, dan Chapter 1 telah bergeser ke indeks tingkat atas 1. Pola praktisnya adalah menambahkan entri dalam urutan membaca terbalik, atau menambahkan setiap induk dan segera mengisi anak-anaknya selagi induk tersebut masih berada di indeks 0

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
    begin
      // Tambahkan bab dalam urutan membaca terbalik: setiap entri
      // tingkat atas baru menjadi /First (indeks 0).
      Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
      Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
      // Chapter 1 sekarang berada di indeks tingkat atas 0; buat bagian bersarang
      // di bawahnya (anak-anak juga ditambahkan di depan, jadi urutan terbalik).
      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;

Parameter X, Y, dan Zoom memetakan langsung ke tujuan /XYZ dan semuanya default ke 0. PDF meletakkan asal koordinat di sudut kiri bawah, sehingga Y = 792 menargetkan bagian atas halaman US Letter, dan menurut ISO 32000-1 nilai nol dalam slot /XYZ memberi tahu viewer untuk mempertahankan pengaturannya saat ini, yang merupakan hal yang Anda inginkan untuk zoom dalam hampir semua kasus. Semuanya terjadi pada grafik objek yang dimuat dalam memori; tidak ada yang menyentuh disk sampai SaveLoadedDocument dijalankan, model edit-lalu-simpan yang sama yang digunakan API dokumen yang dimuat untuk menulis ulang metadata judul, penulis, dan XMP pada PDF yang dimuat

Menargetkan ulang bookmark: lompatan halaman versus tindakan URI

Bookmark yang ada dapat dihubungkan kembali tanpa membuatnya ulang, dan HotPDF memberikan dua panggilan terpisah untuk kedua kasus tersebut karena konstruksi PDF di bawahnya memang berbeda. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) menggantikan /Dest dari entri tingkat atas di Index dengan array /XYZ baru, alat untuk mengatasi kegagalan pasca-penggabungan klasik di mana setiap bookmark masih menunjuk ke nomor halaman berkas asli sebelum digabungkan. SetLoadedOutlineURI(Index, URI) melakukan sesuatu yang berbeda secara struktural: fungsi ini melampirkan kamus tindakan /A dengan /S /URI, mengubah bookmark menjadi tautan web alih-alih lompatan di dalam dokumen

// Penggabungan menggeser semuanya sebanyak 10 halaman: arahkan ulang
// bookmark tingkat atas ketiga ke halaman 12 (berbasis nol 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);

// Ubah entri terakhir menjadi tautan eksternal.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');

Pdf.SaveLoadedDocument('retargeted.pdf');

Pahami perbedaan kedua mekanisme tersebut saat Anda mendesain pohon outline. Sebuah /Dest adalah navigasi murni di dalam dokumen; tindakan URI meninggalkan dokumen sepenuhnya, dan ISO 32000-1 mengharapkan entri outline bernavigasi melalui salah satu mekanisme tersebut, bukan keduanya. Jadi cadangkan SetLoadedOutlineURI untuk entri yang belum membawa tujuan halaman, seperti daun "Halaman beranda produk" atau "Laporkan masalah" di akhir pohon outline alih-alih tajuk bab yang dikonversi. Tindakan URI yang ditulis di sini adalah konstruksi /S /URI yang sama dengan yang digunakan untuk anotasi tautan pada konten halaman, dibahas lebih mendalam dalam panduan membuat hipertautan di dokumen PDF dengan HotPDF

Bagaimana cara kerja tujuan bernama pada PDF yang dimuat?

Tujuan bernama (named destinations) adalah mekanisme jangkar stabil dari ISO 32000-1 §12.3.2.3: alih-alih menyematkan referensi halaman di setiap tautan, dokumen menyimpan peta nama-ke-tujuan di pohon nama /Names /Dests milik katalog, dan tautan merujuk ke entri berdasarkan nama. ResolveLoadedNamedDestination(Name) mencari nama di pohon tersebut dan mengembalikan indeks halaman berbasis nol yang ditunjuknya, atau -1 jika nama tersebut tidak ada. Pemecah masalah (resolver) menangani kedua bentuk penyimpanan yang diizinkan spesifikasi untuk nilai yang dipetakan: array tujuan kosong seperti [page /XYZ x y zoom] dan bentuk kamus yang membungkus array di bawah kunci /D

AddLoadedNamedDestination(Name, DestPageIndex, X, Y) melakukan hal sebaliknya: fungsi ini mendaftarkan jangkar baru di pohon /Names /Dests, membuat pohon itu sendiri ketika dokumen tidak memilikinya, dan mengembalikan nilai True jika berhasil. Bersama-sama, keduanya mencakup alur kerja di mana dokumen yang digabungkan harus menghormati jangkar yang ditulis oleh alat hulu, dan menerbitkan jangkar baru untuk ditargetkan oleh tautan hilir

var
  PageIdx: Integer;
begin
  // Hormati jangkar yang ditulis alat lain ke dalam /Names /Dests.
  PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
  if PageIdx >= 0 then
    Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);

  // Terbitkan jangkar baru untuk ditargetkan oleh tautan lain.
  if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
    Pdf.SaveLoadedDocument('anchored.pdf');
end;

Batasan jujurnya: tujuan yang ditulis HotPDF, baik untuk bookmark maupun jangkar bernama, adalah tujuan /XYZ. Tipe eksplisit lainnya yang didefinisikan spesifikasi, seperti /Fit, /FitH, /FitB, dan sejenisnya, tidak dihasilkan oleh panggilan ini, meskipun /XYZ dengan zoom nol mencakup kasus penggunaan dominan "pergi ke tempat ini, pertahankan zoom saya". Dan GetLoadedBookmarkPageIndex(Title), pencarian praktis yang mengambil judul bookmark dan mengmengembalikan halaman tujuannya, memindai tingkat atas dari pohon outline, jadi gunakan ini untuk memverifikasi entri tingkat bab yang baru saja Anda buat daripada daun yang bersarang dalam

Perangkap /Count: menghitung keturunan, bukan entri

Kunci /Count pada root outline tidak berarti apa yang diasumsikan oleh sebagian besar pengembang, and asumsian yang salah menghasilkan kegagalan pengujian yang terlihat seperti kerusakan data. ISO 32000-1 §12.3.3 mendefinisikan /Count sebagai jumlah total keturunan yang terlihat (visible descendants) di semua tingkatan, bukan jumlah entri tingkat atas. Pada entri individual, tanda tersebut juga membawa arti: nilai positif berarti entri tersebut terbuka dan sekian keturunan ditampilkan di bilah sisi, sedangkan nilai negatif berarti entri tersebut diciutkan (collapsed) dan |N| keturunan disembunyikan di dalamnya

Hal ini terungkap dari rangkaian pengujian regresi HotPDF sendiri saat API bookmark dokumen yang dimuat sedang dibangun. Sebuah dokumen pengujian memiliki tiga bookmark tingkat atas, satu di antaranya memiliki anak; setelah menghapus satu entri tingkat atas, GetLoadedOutlineCount, yang membaca root /Count, melaporkan 1 alih-alih 2 yang diharapkan. Tidak ada yang rusak: nilai awal 3 tidak pernah menjadi "tiga entri tingkat atas" sejak awal, dan perhitungan ulang yang benar harus menyusuri rantai tingkat atas mengumpulkan setiap simpul ditambah /Count positifnya, melewatkan keturunan dari simpul yang diciutkan. Pelajaran untuk kode Anda sendiri sangat jelas: jangan pernah menegaskan (assert) delta /Count setelah pengeditan struktural, karena nilainya bergerak secara non-linear dengan keturunan yang terlihat. Sebagai gantinya, verifikasi struktur berdasarkan konten

// Setelah pengeditan, verifikasi dengan pencarian judul, bukan dengan delta /Count.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
  ShowMessage('Outline verified');

Di mana ini cocok dalam kumpulan alat dokumen yang dimuat

Pembuatan bookmark dan tujuan bernama melengkapi gambaran yang telah diisi oleh API dokumen yang dimuat selama beberapa waktu: siklus muat-edit-simpan yang sama telah menulis ulang metadata dokumen, menambahkan anotasi tautan, dan melakukan perjalanan pulang pergi (round-trip) anotasi melalui impor dan ekspor XFDF. Polanya seragam, yaitu LoadFromFile, serangkaian panggilan *Loaded* terhadap grafik objek dalam memori, lalu satu SaveLoadedDocument, yang membuatnya mudah untuk melipat konstruksi outline ke dalam pipa penggabungan atau pengecapan yang ada sebagai satu langkah lagi sebelum disimpan

API yang dibahas di sini, yaitu AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination, dan GetLoadedBookmarkPageIndex, disertakan dalam HotPDF Component standar untuk Delphi dan C++Builder, tanpa dependensi eksternal dan tidak memerlukan runtime viewer terpisah