Наследявате папка с PDF-и от някъде по веригата и задачата звучи тривиално: кажете ми кои bookmarks прескачат към външен URL, кои пускат JavaScript и къде наистина кацат вътрешните. После отваряте API reference-а и откривате, че библиотеката може да създава всеки един от тези actions, но не предлага нищо, за да ги прочетете обратно. Тази асиметрия е навсякъде в PDF tooling-а. Да напишете bookmark, който отваря https://example.com е едноредовка; да попитате съществуващ bookmark „какво правиш и към какво сочиш?“ обикновено означава да обхождате на ръка суровото object tree през /A, /S, /Dest и fan-out от fit-type варианти, които почти никой не уцелва от първия път
PDFlibPas е нативна Object Pascal PDF библиотека за Delphi и C++Builder и дълго време имаше същата празнина: богати write-side setters, getters, които ви връщаха гола TPDFObject и ви оставяха да ровите. Изданието v3.77.0 затвори част от това с малък набор typed introspection извиквания, които отчитат вида на action-а, payload-а на action-а и геометрията на destination-а като обикновени records. Тази статия е за това как тези извиквания се напасват към ISO 32000-1 action и destination модела и за трите конкретни капана, които карат ръчно написаните версии на този код тихо да грешат
Защо четенето на actions е по-трудно от писането им
Action в PDF е dictionary с /S key, който назовава подтипа му: GoTo, GoToR, URI, Launch, Named, и по-дълга опашка, с която рядко се срещате (ISO 32000-1 §12.6.4). Проблемът е, че payload-ът живее в различен key за всеки subtype и няма единен слот „дай ми target-а“. JavaScript action пази адреса си в URI. /URI или GoToR action пази file specification в Launch. /F action пази скрипта си в JavaScript, който може да е низ или stream. /JS action изобщо не носи свой собствен payload; target-ът му е destination, който виси от GoTo, и после трябва да го разрешите отделно./DКогато пишете action, знаете предварително вида му, така че всичко това няма значение. Когато го четете, трябва първо да branch-нете по
, после да посегнете към правилния key и чак тогава да се справите с факта, че една и съща логическа концепция („онова, към което сочи този action“) е кодирана по три несъвместими начина. Точно това разклоняване поглъщат typed getters-ите. /S и GetOutlineActionInfo и двата връщат GetAnnotActionInfo record:TPDFlibActionInfoRecord-ът ви казва кои полета са смислени чрез
type
TPDFlibActionKind = (akNone, akGoTo, akGoToR, akURI,
akLaunch, akNamed, akJavaScript);
TPDFlibActionInfo = record
Kind: TPDFlibActionKind;
URI: AnsiString; // populated for akURI
JavaScript: WideString; // populated for akJavaScript
FileName: AnsiString; // populated for akGoToR / akLaunch
OpenInNewWindow: Boolean; // akGoToR / akLaunch
end;
. Ако Kind се върне Kind, прочетете akURI и игнорирайте останалото. Ако се върне URI and ignore the rest. If it comes back akGoTo е честният отговор, когато bookmark-ът или annotation-ът изобщо няма action, вместо нула, чийто смисъл трябва да гадаете.akNoneОбхождане на outline tree-то, за да се намери bookmark
Преди да можете да интроспектирате bookmark, трябва да имате неговия handle. PDFlibPas идентифицира outline nodes с integer ID, а
намира един по видимия му текст с изричен контрол върху това докъде стига търсенето:FindOutlineByTitle аргументът е онази част, върху която си струва да се замислите
type
TPDFlibOutlineSearchDepth =
(osdSiblingsOnly, osdChildrenOnly, osdFullSubTree);
function FindOutlineByTitle(const Title: WideString;
StartOutlineID: Integer;
Depth: TPDFlibOutlineSearchDepth): Integer;
сканира sibling chain-а на нивото на стартовия node и спира; ще намери peer bookmark, но никога няма да слезе в children на peer. Depth гледа едно ниво надолу, в непосредствените children на стартовия node. osdSiblingsOnly рекурсира през цялата branch. Да изберете грешния вариант е тих пропуск, не грешка: sibling-only търсене за title, който живее две нива надълбоко, просто връща нула и вие заключавате, че bookmark-ът не съществува, когато е бил там през цялото време. Подайте osdChildrenOnly като стартов ID, за да търсите от document root.osdFullSubTreeСъпоставянето е по точния title string, сравняван като GetFirstOutline, така че е case-sensitive и уважава Unicode текста точно както е записан. Ако изходните ви PDF-и идват от несъгласувани producers, нормализирайте title-а, който търсите, по същия начин, по който документът го е записал, иначе ще гоните призрачни пропуски
var
Lib: TPDFlib;
FoundID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('report.pdf', '') = 1 then
begin
// Search the whole tree from the root for a nested bookmark.
FoundID := Lib.FindOutlineByTitle('Appendix B',
Lib.GetFirstOutline, osdFullSubTree);
if FoundID <> 0 then
// FoundID is now a handle you can pass to the action and
// destination getters below.
;
end;
finally
Lib.Free;
end;
end;
Разрешаване на bookmark action и targetWideStringСъс handle в ръка,
ви дава typed view-то. Моделът е: извиквате го, switch-вате по
, четете полето, което този kind попълва.GetOutlineActionInfoТук живее първият истински капан и той беше изваден на светло от test feedback по време на имплементацията. Има по-стар getter, Kind, и да посегнете към него, за да прочетете
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetOutlineActionInfo(FoundID);
case Info.Kind of
akURI:
Writeln('Opens URL: ', Info.URI);
akGoToR, akLaunch:
Writeln('Opens file: ', Info.FileName,
' (new window: ', Info.OpenInNewWindow, ')');
akJavaScript:
Writeln('Runs script: ', string(Info.JavaScript));
akGoTo:
Writeln('Jumps within this document'); // see destination below
akNamed:
Writeln('Named action (NextPage, Print, etc.)');
akNone:
Writeln('Bookmark has no action');
end;
end;
action, е очевидно-изглеждащата грешка. GetActionURL разрешава file specification през URI key. Това е правилното нещо за GetActionURL и /F, чиито targets наистина са файлове, но е грешният key за GoToR action изобщо. Launch action address-ът е обикновен string в собствения URI key на action-а, не file spec. Подайте URI action към file-spec path-а и получавате празен или безсмислен резултат. Typed getter-ът се справя с това вътрешно, като чете /URI директно за URI и само прилага file-specification resolver-а за /URI и akURI, което е точното разграничение, което ръчно написана версия обикновено размазва.akGoToRFit типове на destination и геометрията зад тяхakLaunchAn
action означава „navigate вътре в този document“, но не ви казва нищо за
къдеakGoTo или как. Това е работата на destination-а и destination-ите носят повече нюанс, отколкото хората очакват. PDF destination не е просто page number; той е page плюс fit specification, който казва как viewer-ът да frame-не тази page (ISO 32000-1 §12.3.2.2). го връща като record:. That is the destination's job, and destinations carry more nuance than people expect. A PDF destination is not just a page number; it is a page plus a "fit" specification that says how the viewer should frame that page (ISO 32000-1 §12.3.2.2). GetOutlineDestinationInfo returns it as a record:
type
TPDFlibDestinationKind = (dkNone, dkXYZ, dkFit, dkFitH,
dkFitV, dkFitR, dkFitB, dkFitBH, dkFitBV);
TPDFlibDestinationInfo = record
Kind: TPDFlibDestinationKind;
Page: Integer; // 1-based; 0 when unresolved
Left, Top, Right, Bottom, Zoom: Double;
end;
, dkXYZ и Left. Top пасва цялата page в window-а и игнорира координатите. Zoom и dkFit пасват page width или height с един релевантен coordinate (горен edge или ляв edge). dkFitH е интересният: той пасва зададен rectangle, така че имат значение и четирите edges. dkFitV семейството прави същите неща спрямо bounding box-а на видимия content, вместо спрямо цялата page. Да знаете кои полета са live за всеки kind е разликата между това да прочетете destination-а правилно и да отпечатате безсмислени координати, които по случайност са нула.dkFitRВсеки bookmark в този navigation panel се разрешава към action и, за вътрешните jumps, към destination със собствен fit type и координати.dkFitB*Под капака имплементацията стъпва върху умишлено подравняване, което си струва да знаете, защото обяснява защо mapping-ът е надежден. Вътрешният

