Teknisk artikkel

Legg til bokmerker i eksisterende PDF-er i Delphi med HotPDF

HotPDF legger til bokmerker i en eksisterende PDF i Delphi via AddLoadedOutline, som oppretter oversiktsoppføringer på ethvert nøstingsnivå i et lastet dokument, og den leser og skriver navngitte destinasjoner via ResolveLoadedNamedDestination og AddLoadedNamedDestination. Last inn filen, bygg opp treet, kall SaveLoadedDocument, og leserens sidestolpe som pleide å være tom, bærer nå en fungerende innholdsfortegnelse

Scenarioet som motiverer alt dette er deprimerende vanlig. Du slår sammen et dusin kontrakter til én gjennomgangspakke, eller setter sammen tre produkthåndbøker til en enkelt distribuerbar fil, og utdataene er strukturelt fine: hver side er på plass, hver font er intakt. Så åpner noen den i Acrobat, og bokmerkepanelet er tomt. Et dokument på 400 sider uten navigering er teknisk sett komplett, men i praksis ubrukelig, og frem til versjon 2.347.0 kunne HotPDF lese, endre og slette bokmerker i lastede dokumenter, men aldri opprette dem. Det gapet er nå lukket, og denne artikkelen går gjennom det nye API-grensesnittet og en del av PDF-arkanaen, nemlig nøkkelen /Count, som vil skape problemer for deg hvis du behandler den som en enkel oppføringsteller

Hva oversiktstreet faktisk er

En PDF-oversikt er et dobbeltkoblet tre av ordbøker, ikke en flat liste, og den strukturen forklarer hver parameter i API-et. ISO 32000-1 §12.3.3 definerer det slik: dokumentkatalogen peker to en /Outlines-rot, roten peker til sine første og siste barn via /First og /Last, og søsken kjeder seg sammen via /Next og /Prev, mens hver oppføring peker oppover igjen via /Parent. Hver oppføring bærer en /Title-streng og vanligvis en /Dest som forteller hvor et klikk på den skal ta leseren

Destinasjoner kommer i to varianter, og ISO 32000-1 §12.3.2 holder dem bevisst adskilt. En eksplisitt destinasjon er en innebygd tabell som [side /XYZ x y zoom]: en direkte referanse til et sideobjekt pluss en visningsspesifikasjon. En navngitt destinasjon lagrer i stedet en navnstreng, og det faktiske målet lever i dokumentkatalogen under navnetreet /Names /Dests, et nivå av indireksjon som lar mange lenker dele ett mål og gjør at målet kan flyttes uten å berøre lenkene. HotPDF skriver eksplisitte /XYZ-destinasjoner når den oppretter bokmerker, og gir deg egne kall for å lese og utvide navnetreet

Hvordan legger du til bokmerker i en eksisterende PDF i Delphi?

Kall AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) etter LoadFromFile: send ParentIndex = -1 for en oppføring på toppnivå, eller den nullbaserte indeksen til en eksisterende toppnivåoppføring for å nøste under den. Funksjonen oppretter /Outlines-roten hvis dokumentet aldri har hatt en, bygger oppføringsordboken, skriver en /Dest [side /XYZ x y zoom]-tabell som peker på den nullbaserte DestPageIndex, og lenker den nye oppføringen inn i søskenkjeden. Den returnerer 0 ved suksess og -1 ved feil, for eksempel når DestPageIndex er utenfor gyldig område

En lenkedetalj har betydning for rekkefølgen: en ny oppføring settes inn i begynnelsen av forelderens barnekjede, som den nye /First. Så hvis du legger til "Chapter 1" og deretter "Chapter 2" som toppnivåoppføringer, Chapter 2 vises over Chapter 1 i sidestolpen, og Chapter 1 forskyves til toppnivåindeks 1. Det praktiske mønsteret er enten å legge til oppføringer i omvendt leserekkefølge, eller å legge til hver forelder og umiddelbart fylle inn barna dens mens den fremdeles ligger på indeks 0

Parameterne X, Y og Zoom kartlegges rett over på /XYZ-destinasjonen, og alle har 0 som standardverdi. PDF plasserer koordinatopprinnelsen i nedre venstre hjørne, så Y = 792 sikter seg inn på toppen av en US Letter-side, og i henhold til ISO 32000-1 en nullverdi i et /XYZ-felt viseren at den skal beholde sin nåværende innstilling, noe som er akkurat det du ønsker for zoom i nesten alle tilfeller. Alt skjer på den lastede objektgrafen i minnet; ingenting berører disken før SaveLoadedDocument kjører, den samme endre-og-lagre-modellen som API-et for lastede dokumenter bruker for å skrive om tittel, forfatter og XMP-metadata i lastede PDF-er

Endring av bokmerkepunkter: sidehopp kontra URI-handlinger

