Technisch artikel

GoToR-, GoToE-, en Launch-acties in Delphi-PDF's

PDFlibPas geeft Delphi- en C++Builder-ontwikkelaars drie actiesoorten voor navigatie die de huidige pagina achterlaat: GoToR (Go To Remote) opent een specifieke pagina in een ander PDF-bestand, GoToE (Go To Embedded) opent een PDF-bestand ingesloten in het huidige document, en Launch draait een extern programma of opent een bestand via de shell van het besturingssysteem. Alle drie leven ze in ISO 32000-1 §12.6.4, de sectie Action Types die ook de alledaagse GoTo-actie definieert, en elk draagt zijn eigen valstrik voor de onoplettende: een paginanummer dat iets anders betekent afhankelijk van welke aanroep het bouwt, een doel dat een naam is in plaats van een bestandspad, en een paar stringparameters die er identiek uitzien maar twee verschillende viewers bedienen

Niets hiervan is hypothetisch. Een technisch referentiepakket — een hoofdhandleiding, een specificaties-PDF die een distributeur op zijn eigen schema bijwerkt, een kalibratietool geïnstalleerd naast beide — leunt precies op dit soort bedrading tussen documenten: een kruisverwijzing die op pagina 5 van het specificatiebestand moet landen, een gegevensblad dat het waard is om binnen de handleiding te verschepen in plaats van ernaast, een link die rechtstreeks doorschakelt naar de kalibratietool. Dit artikel is het spiegelbeeld van het terug uitlezen van bladwijzer- en annotatieacties uit een bestaande PDF: dat stuk behandelt het consumeren van een GoToR-, Launch-, of GoToE-actie die een andere producent al in een bestand heeft geschreven; dit stuk behandelt het vanaf nul bouwen van diezelfde drie actiesoorten, inclusief de regels op veldniveau die PDFlibPas afdwingt voordat het ook maar één byte commit

Drie manieren waarop een PDF-actie de huidige pagina kan verlaten

PDFlibPas scheidt lokale navigatie van al het andere bij de /S-sleutel van de actie, en GoToR, GoToE, en Launch zijn de drie subtypen wier doel buiten de huidige pagina ligt: GoToR onder ISO 32000-1 §12.6.4.3, GoToE onder §12.6.4.4, en Launch onder §12.6.4.5, allemaal binnen de bredere sectie §12.6.4 Action Types die ook de alledaagse GoTo-actie definieert. De bestemming van een gewone GoTo-actie noemt een pagina-object dat al in het document bestaat, dus PDFlibPas kan het onmiddellijk valideren; GoToR en GoToE kunnen dat niet op dezelfde manier, aangezien het externe bestand op deze machine misschien niet eens bestaat en het aantal pagina's van een ingesloten bestand niet iets is dat het hostdocument bijhoudt, dus beide dragen een onopgeloste verwijzing in plaats van een harde koppeling — een bestandsspecificatie plus een bestemming voor GoToR, een ingesloten-bestandsnaam plus een doelpagina voor GoToE — terwijl Launch het bestemmingsconcept volledig laat vallen en gewoon iets noemt voor het besturingssysteem om te draaien of te openen. Die splitsing komt naar voren als twee aanroepfamilies aan de schrijfkant: hoogniveau, eenmalige bouwers zoals AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, en AddLinkToLocalFile maken samen een pagina-hotspot-linkannotatie en zijn actie aan, wat de meeste echte lay-outs dekt — een regel tekst of een icoon waar een lezer op klikt — terwijl lagerniveau-setters zoals SetActionRemoteDestinationEx, SetActionLaunchOptions, en hun AddActionNext*-tegenhangers een actie koppelen aan of vervangen op iets waar u al een handle voor heeft: een bestaande bladwijzer, een formulierveld-trigger, of een document- of paginaniveau-levenscyclusgebeurtenis. Beide families eindigen met het schrijven van dezelfde dictionaryvormen; het verschil is waar u staat wanneer u ze aanroept, en, zoals de volgende sectie behandelt, wat een paginanummer betekent wanneer u dat doet

Hoe bouwt u een GoToR-link die een pagina in een ander PDF-bestand opent?

