Tekninen artikkeli

Kirjanmerkkien lisääminen olemassa oleviin PDF-tiedostoihin Delphissä HotPDF-komponentilla

HotPDF lisää kirjanmerkkejä olemassa olevaan PDF-tiedostoon Delphissä AddLoadedOutline-metodilla. Se luo sisällysluettelomerkintöjä (outline entries) millä tahansa sisäkkäisyystasolla ladattuun asiakirjaan. Lisäksi se lukee ja kirjoittaa nimettyjä kohteita (named destinations) ResolveLoadedNamedDestination- ja AddLoadedNamedDestination-metodeilla. Lataa tiedosto, rakenna puu, kutsu SaveLoadedDocument-metodia, ja lukijan aiemmin tyhjä sivupalkki sisältää nyt toimivan sisällysluettelon

Tämän taustalla oleva skenaario on harmillisen yleinen. Yhdistät tusinan verran sopimuksia yhdeksi tarkastelupaketiksi tai liität kolme tuoteopasta yhdeksi jaettavaksi tiedostoksi, ja tulos on rakenteellisesti kunnossa: jokainen sivu on tallessa ja jokainen fontti ehjä. Sitten joku avaa tiedoston Acrobatissa, ja kirjanmerkkipaneeli on tyhjä. 400-sivuinen asiakirja ilman navigointia on teknisesti valmis mutta käytännössä käyttökelvoton. Versioon 2.347.0 asti HotPDF kykeni lukemaan, muokkaamaan ja poistamaan kirjanmerkkejä ladatuista asiakirjoista, mutta ei koskaan luomaan niitä. Tämä puute on nyt korjattu, ja tässä artikkelissa käydään läpi uusi rajapintakokonaisuus sekä eräs PDF-arkkitehtuurin erikoisuus eli /Count-avain, joka aiheuttaa ongelmia, jos sitä käsitellään pelkkänä merkintöjen laskurina

Mikä sisällysluettelopuu todellisuudessa on

PDF-kirjanmerkkirakenne (outline) on sanakirjojen (dictionaries) kaksoislinkitetty puu, ei litteä luettelo, ja tämä rakenne selittää rajapinnan jokaisen parametrin. ISO 32000-1 §12.3.3 määrittelee sen: asiakirjan katalogi osoittaa /Outlines-juureen, juuri osoittaa ensimmäiseen ja viimeiseen lapseensa avaimilla /First ja /Last, ja sisarukset ketjuttuvat avaimilla /Next ja /Prev samalla, kun jokainen merkintä osoittaa takaisin ylös avaimella /Parent. Jokainen merkintä kantaa mukanaan /Title-merkkijonoa ja yleensä /Dest-kohdetta, joka kertoo minne napsautus vie lukijan

Kohteita on kahdenlaisia, ja ISO 32000-1 §12.3.2 pitää ne tietoisesti erillään. Erityinen kohde (explicit destination) on sisäinen taulukko, kuten [page /XYZ x y zoom]: suora viittaus sivuolioon sekä näkymän määrittely. Nimetty kohde (named destination) puolestaan tallentaa nimen merkkijonona, ja varsinainen kohde sijaitsee asiakirjan katalogissa nimipuussa /Names /Dests. Tämä on yhden tason epäsuoruus (indirection), joka antaa useiden linkkien jakaa saman kohteen ja antaa kohteen siirtyä muuttamatta itse linkkejä. HotPDF kirjoittaa erityisiä /XYZ-kohteita luodessaan kirjanmerkkejä, ja tarjoaa erilliset kutsut nimipuun lukemiseen ja laajentamiseen

Miten lisäät kirjanmerkkejä olemassa olevaan PDF-tiedostoon Delphissä?

Kutsu AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) -metodia LoadFromFile-kutsun jälkeen: anna ParentIndex = -1 päätason merkinnälle, tai olemassa olevan päätason merkinnän nollapohjainen indeksi sen alle sijoittamiseksi. Funktio luo /Outlines-juuren, jos asiakirjalla ei ole koskaan ollut sellaista, rakentaa merkinnän sanakirjan, kirjoittaa /Dest [page /XYZ x y zoom] -taulukon, joka osoittaa nollapohjaiseen DestPageIndex-sivuun, ja linkittää uuden merkinnän sisarusten ketjuun. Se palauttaa arvon 0 onnistuessaan ja -1 epäonnistuessaan, esimerkiksi kun DestPageIndex on alueen ulkopuolella

