HotPDF tilføjer bogmærker til en eksisterende PDF i Delphi gennem AddLoadedOutline, som opretter outline-poster på ethvert indlejringsniveau i et indlæst dokument, og det læser og skriver navngivne destinationer gennem ResolveLoadedNamedDestination og AddLoadedNamedDestination. Indlæs filen, byg træet, kald SaveLoadedDocument, og det sidepanel i læseren, der før var tomt, bærer nu en fungerende indholdsfortegnelse
Scenariet, der motiverer alt dette, er deprimerende almindeligt. Du fletter et dusin kontrakter sammen til én gennemgangspakke eller syr tre produktmanualer sammen til én distribuerbar fil, og outputtet er strukturelt fint: hver side til stede, hver skrifttype intakt. Så åbner nogen det i Acrobat, og bogmærkepanelet er tomt. Et dokument på 400 sider uden navigation er teknisk set komplet og i praksis ubrugeligt, og indtil version 2.347.0 kunne HotPDF læse, ændre og slette bogmærker i indlæste dokumenter, men aldrig oprette dem. Det hul er nu lukket, og denne artikel gennemgår den nye API-overflade og det ene stykke PDF-arkana, nøglen /Count, der bider dig, hvis du behandler den som en simpel posttæller
Hvad outline-træet egentlig er
En PDF-outline er et dobbelt kædet træ af dictionaries, ikke en flad liste, og den struktur forklarer hver parameter i API'en. ISO 32000-1 §12.3.3 definerer det: dokumentkataloget peger på en /Outlines-rod, roden peger på sit første og sidste barn via /First og /Last, og søskende kædes gennem /Next og /Prev, mens hver post peger tilbage opad gennem /Parent. Hver post bærer en /Title-streng og som regel en /Dest, der siger, hvor et klik på den skal føre læseren hen
Destinationer findes i to varianter, og ISO 32000-1 §12.3.2 holder dem bevidst adskilt. En eksplicit destination er et inline-array som [page /XYZ x y zoom]: en direkte reference til et sideobjekt plus en visningsspecifikation. En navngiven destination gemmer i stedet en navnestreng, og det egentlige mål ligger i dokumentkataloget under navnetræet /Names /Dests, ét niveau af indirektion, der lader mange links dele ét mål og lader målet flytte sig uden at røre linkene. HotPDF skriver eksplicitte /XYZ-destinationer, når det opretter bogmærker, og giver dig separate kald til at læse og udvide navnetræet
Hvordan tilføjer man bogmærker til en eksisterende PDF i Delphi?
Kald AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) efter LoadFromFile: send ParentIndex = -1 for en post på øverste niveau eller det nulbaserede indeks for en eksisterende post på øverste niveau for at indlejre under den. Funktionen opretter /Outlines-roden, hvis dokumentet aldrig har haft en, bygger post-dictionaryet, skriver et /Dest [page /XYZ x y zoom]-array, der peger på det nulbaserede DestPageIndex, og kæder den nye post ind i søskendekæden. Den returnerer 0 ved succes og -1 ved fejl, for eksempel når DestPageIndex er uden for intervallet
Én kædningsdetalje betyder noget for rækkefølgen: en ny post indsættes i hovedet af sin forælders børnekæde som den nye /First. Så hvis du tilføjer "Chapter 1" og derefter "Chapter 2" som poster på øverste niveau, vises Chapter 2 over Chapter 1 i sidepanelet, og Chapter 1 er rykket til indeks 1 på øverste niveau. Det praktiske mønster er enten at tilføje poster i omvendt læserækkefølge eller at tilføje hver forælder og straks udfylde dens børn, mens den stadig sidder på indeks 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Tilføj kapitler i omvendt læserækkefølge: hver ny
// post på øverste niveau bliver /First (indeks 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 er nu indeks 0 på øverste niveau; indlejr afsnit
// under det (børn indsættes også forrest, så omvendt rækkefølge).
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;
Parametrene X, Y og Zoom mapper direkte til /XYZ-destinationen, og alle har standardværdien 0. PDF placerer koordinatorigo i nederste venstre hjørne, så Y = 792 sigter på toppen af en US Letter-side, og ifølge ISO 32000-1 fortæller en nulværdi i en /XYZ-plads fremviseren, at den skal beholde sin nuværende indstilling, hvilket er præcis det, du ønsker for zoom i næsten alle tilfælde. Alt sker i den indlæste objektgraf i hukommelsen; intet rører disken, før SaveLoadedDocument kører, den samme redigér-og-gem-model, som API'en for indlæste dokumenter bruger til omskrivning af titel, forfatter og XMP-metadata i indlæste PDF'er
Omdirigering af bogmærker: sidespring kontra URI-handlinger
Eksisterende bogmærker kan kobles om uden at blive genskabt, og HotPDF giver de to tilfælde to forskellige kald, fordi PDF-konstruktionerne nedenunder er reelt forskellige. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) erstatter /Dest for posten på øverste niveau ved Index med et frisk /XYZ-array, værktøjet til den klassiske fejl efter en sammenfletning, hvor hvert bogmærke stadig peger på sidetallene i den oprindelige fil før sammenfletningen. SetLoadedOutlineURI(Index, URI) gør noget strukturelt anderledes: det vedhæfter et /A-handlingsdictionary med /S /URI, hvilket forvandler bogmærket til et weblink i stedet for et spring inde i dokumentet
// Sammenfletningen forskød alt med 10 sider: peg det
// tredje bogmærke på øverste niveau på side 12 (nulbaseret 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Gør den sidste post til et eksternt link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Hold de to mekanismer adskilt, når du designer træet. En /Dest er ren navigation inde i dokumentet; en URI-handling forlader dokumentet helt, og ISO 32000-1 forventer, at en outline-post navigerer gennem den ene mekanisme eller den anden, ikke begge. Så forbehold SetLoadedOutlineURI til poster, der ikke allerede bærer en sidedestination, en "Product homepage"- eller "Report an issue"-bladpost i slutningen af træet frem for en konverteret kapiteloverskrift. Den URI-handling, der skrives her, er den samme /S /URI-konstruktion, som bruges til linkannotationer på sideindhold, dækket mere dybdegående i guiden til oprettelse af hyperlinks i PDF-dokumenter med HotPDF
Hvordan fungerer navngivne destinationer i en indlæst PDF?
Navngivne destinationer er mekanismen med stabile ankre i ISO 32000-1 §12.3.2.3: i stedet for at indlejre en sidereference i hvert link holder dokumentet et navn-til-destination-kort i katalogets navnetræ /Names /Dests, og links refererer til poster ved navn. ResolveLoadedNamedDestination(Name) slår et navn op i det træ og returnerer det nulbaserede sideindeks, det peger på, eller -1, hvis navnet ikke findes. Opløseren håndterer begge de lagringsformer, specifikationen tillader for den mappede værdi: et nøgent destinationsarray som [page /XYZ x y zoom] og dictionary-formen, der pakker arrayet ind under en /D-nøgle
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) går den anden vej: den registrerer et nyt anker i træet /Names /Dests, opretter selve træet, når dokumentet ikke har noget, og returnerer True ved succes. Tilsammen dækker parret den arbejdsgang, hvor et sammenflettet dokument skal respektere ankre, som værktøjer tidligere i kæden skrev, og udgive nye, som links længere nede i kæden kan sigte på
var
PageIdx: Integer;
begin
// Respektér et anker, et andet værktøj skrev ind i /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Udgiv et nyt anker, som andre links kan sigte på.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
De ærlige grænser: de destinationer, HotPDF skriver, for både bogmærker og navngivne ankre, er /XYZ-destinationer. De andre eksplicitte typer, specifikationen definerer, /Fit, /FitH, /FitB og lignende, udsendes ikke af disse kald, selvom /XYZ med nul-zoom dækker det dominerende anvendelsestilfælde "gå til dette sted, behold min zoom". Og GetLoadedBookmarkPageIndex(Title), det praktiske opslag, der tager en bogmærketitel og returnerer dens destinationsside, scanner det øverste niveau af outline-træet, så brug det til at verificere de poster på kapitelniveau, du lige har oprettet, frem for dybt indlejrede blade
/Count-fælden: den tæller efterkommere, ikke poster
Nøglen /Count på outline-roden betyder ikke det, de fleste udviklere antager, og den forkerte antagelse giver testfejl, der ligner datakorruption. ISO 32000-1 §12.3.3 definerer /Count som det samlede antal synlige efterkommere på alle niveauer, ikke antallet af poster på øverste niveau. På en individuel post bærer fortegnet også betydning: en positiv værdi betyder, at posten er åben, og at så mange efterkommere vises i sidepanelet, mens en negativ værdi betyder, at posten er foldet sammen, og at |N| efterkommere er skjult inde i den
Dette kom frem i HotPDFs egen regressionssuite, mens bogmærke-API'erne for indlæste dokumenter blev bygget. Et testdokument havde tre bogmærker på øverste niveau, ét med et barn; efter sletning af én post på øverste niveau rapporterede GetLoadedOutlineCount, som læser rodens /Count, 1 i stedet for de forventede 2. Intet var korrupt: det oprindelige 3-tal havde aldrig været "tre poster på øverste niveau" i første omgang, og den korrekte genberegning skal gennemgå kæden på øverste niveau og akkumulere hver node plus dens positive /Count, mens efterkommere af sammenfoldede noder springes over. Lektien for din egen kode er ligefrem: lav aldrig assertions på /Count-deltaer efter strukturelle redigeringer, fordi værdien bevæger sig ikke-lineært med synlige efterkommere. Verificér struktur ud fra indhold i stedet
// Efter redigeringer: verificér via titelopslag, ikke via /Count-deltaer.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Hvor dette passer ind i værktøjssættet for indlæste dokumenter
Oprettelse af bogmærker og navngivne destinationer fuldender et billede, som API'en for indlæste dokumenter har udfyldt i et stykke tid: den samme indlæs-redigér-gem-cyklus omskriver allerede dokumentmetadata, tilføjer linkannotationer og sender annotationer frem og tilbage gennem XFDF-import og -eksport. Mønstret er ensartet, LoadFromFile, en række *Loaded*-kald mod objektgrafen i hukommelsen og derefter ét SaveLoadedDocument, hvilket gør det ligetil at folde outline-konstruktion ind i en eksisterende sammenfletnings- eller stemplingspipeline som ét trin mere før lagringen
De API'er, der er omtalt her, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination og GetLoadedBookmarkPageIndex, leveres i den standardmæssige HotPDF Delphi Component til Delphi og C++Builder, uden eksterne afhængigheder og uden krav om en separat fremviser-runtime