Een GoToR-actie heeft twee dingen nodig — een bestandsspecificatie en een bestemming binnen dat bestand — en PDFlibPas stelt twee verschillende aanroepen bloot voor het leveren van het tweede deel, elk met zijn eigen paginanummeringsconventie. AddLinkToFile en AddLinkToFileEx, de hoogniveau-pagina-hotspot-bouwers, valideren hun Page- of DestPage-argument als groter dan nul, dezelfde 1-gebaseerde nummering die PDFlibPas overal elders gebruikt, inclusief SelectPage. SetActionRemoteDestinationEx, de lagerniveau-setter gebruikt om een GoToR-actie te koppelen aan of te vervangen op iets waar u al een handle voor heeft, valideert in plaats daarvan DestPage als groter dan of gelijk aan nul en schrijft dit rechtstreeks in de expliciete bestemmingsarray van de actie zonder aanpassing: het wil de ruwe, nulgebaseerde pagina-index van het doeldocument, de nummering die ISO 32000-1 specificeert voor een expliciete externe bestemming. Roep de lagerniveau-setter aan met hetzelfde getal dat u de hoogniveau-bouwer zou geven, en de link opent één pagina te vroeg

var
  Lib: TPDFlib;
  ActionID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('manual.pdf', '') = 1 then
    begin
      Lib.SelectPage(12);
      // Page is 1-based here, same as SelectPage above: this opens
      // the fifth page of specs.pdf.
      Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);

      // A later maintenance pass repoints the same link at a
      // reorganized file. SetActionRemoteDestinationEx edits the
      // action directly, and DestPage here is the zero-based index
      // PDF itself uses for a remote explicit destination -- "the
      // fifth page" is now 4, not 5.
      ActionID := Lib.GetAnnotActionID(1);
      Lib.SetActionRemoteDestinationEx(ActionID, 'specs-2026.pdf',
        4, Ord(dkFit), 0, 0, 0, 0, 0, 0, -1);
    end;
  finally
    Lib.Free;
  end;
end;

De rest van de argumenten van SetActionRemoteDestinationEx is net zo letterlijk. ValueMask is een bitverzameling — 1 voor links, 2 voor boven, 4 voor rechts, 8 voor onder, 16 voor zoom — en PDFlibPas controleert deze tegen DestType voordat het iets schrijft: een dkFitR-bestemming moet precies 15 leveren (alle vier randen, geen zoom), dkFit en dkFitB moeten 0 leveren, en dkFitH/dkFitV accepteren alleen hun ene relevante coördinaat. Bits die u ongezet laat binnen een verder geldig masker worden niet weggelaten uit de array; ze worden geschreven als een expliciete PDF-null, wat ISO 32000-1 behandelt als "behoud welke waarde de viewer ook al heeft" voor die coördinaat — een legitieme manier om te zeggen "spring naar deze pagina, laat de zoom met rust" in plaats van een omissie. Zoom zelf wordt opgeslagen als een fractie van de waarde die u doorgeeft, dus een aanroep die om 150 procent vraagt geeft de array een opgeslagen waarde van 1,5, en het geldige invoerbereik is 0 tot 6400

Hoe linkt u naar een PDF die is ingesloten in uw eigen document?

AddLinkToEmbeddedPDF bouwt de GoToE-actie, en het doelargument ervan, EmbeddedFileName, is een naam in plaats van een pad: het moet overeenkomen met de Title-string die al aan EmbedFile is doorgegeven toen de bijlage werd gemaakt, omdat die titel de letterlijke sleutel is die PDFlibPas opslaat in de naamboom /EmbeddedFiles van het document, en GoToE lost op door die naam op te zoeken, niet door het bestandssysteem opnieuw aan te raken. De functie controleert alleen dat EmbeddedFileName niet leeg is en TargetPage minstens 1 is — geef een naam door die nooit daadwerkelijk is ingesloten en de aanroep geeft nog steeds succes terug, de actie wordt nog steeds geschreven, en de link lukt gewoon niet op te lossen voor elke lezer die erop klikt

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.NewPage;
    // The Title argument becomes the key PDFlibPas stores in the
    // document's EmbeddedFiles name tree -- that string, not
    // "datasheet.pdf", is the target GoToE resolves against.
    if Lib.EmbedFile('Datasheet', 'datasheet.pdf', 'application/pdf') = 1 then
      Lib.AddLinkToEmbeddedPDF(72, 700, 200, 16, 'Datasheet', 3, 0, 0);
    Lib.SaveToFile('manual.pdf');
  finally
    Lib.Free;
  end;
