Technisch artikel

Bladwijzers toevoegen aan bestaande PDF's in Delphi met HotPDF

HotPDF voegt bladwijzers toe aan een bestaande PDF in Delphi via AddLoadedOutline, wat outline-vermeldingen op elk nestingniveau in een geladen document aanmaakt, en het leest en schrijft benoemde bestemmingen via ResolveLoadedNamedDestination en AddLoadedNamedDestination. Laad het bestand, bouw de boomstructuur op, roep SaveLoadedDocument aan, en de zijbalk van de lezer die voorheen leeg was, bevat nu een werkende inhoudsopgave

Het scenario dat dit alles motiveert, is deprimerend herkenbaar. U voegt een tiental contracten samen tot één beoordelingspakket, of voegt drie producthandleidingen samen tot één distribueerbaar bestand, en de uitvoer is structureel in orde: elke pagina is aanwezig, elk lettertype is intact. Vervolgens opent iemand het bestand in Acrobat en is het bladwijzerpaneel leeg. Een document van 400 pagina's zonder navigatie is technisch compleet maar praktisch onbruikbaar, en tot versie 2.347.0 kon HotPDF weliswaar bladwijzers lezen, wijzigen en verwijderen op geladen documenten, maar ze nooit zelf aanmaken. Die leemte is nu opgevuld, en dit artikel bespreekt het nieuwe API-oppervlak en het ene stukje PDF-mysterie, de sleutel /Count, die problemen zal veroorzaken als u het als een eenvoudige invoerteller behandelt

Wat de outline-boomstructuur eigenlijk is

Een PDF-outline is een dubbel gekoppelde boomstructuur van woordenboeken, geen platte lijst, en die structuur verklaart elke parameter in de API. ISO 32000-1 §12.3.3 definieert het: het documentcatalogus verwijst naar een /Outlines-root, de root verwijst naar zijn eerste en laatste kinderen via /First en /Last, en broers en zussen linken aan elkaar door /Next en /Prev, terwijl elke vermelding terugverwijst naar zijn parent via /Parent. Elke vermelding bevat een /Title-tekenreeks en meestal een /Dest die aangeeft waar de lezer naartoe moet gaan bij het klikken erop

Bestemmingen zijn er in twee varianten, en ISO 32000-1 §12.3.2 houdt deze bewust gescheiden. Een expliciete bestemming is een inline array zoals [pagina /XYZ x y zoom]: een directe verwijzing naar een pagina-object plus een weergavespecificatie. Een benoemde bestemming slaat daarentegen een naamtekenreeks op, en het eigenlijke doel bevindt zich in het documentcatalogus onder de naamboom /Names /Dests, een extra niveau van indirectie waardoor meerdere koppelingen hetzelfde doel kunnen delen en het doel kan worden verplaatst zonder de koppelingen aan te passen. HotPDF schrijft expliciete /XYZ-bestemmingen wanneer het bladwijzers maakt, en biedt u afzonderlijke aanroepen voor het lezen en uitbreiden van de naamboom

Hoe voegt u bladwijzers toe aan een bestaande PDF in Delphi?

Roep AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) aan na LoadFromFile: geef ParentIndex = -1 op voor een vermelding op het hoogste niveau, of de op nul gebaseerde index van een bestaande vermelding op het hoogste niveau om eronder te nesten. De functie maakt de root /Outlines aan als het document er nog geen had, bouwt het woordenboek voor de vermelding op, schrijft een array /Dest [pagina /XYZ x y zoom] die naar de op nul gebaseerde DestPageIndex verwijst, en koppelt de nieuwe vermelding in de broers-en-zussenketen. Het retourneert 0 bij succes en -1 bij mislukking, bijvoorbeeld wanneer DestPageIndex buiten het bereik ligt

