Техническа статия

GoToR, GoToE и Launch действия в Delphi PDF документи

PDF Library for Delphi предоставя на разработчиците на Delphi и C++Builder три вида действия за навигация, която напуска текущата страница: GoToR (Go To Remote) отваря конкретна страница в друг PDF файл, GoToE (Go To Embedded) отваря PDF файл, вграден в текущия документ, а Launch стартира външна програма или отваря файл чрез системната обвивка. И трите са описани в ISO 32000-1 §12.6.4, раздела за типовете действия, който определя и обичайното GoTo действие, и всяко носи собствен капан за невнимателния разработчик: номер на страница с различно значение според извикването, цел, която е име, а не файлов път, и двойка string параметри, които изглеждат еднакви, но служат на два различни viewer-а

Това не е хипотетичен сценарий. Пакет с техническа документация — основно ръководство, PDF със спецификации, който дистрибуторът обновява по собствен график, и помощна програма за калибриране, инсталирана заедно с тях — разчита точно на такова свързване между документи: препратка, която трябва да отведе към страница 5 на файла със спецификациите, информационен лист, който е по-добре да бъде доставен вътре в ръководството, и връзка, която предава управлението директно на инструмента за калибриране. Тази статия е огледалната страна на статията за извличане на bookmark и annotation действия от съществуващ PDF: там се разглежда прочитането на GoToR, Launch или GoToE действие, вече записано от друг производител, а тук — изграждането на същите три вида действия от нулата, включително правилата на ниво поле, които PDF Library for Delphi налага, преди да запише и един byte

Три начина PDF действие да напусне текущата страница

PDF Library for Delphi отделя локалната навигация чрез ключа /S на действието, а GoToR, GoToE и Launch са трите подтипа, чиято цел се намира извън текущата страница: GoToR според ISO 32000-1 §12.6.4.3, GoToE според §12.6.4.4, а Launch според §12.6.4.5, всички в рамките на по-широкия раздел §12.6.4 за типовете действия, който определя и обичайното GoTo действие. Дестинацията на обикновено GoTo действие назовава page object, който вече съществува в документа, затова PDF Library for Delphi може да го валидира веднага; GoToR и GoToE не могат да направят това по същия начин, защото външният файл може изобщо да не съществува на тази машина, а броят страници на вграден файл не се следи от хост документа, затова и двата използват нерешена препратка вместо твърда връзка — file specification плюс destination за GoToR, име на embedded file плюс target page за GoToE — докато Launch напълно премахва понятието дестинация и просто назовава нещо, което операционната система трябва да стартира или отвори. Това разделение се вижда като две семейства извиквания при запис: builder-и като AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF и AddLinkToLocalFile създават page-hotspot link annotation и неговото действие заедно, което покрива повечето реални оформления — ред текст или икона, върху които читателят кликва — докато setter-и от по-ниско ниво като SetActionRemoteDestinationEx, SetActionLaunchOptions и техните AddActionNext* варианти прикачват или заменят действие върху нещо, до което вече имате handle: съществуващ bookmark, trigger на form field или събитие от жизнения цикъл на документа или страницата. И двете семейства в крайна сметка записват едни и същи dictionary форми; разликата е от коя позиция ги извиквате и какво означава номерът на страницата при това извикване

Как се изгражда GoToR връзка, която отваря страница в друг PDF файл?

GoToR действието се нуждае от две неща — file specification и destination в този файл — а PDF Library for Delphi предоставя две различни извиквания за задаване на втората част, всяко със собствено правило за номерация на страниците. AddLinkToFile и AddLinkToFileEx, високониво̀вите builder-и за page-hotspot, валидират своя аргумент Page или DestPage като по-голям от нула, същата 1-базирана номерация, която PDF Library for Delphi използва навсякъде другаде, включително при SelectPage. Setter-ът от по-ниско ниво SetActionRemoteDestinationEx, използван за прикачане или замяна на GoToR действие върху нещо, до което вече имате handle, вместо това валидира DestPage като по-голям или равен на нула и го записва директно в explicit destination array на действието без корекция: той очаква суровия zero-based page index на целевия документ, номерацията, определена от ISO 32000-1 за remote explicit destination. Ако подадете на setter-а от по-ниско ниво същото число, което бихте подали на високониво̀вия builder, връзката ще отвори една страница по-рано