end;

Twee versieondergrenzen stapelen zich hier op, niet één. EmbedFile heeft PDF 1.4 nodig voor de naamboom /EmbeddedFiles, en AddLinkToEmbeddedPDF verhoogt de ondergrens apart naar PDF 1.6 voor het GoToE-actietype zelf, dus het effectieve minimum voor elk document dat deze functie gebruikt, is 1.6, niet 1.4. Merk ook op dat TargetPage hier 1-gebaseerd is, de gewone PDFlibPas-conventie — een doelbewust contrast met de nulgebaseerde DestPage die de vorige sectie net behandelde, en een herinnering dat welk paginanummeringsschema van toepassing is, afhangt van de actiesoort en de specifieke aanroep, niet van één blanket-regel. De doeldictionary van de actie kan ook een /R-item dragen van C voor kind of P voor ouder, ter ondersteuning van een keten van twee sprongen naar een ingesloten bestand of terug naar zijn container, hoewel AddLinkToEmbeddedPDF alleen ooit de kindrichting bouwt, aangezien dat degene is die zin heeft voor een document dat het insluiten doet in plaats van ingesloten te worden

Launch-acties: één FileName, twee stringdoelen die niet uitwisselbaar zijn

SetActionLaunchOptions schrijft het bestandsdoel van een Launch-actie naar twee verschillende sleutels vanuit één enkel argument FileName, en de twee sleutels bevatten twee verschillende soorten string. De sleutel /F op het bovenste niveau krijgt een bestandsspecificatiedictionary, gebouwd via dezelfde padconversie die PDFlibPas gebruikt voor GoToR, wat de draagbare vorm is die ISO 32000-1 §7.11.3 definieert voor een bestandsspecificatiedictionary. De subdictionary /Win, wanneer PDFlibPas er eentje schrijft, krijgt zijn eigen sleutel /F ingesteld op de ruwe waarde FileName precies zoals doorgegeven, zonder enige conversie, omdat /Win /F in ISO 32000-1 §12.6.4.5 wordt gedocumenteerd als een gewone Windows-padstring bedoeld alleen om door een Windows-viewer te worden gelezen. Geef een draagbaar, al geconverteerd pad door in de verwachting dat beide sleutels identiek zullen eindigen, en de /Win-kopie zal dragen wat u ook aan de functie heeft gegeven, ongewijzigd

var
  Lib: TPDFlib;
  ActionID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('manual.pdf', '') = 1 then
    begin
      Lib.SelectPage(1);
      Lib.AddLinkToLocalFile(72, 660, 220, 16, 'calibrate.exe', 0);
      ActionID := Lib.GetAnnotActionID(1);
      // Operation 0 leaves this as a normal open -- pass 1 to ask a
      // Windows viewer to print instead. Parameters and
      // DefaultDirectory only ever reach /Win /P and /Win /D, never
      // the top-level /F.
      Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
        '/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
    end;
  finally
    Lib.Free;
  end;
end;

Behandel Launch als de meest wrijvingsvolle actie van de drie, omdat het hele doel ervan is een programma te draaien of een bestand te openen buiten de PDF-sandbox, en elke gangbare viewer behandelt het dienovereenkomstig. Adobe Acrobat's Enhanced Security blokkeert of vraagt bij Launch-acties standaard, tenzij het doel zich in een expliciet vertrouwde locatie bevindt, en de meeste enterprise-Acrobat-implementaties laten die bescherming ingeschakeld. Een Launch-actie in een document dat aan het publiek wordt overhandigd, is daarom geen betrouwbare trigger: plan dat het wordt geblokkeerd, om bevestiging gevraagd, of stilzwijgend genegeerd door welke viewer het bestand ook opent, en bewaar het voor gesloten omgevingen waar u ook de vertrouwensinstellingen van de viewer beheert — een interne kiosk, een gecontroleerde bedrijfsuitrol, een document dat nooit een machine verlaat die u beheert

