Articolo tecnico

Azioni GoToR, GoToE e Launch nei PDF Delphi

PDFlibPas offre agli sviluppatori Delphi e C++Builder tre tipi di azione per la navigazione che lascia indietro la pagina corrente: GoToR (Go To Remote) apre una pagina specifica in un altro file PDF, GoToE (Go To Embedded) apre un file PDF incorporato dentro il documento corrente, e Launch esegue un programma esterno o apre un file tramite la shell del sistema operativo. Tutti e tre vivono in ISO 32000-1 §12.6.4, la sezione Action Types che definisce anche la comune azione GoTo di tutti i giorni, e ognuno porta la propria trappola per gli incauti: un numero di pagina che significa qualcosa di diverso a seconda di quale chiamata lo costruisce, un target che è un nome piuttosto che un percorso file, e una coppia di parametri stringa che sembrano identici ma servono due visualizzatori diversi

Nulla di ciò è ipotetico. Un pacchetto di riferimento tecnico — un manuale principale, un PDF di specifiche che un distributore aggiorna secondo il proprio calendario, un'utilità di calibrazione installata accanto a entrambi — si appoggia esattamente a questo tipo di collegamento tra documenti: un riferimento incrociato che deve atterrare sulla pagina 5 del file delle specifiche, una scheda tecnica che vale la pena spedire dentro il manuale piuttosto che accanto ad esso, un link che passa il controllo direttamente allo strumento di calibrazione. Questo articolo è l'immagine speculare di rileggere azioni di segnalibro e annotazione da un PDF esistente: quel pezzo tratta il consumo di un'azione GoToR, Launch, o GoToE che qualche altro produttore ha già scritto in un file; questo tratta la costruzione di quegli stessi tre tipi di azione da zero, incluse le regole a livello di campo che PDFlibPas impone prima di impegnare un solo byte

Tre modi per un'azione PDF di lasciare la pagina corrente

PDFlibPas separa la navigazione locale da tutto il resto alla chiave /S dell'azione, e GoToR, GoToE, e Launch sono i tre sottotipi il cui target risiede fuori dalla pagina corrente: GoToR sotto ISO 32000-1 §12.6.4.3, GoToE sotto §12.6.4.4, e Launch sotto §12.6.4.5, tutti dentro la più ampia sezione §12.6.4 Action Types che definisce anche la comune azione GoTo. La destinazione di una semplice azione GoTo nomina un oggetto pagina che già esiste dentro il documento, quindi PDFlibPas può validarlo immediatamente; GoToR e GoToE non possono farlo allo stesso modo, poiché il file esterno potrebbe non esistere nemmeno su questa macchina e il conteggio pagine di un file incorporato non è qualcosa che il documento ospitante traccia, quindi entrambi portano un riferimento non risolto invece di un collegamento rigido — una specifica di file più una destinazione per GoToR, un nome di file incorporato più una pagina target per GoToE — mentre Launch abbandona del tutto il concetto di destinazione e si limita a nominare qualcosa che il sistema operativo deve eseguire o aprire. Quella separazione si manifesta come due famiglie di chiamate sul lato scrittura: builder di alto livello, one-shot, come AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, e AddLinkToLocalFile creano insieme un'annotazione link a hotspot di pagina e la propria azione, coprendo la maggior parte dei layout reali — una riga di testo o un'icona su cui un lettore clicca — mentre setter di livello più basso come SetActionRemoteDestinationEx, SetActionLaunchOptions, e le loro controparti AddActionNext* collegano o sostituiscono un'azione su qualcosa di cui già possiedi un handle: un segnalibro esistente, un trigger di campo modulo, o un evento di ciclo di vita a livello documento o pagina. Entrambe le famiglie finiscono per scrivere le stesse forme di dizionario; la differenza è dove ti trovi quando le chiami, e, come tratta la sezione successiva, cosa significhi un numero di pagina quando lo fai

Come costruisci un link GoToR che apre una pagina in un altro file PDF?

Un'azione GoToR ha bisogno di due cose — una specifica di file e una destinazione dentro quel file — e PDFlibPas espone due chiamate diverse per fornire la seconda parte, ciascuna con la propria convenzione di numerazione pagina. AddLinkToFile e AddLinkToFileEx, i builder di alto livello per hotspot di pagina, validano il proprio argomento Page o DestPage come maggiore di zero, la stessa numerazione a base 1 che PDFlibPas usa ovunque altrove, incluso SelectPage. SetActionRemoteDestinationEx, il setter di livello più basso usato per collegare o sostituire un'azione GoToR su qualcosa di cui già hai un handle, valida invece DestPage come maggiore o uguale a zero e lo scrive direttamente nell'array di destinazione esplicita dell'azione senza alcun aggiustamento: vuole l'indice di pagina grezzo, a base zero, del documento target, la numerazione che ISO 32000-1 specifica per una destinazione esplicita remota. Chiama il setter di basso livello con lo stesso numero che daresti al builder di alto livello e il link apre una pagina troppo presto

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;