PDF Library for Delphi сравнение на номерирането на страници от 1 при AddLinkToFile срещу индекса на отдалечена цел от 0 при SetActionRemoteDestinationEx
AddLinkToFile валидира аргумента си за страница като еднобазиран, докато SetActionRemoteDestinationEx записва нулебазиран отдалечен индекс непроменен. Едно и също число, подадено и на двете извиквания, отваря две различни страници
var
  Lib: TPDFlib;
  ActionID: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('manual.pdf', '') = 1 then
    begin
      Lib.SelectPage(12);
      // Page тук е 1-базиран, както SelectPage по-горе: това отваря
      // петата страница на specs.pdf.
      Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);

      // По-късен поддържащ проход пренасочва същата връзка към
      // реорганизиран файл. SetActionRemoteDestinationEx редактира
      // действието директно, а DestPage тук е нулево-базираният индекс,
      // който самият PDF използва за remote explicit destination -- "петата
      // страница" вече е 4, не 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;

Останалите аргументи на SetActionRemoteDestinationEx са също толкова буквални. ValueMask е bit set — 1 за left, 2 за top, 4 за right, 8 за bottom и 16 за zoom — а PDF Library for Delphi го проверява спрямо DestType, преди да запише каквото и да е: destination от тип dkFitR трябва да подаде точно 15 (и четирите ръба без zoom), dkFit и dkFitB трябва да подадат 0, а dkFitH/dkFitV приемат само съответната координата. Неустановените битове във валидна маска не се пропускат в array-а, а се записват като явен PDF null, който ISO 32000-1 разглежда като „запази текущата стойност на viewer-а“ за тази координата — легитимен начин да кажете „отиди на тази страница, но не променяй zoom-а“, а не пропуск. Самият zoom се съхранява като част от подадената стойност, така че заявка за 150 процента записва 1.5 в array-а, а допустимият входен диапазон е от 0 до 6400

Как се създава връзка към PDF, вграден в собствения документ?

AddLinkToEmbeddedPDF изгражда GoToE действието, а неговият аргумент за цел, EmbeddedFileName, е име, а не път: то трябва да съвпадне с string-а Title, подаден по-рано на EmbedFile при създаването на attachment-а, защото това заглавие е literal key-ят, който PDF Library for Delphi съхранява в name tree-то /EmbeddedFiles на документа, а GoToE търси по това име и не докосва повторно файловата система. Функцията проверява само дали EmbeddedFileName не е празно и дали TargetPage е поне 1 — ако подадете име, което никога не е било вградено, извикването пак връща success, действието пак се записва, а връзката просто не успява да се разреши за всеки reader, който я натисне

PDF Library for Delphi: Поток на разрешаване на GoToE от линк хотспот през дървото с имена EmbeddedFiles, ключувано по Title, до страница вътре във вградения PDF
GoToE открива целта си чрез съвпадение на Title, записан в дървото с имена на EmbeddedFiles, а не по път във файлова система. Несъвпаднало име пак записва действие, което остава мъртво за всеки четец
var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.NewDocument;
    Lib.NewPage;
    // Аргументът Title става ключът, който PDF Library for Delphi съхранява в
    // name tree-то EmbeddedFiles на документа -- този string, не
    // "datasheet.pdf", е целта, спрямо която се разрешава GoToE.
    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;

Тук се наслагват две минимални версии, а не една. EmbedFile се нуждае от PDF 1.4 за name tree-то /EmbeddedFiles, а AddLinkToEmbeddedPDF отделно повишава минималната версия до PDF 1.6 за самия тип GoToE действие, така че ефективният минимум за документ, който използва тази възможност, е 1.6, а не 1.4. Забележете и че TargetPage тук е 1-базиран, според обичайната конвенция на PDF Library for Delphi — умишлен контраст с zero-based DestPage от предишния раздел и напомняне, че приложимата схема зависи от вида на действието и конкретното извикване, а не от едно общо правило. Target dictionary-то на действието може да съдържа и entry /R със стойност C за child или P за parent, което поддържа двустъпкова верига към embedded file или обратно към неговия контейнер, макар че AddLinkToEmbeddedPDF изгражда само child посоката, защото тя има смисъл от документ, който вгражда файла, а не е вграден в него

Launch действия: един FileName, две string цели, които не са взаимозаменяеми

SetActionLaunchOptions записва файловата цел на Launch действието в два различни ключа от един аргумент FileName, а двата ключа съдържат два различни вида string. Ключът /F на най-горно ниво получава file-specification dictionary, изграден чрез същото преобразуване на пътя, което PDF Library for Delphi използва за GoToR, и това е преносимата форма, определена от ISO 32000-1 §7.11.3 за file specification dictionary. Подречникът /Win, когато PDF Library for Delphi го запише, получава свой ключ /F, зададен точно с необработената стойност FileName, без никакво преобразуване, защото /Win /F е описан в ISO 32000-1 §12.6.4.5 като обикновен Windows path string, предназначен само за прочитане от Windows viewer. Ако подадете portable, вече преобразуван path с очакването двата ключа да станат еднакви, копието в /Win ще съдържа точно това, което сте подали, без промяна