Yksi linkitysyksityiskohta vaikuttaa järjestykseen: uusi merkintä lisätään vanhempansa lapsiketjun alkuun uutena /First-kohteena. Siten jos lisäät kirjanmerkit "Chapter 1" ja sen jälkeen "Chapter 2" päätason merkinnöiksi, Chapter 2 ilmestyy Chapter 1:n yläpuolelle sivupalkissa, ja Chapter 1 siirtyy päätason indeksiksi 1. Käytännön malli on joko lisätä merkinnät käänteisessä lukujärjestyksessä tai lisätä kukin vanhempi ja täyttää heti sen lapset sen ollessa yhä indeksissä 0

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
    begin
      // Lisää luvut käänteisessä lukujärjestyksessä: jokaisesta uudesta
      // päätason merkinnästä tulee /First (indeksi 0)
      Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
      Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
      // Chapter 1 on nyt päätason indeksi 0; sijoita osiot sen alle
      // (lapset myös lisätään alkuun, joten käänteinen järjestys)
      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;

Parametrit X, Y ja Zoom kohdistuvat suoraan /XYZ-kohteeseen ja niiden kaikkien oletusarvo on 0. PDF asettaa koordinaatiston alkupisteen vasempaan alanurkkaan, joten Y = 792 kohdistuu US Letter -sivun yläreunaan, ja ISO 32000-1 -standardin mukaisesti nolla-arvo /XYZ-paikassa käskee katseluohjelma säilyttämään nykyisen asetuksensa. Tämä on juuri se, mitä zoom-asetukselta halutaan lähes jokaisessa tapauksessa. Kaikki tapahtuu muistissa ladatussa oliograafissa; mikään ei kosketa levyä ennen kuin SaveLoadedDocument ajetaan. Tämä on sama "muokkaa ja tallenna" -malli, jota ladattujen asiakirjojen rajapinta käyttää otsikon, tekijän ja XMP-metatietojen kirjoittamiseen ladattuihin PDF-tiedostoihin

Kirjanmerkkien uudelleenkohdistus: sivuhypyt vs. URI-toiminnot

Olemassa olevien kirjanmerkkien kohteet voidaan muuttaa luomatta niitä uudelleen, ja HotPDF tarjoaa näille kahdelle tapaukselle kaksi erillistä kutsua, koska niiden taustalla olevat PDF-rakenteet ovat täysin erilaisia. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) korvaa indeksissä Index olevan päätason merkinnän /Dest-kohteen uudella /XYZ-taulukolla. Tämä on työkalu klassiseen yhdistämisen jälkeiseen virheeseen, jossa jokainen kirjanmerkki osoittaa edelleen alkuperäisen, yhdistämistä edeltävän tiedoston sivunumeroihin. SetLoadedOutlineURI(Index, URI) tekee jotain rakenteellisesti erilaista: se liittää mukaan /A-toimintasanakirjan tyypillä /S /URI muuttaen kirjanmerkin verkkolinkiksi asiakirjan sisäisen hypyn sijaan

// Yhdistäminen siirsi kaikkea 10 sivulla: kohdista kolmas
// päätason kirjanmerkki sivulle 12 (nollapohjainen 11)
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);

// Muuta viimeinen merkintä ulkoiseksi linkiksi
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');

Pdf.SaveLoadedDocument('retargeted.pdf');

Pidä nämä kaksi mekanismia erillään puuta suunnitellessasi. /Dest on puhdasta navigointia asiakirjan sisällä; URI-toiminto poistuu asiakirjasta kokonaan, ja ISO 32000-1 edellyttää kirjanmerkin navigoivan joko toisella tai toisella tavalla, ei molemmilla. Varaa siis SetLoadedOutlineURI merkinnöille, joilla ei ole vielä sivukohdetta, kuten puun loppuun sijoitettaville lehdyköille "Product homepage" tai "Report an issue" ennemmin kuin muunnetuille lukujen otsikoille. Tässä kirjoitettu URI-toiminto on sama /S /URI-rakenne, jota käytetään sivusisällön linkkihuomautuksissa, mistä kerrotaan tarkemmin oppaassa hyperlinkkien luomisesta PDF-asiakirjoihin HotPDF-komponentilla

Miten nimetyt kohteet toimivat ladatussa PDF-tiedostossa?

Nimetyt kohteet ovat standardin ISO 32000-1 §12.3.2.3 mukainen vakaa ankkurointimekanismi: sen sijaan, että jokaiseen linkkiin upotettaisiin sivukohde, asiakirja säilyttää nimen ja kohteen välisen kartoituksen katalogin nimipuussa /Names /Dests, ja linkit viittaavat merkintöihin nimen perusteella. ResolveLoadedNamedDestination(Name) etsii nimeä kyseisestä puusta ja palauttaa nollapohjaisen sivun indeksin, johon se osoittaa, tai -1 jos nimeä ei löydy. Ratkaisija käsittelee molemmat spesifikaation sallimat tallennusmuodot kartoitetulle arvolle: pelkän kohdetaulukon kuten [page /XYZ x y zoom] sekä sanakirjamuodon, joka käärii taulukon avaimen /D alle

