HotPDF dodaje obeleživače (bookmarks) u postojeći PDF u Delphi-ju preko metode AddLoadedOutline, koja kreira stavke strukture na bilo kom nivou ugnježđivanja u učitanom dokumentu, a čita i piše imenovana odredišta (named destinations) kroz ResolveLoadedNamedDestination i AddLoadedNamedDestination. Učitajte fajl, izgradite stablo, pozovite SaveLoadedDocument, i bočna traka čitača koja je nekada bila prazna sada nosi funkcionalan sadržaj
Scenario koji motiviše sve ovo je depresivno čest. Spojite desetak ugovora u jedan paket za pregled, ili sastavite tri uputstva za proizvod u jedan distributivni fajl, i izlaz je strukturno dobar: svaka stranica je prisutna, svaki font netaknut. Zatim ga neko otvori u Acrobat-u i panel obeleživača je prazan. Dokument od 400 stranica bez navigacije je tehnički kompletan a praktično neupotrebljiv. Do verzije 2.347.0 HotPDF je mogao da čita, menja i briše obeleživače u učitanim dokumentima, ali nikada da ih kreira. Taj jaz je sada premošćen, i ovaj članak vodi kroz novi API i jedan deo tajni PDF formata — ključ /Count, koji će vas prevariti ako ga tretirate kao jednostavan brojač stavki
Šta je zapravo stablo strukture
Struktura obeleživača (outline) u PDF-u je dvostruko povezano stablo rečnika, a ne ravna lista, i ta struktura objašnjava svaki parametar u API-ju. ISO 32000-1 §12.3.3 to definiše: katalog dokumenta ukazuje na koren /Outlines, koren ukazuje na svoje prvo i poslednje dete preko /First i /Last, a srodne stavke su povezane preko /Next i /Prev, dok svaka stavka ukazuje nazad nagore preko /Parent. Svaka stavka nosi string naslova /Title i, obično, odredište /Dest koje govori gde čitaoca treba odvesti kada klikne na nju
Odredišta dolaze u dva oblika, i specifikacija ISO 32000-1 §12.3.2 ih drži namerno odvojenim. Eksplicitno odredište je ugrađeni niz kao što je [page /XYZ x y zoom]: direktna referenca na objekat stranice plus specifikacija prikaza. Imenovano odredište (named destination) umesto toga čuva string imena, a stvarni cilj živi u katalogu dokumenta pod stablom imena /Names /Dests. To je jedan nivo indirekcije koji omogućava da više veza deli isti cilj, i da se cilj pomera bez potrebe za menjanjem veza. HotPDF piše eksplicitna /XYZ odredišta kada kreira obeleživače, i nudi posebne pozive za čitanje i proširivanje stabla imena
Kako dodati obeleživače u postojeći PDF u Delphi-ju?
Pozovite AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) nakon LoadFromFile: prosledite ParentIndex = -1 za stavku najvišeg nivoa, ili indeks od nule postojeće stavke najvišeg nivoa da biste je ugnijezdili ispod nje. Funkcija kreira koren /Outlines ako ga dokument nikada nije imao, gradi rečnik stavke, upisuje niz /Dest [page /XYZ x y zoom] koji ukazuje na DestPageIndex (indeksiran od nule) i povezuje novu stavku u lanac srodnika. Vraća 0 u slučaju uspeha i -1 u slučaju neuspeha, na primer kada je DestPageIndex van opsega
Jedan detalj povezivanja je važan za redosled: nova stavka se ubacuje na početak lanca dece svog roditelja, kao novi /First. Dakle, ako dodate "Chapter 1" pa "Chapter 2" as stavke najvišeg nivoa, Chapter 2 se pojavljuje iznad Chapter 1 u bočnoj traci, a Chapter 1 se pomera na indeks 1 najvišeg nivoa. Praktičan obrazac je ili da dodajete stavke u obrnutom redosledu čitanja, ili da dodate svakog roditelja i odmah popunite njegovu decu dok se on još uvek nalazi 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 i Zoom se direktno mapiraju na odredište /XYZ i svi podrazumevano imaju vrednost 0. PDF postavlja koordinatni početak u donji levi ugao, tako da Y = 792 cilja vrh stranice veličine US Letter. Prema ISO 32000-1, nulta vrednost u /XYZ slotu govori čitaču da zadrži svoje trenutno podešavanje, što je upravo ono što želite za zum u gotovo svakom slučaju. Sve se dešava na učitanom grafu objekata u memoriji; ništa ne dodiruje disk dok se ne pokrene SaveLoadedDocument, što je isti model "izmeni pa sačuvaj" koji API za učitane dokumente koristi za prepisivanje naslova, autora i XMP metapodataka na učitanim PDF-ovima
Preusmeravanje obeleživača: skokovi na stranice naspram URI akcija
Postojeći obeleživači se mogu preusmeriti bez njihovog ponovnog kreiranja, a HotPDF nudi dva različita poziva za ova dva slučaja jer su PDF strukture u pozadini zaista različite. Metoda SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) zamenjuje /Dest stavke najvišeg nivoa na indeksu Index svežim nizom /XYZ, što je alat za rešavanje klasičnog problema nakon spajanja gde svaki obeleživač i dalje ukazuje na brojeve stranica originalnog fajla pre spajanja. SetLoadedOutlineURI(Index, URI) radi nešto strukturno drugačije: on prilaže rečnik akcije /A sa /S /URI, pretvarajući obeleživač u veb vezu umesto skoka unutar 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');
Držite ova dva mehanizma odvojeno kada dizajnirate stablo. /Dest je čista navigacija unutar dokumenta; URI akcija u potpunosti napušta dokument, a ISO 32000-1 očekuje da stavka strukture koristi jedan ili drugi mehanizam, a ne oba. Zato rezervišite SetLoadedOutlineURI za stavke koje već nemaju odredište stranice, kao što je list "Početna stranica proizvoda" ili "Prijavite problem" na kraju stabla, a ne za konvertovani naslov poglavlja. URI akcija koja se ovde upisuje je ista struktura /S /URI koja se koristi za anotacije veza na sadržaju stranice, što je detaljnije pokriveno u vodiču za kreiranje hiperveza u PDF dokumentima pomoću HotPDF-a
Kako rade imenovana odredišta na učitanom PDF-u?
Imenovana odredišta su mehanizam stabilnog sidra (anchor) iz specifikacije ISO 32000-1 §12.3.2.3: umesto da se referenca stranice ugrađuje u svaku vezu, dokument drži mapu imena-prema-odredištima u stablu imena katalogovog /Names /Dests, a veze se odnose na stavke po imenu. ResolveLoadedNamedDestination(Name) traži ime u tom stablu i vraća indeks stranice od nule na koji ono ukazuje, ili -1 ako ime nije prisutno. Razrešivač rukuje sa oba oblika skladištenja koje specifikacija dozvoljava za mapiranu vrednost: čistim nizom odredišta poput [page /XYZ x y zoom] i formom rečnika koja obavija niz pod ključem /D
Metoda AddLoadedNamedDestination(Name, DestPageIndex, X, Y) radi obrnuto: ona registruje novo sidro u stablu /Names /Dests, kreirajući samo stablo kada ga dokument nema, i vraća True u slučaju uspeha. Zajedno, ovaj par pokriva radni tok u kojem spojeni dokument mora da poštuje sidra koja su upisali prethodni alati i da objavi nova za naredne veze koje ih ciljaju
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;
Stvarna ograničenja: odredišta koja HotPDF piše, kako za obeleživače tako i za imenovana sidra, jesu /XYZ odredišta. Ostali eksplicitni tipovi koje specifikacija definiše, kao što su /Fit, /FitH, /FitB i slični, ne emituju se ovim pozivima, iako /XYZ sa nultim zumom pokriva dominantan slučaj upotrebe "idi na ovo mesto, zadrži moj zum". A GetLoadedBookmarkPageIndex(Title), pogodno pretraživanje koje prihvata naslov obeleživača i vraća njegovu odredišnu stranicu, skenira najviši nivo stabla strukture, pa ga koristite za verifikaciju stavki na nivou poglavlja koje ste upravo kreirali, a ne za duboko ugnježđene listove
Zamka ključa /Count: on broji potomke, a ne stavke
Ključ /Count u korenu strukture ne znači ono što većina programera pretpostavlja, a pogrešna pretpostavka dovodi do neuspeha testova koji izgledaju kao korupcija podataka. ISO 32000-1 §12.3.3 definiše /Count kao ukupan broj vidljivih potomaka na svim nivoima, a ne kao broj stavki najvišeg nivoa. Na pojedinačnoj stavci znak takođe ima značenje: pozitivna vrednost znači da je stavka otvorena i da se taj broj potomaka prikazuje u bočnoj traci, dok negativna vrednost znači da je stavka skupljena i da je |N| potomaka skriveno unutar nje
Ovo je uočeno u sopstvenom test paketu HotPDF-a dok su se pravili API-ji za obeleživače učitanih dokumenata. Test dokument je imao tri obeleživača najvišeg nivoa, od kojih je jedan imao dete; nakon brisanja jedne stavke najvišeg nivoa, metoda GetLoadedOutlineCount, koja čita koren /Count, prijavila je 1 umesto očekivanih 2. Ništa nije bilo oštećeno: početnih 3 nikada nije predstavljalo "tri stavke najvišeg nivoa", a ispravan proračun mora proći kroz lanac najvišeg nivoa sabirajući svaki čvor plus njegov pozitivan /Count, preskačući potomke skupljenih čvorova. Pouka za vaš sopstveni kod je jasna: nikada ne donosite tvrdnje o razlikama u /Count-u nakon strukturnih izmena, jer se vrednost pomera nelinearno sa vidljivim potomcima. Umesto toga, verifikujte strukturu po sadržaju
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Gde se ovo uklapa u set alata za učitane dokumente
Kreiranje obeleživača i imenovanih odredišta upotpunjuje sliku koju API za učitane dokumente gradi već neko vreme: isti ciklus učitaj-izmeni-sačuvaj već prepisuje metapodatke dokumenta, dodaje anotacije veza i prenosi anotacije u oba smera kroz uvoz i izvoz XFDF-a. Obrazac je jedinstven: LoadFromFile, sekvenca *Loaded* poziva nad grafom objekata u memoriji, pa onda jedan SaveLoadedDocument, što olakšava ugradnju kreiranja strukture u postojeći proces spajanja ili žigosanja kao još jedan korak pre čuvanja
API-ji o kojima se ovde govori: AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination i GetLoadedBookmarkPageIndex, isporučuju se u standardnoj HotPDF komponenti za Delphi i C++Builder, bez spoljnih zavisnosti i bez potrebe za posebnim okruženjem za pregledanje