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

Действия GoToR, GoToE и Launch в PDF на Delphi

PDFlibPas даёт разработчикам на Delphi и C++Builder три вида действий для навигации, покидающей текущую страницу: GoToR (Go To Remote) открывает конкретную страницу в другом файле PDF, GoToE (Go To Embedded) открывает файл PDF, встроенный внутрь текущего документа, а Launch запускает внешнюю программу или открывает файл через оболочку операционной системы. Все три живут в ISO 32000-1 §12.6.4, разделе Action Types, что также определяет повседневное действие GoTo, и у каждого своя ловушка для неосторожных: номер страницы, значащий разное в зависимости от того, какой вызов его строит, цель, что является именем, а не путём к файлу, и пара строковых параметров, что выглядят идентично, но обслуживают два разных просмотрщика

Ничто из этого не гипотетично. Пакет технической документации — основное руководство, PDF спецификаций, что дистрибьютор обновляет по собственному расписанию, утилита калибровки, установленная рядом с обоими, — опирается именно на такую межфайловую проводку: перекрёстную ссылку, что должна попасть на страницу 5 файла спецификаций, лист данных, который стоит поставлять внутри руководства, а не рядом с ним, ссылку, что передаёт управление прямо инструменту калибровки. Эта статья — зеркальное отражение статьи о чтении действий закладок и аннотаций обратно из существующего PDF: та статья описывает потребление действия GoToR, Launch или GoToE, что уже записал в файл какой-то другой производитель; эта описывает построение тех же трёх видов действий с нуля, включая правила уровня полей, что PDFlibPas применяет прежде, чем зафиксировать хотя бы один байт

Три способа для действия PDF покинуть текущую страницу

PDFlibPas отделяет локальную навигацию от всего остального по ключу /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 Action Types, что также определяет повседневное действие GoTo. Назначение обычного действия GoTo называет объект страницы, что уже существует внутри документа, так что PDFlibPas может проверить его немедленно; GoToR и GoToE не могут сделать это так же, поскольку внешний файл может даже не существовать на этой машине, а число страниц встроенного файла — не то, что отслеживает принимающий документ, так что оба несут неразрешённую ссылку вместо жёсткой связи — спецификацию файла плюс назначение для GoToR, имя встроенного файла плюс целевую страницу для GoToE, — тогда как Launch вовсе отбрасывает понятие назначения и просто называет то, что операционной системе нужно запустить или открыть. Это разделение проявляется как два семейства вызовов на стороне записи: высокоуровневые, разовые построители, такие как AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF и AddLinkToLocalFile, создают аннотацию-ссылку горячей зоны страницы и её действие вместе, покрывая большинство реальных макетов — строку текста или значок, по которому кликает читатель, — тогда как низкоуровневые устанавливающие функции, такие как SetActionRemoteDestinationEx, SetActionLaunchOptions и их аналоги AddActionNext*, прикрепляют или заменяют действие на чём-то, дескриптор чего у вас уже есть: существующей закладке, триггере поля формы или событии жизненного цикла уровня документа или страницы. Оба семейства в итоге записывают одни и те же формы словарей; разница в том, где вы стоите, когда их вызываете, и, как описывает следующий раздел, что означает номер страницы, когда вы это делаете

Как построить ссылку GoToR, что открывает страницу в другом файле PDF?

Действию GoToR нужны две вещи — спецификация файла и назначение внутри этого файла, — и PDFlibPas предоставляет два разных вызова для передачи второй части, каждый со своим соглашением о нумерации страниц. AddLinkToFile и AddLinkToFileEx, высокоуровневые построители горячей зоны страницы, проверяют свой аргумент Page или DestPage как больший нуля — та же нумерация с отсчётом от единицы, что PDFlibPas использует всюду ещё, включая SelectPage. SetActionRemoteDestinationEx, низкоуровневая устанавливающая функция, используемая для прикрепления или замены действия GoToR на чём-то, дескриптор чего у вас уже есть, вместо этого проверяет DestPage как больший или равный нулю и записывает его прямо в массив явного назначения действия без коррекции: ей нужен сырой индекс страницы целевого документа с отсчётом от нуля — нумерация, которую ISO 32000-1 определяет для внешнего явного назначения. Вызовите низкоуровневую устанавливающую функцию с тем же числом, что вы бы передали высокоуровневому построителю, и ссылка откроется на страницу раньше

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;

