Tehnički članak

Dodajte obeleživače u postojeće PDF-ove u Delphi-ju pomoću HotPDF-a

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