Il resto degli argomenti di SetActionRemoteDestinationEx è altrettanto letterale. ValueMask è un insieme di bit — 1 per sinistra, 2 per alto, 4 per destra, 8 per basso, 16 per zoom — e PDFlibPas lo verifica contro DestType prima di scrivere qualsiasi cosa: una destinazione dkFitR deve fornire esattamente 15 (tutti e quattro i bordi, nessuno zoom), dkFit e dkFitB devono fornire 0, e dkFitH/dkFitV accettano solo la loro unica coordinata rilevante. I bit che lasci non impostati dentro una maschera altrimenti valida non vengono omessi dall'array; vengono scritti come un null PDF esplicito, che ISO 32000-1 tratta come "mantieni qualunque valore il visualizzatore abbia già" per quella coordinata — un modo legittimo di dire "salta a questa pagina, lascia lo zoom stare" piuttosto che una svista. Lo zoom stesso viene memorizzato come una frazione del valore che passi, quindi una chiamata che chiede 150 percento passa all'array un valore memorizzato di 1.5, e l'intervallo di input valido è 0-6400

Come colleghi un link a un PDF incorporato dentro il tuo stesso documento?

AddLinkToEmbeddedPDF costruisce l'azione GoToE, e il suo argomento target, EmbeddedFileName, è un nome piuttosto che un percorso: deve corrispondere alla stringa Title già passata a EmbedFile quando l'allegato è stato creato, perché quel titolo è la chiave letterale che PDFlibPas memorizza nell'albero dei nomi /EmbeddedFiles del documento, e GoToE si risolve cercando quel nome, non toccando di nuovo il filesystem. La funzione controlla solo che EmbeddedFileName non sia vuoto e che TargetPage sia almeno 1 — passa un nome che non è mai stato realmente incorporato e la chiamata restituisce comunque successo, l'azione viene comunque scritta, e il link semplicemente non si risolve per ogni lettore che ci clicca

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;

Qui si accumulano due pavimenti di versione, non uno solo. EmbedFile ha bisogno di PDF 1.4 per l'albero dei nomi /EmbeddedFiles, e AddLinkToEmbeddedPDF alza separatamente il pavimento a PDF 1.6 per il tipo di azione GoToE stesso, quindi il minimo effettivo per qualsiasi documento che usi questa funzionalità è 1.6, non 1.4. Nota anche che TargetPage qui è a base 1, la convenzione ordinaria di PDFlibPas — un contrasto deliberato con il DestPage a base zero trattato nella sezione precedente, e un promemoria che quale schema di numerazione pagina si applichi dipende dal tipo di azione e dalla chiamata specifica, non da una regola generale. Il dizionario target dell'azione può anche portare una voce /R di C per figlio o P per genitore, supportando una catena a due salti dentro un file incorporato o indietro verso il suo contenitore, sebbene AddLinkToEmbeddedPDF costruisca sempre e solo la direzione figlio, poiché è quella che ha senso da un documento che sta incorporando piuttosto che essendo incorporato

Azioni Launch: un FileName, due target stringa che non sono intercambiabili

SetActionLaunchOptions scrive il target file di un'azione Launch su due chiavi diverse a partire da un singolo argomento FileName, e le due chiavi contengono due tipi di stringa diversi. La chiave /F di livello superiore riceve un dizionario di specifica file, costruito tramite la stessa conversione di percorso che PDFlibPas usa per GoToR, che è la forma portabile che ISO 32000-1 §7.11.3 definisce per un dizionario di specifica file. Il sotto-dizionario /Win, quando PDFlibPas ne scrive uno, riceve la propria chiave /F impostata al valore FileName grezzo esattamente come passato, senza alcuna conversione affatto, perché /Win /F è documentato in ISO 32000-1 §12.6.4.5 come una semplice stringa di percorso Windows destinata a essere letta solo da un visualizzatore Windows. Passa un percorso portabile, già convertito, aspettandoti che entrambe le chiavi finiscano identiche e la copia /Win porterà qualunque cosa tu abbia passato alla funzione, invariata

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;

