Teknik Makale

Delphi'de HotPDF ile Mevcut PDF'lere Yer İmleri Ekleme

HotPDF, yüklü bir belgede herhangi bir yuvalama düzeyinde anahat (outline) girişleri oluşturan AddLoadedOutline aracılığıyla Delphi'de mevcut bir PDF'e yer imleri ekler ve ResolveLoadedNamedDestination ile AddLoadedNamedDestination aracılığıyla adlandırılmış hedefleri okur ve yazar. Dosyayı yükleyin, ağacı oluşturun, SaveLoadedDocument çağrısı yapın; eskiden boş olan okuyucu kenar çubuğu artık çalışan bir içindekiler tablosu taşır

Tüm bunlara motivasyon sağlayan senaryo iç karartıcı derecede yaygındır. Bir düzine sözleşmeyi tek bir inceleme paketinde birleştirirsiniz veya üç ürün kılavuzunu dağıtılabilir tek bir dosyada bir araya getirirsiniz ve çıktı yapısal olarak iyidir: Her sayfa mevcuttur, her yazı tipi sağlamdır. Sonra birisi bunu Acrobat'ta açar ve yer imleri paneli boştur. Navigasyonu olmayan 400 sayfalık bir belge teknik olarak eksiksizdir ancak pratik olarak kullanılamaz durumdadır. Sürüm 2.347.0'a kadar HotPDF, yüklü belgelerdeki yer imlerini okuyabilir, değiştirebilir ve silebilir ancak asla oluşturamazdı. Bu boşluk artık kapatıldı ve bu makale yeni API yüzeyini ve basit bir giriş sayacı gibi ele alırsanız sizi ısıracak olan tek PDF gizemi, /Count anahtarını incelemektedir

Anahat (outline) ağacı gerçekte nedir?

Bir PDF anahattı (outline), düz bir liste değil, çift yönlü bağlı bir sözlük ağacıdır ve bu yapı API'deki her parametreyi açıklar. ISO 32000-1 §12.3.3 bunu tanımlar: Belge kataloğu bir /Outlines kökünü işaret eder, kök /First ve /Last aracılığıyla ilk ve son çocuklarını işaret eder ve kardeşler /Next ve /Prev aracılığıyla zincirlenirken, her giriş /Parent aracılığıyla yukarıya doğru işaret eder. Her giriş bir /Title dizesi ve genellikle tıklandığında okuyucuyu nereye götürmesi gerektiğini belirten bir /Dest taşır

Hedefler iki çeşittir ve ISO 32000-1 §12.3.2 bunları kasıtlı olarak ayrı tutar. Açık (explicit) bir hedef, [page /XYZ x y zoom] gibi satır içi bir dizidir: Bir sayfa nesnesine doğrudan referans artı bir görünüm belirtimi. Adlandırılmış (named) bir hedef ise bir ad dizesi saklar ve gerçek hedef, belge kataloğunda /Names /Dests ad ağacı altında yaşar; bu, birçok bağlantının tek bir hedefi paylaşmasına izin veren ve bağlantılara dokunmadan hedefin taşınmasına izin veren bir dolaylılık seviyesidir. HotPDF yer imleri oluştururken açık /XYZ hedefleri yazar ve ad ağacını okumak ve genişletmek için size ayrı çağrılar sunar

Delphi'de mevcut bir PDF'e nasıl yer imleri eklersiniz?

LoadFromFile sonrasında AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) çağrısı yapın: Üst düzey bir giriş için ParentIndex = -1 veya altına yerleştirmek için mevcut bir üst düzey girişin sıfır tabanlı dizinini geçirin. İşlev, belgede daha önce hiç yoksa /Outlines kökünü oluşturur, giriş sözlüğünü oluşturur, sıfır tabanlı DestPageIndex değerini gösteren bir /Dest [page /XYZ x y zoom] dizisi yazar ve yeni girişi kardeş zincirine bağlar. Başarı durumunda 0, örneğin DestPageIndex aralık dışı olduğunda -1 döndürür