De PDF/A-poort: waarom GoToR- en Launch-aanroepen nul kunnen teruggeven

SetActionRemoteDestinationEx en SetActionLaunchOptions weigeren beide regelrecht wanneer het doeldocument zich in enige PDF/A-conformiteitsmodus bevindt: beide controleren de PDF/A-modus van het document als hun allereerste voorwaarde en stoppen met een resultaat van 0 voordat ze de actie aanraken, geen uitzondering opgeworpen. Dit is doelbewust. De beperkingen van PDF/A op interactieve acties sluiten Launch specifiek uit, aangezien een archiefbestand de mogelijkheid geven om een willekeurig programma te draaien precies het soort omgevingsafhankelijk gedrag is dat langetermijnarchiveringsformaten bestaan om te voorkomen, en PDFlibPas past dezelfde conservatieve poort toe op de externe-go-to-setter in hetzelfde codepad. Het praktische gevolg is gemakkelijk over het hoofd te zien tijdens ontwikkeling: dezelfde aanroep die werkt op een gewone PDF, compileert, draait, en doet stilzwijgend niets op een document geladen met een ingesteld PDF/A-conformiteitsniveau, dus controleer de retourwaarde in plaats van succes aan te nemen — een 0 hier is geen misvormde-invoerfout, het is de bibliotheek die een verzoek weigert dat conflicteert met de eigen conformiteitsclaim van het document

Waar passen GoToR, GoToE, en Launch in een grotere PDFlibPas-workflow?

De drie actiesoorten in dit artikel bereiken niet allemaal dezelfde plekken. Het begeleidende artikel over document- en paginaniveau-levenscyclusactietriggers behandelt SetDocumentAction en SetPageAction, die een GoToR- of een Launch-actie kunnen koppelen aan een trigger zoals WillClose via de gedeelde constanten PDF_ACTION_BUILDER_REMOTE_DESTINATION en PDF_ACTION_BUILDER_LAUNCH — dezelfde bouwer die ook een gewone URI- of JavaScript-trigger dekt. GoToE heeft geen dergelijke constante en helemaal geen pad naar die generieke bouwer; AddLinkToEmbeddedPDF is de enige manier waarop PDFlibPas er eentje construeert, wat het strikt een pagina-hotspot-actie maakt, nooit een document- of paginaniveau-trigger. Waar GoToR en Launch de generieke bouwer wel bereiken, is de afweging controle: het bouwt een GoToR die alleen naar een benoemde externe bestemming wijst en een Launch-actie met slechts een bestandsnaam en parameters, terwijl de expliciete pagina-en-fit-type-adressering en de Windows-specifieke launch-opties die in dit artikel worden behandeld, alleen bereikt worden via rechtstreeks SetActionRemoteDestinationEx en SetActionLaunchOptions

Eén veiligheidseigenschap is het waard om te kennen voordat u een onderhoudstool rond deze setters bouwt. SetActionRemoteDestinationEx en SetActionLaunchOptions bouwen de hele vervangende actie eerst in een scratch-dictionary, en verwijderen en kopiëren pas de sleutels /F, /D of /Win, en /NewWindow naar de levende actie zodra die scratch-kopie valideert — dus een aanroep die de validatie niet doorstaat, of dat nu komt door een ValueMask buiten bereik of een lege FileName, laat de oorspronkelijke actie, en elke /Next-keten die er al aan hangt, volledig ongemoeid in plaats van half overschreven. Dat doet ertoe omdat GoToR- en Launch-acties beide binnen een /Next-keten kunnen zitten die is opgebouwd met AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, of de meer algemene AddActionNextEx, waardoor één trigger een JavaScript-logvermelding en dan een externe sprong achtereenvolgens kan afvuren. Constructie van GoToR, GoToE, en Launch zoals hier beschreven maakt deel uit van PDFlibPas, de native PDF-bibliotheek voor Delphi en C++Builder