PDFlibPas gibt Delphi- und C++Builder-Entwicklern drei Aktionsarten für Navigation, die die aktuelle Seite hinter sich lässt: GoToR (Go To Remote) öffnet eine bestimmte Seite in einer anderen PDF-Datei, GoToE (Go To Embedded) öffnet ein PDF, das im aktuellen Dokument eingebettet ist, und Launch führt ein externes Programm aus oder öffnet eine Datei über die Betriebssystem-Shell. Alle drei leben in ISO 32000-1 §12.6.4, dem Abschnitt Action Types, der auch die alltägliche GoTo-Aktion definiert, und jede trägt ihre eigene Falle für die Unvorsichtigen: eine Seitenzahl, die je nachdem, welcher Aufruf sie baut, etwas anderes bedeutet, ein Ziel, das ein Name statt eines Dateipfads ist, und ein Paar von String-Parametern, die identisch aussehen, aber zwei unterschiedlichen Viewern dienen
Nichts davon ist hypothetisch. Ein technisches Referenzpaket — ein Hauptmanual, eine Spezifikations-PDF, die ein Distributor nach eigenem Zeitplan aktualisiert, ein Kalibrierungswerkzeug, installiert neben beiden — stützt sich genau auf diese Art dokumentübergreifender Verdrahtung: eine Querverweisung, die auf Seite 5 der Spezifikationsdatei landen muss, ein Datenblatt, das es wert ist, innerhalb des Manuals versendet zu werden statt daneben, ein Link, der direkt an das Kalibrierungswerkzeug übergibt. Dieser Artikel ist das Spiegelbild von dem Zurücklesen von Lesezeichen- und Annotationsaktionen aus einem bestehenden PDF: Jener Beitrag behandelt das Konsumieren einer GoToR-, Launch- oder GoToE-Aktion, die ein anderer Erzeuger bereits in eine Datei geschrieben hat; dieser hier behandelt das Bauen genau dieser drei Aktionsarten von Grund auf, einschließlich der Feld-Ebenen-Regeln, die PDFlibPas durchsetzt, bevor es auch nur ein Byte committet
Drei Wege für eine PDF-Aktion, die aktuelle Seite zu verlassen
PDFlibPas trennt lokale Navigation von allem anderen am /S-Schlüssel der Aktion, und GoToR, GoToE und Launch sind die drei Subtypen, deren Ziel außerhalb der aktuellen Seite liegt: GoToR unter ISO 32000-1 §12.6.4.3, GoToE unter §12.6.4.4 und Launch unter §12.6.4.5, alle innerhalb des breiteren Abschnitts §12.6.4 Action Types, der auch die alltägliche GoTo-Aktion definiert. Das Ziel einer schlichten GoTo-Aktion benennt ein Seitenobjekt, das bereits innerhalb des Dokuments existiert, sodass PDFlibPas es sofort validieren kann; GoToR und GoToE können das nicht auf dieselbe Weise tun, da die externe Datei auf dieser Maschine vielleicht nicht einmal existiert und die Seitenanzahl einer eingebetteten Datei nichts ist, was das Host-Dokument verfolgt, sodass beide stattdessen eine ungelöste Referenz tragen statt eines harten Links — eine Dateispezifikation plus ein Ziel für GoToR, ein Name für eingebettete Datei plus eine Zielseite für GoToE —, während Launch das Ziel-Konzept vollständig fallen lässt und nur etwas benennt, das das Betriebssystem ausführen oder öffnen soll. Diese Aufteilung zeigt sich als zwei Aufruf-Familien auf der Schreibseite: hochrangige Ein-Aufruf-Builder wie AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF und AddLinkToLocalFile erzeugen einen Seiten-Hotspot-Link-Annotation und seine Aktion zusammen, was die meisten realen Layouts abdeckt — eine Textzeile oder ein Symbol, auf das ein Leser klickt —, während niederrangigere Setter wie SetActionRemoteDestinationEx, SetActionLaunchOptions und ihre AddActionNext*-Gegenstücke eine Aktion an etwas anhängen oder ersetzen, für das Sie bereits ein Handle halten: ein bestehendes Lesezeichen, einen Formularfeld-Auslöser, oder ein dokument- oder seitenweites Lebenszyklus-Ereignis. Beide Familien enden damit, dieselben Wörterbuchformen zu schreiben; der Unterschied ist, wo Sie stehen, wenn Sie sie aufrufen, und, wie der nächste Abschnitt behandelt, was eine Seitenzahl bedeutet, wenn Sie es tun
Wie baut man einen GoToR-Link, der eine Seite in einer anderen PDF-Datei öffnet?
Eine GoToR-Aktion braucht zwei Dinge — eine Dateispezifikation und ein Ziel innerhalb dieser Datei —, und PDFlibPas legt zwei unterschiedliche Aufrufe zur Lieferung des zweiten Teils frei, jeder mit seiner eigenen Seitennummerierungs-Konvention. AddLinkToFile und AddLinkToFileEx, die hochrangigen Seiten-Hotspot-Builder, validieren ihr Page- oder DestPage-Argument als größer als null, dieselbe 1-basierte Nummerierung, die PDFlibPas überall sonst verwendet, einschließlich SelectPage. SetActionRemoteDestinationEx, der niederrangigere Setter, der verwendet wird, um eine GoToR-Aktion an etwas anzuhängen oder zu ersetzen, für das Sie bereits ein Handle haben, validiert DestPage stattdessen als größer-gleich null und schreibt es ohne Anpassung direkt in das explizite Ziel-Array der Aktion: Es will den rohen, nullbasierten Seitenindex des Zieldokuments, die Nummerierung, die ISO 32000-1 für ein Remote-Explicit-Destination spezifiziert. Rufen Sie den Low-Level-Setter mit derselben Zahl auf, die Sie dem High-Level-Builder übergeben würden, öffnet der Link eine Seite zu früh
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;
Der Rest der Argumente von SetActionRemoteDestinationEx ist genauso wörtlich. ValueMask ist eine Bitmenge — 1 für links, 2 für oben, 4 für rechts, 8 für unten, 16 für Zoom —, und PDFlibPas prüft sie gegen DestType, bevor es irgendetwas schreibt: Ein dkFitR-Ziel muss genau 15 liefern (alle vier Kanten, kein Zoom), dkFit und dkFitB müssen 0 liefern, und dkFitH/dkFitV akzeptieren nur ihre eine relevante Koordinate. Bits, die Sie innerhalb einer ansonsten gültigen Maske ungesetzt lassen, werden nicht aus dem Array ausgelassen; sie werden als expliziter PDF-Null geschrieben, was ISO 32000-1 als "behalte, welchen Wert der Viewer bereits hat" für diese Koordinate behandelt — eine legitime Art zu sagen "spring zu dieser Seite, lass den Zoom in Ruhe" statt eines Versehens. Zoom selbst wird als Bruchteil des übergebenen Werts gespeichert, sodass ein Aufruf, der 150 Prozent verlangt, dem Array einen gespeicherten Wert von 1,5 übergibt, und der gültige Eingabebereich ist 0 bis 6400
Wie verlinkt man auf ein PDF, das im eigenen Dokument eingebettet ist?
AddLinkToEmbeddedPDF baut die GoToE-Aktion, und ihr Zielargument, EmbeddedFileName, ist ein Name statt eines Pfads: Er muss zum Title-String passen, der bereits an EmbedFile übergeben wurde, als der Anhang gemacht wurde, weil dieser Titel der wörtliche Schlüssel ist, den PDFlibPas im /EmbeddedFiles-Namensbaum des Dokuments speichert, und GoToE löst auf, indem es diesen Namen nachschlägt, nicht indem es das Dateisystem erneut berührt. Die Funktion prüft nur, dass EmbeddedFileName nicht leer ist und TargetPage mindestens 1 beträgt — übergeben Sie einen Namen, der nie tatsächlich eingebettet wurde, gibt der Aufruf trotzdem Erfolg zurück, die Aktion wird trotzdem geschrieben, und der Link scheitert einfach beim Auflösen, für jeden Leser, der ihn anklickt
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;
Hier stapeln sich zwei Versions-Untergrenzen, nicht eine. EmbedFile braucht PDF 1.4 für den /EmbeddedFiles-Namensbaum, und AddLinkToEmbeddedPDF hebt die Untergrenze separat auf PDF 1.6 für die GoToE-Aktionsart selbst, sodass das effektive Minimum für jedes Dokument, das dieses Feature verwendet, 1.6 ist, nicht 1.4. Beachten Sie auch, dass TargetPage hier 1-basiert ist, die gewöhnliche PDFlibPas-Konvention — ein bewusster Kontrast zum nullbasierten DestPage, das der vorherige Abschnitt gerade behandelte, und eine Erinnerung daran, dass welches Seitenzahl-Schema gilt von der Aktionsart und dem spezifischen Aufruf abhängt, nicht von einer pauschalen Regel. Das Ziel-Wörterbuch der Aktion kann auch einen /R-Eintrag von C für Child oder P für Parent tragen, was eine Zwei-Sprung-Kette in eine eingebettete Datei hinein oder zurück zu ihrem Container unterstützt, obwohl AddLinkToEmbeddedPDF nur je die Child-Richtung baut, da das diejenige ist, die für ein Dokument Sinn ergibt, das einbettet, statt eingebettet zu werden
Launch-Aktionen: ein FileName, zwei String-Ziele, die nicht austauschbar sind
SetActionLaunchOptions schreibt das Dateiziel einer Launch-Aktion in zwei unterschiedliche Schlüssel aus einem einzigen FileName-Argument, und die zwei Schlüssel halten zwei unterschiedliche Arten von String. Der oberste /F-Schlüssel bekommt ein Dateispezifikations-Wörterbuch, gebaut über dieselbe Pfad-Konvertierung, die PDFlibPas für GoToR verwendet, die portable Form, die ISO 32000-1 §7.11.3 für ein Dateispezifikations-Wörterbuch definiert. Das /Win-Unterwörterbuch, wenn PDFlibPas eines schreibt, bekommt seinen eigenen /F-Schlüssel, gesetzt auf den rohen FileName-Wert genau so, wie er übergeben wurde, ganz ohne Konvertierung, weil /Win /F in ISO 32000-1 §12.6.4.5 als schlichter Windows-Pfad-String dokumentiert ist, gedacht nur zum Lesen durch einen Windows-Viewer. Übergeben Sie einen portablen, bereits konvertierten Pfad in der Erwartung, dass beide Schlüssel identisch enden, trägt die /Win-Kopie unverändert, was Sie der Funktion übergeben haben
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;
Behandeln Sie Launch als die reibungsintensivste der drei Aktionen, weil ihr gesamter Zweck darin besteht, ein Programm auszuführen oder eine Datei außerhalb der PDF-Sandbox zu öffnen, und jeder gängige Viewer behandelt sie entsprechend. Adobe Acrobats Enhanced Security blockiert oder fragt bei Launch-Aktionen standardmäßig nach, sofern das Ziel nicht an einem ausdrücklich vertrauenswürdigen Ort liegt, und die meisten Unternehmens-Acrobat-Bereitstellungen lassen diesen Schutz eingeschaltet. Eine Launch-Aktion in einem öffentlich verteilten Dokument ist daher kein zuverlässiger Auslöser: Planen Sie damit, dass sie von welchem Viewer auch immer die Datei öffnet, blockiert, nachgefragt oder still ignoriert wird, und heben Sie sie sich für geschlossene Umgebungen auf, in denen Sie auch die Vertrauenseinstellungen des Viewers kontrollieren — ein interner Kiosk, ein kontrollierter Unternehmens-Rollout, ein Dokument, das nie eine von Ihnen verwaltete Maschine verlässt
Das PDF/A-Tor: warum GoToR- und Launch-Aufrufe null zurückgeben können
SetActionRemoteDestinationEx und SetActionLaunchOptions weigern sich beide rundweg, wenn sich das Zieldokument in irgendeinem PDF/A-Konformitätsmodus befindet: Beide prüfen den PDF/A-Modus des Dokuments als allererste Bedingung und verlassen mit einem Ergebnis von 0, bevor sie die Aktion anfassen, ohne ausgelöste Exception. Das ist absichtlich. PDF/As Einschränkungen bei interaktiven Aktionen schließen Launch speziell aus, denn einer Archivdatei die Fähigkeit zu geben, ein beliebiges Programm auszuführen, ist genau die Art von umgebungsabhängigem Verhalten, das Langzeit-Archivierungsformate verhindern sollen, und PDFlibPas wendet dasselbe konservative Tor auf den Remote-Go-To-Setter im selben Codepfad an. Die praktische Konsequenz ist während der Entwicklung leicht zu übersehen: Der identische Aufruf, der auf einem gewöhnlichen PDF funktioniert, kompiliert, läuft und tut still nichts auf einem Dokument, das mit gesetzter PDF/A-Konformitätsstufe geladen wurde, also prüfen Sie den Rückgabewert, statt Erfolg anzunehmen — eine 0 hier ist kein Fehler wegen fehlerhafter Eingabe, es ist die Bibliothek, die eine Anfrage ablehnt, die dem eigenen Konformitätsanspruch des Dokuments widerspricht
Wo GoToR, GoToE und Launch in einen größeren PDFlibPas-Workflow passen
Die drei Aktionsarten in diesem Artikel erreichen nicht alle dieselben Orte. Der begleitende Artikel zu Dokument- und Seiten-Lebenszyklus-Aktionsauslösern behandelt SetDocumentAction und SetPageAction, die eine GoToR- oder eine Launch-Aktion an einen Auslöser wie WillClose anhängen können, über die gemeinsamen Konstanten PDF_ACTION_BUILDER_REMOTE_DESTINATION und PDF_ACTION_BUILDER_LAUNCH — derselbe Builder, der auch einen schlichten URI- oder JavaScript-Auslöser abdeckt. GoToE hat keine solche Konstante und überhaupt keinen Pfad in diesen generischen Builder; AddLinkToEmbeddedPDF ist der einzige Weg, wie PDFlibPas eines konstruiert, was es strikt zu einer Seiten-Hotspot-Aktion macht, nie zu einem dokument- oder seitenweiten Auslöser. Wo GoToR und Launch den generischen Builder tatsächlich erreichen, liegt der Kompromiss bei der Kontrolle: Er baut ein GoToR, das nur auf ein benanntes Remote-Ziel zeigt, und eine Launch-Aktion mit nur Dateiname und Parametern, während die explizite Seiten-und-Fit-Typ-Adressierung und die windows-spezifischen Launch-Optionen, die dieser Artikel behandelt, nur direkt über SetActionRemoteDestinationEx und SetActionLaunchOptions erreicht werden
Eine Sicherheitseigenschaft lohnt es sich zu kennen, bevor man ein Wartungswerkzeug um diese Setter herum baut. SetActionRemoteDestinationEx und SetActionLaunchOptions bauen die gesamte Ersatzaktion zuerst in einem Scratch-Wörterbuch, und löschen und kopieren erst die Schlüssel /F, /D oder /Win, und /NewWindow auf die lebende Aktion, sobald diese Scratch-Kopie validiert — sodass ein Aufruf, der die Validierung nicht besteht, sei es wegen einer außerhalb des Bereichs liegenden ValueMask oder eines leeren FileName, die ursprüngliche Aktion und jede bereits daran hängende /Next-Kette vollständig unangetastet lässt, statt halb überschrieben. Das zählt, weil GoToR- und Launch-Aktionen beide innerhalb einer mit AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, oder dem allgemeineren AddActionNextEx gebauten /Next-Kette sitzen können, was es einem einzigen Auslöser erlaubt, in Sequenz einen JavaScript-Log-Eintrag und dann einen Remote-Sprung auszulösen. GoToR-, GoToE- und Launch-Konstruktion wie hier beschrieben ist Teil von PDFlibPas, der nativen PDF-Bibliothek für Delphi und C++Builder