PDFlibPas предоставя на разработчиците на 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 действие, вече записано от друг производител, а тук — изграждането на същите три вида действия от нулата, включително правилата на ниво поле, които PDFlibPas налага, преди да запише и един byte
Три начина 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 за типовете действия, който определя и обичайното GoTo действие. Дестинацията на обикновено GoTo действие назовава page object, който вече съществува в документа, затова PDFlibPas може да го валидира веднага; 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 в този файл — а PDFlibPas предоставя две различни извиквания за задаване на втората част, всяко със собствено правило за номерация на страниците. AddLinkToFile и AddLinkToFileEx, високониво̀вите builder-и за page-hotspot, валидират своя аргумент Page или DestPage като по-голям от нула, същата 1-базирана номерация, която PDFlibPas използва навсякъде другаде, включително при SelectPage. Setter-ът от по-ниско ниво SetActionRemoteDestinationEx, използван за прикачане или замяна на GoToR действие върху нещо, до което вече имате handle, вместо това валидира DestPage като по-голям или равен на нула и го записва директно в explicit destination array на действието без корекция: той очаква суровия zero-based page index на целевия документ, номерацията, определена от ISO 32000-1 за remote explicit destination. Ако подадете на setter-а от по-ниско ниво същото число, което бихте подали на високониво̀вия builder, връзката ще отвори една страница по-рано
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 е bit set — 1 за left, 2 за top, 4 за right, 8 за bottom и 16 за zoom — а PDFlibPas го проверява спрямо 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-ят, който PDFlibPas съхранява в name tree-то /EmbeddedFiles на документа, а GoToE търси по това име и не докосва повторно файловата система. Функцията проверява само дали EmbeddedFileName не е празно и дали TargetPage е поне 1 — ако подадете име, което никога не е било вградено, извикването пак връща success, действието пак се записва, а връзката просто не успява да се разреши за всеки reader, който я натисне
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 за name tree-то /EmbeddedFiles, а AddLinkToEmbeddedPDF отделно повишава минималната версия до PDF 1.6 за самия тип GoToE действие, така че ефективният минимум за документ, който използва тази възможност, е 1.6, а не 1.4. Забележете и че TargetPage тук е 1-базиран, според обичайната конвенция на PDFlibPas — умишлен контраст с 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, изграден чрез същото преобразуване на пътя, което PDFlibPas използва за GoToR, и това е преносимата форма, определена от ISO 32000-1 §7.11.3 за file specification dictionary. Подречникът /Win, когато PDFlibPas го запише, получава свой ключ /F, зададен точно с необработената стойност FileName, без никакво преобразуване, защото /Win /F е описан в ISO 32000-1 §12.6.4.5 като обикновен Windows path string, предназначен само за прочитане от Windows viewer. Ако подадете portable, вече преобразуван path с очакването двата ключа да станат еднакви, копието в /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 е най-неудобното за използване от трите действия, защото предназначението му е да стартира програма или да отвори файл извън 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 по-специално, защото възможността архивен файл да стартира произволна програма е точно зависимо от средата поведение, което форматите за дългосрочно архивиране целят да предотвратят, а PDFlibPas прилага същата консервативна проверка и към setter-а за remote go-to в същия code path. Практическата последица лесно се пропуска при разработка: същото извикване, което работи върху обикновен PDF, ще се компилира и изпълни, но тихо няма да направи нищо върху документ, зареден с зададено ниво на PDF/A conformance, затова проверявайте return value, вместо да приемате успех — 0 тук не означава malformed input, а че библиотеката отказва заявка, несъвместима със собственото conformance твърдение на документа
Къде се вписват GoToR, GoToE и Launch в по-голям PDFlibPas workflow
Трите вида действия от тази статия не достигат до едни и същи места. Статията за trigger-ите в жизнения цикъл на документа и страницата разглежда SetDocumentAction и SetPageAction, които могат да прикачат GoToR или Launch действие към trigger като WillClose чрез споделените константи PDF_ACTION_BUILDER_REMOTE_DESTINATION и PDF_ACTION_BUILDER_LAUNCH — същият builder, който обхваща и обикновен URI или JavaScript trigger. GoToE няма такава константа и изобщо няма път към този общ builder; AddLinkToEmbeddedPDF е единственият начин, по който PDFlibPas изгражда такова действие, което го прави строго 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, описано тук, е част от PDFlibPas, native PDF библиотеката за Delphi и C++Builder