Остальные аргументы SetActionRemoteDestinationEx так же буквальны. ValueMask — битовый набор: 1 для левого края, 2 для верхнего, 4 для правого, 8 для нижнего, 16 для масштаба, — и PDFlibPas сверяет его с DestType прежде, чем что-либо записать: назначение dkFitR должно предоставить ровно 15 (все четыре края, без масштаба), dkFit и dkFitB должны предоставить 0, а dkFitH/dkFitV принимают только одну свою актуальную координату. Биты, что вы оставляете неустановленными внутри в остальном корректной маски, не опускаются из массива; они записываются как явный null PDF, что ISO 32000-1 трактует как «сохранить то значение, что у просмотрщика уже есть» для этой координаты, — законный способ сказать «перейти на эту страницу, оставить масштаб как есть», а не недосмотр. Сам масштаб хранится как доля от переданного вами значения, так что вызов, запрашивающий 150 процентов, передаёт массиву хранимое значение 1.5, а допустимый диапазон входа — от 0 до 6400

Как связать со страницей PDF, что встроена внутрь вашего собственного документа?

AddLinkToEmbeddedPDF строит действие GoToE, и его целевой аргумент, EmbeddedFileName, — имя, а не путь: оно должно совпадать со строкой Title, уже переданной в EmbedFile при вложении, потому что этот заголовок — буквальный ключ, что PDFlibPas хранит в дереве имён /EmbeddedFiles документа, а GoToE разрешается поиском этого имени, а не повторным обращением к файловой системе. Функция лишь проверяет, что EmbeddedFileName не пусто, а TargetPage не меньше 1, — передайте имя, что никогда реально не было встроено, и вызов всё равно вернёт успех, действие всё равно будет записано, а ссылка попросту не разрешится для каждого читателя, что по ней кликнет

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;

Здесь складываются два минимума версии, не один. EmbedFile нужен PDF 1.4 для дерева имён /EmbeddedFiles, а AddLinkToEmbeddedPDF отдельно поднимает минимум до PDF 1.6 для самого типа действия GoToE, так что эффективный минимум для любого документа, использующего эту функцию, — 1.6, а не 1.4. Заметьте также, что TargetPage здесь с отсчётом от единицы, обычное соглашение PDFlibPas, — намеренный контраст с нумерацией DestPage от нуля, только что описанной в предыдущем разделе, и напоминание, что какая схема нумерации страниц применяется, зависит от вида действия и конкретного вызова, а не от одного общего правила. Целевой словарь действия также может нести запись /R со значением C для дочернего или P для родительского, поддерживая двухшаговую цепочку во встроенный файл или обратно наружу к его контейнеру, хотя AddLinkToEmbeddedPDF строит только дочернее направление, поскольку это то, что имеет смысл со стороны документа, выполняющего вложение, а не со стороны того, что вкладывают

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

SetActionLaunchOptions записывает файловую цель действия Launch в два разных ключа из одного аргумента FileName, и эти два ключа держат два разных вида строки. Ключ верхнего уровня /F получает словарь спецификации файла, построенный тем же путём преобразования, что PDFlibPas использует для GoToR, — портируемая форма, которую ISO 32000-1 §7.11.3 определяет для словаря спецификации файла. Подсловарь /Win, когда PDFlibPas его пишет, получает собственный ключ /F, установленный в сырое значение FileName точно так, как оно было передано, вообще без преобразования, потому что /Win /F документирован в ISO 32000-1 §12.6.4.5 как обычная строка пути Windows, предназначенная только для чтения просмотрщиком Windows. Передайте портируемый, уже преобразованный путь в ожидании, что оба ключа окажутся идентичными, и копия /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 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;