е деклариран така, че ordinal-ите му да съвпадат едно към едно: GetDestType е ordinal 1, TPDFlibDestinationKind е ordinal 8, а dkXYZ стои на нула. Така conversion-ът е директно ordinal cast с range guard, а не lookup table, който може да изпадне от синхрон, когато enum-ът расте. Това е малък детайл, но е от онези неща, които, ако се направят по наивния начин, стават off-by-one bug при първото пренареждане на enumeration.dkFitBVA dkNone от нула е сигналът, че destination-ът не се е разрешил, обикновено защото action-ът няма destination или named destination-ът не е намерен. Проверете го, преди да се доверите на който и да е coordinate. Забележете също, че
var
Dest: TPDFlibDestinationInfo;
begin
Dest := Lib.GetOutlineDestinationInfo(FoundID);
if Dest.Page = 0 then
Exit; // destination did not resolve
case Dest.Kind of
dkXYZ:
Writeln(Format('Page %d at (%.0f, %.0f), zoom %.2f',
[Dest.Page, Dest.Left, Dest.Top, Dest.Zoom]));
dkFitR:
Writeln(Format('Page %d, rect L%.0f T%.0f R%.0f B%.0f',
[Dest.Page, Dest.Left, Dest.Top, Dest.Right, Dest.Bottom]));
dkFit, dkFitB:
Writeln(Format('Page %d, fit whole page', [Dest.Page]));
else
Writeln(Format('Page %d, fit kind %d',
[Dest.Page, Ord(Dest.Kind)]));
end;
end;
търси и на двете места, където един destination може да живее: директно върху bookmark-а Page, и вътре в embedded GetOutlineDestinationInfo action’s /Dest. Не е нужно да знаете коя форма е използвал producer-ът.GoToAnnotation actions и SelectPage капанът/DLink annotations носят actions точно както bookmark-ите, а
връща същия
record със същия kind-then-payload модел. Но тук има stateful уловка, която не важи за outlines, и това е третият капан.GetAnnotActionInfoAnnotations принадлежат на pages и PDFlibPas излага annotation-ите на текущата page чрез state, който става валиден едва след като изберете тази page. Извикайте TPDFlibActionInfo без първо да извикате
и annotation handle-ът е нула; call-ът връща GetAnnotActionInfo и вие погрешно заключавате, че page-ът няма actionable annotations. Fix-ът е един ред, но е лесно да го забравите, когато обхождате pages:SelectPage(N)Две неща в този loop са умишлени. Първо, akNone стои преди всеки annotation access на всяка итерация; per-page annotation state не се пренася. Второ, existence test-ът използва
var
P: Integer;
Info: TPDFlibActionInfo;
begin
for P := 1 to Lib.PageCount do
begin
Lib.SelectPage(P); // mandatory before touching annotations
// GetAnnotActionID(1) <> 0 is the reliable "has an action"
// test. CheckPageAnnots returns a boolean-style flag, not a
// count, so it is the weaker signal here.
if Lib.GetAnnotActionID(1) <> 0 then
begin
Info := Lib.GetAnnotActionInfo(1);
if Info.Kind = akURI then
Writeln(Format('Page %d link -> %s', [P, Info.URI]));
end;
end;
end;
вместо SelectPage(P). Последното отчита presence като boolean-style flag, а не като count, така че non-zero action ID е по-точният начин да попитате „има ли първи annotation и носи ли action, който мога да прочета?" Още една финес, която си струва да отбележите: за annotations, GetAnnotActionID(1) <> 0 action скриптът се чете директно от CheckPageAnnots, декодирайки stream, когато скриптът е съхранен така, и четейки string иначе, така че издържа и двата обичайни encoding-а.JavaScriptКъде се вписва read-side introspection/JSТези getters са умишлено тесни. Те са pure reads върху съществуващите integer-handle action и destination слоеве на библиотеката, така че не пипат write path-а и не добавят риск за документи, които едновременно редактирате. Те отчитат какво има във файла; не го валидират срещу policy и не пренаписват нищо. Ако целта ви е обратната - да изграждате bookmarks и link annotations, които носят тези actions от самото начало - това е write side, а companion piece-ът за
interactive form actions and JavaScript in Delphi
обхожда как да ги създавате. За да извадите видимото и structural съдържание от PDF, вместо неговия navigation graph, вижте extracting text, images, and fonts with PDFlibPas обхожда как да ги създавате. За да извадите видимото и structural съдържание от PDF, вместо неговия navigation graph, вижте extracting text, images, and fonts with PDFlibPas
Честната граница, която трябва да имате предвид: introspection-ът вижда само това, което producer-ът наистина е записал. Bookmark, чийто action generator-ът е оставил malformed, или destination, който сочи към named target, който никога не е бил дефиниран, ще се появи като akNone или zero page, а не като exception. Това е правилното поведение за read API, който одитира untrusted файлове, но означава, че кодът ви трябва да третира тези zero резултати като „absent or unresolved“, а не като гаранция за добре оформен input. Typed action и destination introspection-ът, показан тук, е част от PDFlibPas, native PDF библиотеката за Delphi и C++Builder