Sıralama için bir bağlantı ayrıntısı önemlidir: Yeni bir giriş, ebeveyninin çocuk zincirinin başına, yeni /First olarak eklenir. Dolayısıyla, üst düzey girişler olarak "Bölüm 1" ve ardından "Bölüm 2" eklerseniz, Bölüm 2 kenar çubuğunda Bölüm 1'in üzerinde görünür ve Bölüm 1 üst düzey indeks 1'e kaymıştır. Pratik düzen, girişleri ters okuma sırasına göre eklemek ya da her bir ebeveyni ekleyip hala indeks 0'da otururken çocuklarını hemen doldurmaktır

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
    begin
      // Üst düzey girişleri ters okuma sırasına göre ekleyin: her yeni
      // üst düzey giriş /First (indeks 0) olur.
      Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
      Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
      // Bölüm 1 artık üst düzey indeks 0; bölümleri altına yerleştirin
      // (çocuklar da başa eklenir, bu nedenle ters sıra).
      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 ve Zoom parametreleri doğrudan /XYZ hedefiyle eşleşir ve tümü varsayılan olarak 0'dır. PDF koordinat başlangıcını sol alt köşeye yerleştirir, bu nedenle Y = 792 bir US Letter sayfasının üstünü hedefler ve ISO 32000-1 uyarınca bir /XYZ yuvasındaki sıfır değeri, görüntüleyiciye mevcut ayarını korumasını söyler; bu, neredeyse her durumda yakınlaştırma için tam olarak istediğiniz şeydir. Her şey bellek içi yüklü nesne grafiğinde gerçekleşir; SaveLoadedDocument çalışana kadar hiçbir şey diske dokunmaz; bu, yüklü belge API'sinin yüklü PDF'lerde başlık, yazar ve XMP meta verilerini yeniden yazmak için kullandığı düzenle-ve-kaydet modelinin aynısıdır

Yer imlerini yeniden hedefleme: Sayfa atlamaları ve URI eylemleri karşılaştırması

Mevcut yer imleri, onları yeniden oluşturmadan yeniden bağlanabilir ve altındaki PDF yapıları gerçekten farklı olduğundan HotPDF iki durum için iki ayrı çağrı sunar. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom), Index'teki üst düzey girişin /Dest kısmını yeni bir /XYZ dizisiyle değiştirir; bu, her yer iminin hala birleştirme öncesi orijinal dosyanın sayfa numaralarını işaret ettiği klasik birleştirme sonrası hatası için kullanılan araçtır. SetLoadedOutlineURI(Index, URI) ise yapısal olarak farklı bir şey yapar: /S /URI ile bir /A eylem sözlüğü ekleyerek yer imini belge içi atlama yerine bir web bağlantısına dönüştürür

// Birleştirme her şeyi 10 sayfa kaydırdı: üçüncü üst düzey yer imini
// sayfa 12'yi (sıfır tabanlı 11) gösterecek şekilde yeniden yönlendirin.
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);

// Son girişi harici bir bağlantıya dönüştürün.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');

Pdf.SaveLoadedDocument('retargeted.pdf');

Ağacı tasarlarken iki mekanizmayı birbirinden net olarak ayırın. Bir /Dest, belgenin içinde saf bir navigasyondur; bir URI eylemi ise belgeden tamamen ayrılır ve ISO 32000-1, bir anahat girişinin her ikisini birden değil, bir mekanizmayı veya diğerini kullanarak gezinmesini bekler. Dolayısıyla, SetLoadedOutlineURI'yi halihazırda bir sayfa hedefi taşımayan girişler için saklayın; dönüştürülmüş bir bölüm başlığı yerine ağacın sonundaki bir "Ürün ana sayfası" veya "Sorun bildir" yaprağı gibi. Burada yazılan URI eylemi, sayfa içeriğindeki bağlantı açıklamaları için kullanılan /S /URI yapısıyla aynıdır; bu konu HotPDF ile PDF belgelerinde köprüler oluşturma kılavuzunda daha derinlemesine ele alınmıştır

Adlandırılmış hedefler yüklü bir PDF'te nasıl çalışır?

Adlandırılmış hedefler, ISO 32000-1 §12.3.2.3'ün kararlı çapa mekanizmasıdır: Her bağlantıya bir sayfa referansı gömmek yerine, belge kataloğun /Names /Dests ad ağacında bir addan hedefe harita tutar ve bağlantılar girişlere adıyla başvurur. ResolveLoadedNamedDestination(Name) o ağaçta bir adı arar ve işaret ettiği sıfır tabanlı sayfa dizinini döndürür veya ad yoksa -1 döndürür. Çözümleyici, spesifikasyonun eşlenen değer için izin verdiği her iki depolama biçimini de işler: [page /XYZ x y zoom] gibi çıplak bir hedef dizisi ve diziyi bir /D anahtarı altında saran sözlük biçimi

