HotPDF adaugă semne de carte (bookmarks) într-un PDF existent în Delphi prin intermediul AddLoadedOutline, care creează intrări de structură (outline entries) la orice nivel de cuibărire pe un document încărcat, și citește și scrie destinații denumite prin ResolveLoadedNamedDestination și AddLoadedNamedDestination. Încărcați fișierul, construiți arborele, apelați SaveLoadedDocument, iar bara laterală a cititorului care obișnuia să fie goală conține acum un cuprins funcțional
Scenariul care motivează toate acestea este deosebit de comun. Îmbinați o duzină de contracte într-un singur pachet de revizuire sau reuniți trei manuale de produs într-un singur fișier distribuibil, iar rezultatul este corect din punct de vedere structural: fiecare pagină este prezentă, fiecare font este intact. Apoi cineva îl deschide în Acrobat și panoul de semne de carte este gol. Un document de 400 de pagini fără navigare este complet din punct de vedere tehnic și practic inutilizabil, iar până la versiunea 2.347.0, HotPDF putea citi, modifica și șterge semne de carte pe documente încărcate, dar nu le putea crea niciodată. Această lacună este acum închisă, iar acest articol parcurge noua structură a API-ului și acea piesă din arcanul PDF, cheia /Count, care vă va pune probleme dacă o tratați ca pe un simplu contor de intrări
Ce este de fapt arborele de structură (outline tree)
O structură (outline) PDF este un arbore dublu înlănțuit de dicționare, nu o listă plată, iar această structură explică fiecare parametru din API. ISO 32000-1 §12.3.3 o definește astfel: catalogul documentului indică o rădăcină /Outlines, rădăcina indică primul și ultimul său copil prin /First și /Last, iar frații se înlănțuiesc prin /Next și /Prev în timp ce fiecare intrare indică înapoi în sus prin /Parent. Fiecare intrare conține un șir de caractere /Title și, de obicei, un /Dest care indică unde ar trebui să fie direcționat cititorul la clic
Destinațiile sunt de două feluri, iar specificația ISO 32000-1 §12.3.2 le menține în mod deliberat separate. O destinație explicită este un tablou inline precum [pagină /XYZ x y zoom]: o referință directă la un obiect pagină plus o specificație de vizualizare. O destinație denumită stochează în schimb un șir de caractere ca nume, iar ținta reală se află în catalogul documentului sub arborele de nume /Names /Dests, un nivel de indirecție care permite multor linkuri să partajeze o singură țintă și permite țintei să fie mutată fără a atinge linkurile. HotPDF scrie destinații explicite /XYZ atunci când creează semne de carte și vă oferă apeluri separate pentru citirea și extinderea arborelui de nume
Cum se adresează semnele de carte în Delphi?
Apelați AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) după LoadFromFile: transmiteți ParentIndex = -1 pentru o intrare de nivel superior sau indexul (baza zero) al unei intrări existente de nivel superior pentru a o cuibări sub aceasta. Funcția creează rădăcina /Outlines dacă documentul nu a avut niciodată una, construiește dicționarul de intrare, scrie un tablou /Dest [pagină /XYZ x y zoom] care indică spre DestPageIndex (baza zero) și conectează noua intrare în lanțul de frați. Aceasta returnează 0 în caz de succes și -1 în caz de eșec, de exemplu când DestPageIndex este în afara intervalului
Un detaliu legat de ordonarea legăturilor contează: o nouă intrare este inserată în capul lanțului de copii al părintelui său, ca nou /First. Deci, dacă adăugați „Chapter 1” și apoi „Chapter 2” as intrări de nivel superior, Chapter 2 apare deasupra lui Chapter 1 în bara laterală, iar Chapter 1 s-a mutat pe indexul de nivel superior 1. Modelul practic este fie să adăugați intrările în ordinea inversă a citirii, fie să adăugați fiecare părinte și să populați imediat copiii săi în timp ce acesta se află încă la indexul 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;
Parametrii X, Y și Zoom se mapează direct pe destinația /XYZ și toți au valoarea implicită 0. PDF-ul plasează originea coordonatelor în colțul din stânga-jos, deci Y = 792 indică spre partea superioară a unei pagini US Letter, iar conform ISO 32000-1 o valoare zero într-un slot /XYZ îi indică vizualizatorului să își păstreze setarea curentă, ceea ce este exact ceea ce doriți pentru zoom în aproape orice caz. Totul se întâmplă pe graful de obiecte încărcat în memorie; nimic nu atinge discul până când rulează SaveLoadedDocument, același model editează-și-salvează pe care API-ul pentru documente încărcate îl folosește pentru rescrierea metadatelor de titlu, autor și XMP în PDF-uri încărcate
Redirecționarea semnelor de carte: salturi de pagină versus acțiuni URI
Semnele de carte existente pot fi reconectate fără a fi recreate, iar HotPDF oferă pentru cele două cazuri două apeluri distincte, deoarece structurile PDF subiacente sunt cu totul diferite. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) înlocuiește /Dest al intrării de nivel superior de la Index cu un tablou /XYZ nou, fiind instrumentul folosit pentru remedierea eșecurilor clasice de după îmbinare, unde fiecare semn de carte continuă să indice spre numerele de pagină ale fișierului original de dinainte de îmbinare. SetLoadedOutlineURI(Index, URI) face ceva diferit din punct de vedere structural: atașează un dicționar de acțiune /A cu /S /URI, transformând semnul de carte într-un link web în loc de un salt în interiorul documentului
// 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');
Mențineți delimitate cele două mecanisme când proiectați arborele. Un /Dest este pură navigare în interiorul documentului; o acțiune URI părăsește complet documentul, iar specificația ISO 32000-1 așteaptă ca o intrare de structură să navigheze printr-un mecanism sau celălalt, nu prin ambele. Prin urmare, rezervați SetLoadedOutlineURI pentru intrările care nu conțin deja o destinație de pagină, de exemplu o frunză „Pagina de pornire a produsului” sau „Raportați o problemă” la sfârșitul arborelui, mai degrabă decât o etichetă de capitol convertită. Acțiunea URI scrisă aici este aceeași structură /S /URI utilizată pentru adnotările de legături pe conținutul paginii, acoperită în detaliu în ghidul pentru crearea de hyperlink-uri în documente PDF cu HotPDF
Cum funcționează destinațiile denumite într-un PDF încărcat?
Destinațiile denumite reprezintă mecanismul de ancoră stabilă din ISO 32000-1 §12.3.2.3: în loc să încorporeze o referință de pagină în fiecare legătură, documentul păstrează o hartă nume-destinație în arborele de nume /Names /Dests al catalogului, iar linkurile se referă la intrări prin nume. ResolveLoadedNamedDestination(Name) caută un nume în acel arbore și returnează indexul de pagină (baza zero) spre care indică acesta, sau -1 dacă numele lipsește. Rezolvatorul gestionează ambele forme de stocare pe care specificația le permite pentru valoarea mapată: un tablou de destinație simplu precum [pagină /XYZ x y zoom] și forma de dicționar care împachetează tabloul sub o cheie /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) funcționează invers: înregistrează o nouă ancoră în arborele /Names /Dests, creând arborele în sine când documentul nu are niciunul, și returnează True în caz de succes. Împreună, această pereche acoperă fluxul de lucru în care un document îmbinat trebuie să respecte ancorele scrise de instrumentele din amonte și să le publice pe cele noi pentru ca legăturile din aval să le poată accesa ca țintă
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;
Limitele oneste: destinațiile pe care le scrie HotPDF, atât pentru semne de carte, cât și pentru ancore denumite, sunt destinații de tip /XYZ. Celelalte tipuri explicite pe care le definește specificația (/Fit, /FitH, /FitB și altele) nu sunt emise de aceste apeluri, deși /XYZ cu zoom zero acoperă scenariul dominant de utilizare „mergi la acest punct, păstrează zoom-ul curent”. Iar GetLoadedBookmarkPageIndex(Title), funcția utilă de căutare care acceptă un titlu de semn de carte și returnează pagina sa de destinație, scanează doar nivelul superior al arborelui de structură, deci utilizați-o pentru a verifica intrările de la nivel de capitol pe care tocmai le-ați creat și nu frunzele profund cuibărite
Capcana /Count: numără descendenții, nu intrările
Cheia /Count de pe rădăcina structurii nu înseamnă ceea ce presupun majoritatea dezvoltatorilor, iar presupunerea greșită produce eșecuri de testare care arată ca o corupere de date. Specificația ISO 32000-1 §12.3.3 definește /Count ca fiind numărul total de descendenți vizibili la toate nivelurile, nu numărul de intrări de nivel superior. Pe o intrare individuală, semnul poartă și el un sens: o valoare pozitivă înseamnă că intrarea este deschisă și că acel număr de descendenți apar în bara laterală, în timp ce o valoare negativă înseamnă că intrarea este restrânsă (collapsed) și că cei |N| descendenți sunt ascunși în interiorul ei
Această situație a reieșit din propria suită de teste de regresie a HotPDF în timp ce se construiau API-urile de semne de carte pentru documentele încărcate. Un document de test conținea trei semne de carte de nivel superior, dintre care unul avea un copil; după ștergerea unei intrări de nivel superior, GetLoadedOutlineCount, care citește rădăcina /Count, a raportat 1 în loc de valoarea așteptată de 2. Nimic nu a fost corupt: valoarea inițială 3 nu fusese în primul rând „trei intrări de nivel superior”, iar recalcularea corectă trebuie să parcurgă lanțul de nivel superior cumulând fiecare nod plus valoarea sa pozitivă /Count, omițând descendenții nodurilor restrânse. Învățătura pentru propriul cod este directă: nu faceți niciodată aserțiuni pe diferențele (deltas) /Count după modificări structurale, deoarece valoarea se modifică neliniar în funcție de descendenții vizibili. Verificați structura după conținut în schimb
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Unde se încadrează acest element în setul de instrumente pentru documente încărcate
Crearea de semne de carte și destinații denumite completează o imagine pe care API-ul pentru documente încărcate o conturează de ceva timp: același ciclu de încărcare-editare-salvare rescrie deja metadatele documentului, adaugă adnotări de legături și asigură transferul bidirecțional al adnotărilor prin import și export XFDF. Modelul este uniform: LoadFromFile, o secvență de apeluri *Loaded* pe graful de obiecte din memorie, apoi un singur SaveLoadedDocument, ceea ce face simplă integrarea construirii structurii într-o conductă existentă de îmbinare sau aplicare de ștampile ca un pas suplimentar înainte de salvare
API-urile discutate aici, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination și GetLoadedBookmarkPageIndex, sunt incluse în versiunea standard a HotPDF Component pentru Delphi și C++Builder, fără dependențe externe și fără a necesita un timp de execuție separat pentru vizualizator