Tratta Launch come l'azione a più alto attrito delle tre, perché il suo intero scopo è eseguire un programma o aprire un file al di fuori della sandbox PDF, e ogni visualizzatore mainstream lo tratta di conseguenza. L'Enhanced Security di Adobe Acrobat blocca o richiede conferma sulle azioni Launch per default a meno che il target non risieda in una posizione esplicitamente affidabile, e la maggior parte delle distribuzioni Acrobat aziendali lascia attiva quella protezione. Un'azione Launch in un documento consegnato al pubblico non è quindi un trigger affidabile: pianifica che venga bloccata, che chieda conferma, o che venga silenziosamente ignorata da qualunque visualizzatore apra il file, e riservala per ambienti chiusi dove controlli anche le impostazioni di fiducia del visualizzatore — un chiosco interno, un rollout aziendale controllato, un documento che non lascia mai una macchina che gestisci

Il cancello PDF/A: perché le chiamate GoToR e Launch possono restituire zero

SetActionRemoteDestinationEx e SetActionLaunchOptions rifiutano entrambe apertamente quando il documento target è in qualsiasi modalità di conformità PDF/A: entrambe controllano la modalità PDF/A del documento come prima condizione ed escono con un risultato di 0 prima di toccare l'azione, nessuna eccezione sollevata. Questo è deliberato. Le restrizioni di PDF/A sulle azioni interattive escludono specificamente Launch, poiché dare a un file archivistico la capacità di eseguire un programma arbitrario è esattamente il tipo di comportamento dipendente dall'ambiente che i formati di archiviazione a lungo termine esistono per prevenire, e PDFlibPas applica lo stesso cancello conservativo al setter di go-to remoto nello stesso percorso di codice. La conseguenza pratica è facile da perdere durante lo sviluppo: la chiamata identica che funziona su un PDF ordinario compilerà, girerà, e silenziosamente non farà nulla su un documento caricato con un livello di conformità PDF/A impostato, quindi controlla il valore di ritorno invece di presupporre il successo — uno 0 qui non è un errore di input malformato, è la libreria che rifiuta una richiesta in conflitto con la dichiarazione di conformità propria del documento

Dove si inseriscono GoToR, GoToE e Launch in un flusso di lavoro PDFlibPas più ampio

I tre tipi di azione in questo articolo non raggiungono tutti gli stessi posti. L'articolo di approfondimento sui trigger di azione di ciclo di vita documento e pagina tratta SetDocumentAction e SetPageAction, che possono collegare un'azione GoToR o Launch a un trigger come WillClose tramite le costanti condivise PDF_ACTION_BUILDER_REMOTE_DESTINATION e PDF_ACTION_BUILDER_LAUNCH — lo stesso builder che copre anche un semplice trigger URI o JavaScript. GoToE non ha alcuna costante simile e nessun percorso affatto in quel builder generico; AddLinkToEmbeddedPDF è l'unico modo in cui PDFlibPas ne costruisce una, il che la rende strettamente un'azione di hotspot di pagina, mai un trigger a livello documento o pagina. Dove GoToR e Launch raggiungono invece il builder generico, il compromesso è il controllo: costruisce un GoToR che punta solo a una destinazione remota nominata e un'azione Launch con solo un nome file e parametri, mentre l'indirizzamento esplicito pagina-e-tipo-di-adattamento e le opzioni di lancio specifiche di Windows trattate in questo articolo si raggiungono solo direttamente tramite SetActionRemoteDestinationEx e SetActionLaunchOptions

Una proprietà di sicurezza vale la pena conoscerla prima di costruire uno strumento di manutenzione attorno a questi setter. SetActionRemoteDestinationEx e SetActionLaunchOptions costruiscono prima l'intera azione di sostituzione in un dizionario di scratch, ed eliminano e copiano le chiavi /F, /D o /Win, e /NewWindow sull'azione viva solo una volta che quella copia di scratch si valida — quindi una chiamata che fallisce la validazione, sia da un ValueMask fuori intervallo sia da un FileName vuoto, lascia l'azione originale, e qualsiasi catena /Next già appesa ad essa, completamente intatta invece che sovrascritta a metà. Questo conta perché sia le azioni GoToR sia Launch possono risiedere dentro una catena /Next costruita con AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, o il più generale AddActionNextEx, permettendo a un singolo trigger di generare in sequenza una voce di log JavaScript e poi un salto remoto. La costruzione di GoToR, GoToE e Launch come descritta qui fa parte di PDFlibPas, la libreria PDF nativa per Delphi e C++Builder