HotPDF dodaja zaznamke (bookmarks) v obstoječi PDF v Delphiju prek metode AddLoadedOutline, ki ustvarja vnose kazala na kateri koli ravni gnezdenja v naloženem dokumentu, ter bere in piše imenovane cilje prek ResolveLoadedNamedDestination in AddLoadedNamedDestination. Naložite datoteko, zgradite drevo, pokličete SaveLoadedDocument in stranska vrstica bralnika, ki je bila prej prazna, bo zdaj vsebovala delujoče kazalo vsebine
Scenarij, ki motivira vse to, je žalostno pogost. Združite ducat pogodb v en paket za pregled ali spojite tri priročnike za izdelke v eno datoteko za distribucijo in izhod je strukturno v redu: vsaka stran je prisotna, vsaka pisava nedotaknjena. Nato pa jo nekdo odpre v programu Acrobat in plošča z zaznamki je prazna. Dokument s 400 stranmi brez navigacije je tehnično dokončan, a praktično neuporaben in vse do različice 2.347.0 je HotPDF lahko bral, spreminjal in brisal zaznamke v naloženih dokumentih, nikoli pa jih ni mogel ustvariti. Ta vrzel je zdaj odpravljena, ta članek pa predstavlja nov nabor API-jev in en del PDF skrivnosti — ključ /Count —, ki vas bo presenetil, če ga boste obravnavali kot preprost števec vnosov
Kaj drevo kazala dejansko je
Kazalo PDF (outline) je dvojno povezano drevo slovarjev in ne raven seznam, ta struktura pa pojasnjuje vsak parameter v API-ju. Standard ISO 32000-1 §12.3.3 ga opredeljuje: katalog dokumenta kaže na koren /Outlines, koren kaže na prvega in zadnjega otroka prek /First in /Last, sorodniki (siblings) se verižijo prek /Next in /Prev, vsak vnos pa kaže nazaj navzgor prek /Parent. Vsak vnos nosi niz /Title in običajno /Dest, ki določa, kam naj klik nanj popelje bralca
Cilji (destinations) prihajajo v dveh oblikah, standard ISO 32000-1 §12.3.2 verige pa ju namenoma ohranja ločeni. Ekspliciten cilj je v vrstici poravnan niz, kot je [page /XYZ x y zoom]: neposredna referenca na objekt strani in specifikacija pogleda. Imenovani cilj namesto tega shranjuje niz imena, dejanska ciljna točka pa živi v katalogu dokumenta pod drevesom imen /Names /Dests name tree — to je ena raven posrednosti, ki omogoča več povezavam deliti en cilj, cilj pa se lahko premika, ne da bi se dotikali povezav. HotPDF pri ustvarjanju zaznamkov zapisuje eksplicitne cilje /XYZ, ponuja pa tudi ločene klice za branje in razširitev drevesa imen
Kako dodati zaznamke v obstoječi PDF v Delphiju?
Pokličete AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) po nalaganju LoadFromFile: posredujte ParentIndex = -1 za vnos na najvišji ravni ali z 0 indeksirani indeks obstoječega vnosa najvišje ravni, pod katerega ga želite ugnezditi. Funkcija ustvari koren /Outlines, če ga dokument še ni imel, zgradi slovar vnosa, zapiše matriko /Dest [page /XYZ x y zoom], ki kaže na z 0 indeksirani DestPageIndex, in poveže nov vnos v verigo sorodnikov. Ob uspehu vrne 0, ob neuspehu pa -1, na primer ko je DestPageIndex izven obsega
Ena podrobnost povezovanja je pomembna za vrstni red: nov vnos se vstavi na začetek verige otrok svojega starša, kot novi /First. Če torej dodate "Poglavje 1" in nato "Poglavje 2" kot vnos najvišje ravni, se Poglavje 2 pojavi nad Poglavjem 1 v stranski vrstici, Poglavje 1 pa se premakne na indeks najvišje ravni 1. Praktični vzorec je, da vnose dodajate v obratnem vrstnem redu branja ali pa dodate vsakega starša in takoj napolnite njegove otroke, dokler se še nahaja na indeksu 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;
Parametri X, Y in Zoom se preslikajo neposredno v cilj /XYZ in so vsi privzeto nastavljeni na 0. Format PDF postavi koordinatno izhodišče v spodnji levi kot, zato Y = 792 cilja na vrh strani velikosti US Letter, po standardu ISO 32000-1 pa ničelna vrednost v mestu /XYZ naroči pregledovalniku, naj ohrani svojo trenutno nastavitev, kar je pri povečavi skoraj vedno tisto, kar želite. Vse se zgodi na naloženem grafu objektov v pomnilniku; nič se ne dotakne diska, dokler se ne zažene SaveLoadedDocument — to je enak model uredi-in-shrani, kot ga API za naložene dokumente uporablja za prepisu naslova, avtorja in XMP metapodatkov v naloženih PDF-jih
Spreminjanje ciljev zaznamkov: skoki na strani proti akcijam URI
Obstoječe zaznamke je mogoče ponovno povezati brez ponovnega ustvarjanja, HotPDF pa za ta dva primera ponuja dva ločena klica, saj so strukture PDF v ozadju resnično drugačne. Metoda SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) zamenja /Dest vnosa najvišje ravni na indeksu Index s svežo matriko /XYZ, kar je orodje za klasično napako po združevanju, ko vsak zaznamek še vedno kaže na številke strani prvotne datoteke pred združevanjem. Klic SetLoadedOutlineURI(Index, URI) pa naredi nekaj strukturno drugačnega: priloži akcijski slovar /A z /S /URI, kar zaznamek spremeni v spletno povezavo namesto skoka znotraj dokumenta
// 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');
Pri načrtovanju drevesa ohranite oba mehanizma strogo ločena. Vnos /Dest predstavlja čisto navigacijo znotraj dokumenta; akcija URI pa dokument v celoti zapusti, standard ISO 32000-1 pa pričakuje, da vnos kazala navigira prek enega ali drugega mehanizma, ne pa prek obeh hkrati. Zato klic SetLoadedOutlineURI rezervirajte za vnose, ki še ne nosijo ciljne strani, na primer za listo "Domača stran izdelka" ali "Prijavi težavo" na koncu drevesa, namesto za pretvorbo naslova poglavja. Tukaj zapisana akcija URI je enaka struktura /S /URI, ki se uporablja za povezovalne anotacije na vsebini strani, kar je podrobneje opisano v priročniku za ustvarjanje hiper povezav v dokumentih PDF s HotPDF
Kako delujejo imenovani cilji v naloženem PDF-ju?
Imenovani cilji (named destinations) so mehanizem stabilnih sidrišč po standardu ISO 32000-1 §12.3.2.3: namesto vgradnje reference strani v vsako povezavo dokument ohranja preslikavo imen v cilje v drevesu imen /Names /Dests v katalogu, povezave pa se na vnose sklicujejo po imenu. Metoda ResolveLoadedNamedDestination(Name) poišče ime v tem drevesu in vrne z 0 indeksirani indeks strani, na katero kaže, oziroma -1, če ime ni prisotno. Razreševalnik podpira obe obliki shranjevanja, ki ju specifikacija dovoljuje za preslikano vrednost: golo matriko cilja, kot je [page /XYZ x y zoom], in oblika slovarja, ki matriko ovije pod ključem /D
Metoda AddLoadedNamedDestination(Name, DestPageIndex, X, Y) deluje v nasprotni smeri: registrira novo sidrišče v drevesu /Names /Dests (pri čemer drevo ustvari, če ga dokument še nima) in ob uspehu vrne True. Skupaj ta par pokriva potek dela, kjer mora združeni dokument spoštovati sidrišča, ki so jih zapisala orodja v nadaljevanju, in objaviti nova za ciljanje povezav v nadaljevanju
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;
Iskrene omejitve: cilji, ki jih piše HotPDF, tako za zaznamke kot za imenovana sidrišča, so cilji /XYZ. Druge eksplicitne vrste, ki jih opredeljuje specifikacija, kot so /Fit, /FitH, /FitB in podobne, se s temi klici ne oddajajo, čeprav /XYZ z ničelno povečavo pokriva prevladujoči primer uporabe "pojdi na to mesto, ohrani mojo povečavo". Metoda GetLoadedBookmarkPageIndex(Title), priročno iskanje, ki sprejme naslov zaznamka in vrne njegovo ciljno stran, pa preišče le najvišjo raven drevesa kazala, zato jo uporabite za preverjanje vnosov na ravni poglavij, ki ste jih pravkar ustvarili, in ne za globoko ugnezdene liste
Past /Count: šteje potomce in ne vnosov
Ključ /Count na korenu kazala ne pomeni tistega, kar domneva večina razvijalcev, napačna domneva pa povzroči napake pri testiranju, ki so videti kot poškodovanje podatkov. Standard ISO 32000-1 §12.3.3 opredeljuje /Count kot skupno število vidnih potomcev na vseh ravneh in ne kot število vnosov najvišje ravni. Tudi na posameznem vnosu ima predznak svoj pomen: pozitivna vrednost pomeni, da je vnos odprt in da se v stranski vrstici prikazuje toliko potomcev, medtem ko negativna vrednost pomeni, da je vnos zložen in je |N| potomcev skritih znotraj njega
To je prišlo na dan iz lastne testne zbirke regresij HotPDF, medtem ko so se gradili API-ji za zaznamke naloženih dokumentov. Testni dokument je imel tri zaznamke najvišje ravni, enega z otrokom; po izbrisu enega vnosa najvišje ravni je klic GetLoadedOutlineCount, ki bere korenski /Count, javil 1 namesto pričakovanih 2. Nič ni bilo poškodovano: začetna vrednost 3 sploh nikoli ni pomenila "treh vnosov najvišje ravni", pravilen ponovni izračun pa mora prehoditi verigo najvišje ravni ter seštevati vsako vozlišče in njegov pozitivni /Count, pri čemer preskoči potomce zloženih vozlišč. Lekcija za vašo kodo je neposredna: nikoli ne uveljavljajte trditev o razlikah v /Count po strukturnih spremembah, saj se ta vrednost spreminja nelinearno z vidnimi potomci. Namesto tega preverite strukturo glede na vsebino
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Kje se to umešča v nabor orodij za naložene dokumente
Ustvarjanje zaznamkov in imenovanih ciljev dopolnjuje sliko, ki jo API za naložene dokumente dopolnjuje že nekaj časa: enak cikel naloži-uredi-shrani že prepisuje metapodatke dokumenta, dodaja povezovalne anotacije in prenese anotacije prek uvoza in izvoza XFDF. Vzorec je enoten: LoadFromFile, zaporedje klicev *Loaded* nad grafom objektov v pomnilniku, nato pa en klic SaveLoadedDocument, kar omogoča preprosto vključitev gradnje kazala v obstoječi cevovod za združevanje ali označevanje kot še en korak pred shranjevanjem
API-ji, ki jih obravnavamo tukaj — AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination in GetLoadedBookmarkPageIndex —, se prinaša v standardni komponenti HotPDF Component za Delphi in C++Builder, brez zunanjih odvisnosti in brez potrebe po ločenem okolju za pregledovanje