PDF Library for Delphi: Launch действие, записващо аргумента FileName в преносима файлова спецификация /F и дословно копие на подречника /Win, пазещо параметри и директория по подразбиране
SetActionLaunchOptions разклонява един FileName към два различни низови целеви обекта. Ключът от горно ниво /F получава преобразуване на пътя, докато /Win запазва суровата стойност недокосната
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 оставя това като нормално отваряне -- подайте 1, за да
      // помолите Windows viewer вместо това да отпечата. Parameters и
      // DefaultDirectory достигат единствено до /Win /P и /Win /D, никога
      // до горноравнинния /F.
      Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
        '/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
    end;
  finally
    Lib.Free;
  end;
end;

Launch е най-неудобното за използване от трите действия, защото предназначението му е да стартира програма или да отвори файл извън sandbox-а на PDF, а всеки съвременен viewer се отнася към него с повишено внимание. Enhanced Security на Adobe Acrobat блокира или изисква потвърждение за Launch действия по подразбиране, освен ако целта не се намира в изрично доверено място, а повечето корпоративни инсталации на Acrobat оставят тази защита включена. Следователно Launch действие в документ за публично разпространение не е надежден trigger: предвидете, че viewer-ът може да го блокира, да поиска потвърждение или да го игнорира, и го запазете за затворени среди, в които контролирате и настройките за доверие на viewer-а — вътрешен kiosk, контролиран корпоративен rollout или документ, който никога не напуска управлявана машина

Ограничението PDF/A: защо извикванията GoToR и Launch могат да върнат нула

SetActionRemoteDestinationEx и SetActionLaunchOptions отказват операцията изцяло, когато целевият документ е в какъвто и да е PDF/A conformance mode: и двете първо проверяват PDF/A режима на документа и излизат с резултат 0, преди да докоснат действието, без да хвърлят exception. Това е умишлено. Ограниченията на PDF/A върху интерактивните действия изключват Launch по-специално, защото възможността архивен файл да стартира произволна програма е точно зависимо от средата поведение, което форматите за дългосрочно архивиране целят да предотвратят, а PDF Library for Delphi прилага същата консервативна проверка и към setter-а за remote go-to в същия code path. Практическата последица лесно се пропуска при разработка: същото извикване, което работи върху обикновен PDF, ще се компилира и изпълни, но тихо няма да направи нищо върху документ, зареден с зададено ниво на PDF/A conformance, затова проверявайте return value, вместо да приемате успех — 0 тук не означава malformed input, а че библиотеката отказва заявка, несъвместима със собственото conformance твърдение на документа

Къде се вписват GoToR, GoToE и Launch в по-голям PDF Library for Delphi workflow

Трите вида действия от тази статия не достигат до едни и същи места. Статията за trigger-ите в жизнения цикъл на документа и страницата разглежда SetDocumentAction и SetPageAction, които могат да прикачат GoToR или Launch действие към trigger като WillClose чрез споделените константи PDF_ACTION_BUILDER_REMOTE_DESTINATION и PDF_ACTION_BUILDER_LAUNCH — същият builder, който обхваща и обикновен URI или JavaScript trigger. GoToE няма такава константа и изобщо няма път към този общ builder; AddLinkToEmbeddedPDF е единственият начин, по който PDF Library for Delphi изгражда такова действие, което го прави строго page-hotspot действие, а не trigger на ниво документ или страница. Където GoToR и Launch достигат до общия builder, компромисът е в контрола: той изгражда GoToR, сочещ само към named remote destination, и Launch действие само с име на файл и параметри, докато explicit page-and-fit-type адресирането и Windows-специфичните launch options от тази статия се достигат само директно чрез SetActionRemoteDestinationEx и SetActionLaunchOptions

Преди да изградите maintenance tool около тези setter-и, е важно да знаете още едно свойство за безопасност. SetActionRemoteDestinationEx и SetActionLaunchOptions първо изграждат цялото replacement action в scratch dictionary и едва след като това копие премине валидацията, изтриват и копират ключовете /F, /D или /Win и /NewWindow върху живото действие — затова извикване, което не премине валидацията заради ValueMask извън диапазона или празен FileName, оставя оригиналното действие и всяка вече закачена към него /Next верига напълно непокътнати, вместо да ги презапише наполовина. Това е важно, защото GoToR и Launch действията могат да участват в /Next верига, изградена с AddActionNextRemoteDestinationEx, AddActionNextLaunchEx или по-общия AddActionNextEx, което позволява един trigger да запише JavaScript log entry и след това да изпълни remote jump. Конструирането на GoToR, GoToE и Launch, описано тук, е част от PDF Library for Delphi, native PDF библиотеката за Delphi и C++Builder