AddLoadedNamedDestination(Name, DestPageIndex, X, Y) toimii päinvastoin: se rekisteröi uuden ankkurin puuhun /Names /Dests luoden itse puun, jos asiakirjalla ei ole sellaista, ja palauttaa arvon True onnistuessaan. Tämä pari kattaa yhdessä työnkulun, jossa yhdistetyn asiakirjan on kunnioitettava aiempien työkalujen kirjoittamia ankkureita ja julkaistava uusia myöhempien linkkien kohdistettavaksi

var
  PageIdx: Integer;
begin
  // Kunnioita ankkuria, jonka toinen työkalu kirjoitti polkuun /Names /Dests
  PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
  if PageIdx >= 0 then
    Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);

  // Julkaise uusi ankkuri muiden linkkien kohdistettavaksi
  if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
    Pdf.SaveLoadedDocument('anchored.pdf');
end;

Rehelliset rajat: kohteet, joita HotPDF kirjoittaa sekä kirjanmerkeille että nimetyille ankkureille, ovat /XYZ-kohteita. Muita spesifikaation määrittelemiä erityisiä tyyppejä kuten /Fit, /FitH, /FitB ja vastaavia ei tuoteta näillä kutsuilla, vaikkakin /XYZ nollapohjaisella zoomilla kattaa vallitsevan käyttötapauksen "mene tähän kohtaan, säilytä zoomini". Ja GetLoadedBookmarkPageIndex(Title), eli kätevä haku, joka ottaa vastaan kirjanmerkin otsikon ja palauttaa sen kohdesivun, skannaa kirjanmerkkipuun ylintä tasoa. Käytä sitä siis juuri luomiesi lukutason merkintöjen todentamiseen syvästi sisäkkäisten lehtien sijaan

/Count-sudenkuoppa: se laskee jälkeläiset, ei merkintöjä

Kirjanmerkkien juuren /Count-avain ei tarkoita sitä, mitä useimmat kehittäjät olettavat, ja väärä oletus tuottaa testivirheitä, jotka vaikuttavat tietojen korruptoitumiselta. ISO 32000-1 §12.3.3 määrittelee /Count-arvon kaikkien tasojen näkyvien jälkeläisten kokonaismääräksi, ei päätason merkintöjen määräksi. Yksittäisessä merkinnässä myös etumerkillä on merkitys: positiivinen arvo tarkoittaa merkinnän olevan avoinna ja kyseisen määrän jälkeläisiä näkyvän sivupalkissa, kun taas negatiivinen arvo tarkoittaa merkinnän olevan suljettuna ja |N| jälkeläisen olevan piilotettuna sen sisään

Tämä paljastui HotPDF:n omasta regressiotestistöstä, kun ladattujen asiakirjojen kirjanmerkkirajapintoja rakennettiin. Testiasiakirja sisälsi kolme päätason kirjanmerkkiä, joista yhdellä oli lapsi. Yhden päätason merkinnän poistamisen jälkeen GetLoadedOutlineCount, joka lukee juuren /Count-arvon, raportoi 1 odotetun 2:n sijaan. Mikään ei ollut korruptoitunut: alkuperäinen 3 ei ollut koskaan ollut "kolme päätason merkintää" alun alkaenkaan, ja oikean uudelleenlaskennan on käytävä läpi päätason ketju laskien yhteen kukin solmu sekä sen positiivinen /Count-arvo, ohittaen suljettujen solmujen jälkeläiset. Opetus omalle koodillesi on selkeä: älä koskaan luota /Count-arvon muutoksiin rakenteellisten muokkausten jälkeen, koska arvo muuttuu epälineaarisesti näkyvien jälkeläisten mukaan. Varmista rakenne sen sijaan sisällön perusteella

// Varmista muokkausten jälkeen otsikkohaulla, älä /Count-erotuksilla
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
  ShowMessage('Outline verified');

Mihin tämä sijoittuu ladattujen asiakirjojen työkaluissa

Kirjanmerkkien ja nimettyjen kohteiden luominen täydentää kuvaa, jota ladattujen asiakirjojen rajapinta on täyttänyt jo jonkin aikaa: sama lataa-muokkaa-tallenna-sykli kirjoittaa jo asiakirjan metatiedot uudelleen, lisää linkkihuomautuksia ja kierrättää huomautukset XFDF-viennin ja -tuonnin kautta. Malli on yhtenäinen: LoadFromFile, sarja *Loaded*-kutsuja muistissa olevaa oliograafia vasten ja lopuksi SaveLoadedDocument. Tämän ansiosta sisällysluettelon rakentaminen on suoraviivaista liittää osaksi olemassa olevaa yhdistämis- tai leimausputkea yhtenä lisävaiheena ennen tallennusta

Tässä käsitellyt rajapinnat AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination ja GetLoadedBookmarkPageIndex toimitetaan standardissa HotPDF Component -tuotteessa Delphille ja C++Builderille ilman ulkoisia riippuvuuksia tai erillistä katseluohjelman ajoympäristöä