A HotPDF az AddLoadedOutline segítségével ad hozzá könyvjelzőket egy meglévő PDF-hez Delphi-ben, ami tetszőleges beágyazási szinten hoz létre vázlatbejegyzéseket (outline entries) egy betöltött dokumentumban, valamint elnevezett célhelyeket olvas és ír a ResolveLoadedNamedDestination és AddLoadedNamedDestination metódusokon keresztül. Töltse be a fájlt, építse fel a fát, hívja meg a SaveLoadedDocument eljárást, és az olvasó oldalsávja, amely korábban üres volt, most egy működő tartalomjegyzéket fog tartalmazni.
A helyzet, ami mindezt motiválja, lehangolóan gyakori. Egy tucat szerződést egyesít egyetlen áttekintő csomagba, vagy három termékkézikönyvet fűz össze egyetlen terjeszthető fájllá, és a kimenet szerkezetileg teljesen rendben van: minden oldal megvan, minden betűtípus érintetlen. Aztán valaki megnyitja az Acrobatban, és a könyvjelzők panel üres. Egy 400 oldalas dokumentum navigáció nélkül technikailag ugyan kész, de a gyakorlatban használhatatlan, és a 2.347.0 verzióig a HotPDF meg tudta nyitni, módosítani és törölni tudta a könyvjelzőket a betöltött dokumentumokban, de létrehozni soha. Ez a hiányosság most megszűnt, és ez a cikk bemutatja az új API-felületet, valamint a PDF egyik titkát, a /Count kulcsot, amely gondot okozhat Önnek, ha egyszerű bejegyzés-számlálóként kezeli.
Mi is valójában a vázlatfa?
A PDF-vázlat (outline) szótárak (dictionaries) kétszeresen láncolt fája, nem pedig egy sík lista, és ez a struktúra magyarázza az API minden paraméterét. Az ISO 32000-1 §12.3.3 szabvány határozza meg: a dokumentumkatalógus egy /Outlines gyökérre mutat, a gyökér a /First és /Last elemeken keresztül mutat az első és utolsó gyermekére, a testvérelemek pedig a /Next és /Prev elemeken keresztül kapcsolódnak egymáshoz, miközben minden bejegyzés a /Parent elemen keresztül mutat vissza a szülőre. Minden bejegyzés tartalmaz egy /Title karakterláncot és általában egy /Dest értéket, amely megadja, hogy hova kell irányítani az olvasót, ha rákattint.
A célhelyek (destinations) kétfélék lehetnek, és az ISO 32000-1 §12.3.2 szabvány szándékosan elkülöníti őket. Az explicit célhely egy belső tömb, például [page /XYZ x y zoom]: közvetlen hivatkozás egy oldalobjektumra és egy nézet-specifikáció. Az elnevezett célhely (named destination) ehelyett egy név-karakterláncot tárol, és a tényleges cél a dokumentumkatalógusban, a /Names /Dests névfa alatt található. Ez a közvetett hivatkozás lehetővé teszi, hogy több link osztozzon ugyanazon a célon, és a cél elmozdulhasson a linkek módosítása nélkül. A HotPDF explicit /XYZ célhelyeket ír a könyvjelzők létrehozásakor, és külön hívásokat biztosít a névfa olvasására és bővítésére.
Hogyan adhatók könyvjelzők egy meglévő PDF-hez Delphi-ben?
Hívja meg az AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) metódust a LoadFromFile után: adjon át ParentIndex = -1 értéket a legfelső szintű bejegyzéshez, vagy egy meglévő legfelső szintű bejegyzés nulla alapú indexét az alá történő beágyazáshoz. A függvény létrehozza az /Outlines gyökeret, ha a dokumentumnak még nem volt ilyenje, felépíti a bejegyzés-szótárt, beír egy /Dest [page /XYZ x y zoom] tömböt, amely a nulla alapú DestPageIndex-re mutat, és összekapcsolja az új bejegyzést a testvérlánccal. Siker esetén 0-val, hiba esetén pedig -1-gyel tér vissza (például ha a DestPageIndex tartományon kívül esik).
A sorrend szempontjából fontos részlet a láncolás módja: az új bejegyzés a szülő gyermekláncának az elejére szúródik be, mint az új /First. Ha tehát a „Chapter 1”, majd a „Chapter 2” fejezetet adja hozzá legfelső szintű bejegyzésként, a Chapter 2 a Chapter 1 felett jelenik meg az oldalsávban, a Chapter 1 pedig eltolódik az 1-es indexre a legfelső szinten. A gyakorlati módszer az, hogy a bejegyzéseket fordított olvasási sorrendben adja hozzá, vagy mindegyik szülőt hozzáadja, és azonnal feltölti a gyermekeit, miközben az még a 0. indexen áll.
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Fejezetek hozzáadása fordított olvasási sorrendben: minden új
// legfelső szintű bejegyzés lesz a /First (0. index).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// A Chapter 1 most a legfelső szintű 0. index; ágyazzon be szakaszokat
// alá (a gyermekek is az elejére szúródnak be, így fordított sorrend).
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;
Az X, Y és Zoom paraméterek közvetlenül lefordíthatók az /XYZ célhelyre, és mindegyik alapértelmezett értéke 0. A PDF a koordináta-rendszer kezdőpontját a bal alsó sarokba helyezi, így a Y = 792 egy US Letter oldal tetejét célozza meg, és az ISO 32000-1 szerint az /XYZ pozícióban lévő nulla érték arra utasítja a megjelenítőt, hogy tartsa meg a jelenlegi beállításait, ami szinte minden esetben pontosan az, amit a nagyításnál szeretne. Minden a memóriában lévő objektumgráfon történik; semmi sem érinti a lemezt, amíg a SaveLoadedDocument le nem fut. Ez ugyanaz a szerkesztés-majd-mentés modell, amelyet a betöltött dokumentum API-ja használ a cím, szerző és XMP metaadatok felülírására meglévő PDF-ekben.
Könyvjelzők újracélzása: oldalugrások versus URI műveletek
A meglévő könyvjelzők átvezetékelhetők anélkül, hogy újra létrehoznánk őket, és a HotPDF a két esetre két külön hívást biztosít, mivel a mögöttes PDF-struktúrák valóban különböznek. A SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) kicseréli a Index helyen lévő legfelső szintű bejegyzés /Dest értékét egy friss /XYZ tömbre, amely megoldás a klasszikus egyesítés utáni hibára, amikor minden könyvjelző továbbra is az eredeti, egyesítés előtti fájl oldalszámaira mutat. A SetLoadedOutlineURI(Index, URI) szerkezetileg mást tesz: egy /A műveleti szótárt csatol /S /URI értékkel, így a könyvjelzőt egy webes linkké alakítja át a dokumentumon belüli ugrás helyett.
// Az egyesítés mindent eltolt 10 oldallal: irányítsa újra
// a harmadik legfelső szintű könyvjelzőt a 12. oldalra (nulla alapú 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Alakítsa át az utolsó bejegyzést külső linkké.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
A fa tervezésekor tartsa tisztán a két mechanizmust. A /Dest tiszta belső navigáció a dokumentumon belül; a URI művelet teljesen elhagyja a dokumentumot, és az ISO 32000-1 elvárja, hogy egy vázlatbejegyzés az egyik vagy a másik mechanizmussal navigáljon, de ne mindkettővel. Ezért tartsa fenn a SetLoadedOutlineURI hívást olyan bejegyzéseknek, amelyek még nem rendelkeznek oldalcélponttal, például egy „Product homepage” vagy „Report an issue” ágnak a fa végén, nem pedig egy átalakított fejezetcímnek. Az itt beírt URI művelet ugyanaz a /S /URI konstrukció, amelyet az oldaltartalom link-annotációihoz használnak, erről részletesebben a HotPDF hyperlinkek készítéséről szóló útmutatóban olvashat.
Hogyan működnek az elnevezett célhelyek egy betöltött PDF-en?
A named destination-ök (elnevezett célhelyek) a stabil horgonymechanizmust jelentik az ISO 32000-1 §12.3.2.3 szerint: ahelyett, hogy minden linkbe beágyaznánk egy oldalhivatkozást, a dokumentum egy név-célhely leképezést tart fenn a katalógus /Names /Dests név fájában, és a linkek név alapján hivatkoznak a bejegyzésekre. A ResolveLoadedNamedDestination(Name) kikeres egy nevet abban a fában, és visszaadja a nulla alapú oldalindexet, amelyre mutat, vagy -1-et, ha a név hiányzik. A feloldó mindkét tárolási formát kezeli, amelyet a specifikáció engedélyez a leképezett értékre: egy egyszerű céltömböt, mint a [page /XYZ x y zoom], és a szótár formátumot, amely a tömböt egy /D kulcs alá csomagolja.
Az AddLoadedNamedDestination(Name, DestPageIndex, X, Y) a másik irányba működik: regisztrál egy új horgonyt a /Names /Dests fában, létrehozva magát a fát, ha a dokumentumnak még nincs ilyenje, és siker esetén True értékkel tér vissza. Ez a pár lefedi azt a munkafolyamatot, ahol az egyesített dokumentumnak tiszteletben kell tartania a korábbi eszközök által írt horgonyokat, és újakat kell közzétennie a későbbi linkek célzásához.
var
PageIdx: Integer;
begin
// Másik eszköz által a /Names /Dests-be írt horgony tiszteletben tartása.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Új horgony közzététele más linkek célzásához.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Az őszinte határok: a HotPDF által írt célhelyek, mind a könyvjelzők, mind az elnevezett horgonyok esetében, /XYZ célhelyek. A specifikáció által meghatározott többi explicit típust, mint a /Fit, /FitH, /FitB stb., ezek a hívások nem hozzák létre, bár a /XYZ nulla nagyítással lefedi a leggyakoribb használati esetet („menj erre a helyre, tartsd meg a nagyítást”). A GetLoadedBookmarkPageIndex(Title) pedig – az a kényelmi kereső, amely a könyvjelző címe alapján visszaadja a céloldalt – a vázlatfa legfelső szintjét vizsgálja, így a mélyen beágyazott levelek helyett a frissen létrehozott fejezet szintű bejegyzések ellenőrzésére használja.
A /Count csapda: a leszármazottakat számolja, nem a bejegyzéseket
Az outline gyökéren lévő /Count kulcs nem azt jelenti, amit a legtöbb fejlesztő feltételez, és a hibás feltételezés adatkorrupciónak tűnő teszthibákat eredményez. Az ISO 32000-1 §12.3.3 a /Count-ot az összes szinten lévő látható leszármazott számaként határozza meg, nem pedig a legfelső szintű bejegyzések számaként. Egy egyedi bejegyzésnél a jelnek is van jelentése: a pozitív érték azt jelenti, hogy a bejegyzés nyitva van, és ennyi leszármazott látható az oldalsávban, míg a negatív érték azt jelenti, hogy a bejegyzés össze van csukva, és |N| leszármazott van elrejtve benne.
Ez a HotPDF saját regressziós tesztcsomagjából derült ki a betöltött dokumentum könyvjelző API-jainak fejlesztése során. A tesztdokumentum három legfelső szintű könyvjelzőt tartalmazott, amelyek közül az egyiknek volt egy gyermeke; egy legfelső szintű bejegyzés törlése után a gyökér /Count értékét olvasó GetLoadedOutlineCount 1-et jelentett a várt 2 helyett. Semmi sem sérült meg: a kezdeti 3 eleve soha nem jelentett „három legfelső szintű bejegyzést”, és a helyes újraszámításnak végig kell járnia a legfelső szintű láncot, összegezve az egyes csomópontokat és azok pozitív /Count értékét, kihagyva az összecsukott csomópontok leszármazottait. A tanulság az Ön kódjára nézve egyértelmű: soha ne hagyatkozzon a /Count változásaira strukturális szerkesztések után, mert az érték nem lineárisan változik a látható leszármazottakkal. Ellenőrizze a struktúrát inkább a tartalom alapján.
// Szerkesztések után címkereséssel ellenőrizze, ne a /Count eltéréseivel.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Hol helyezkedik el ez a betöltött dokumentumok eszköztárában?
A könyvjelzők és az elnevezett célhelyek létrehozása kiegészíti azt a képet, amelyet a betöltött dokumentum API már egy ideje épít: ugyanaz a betöltés-szerkesztés-mentés ciklus már felülírja a dokumentum metaadatait, link-annotációkat ad hozzá, és oda-vissza utaztatja az annotációkat az XFDF importáláson és exportáláson keresztül. A minta egységes: LoadFromFile, a memóriában lévő objektumgráfon végzett *Loaded* hívások sorozata, majd egy SaveLoadedDocument, ami egyszerűvé teszi a vázlatépítés beillesztését egy meglévő egyesítési vagy pecsételési folyamatba, mint a mentés előtti utolsó lépést.
Az itt tárgyalt API-k, mint az AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination és GetLoadedBookmarkPageIndex, a standard Delphi és C++Builder rendszerekhez készült HotPDF Component szoftverben találhatók meg, külső függőségek és külön megjelenítő futtatókörnyezet igénye nélkül.