Eén verbindingsdetail is belangrijk voor de volgorde: een nieuwe vermelding wordt ingevoegd aan het begin van de kinderketen van zijn parent, als de nieuwe /First. Dus als u "Hoofdstuk 1" en vervolgens "Hoofdstuk 2" toevoegt als vermeldingen op het hoogste niveau, verschijnt Hoofdstuk 2 boven Hoofdstuk 1 in de zijbalk, en is Hoofdstuk 1 verschoven naar index 1 op het hoogste niveau. De praktische methode is om vermeldingen in omgekeerde leesvolgorde toe te voegen, of om elke parent toe te voegen en direct zijn kinderen te vullen terwijl deze zich nog op index 0 bevindt

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;

De parameters X, Y en Zoom worden rechtstreeks gekoppeld aan de /XYZ-bestemming en hebben allemaal 0 als standaardwaarde. PDF plaatst de coördinatenoorsprong in de linkerbenedenhoek, dus Y = 792 verwijst naar de bovenkant van een US Letter-pagina, en volgens ISO 32000-1 vertelt een nulwaarde in een /XYZ-positie de viewer om de huidige instelling te behouden, wat in bijna elk geval precies is wat u wilt voor de zoom. Alles gebeurt op de geladen objectgrafiek in het geheugen; er wordt niets naar de schijf geschreven totdat SaveLoadedDocument wordt uitgevoerd, hetzelfde bewerk-en-bewaar-model dat de API voor geladen documenten gebruikt voor het herschrijven van titel, auteur en XMP-metadata op geladen PDF's

Bladwijzers opnieuw richten: paginasprongen versus URI-acties

Bestaande bladwijzers kunnen opnieuw worden gekoppeld zonder ze opnieuw aan te maken, en HotPDF biedt voor de twee gevallen twee verschillende aanroepen omdat de onderliggende PDF-constructies wezenlijk verschillen. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) vervangt de /Dest van de vermelding op het hoogste niveau op Index door een nieuwe /XYZ-array. Dit is het hulpmiddel voor de klassieke fout na het samenvoegen, waarbij elke bladwijzer nog steeds naar de paginanummers van het oorspronkelijke bestand van vóór het samenvoegen verwijst. SetLoadedOutlineURI(Index, URI) doet structureel iets anders: het koppelt een actiewoordenboek /A met /S /URI, waardoor de bladwijzer verandert in een weblink in plaats van een sprong binnen het document

// 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');

Houd de twee mechanismen goed gescheiden wanneer u de boomstructuur ontwerpt. Een /Dest is pure navigatie binnen het document; een URI-actie verlaat het document volledig, en ISO 32000-1 verwacht dat een outline-vermelding via het ene of het andere mechanisme navigeert, niet via beide. Reserveer SetLoadedOutlineURI daarom voor vermeldingen die nog geen paginabestemming bevatten, zoals een "Product-homepage" of een "Probleem melden"-blad aan het einde van de boomstructuur, in plaats van een geconverteerde hoofdstuktitel. De hier geschreven URI-actie is dezelfde constructie /S /URI die wordt gebruikt voor linkannotaties op paginainhoud, wat uitgebreider wordt behandeld in de handleiding voor het maken van hyperlinks in PDF-documenten met HotPDF

Hoe werken benoemde bestemmingen op een geladen PDF?

Benoemde bestemmingen zijn het stabiele-ankermechanisme van ISO 32000-1 §12.3.2.3: in plaats van een paginaverwijzing in elke koppeling in te sluiten, bewaart het document een naam-naar-bestemming-koppeling in de naamboom /Names /Dests van de catalogus, en verwijzen koppelingen bij naam naar de vermeldingen. ResolveLoadedNamedDestination(Name) zoekt een naam op in die boomstructuur en retourneert de op nul gebaseerde pagina-index waarnaar deze verwijst, of -1 als de naam afwezig is. De resolver verwerkt beide opslagvormen die de specificatie toestaat voor de gekoppelde waarde: een kale bestemmingsarray zoals [pagina /XYZ x y zoom] en de woordenboekvorm die de array verpakt onder een sleutel /D