AddLoadedNamedDestination(Name, DestPageIndex, X, Y) ise tersi şekilde çalışır: Belgede hiç yoksa ağacın kendisini oluşturarak /Names /Dests ağacında yeni bir çapa kaydeder ve başarı durumunda True döndürür. Çift birlikte, birleştirilmiş bir belgenin yukarı akış araçlarının yazdığı çapaları onurlandırması ve aşağı akış bağlantılarının hedeflemesi için yenilerini yayınlaması gereken iş akışını kapsar

var
  PageIdx: Integer;
begin
  // Başka bir aracın /Names /Dests içine yazdığı bir çapayı onurlandırın.
  PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
  if PageIdx >= 0 then
    Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);

  // Diğer bağlantıların hedeflemesi için yeni bir çapa yayınlayın.
  if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
    Pdf.SaveLoadedDocument('anchored.pdf');
end;

Dürüst sınırlar: HotPDF'in hem yer imleri hem de adlandırılmış çapalar için yazdığı hedefler /XYZ hedefleridir. Spesifikasyonun tanımladığı diğer açık türler (/Fit, /FitH, /FitB ve arkadaşlar) bu çağrılar tarafından yayılmaz, ancak sıfır yakınlaştırmalı /XYZ, "bu noktaya git, yakınlaştırmamı koru" şeklindeki baskın kullanım durumunu kapsar. Ve yer imi başlığını alıp hedef sayfasını döndüren kolaylık araması olan GetLoadedBookmarkPageIndex(Title), anahat ağacının üst düzeyini tarar, bu nedenle derinlemesine yuvalanmış yapraklar yerine az önce oluşturduğunuz bölüm düzeyindeki girişleri doğrulamak için kullanın

/Count tuzağı: Girişleri değil, alt nesneleri (descendants) sayar

Anahat kökündeki /Count anahtarı çoğu geliştiricinin varsaydığı anlama gelmez ve yanlış varsayım veri bozulması gibi görünen test hataları üretir. ISO 32000-1 §12.3.3, /Count değerini üst düzey giriş sayısı olarak değil, tüm düzeylerdeki toplam görünür alt nesne (visible descendants) sayısı olarak tanımlar. Bireysel bir girişte de işaret anlam taşır: Pozitif bir değer, girişin açık olduğunu ve kenar çubuğunda o kadar çok alt nesnenin göründüğünü gösterirken, negatif bir değer girişin daraltılmış (collapsed) olduğunu ve |N| alt nesnenin içinde gizlendiğini gösterir

Bu durum, yüklü belge yer imi API'leri oluşturulurken HotPDF'in kendi regresyon paketinden ortaya çıktı. Bir test belgesi, biri çocuklu olmak üzere üç üst düzey yer imi tutuyordu; üst düzey bir girişi sildikten sonra, kök /Count'u okuyan GetLoadedOutlineCount, beklenen 2 yerine 1 bildirdi. Hiçbir şey bozulmamıştı: Başlangıçtaki 3 zaten hiçbir zaman "üç üst düzey giriş" olmamıştı ve doğru yeniden hesaplama, daraltılmış düğümlerin alt nesnelerini atlayarak, her düğüm artı pozitif /Count değerini biriktirerek üst düzey zinciri yürümek zorundadır. Kendi kodunuz için ders doğrudan nettir: Yapısal düzenlemelerden sonra asla /Count değişimleri üzerinde doğrulama (assert) yapmayın, çünkü değer görünür alt nesnelerle doğrusal olmayan şekilde hareket eder. Bunun yerine yapıyı içeriğe göre doğrulayın

// Düzenlemelerden sonra, /Count değişimleriyle değil, başlık aramasıyla doğrulayın.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
  ShowMessage('Outline verified');

Yüklü belge araç setinde bu nereye oturur?

Yer imi ve adlandırılmış hedef oluşturma, yüklü belge API'sinin bir süredir doldurduğu resmi tamamlar: Aynı yükle-düzenle-kaydet döngüsü zaten belge meta verilerini yeniden yazar, bağlantı açıklamaları ekler ve açıklamaları XFDF içe aktarma ve dışa aktarma yoluyla gidiş-dönüş yaptırır. Kalıp tek tiptir: LoadFromFile, bellek içi nesne grafiğine karşı bir dizi *Loaded* çağrısı, ardından kaydetmeden önce anahat oluşturmayı mevcut bir birleştirme veya damgalama süreç hattına bir adım daha olarak eklemeyi kolaylaştıran bir SaveLoadedDocument

Burada tartışılan AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination ve GetLoadedBookmarkPageIndex API'leri, harici bir bağımlılık ve ayrı bir görüntüleyici çalışma zamanı gerektirmeksizin Delphi ve C++Builder için standart HotPDF Bileşeni içinde gönderilmektedir