HotPDF tilføjer bogmærker til en eksisterende PDF i Delphi via AddLoadedOutline, som opretter dispositionsindgange på ethvert lejringsniveau i et indlæst dokument, og det læser og skriver navngivne destinationer via ResolveLoadedNamedDestination og AddLoadedNamedDestination. Indlæs filen, opbyg træet, kald SaveLoadedDocument, og læserens sidebjælke, der før var tom, indeholder nu en fungerende indholdsfortegnelse
Scenariet, der motiverer alt dette, er deprimerende almindeligt. Du fletter et dusin kontrakter til én samlet pakke eller sammenføjer tre produktmanualer til en enkelt distribuerbar fil, og outputtet er strukturelt i orden: Hver side er til stede, hver skrifttype er intakt. Derefter åbner nogen den i Acrobat, og bogmærkepanelet er tomt. Et dokument på 400 sider uden navigation er teknisk set komplet, verm praktisk talt 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 den ene del af PDF-mystik, nemlig nøglen /Count, som vil ramme dig, hvis du behandler den som en simpel indgangstæller
Hvad dispositionstræet rent faktisk er
En PDF-disposition er et dobbeltlinket træ af ordbøger, ikke en flad liste, og denne struktur forklarer alle parametre i API'en. ISO 32000-1 §12.3.3 definerer det: Dokumentkataloget peger på en /Outlines-rod, roden peger på sine første og sidste børn via /First og /Last, og søskende kædes sammen via /Next og /Prev, mens hver indgang peger tilbage op via /Parent. Hver indgang bærer en /Title-streng og normalt en /Dest, der fortæller, 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 f.eks. [page /XYZ x y zoom]: En direkte reference til et sideobjekt plus en visningsspecifikation. En navngiven destination gemmer i stedet en navnestring, og det faktiske mål lever i dokumentkataloget under navnetræet /Names /Dests, hvilket giver et niveau af indirektion, så mange links kan dele ét mål, og målet kan flyttes uden at berø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 du bogmærker til en eksisterende PDF i Delphi?
Kald AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) efter LoadFromFile: Send ParentIndex = -1 for en indgang på øverste niveau, eller det nulbaserede indeks for en eksisterende indgang på øverste niveau for at lejre den under den. Funktionen opretter /Outlines-roden, hvis dokumentet aldrig har haft en, bygger ordbogen for indgangen, skriver et /Dest [page /XYZ x y zoom]-array, der peger på det nulbaserede DestPageIndex, og linker den nye indgang ind i søskendekæden. Den returnerer 0 ved succes og -1 ved fejl, for eksempel når DestPageIndex is uden for området
Én detalje ved linkningen har betydning for rækkefølgen: En ny indgang indsættes i begyndelsen af sin forælders børnekæde, som den nye /First. Så hvis du tilføjer "Kapitel 1" og derefter "Kapitel 2" as indgange på øverste niveau, vises Kapitel 2 over Kapitel 1 i sidebjælken, og Kapitel 1 er flyttet to indeks 1 på øverste niveau. Det praktiske mønster er enten at tilføje indgange i omvendt læserækkefølge eller at tilføje hver forælder og straks udfylde dens børn, mens den stadig befinder sig på indeks 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;
Parametrene X, Y og Zoom kortlægges direkte til /XYZ-destinationen og har alle 0 som standard. PDF placerer koordinatopprindelsen i nederste venstre hjørne, så Y = 792 peger på toppen af en US Letter-side, og i henhold til ISO 32000-1 en nulværdi i et /XYZ-felt fortæller fremviseren, at den skal beholde sin aktuelle indstilling, hvilket er præcis, hvad du ønsker for zoom i næsten alle tilfælde. Alt sker på den indlæste objektgraf i hukommelsen; intet berører disken, før SaveLoadedDocument kører, den samme rediger-og-gem-model, som loaded-document-API'en bruger til at genskrive titel, forfatter og XMP-metadata i indlæste PDF'er
Omdirigering af bogmærker: Sidehop kontra URI-handlinger
Eksisterende bogmærker kan omdirigeres uden at genoprette dem, og HotPDF giver de to tilfælde to forskellige kald, fordi de underliggende PDF-konstruktioner er reelt forskellige. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) erstatter /Dest for indgangen på øverste niveau på Index med et nyt /XYZ-array, værktøjet til den klassiske fejl efter fletning, hvor hvert bogmærke stadig peger på sidetallene i den oprindelige fil fra før fletningen. SetLoadedOutlineURI(Index, URI) gør noget strukturelt anderledes: Det vedhæfter en /A-handlingsordbog med /S /URI, hvilket gør bogmærket til et weblink i stedet for et hop i dokumentet
// 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');
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 dispositionsindgang navigerer via den ene eller den anden mekanisme, ikke begge. Reserver derfor SetLoadedOutlineURI til indgange, der ikke i forvejen bærer en sidedestination, et "Produkthjemmeside"- eller "Rapporter et problem"-blad i slutningen af træet snarere end en konverteret kapiteloverskrift. URI-handling, der skrives her, er den samme /S /URI-konstruktion, som bruges til linkannoteringer på sideindhold, hvilket er dækket mere i dybden i vejledningen til oprettelse af hyperlinks i PDF-dokumenter med HotPDF
Hvordan fungerer navngivne destinationer i en indlæst PDF?
Navngivne destinationer er den stabile ankermekanisme i ISO 32000-1 §12.3.2.3: I stedet for at indlejre en sidereference i hvert link beholder dokumentet et navn-til-destination-kort i katalogets navnetræ /Names /Dests, og links henviser til indgange efter navn. ResolveLoadedNamedDestination(Name) slår et navn op i dette træ og returnerer det nulbaserede sideindeks, det peger på, eller -1 hvis navnet mangler. Opløseren håndterer begge lagringsformer, som specifikationen tillader for den kortlagte værdi: Et råt destinationsarray som [page /XYZ x y zoom] og ordbogsformen, 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 det workflow, hvor et flettet dokument skal respektere ankre, som opstrømsværktøjer har skrevet, og publicere nye, som nedstrømslinks kan pege på
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;
De ærlige grænser: Destinationerne, HotPDF skriver til bogmærker og navngivne ankre, er /XYZ-destinationer. De andre eksplicitte typer, som specifikationen definerer, /Fit, /FitH, /FitB og lignende, udsendes ikke af disse kald, selvom /XYZ med nul zoom dækker den dominerende brugssituation "gå til dette sted, behold min zoom". Og GetLoadedBookmarkPageIndex(Title), den praktiske opslagsknap, der tager en bogmærketitel og returnerer dens destinationsside, scanner det øverste niveau af dispositionstræet, så brug den til at verificere de indgange på kapiteliveau, du lige har oprettet, frem for dybt lejrede blade
Fælden med /Count: Den tæller efterkommere, ikke indgange
Nøglen /Count på dispositionsroden betyder ikke det, som de fleste udviklere antager, og den forkerte antagelse producerer testfejl, der ligner datakorruption. ISO 32000-1 §12.3.3 definerer /Count som det samlede antal af synlige efterkommere på alle niveauer, ikke antallet af indgange på øverste niveau. På en enkelt indgang har tegnet også en betydning: En positiv værdi betyder, at indgangen er åben, og at det pågældende antal efterkommere vises i sidebjælken, mens een negativ værdi betyder, at indgangen er foldet sammen, og |N| efterkommere er skjult inde i den
Dette kom ud af HotPDF's egen regressionssuite, mangt API'erne til bogmærker i indlæste dokumenter blev bygget. Et testdokument indeholdt tre bogmærker på øverste niveau, heraf ét med et barn; efter sletning af én indgang på øverste niveau rapporterede GetLoadedOutlineCount, som læser rodens /Count, 1 i stedet for de forventede 2. Intet var korrumperet: Det oprindelige 3 havde aldrig været "tre indgange på øverste niveau" til at begynde med, og den korrekte genberegning skal gå gennem kæden på øverste niveau og akkumulere hver knude plus dens positive /Count, idet efterkommere af sammenfoldede knuder springes over. Læren for din egen kode er direkte: Foretag aldrig assert på /Count-deltaer efter strukturelle ændringer, fordi værdien bevæger sig ikke-lineært med synlige efterkommere. Verificer struktur efter indhold i stedet
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Hvor dette passer ind i værktøjssættet til indlæste dokumenter
Oprettelse af bogmærker og navngivne destinationer fuldender et billede, som API'en til indlæste dokumenter har udfyldt i et stykke tid: Den samme indlæs-rediger-gem-cyklus omskriver allerede dokumentmetadata, tilføjer linkannoteringer og sender annoteringer frem og tilbage via XFDF-import og -eksport. Mønsteret er ensartet: LoadFromFile, en sekvens af *Loaded*-kald mod objektgrafen i hukommelsen, derefter én SaveLoadedDocument, hvilket gør det ligetil at indpasse dispositionsopbygning i et eksisterende flette- eller stemplingsworkflow som endnu et trin før lagring
De API'er, der diskuteres her, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination og GetLoadedBookmarkPageIndex, leveres i standarden HotPDF Component til Delphi og C++Builder, uden eksterne afhængigheder og uden krav om separat visningskørselstid