AddLoadedNamedDestination(Name, DestPageIndex, X, Y) werkt andersom: het registreert een nieuw anker in de boomstructuur /Names /Dests, waarbij de boomstructuur zelf wordt aangemaakt als het document er nog geen had, en retourneert True bij succes. Samen dekken de twee aanroepen de workflow waarbij een samengevoegd document de ankers moet respecteren die door eerdere tools zijn geschreven, en nieuwe ankers moet publiceren waarnaar latere koppelingen kunnen verwijzen

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 eerlijke grenzen: de bestemmingen die HotPDF schrijft, zowel voor bladwijzers als voor benoemde ankers, zijn /XYZ-bestemmingen. De andere expliciete typen die de specificatie definieert, zoals /Fit, /FitH, /FitB en dergelijke, worden door deze aanroepen niet gegenereerd, hoewel /XYZ met nul zoom in bijna alle belangrijke scenario's de lading dekt van "ga naar deze plek, behoud mijn zoom". En GetLoadedBookmarkPageIndex(Title), de handige zoekfunctie die een bladwijzertitel accepteert en de bijbehorende bestemmingspagina retourneert, scant het hoogste niveau van de outline-boomstructuur. Gebruik deze dus om de hoofdstukniveau-vermeldingen te verifiëren die u zojuist hebt gemaakt, in plaats van diep geneste bladeren

De /Count-valkuil: het telt nakomelingen, geen vermeldingen

De sleutel /Count op de outline-root betekent niet wat de meeste ontwikkelaars aannemen, en de verkeerde aanname leidt tot testfouten die lijken op gegevenscorruptie. ISO 32000-1 §12.3.3 definieert /Count als het totale aantal zichtbare nakomelingen op alle niveaus, niet het aantal vermeldingen op het hoogste niveau. Bij een individuele vermelding heeft het teken ook betekenis: een positieve waarde betekent dat de vermelding open is en dat dat aantal nakomelingen in de zijbalk wordt getoond, terwijl een negatieve waarde betekent dat de vermelding is ingeklapt en dat er |N| nakomelingen in verborgen zijn

Dit kwam naar voren uit de regressietests van HotPDF zelf toen de bladwijzer-API's voor geladen documenten werden gebouwd. Een testdocument bevatte drie bladwijzers op het hoogste niveau, waarvan één met een kind; na het verwijderen van één vermelding op het hoogste niveau rapporteerde GetLoadedOutlineCount, dat de root /Count leest, 1 in plaats van de verwachte 2. Er was niets gecorrumpeerd: de initiële 3 was in de eerste plaats nooit "drie vermeldingen op het hoogste niveau" geweest, en de correcte herberekening moet de keten op het hoogste niveau doorlopen en elk knooppunt plus de bijbehorende positieve /Count optellen, waarbij nakomelingen van ingeklapte knooppunten worden overgeslagen. De les voor uw eigen code is duidelijk: ga er na structurele wijzigingen nooit van uit dat de /Count lineair verandert met het aantal zichtbare nakomelingen. Verifieer de structuur in plaats daarvan op basis van de inhoud

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

Waar dit past in de toolset voor geladen documenten

De creatie van bladwijzers en benoemde bestemmingen completeert een beeld dat de API voor geladen documenten al een tijdje invult: dezelfde laad-bewerk-bewaar-cyclus herschrijft al documentmetadata, voegt linkannotaties toe en transporteert annotaties heen en weer via XFDF-import en -export. Het patroon is uniform: LoadFromFile, een reeks *Loaded*-aanroepen tegen de objectgrafiek in het geheugen, en vervolgens één SaveLoadedDocument. Dit maakt het eenvoudig om outline-constructie als extra stap toe te voegen aan een bestaande samenvoeg- of stempel-pipeline voor het opslaan

De hier besproken API's, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination en GetLoadedBookmarkPageIndex, worden geleverd in de standaard HotPDF Component voor Delphi en C++Builder, zonder externe afhankelijkheden en zonder dat er een afzonderlijke viewer-runtime vereist is