Относитесь к Launch как к самому проблемному из трёх действий, потому что его единственная цель — запуск программы или открытие файла вне песочницы PDF, и любой массовый просмотрщик обращается с ним соответственно. Enhanced Security в Adobe Acrobat по умолчанию блокирует или запрашивает подтверждение на действиях Launch, если только цель не находится в явно доверенном расположении, и большинство корпоративных развёртываний Acrobat оставляют эту защиту включённой. Действие Launch в документе, переданном публике, поэтому не надёжный триггер: планируйте, что оно будет заблокировано, запрошено или молча проигнорировано тем просмотрщиком, что откроет файл, и приберегите его для закрытых сред, где вы также контролируете настройки доверия просмотрщика, — внутреннего киоска, контролируемого корпоративного развёртывания, документа, что никогда не покидает управляемую вами машину

Барьер PDF/A: почему вызовы GoToR и Launch могут вернуть ноль

SetActionRemoteDestinationEx и SetActionLaunchOptions оба безоговорочно отказывают, когда целевой документ находится в каком-либо режиме соответствия PDF/A: оба проверяют режим PDF/A документа как самое первое условие и выходят с результатом 0, даже не коснувшись действия, без выброса исключения. Это намеренно. Ограничения PDF/A на интерактивные действия исключают Launch особо, поскольку предоставление архивному файлу возможности запускать произвольную программу — именно тот вид зависящего от окружения поведения, ради предотвращения которого существуют форматы долгосрочного архивирования, а PDFlibPas применяет тот же консервативный барьер к устанавливающей функции внешнего перехода в том же пути кода. Практическое следствие легко упустить во время разработки: идентичный вызов, что работает на обычном PDF, скомпилируется, выполнится и молча ничего не сделает на документе, загруженном с установленным уровнем соответствия PDF/A, так что проверяйте возвращаемое значение, а не предполагайте успех — 0 здесь не ошибка некорректного ввода, это библиотека, отклоняющая запрос, конфликтующий с собственной декларацией соответствия документа

Куда вписываются GoToR, GoToE и Launch в более крупный рабочий процесс PDFlibPas

Три вида действий в этой статье не все достигают одних и тех же мест. Сопутствующая статья о триггерах действий жизненного цикла документа и страницы описывает SetDocumentAction и SetPageAction, что могут прикрепить действие GoToR или Launch к триггеру вроде WillClose через общие константы PDF_ACTION_BUILDER_REMOTE_DESTINATION и PDF_ACTION_BUILDER_LAUNCH, — тот же построитель, что также покрывает простой триггер URI или JavaScript. У GoToE нет такой константы и вообще нет пути в этот обобщённый построитель; AddLinkToEmbeddedPDF — единственный способ, которым PDFlibPas конструирует его, что делает его строго действием горячей зоны страницы, никогда не триггером уровня документа или страницы. Там, где GoToR и Launch действительно достигают обобщённого построителя, компромисс — в контроле: он строит GoToR, указывающий только на именованное внешнее назначение, и действие Launch только с именем файла и параметрами, тогда как явная адресация страницы и типа подгонки, а также специфичные для Windows опции запуска, описанные в этой статье, достижимы только напрямую через SetActionRemoteDestinationEx и SetActionLaunchOptions

Одно свойство безопасности стоит знать перед построением инструмента обслуживания вокруг этих устанавливающих функций. SetActionRemoteDestinationEx и SetActionLaunchOptions сначала строят всё заменяющее действие целиком в черновом словаре и только удаляют и копируют ключи /F, /D или /Win и /NewWindow в живое действие, как только этот черновой экземпляр проходит проверку, — так что вызов, что не проходит проверку, будь то из-за ValueMask вне диапазона или пустого FileName, оставляет исходное действие и любую цепочку /Next, уже висящую на нём, полностью нетронутыми, а не наполовину перезаписанными. Это важно, потому что действия GoToR и Launch оба могут находиться внутри цепочки /Next, построенной через AddActionNextRemoteDestinationEx, AddActionNextLaunchEx или более общий AddActionNextEx, позволяя одному триггеру последовательно запустить запись лога JavaScript, а затем внешний переход. Построение GoToR, GoToE и Launch, как описано здесь, — часть PDFlibPas, нативной PDF-библиотеки для Delphi и C++Builder