Eksisterende bokmerker kan kobles om uten å opprette dem på nytt, og HotPDF gir de to tilfellene to forskjellige kall fordi PDF-konstruksjonene under er reelt forskjellige. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) erstatter /Dest for toppnivåoppføringen på Index med en ny /XYZ-tabell, som er verktøyet for den klassiske feilen etter sammenslåing der hvert bokmerke fremdeles peker på sidenumrene til den opprinnelige filen før sammenslåing. SetLoadedOutlineURI(Index, URI) gjør noe strukturelt annerledes: den fester en /A-handlingsordbok med /S /URI, noe som gjør bokmerket til en nettlenke i stedet for et hopp internt 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 mekanismene adskilt når du designer treet. En /Dest is ren navigering internt i dokumentet; en URI-handling forlater dokumentet helt, og ISO 32000-1 forventer at en oversiktsoppføring navigerer via enten den ene eller den andre mekanismen, ikke begge. Så reserver SetLoadedOutlineURI for oppføringer som ikke allerede bærer en sidedestinasjon, som for eksempel et "Produktets hjemmeside"- eller "Rapporter et problem"-blad i enden av treet, snarere enn en konvertert kapitteloverskrift. URI-handlingen som skrives her er den samme /S /URI-konstruksjonen som brukes for lenke-annotasjoner på sideinnhold, dekket i mer dybde i veiledningen for oppretting av hyperkoblinger i PDF-dokumenter med HotPDF

Hvordan fungerer navngitte destinasjoner i en lastet PDF?

Navngitte destinasjoner er mekanismen for stabile ankere i ISO 32000-1 §12.3.2.3: i stedet for å bygge inn en sidereferanse i hver lenke, beholder dokumentet et kart over navn-til-destinasjon i katalogens navnetre /Names /Dests, og lenker refererer til oppføringer ved navn. ResolveLoadedNamedDestination(Name) slår opp et navn i det treet og returnerer den nullbaserte sideindeksen det peker på, eller -1 hvis navnet er fraværende. Oppløseren håndterer begge lagringsformer spesifikasjonen tillater for den kartlagte verdien: en ren destinasjonstabell som [side /XYZ x y zoom] og ordboksformen som pakker inn tabellen under en /D-nøkkel

AddLoadedNamedDestination(Name, DestPageIndex, X, Y) går den andre veien: den registrerer et nytt anker i treet /Names /Dests, og oppretter selve treet når dokumentet ikke har noen, og returnerer True ved suksess. Sammen dekker paret arbeidsflyten der et sammenslått dokument må respektere ankere som oppstrømsverktøy har skrevet, og publisere nye for at nedstrømslenker kan peke på dem

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;

/Count-fellen: den teller etterkommere, ikke oppføringer

Nøkkelen /Count på oversiktsroten betyr ikke det de fleste utviklere antar, og feil antakelse produserer testfeil som ser ut som datakorrupsjon. ISO 32000-1 §12.3.3 definerer /Count som det totale antallet synlige etterkommere på alle nivåer, ikke antallet oppføringer på toppnivå. På en individuell oppføring har tegnet også en betydning: en positiv verdi betyr at oppføringen er åpen og at så mange etterkommere vises i sidestolpen, mens en negativ verdi betyr at oppføringen er sammenslått og |N| etterkommere er skjult inni den

Dette kom ut av HotPDFs egen regresjonstest-pakke da API-ene for bokmerker i lastede dokumenter ble bygget. Et testdokument hadde tre toppnivåbokmerker, ett med et barn; etter å ha slettet én toppnivåoppføring, rapporterte GetLoadedOutlineCount, som leser roten /Count, rapporterte 1 i stedet for de forventede 2. Ingenting var korrupt: den opprinnelige tretallet hadde aldri vært "tre toppnivåoppføringer" til å begynne med, og den korrekte rekalkuleringen må gå gjennom toppnivåkjeden og akkumulere hver node pluss dens positive /Count, og hoppe over etterkommere av sammenslåtte noder. Lærdommen for din egen kode er direkte: aldri test på differanser i /Count etter strukturelle endringer, fordi verdien endres ikke-lineært med antall synlige etterkommere. Verifiser heller struktur ut fra innhold

// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
  ShowMessage('Outline verified');

Hvor dette passer inn i verktøysettet for lastede dokumenter

Oppretting av bokmerker og navngitte destinasjoner fullfører et bilde som API-et for lastede dokumenter har tegnet opp en stund: den samme laste-endre-lagre-syklusen skriver allerede om dokumentmetadata, legger til lenke-annotasjoner og sirkulerer annotasjoner gjennom XFDF-import og -eksport. Mønsteret er ensartet: LoadFromFile, en sekvens med *Loaded*-kall mot objektgrafen i minnet, og deretter ett SaveLoadedDocument, noe som gjør det enkelt å legge oversiktskonstruksjon inn i en eksisterende sammenslåings- eller stemplingsrørledning som ett trinn til før lagring

API-ene som er diskutert her, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination og GetLoadedBookmarkPageIndex, leveres i standard HotPDF Component for Delphi og C++Builder, uten eksterne avhengigheter og uten at det kreves eget visningsbibliotek