HotPDF добавя отметки (bookmarks) към съществуващ PDF в Delphi чрез AddLoadedOutline, което създава записи в контура (outline entries) на всякакво ниво на влагане в зареден документ, и чете и записва наименувани дестинации (named destinations) чрез ResolveLoadedNamedDestination и AddLoadedNamedDestination. Заредете файла, изградете дървото, извикайте SaveLoadedDocument и страничната лента на четеца, която досега е била празна, вече ще съдържа работещо съдържание
Сценарият, който мотивира всичко това, е потискащо често срещан. Обединявате дузина договори в един пакет за преглед или свързвате три продуктови ръководства в един файл за разпространение и резултатът е структурно перфектен: всяка страница е налице, всеки шрифт е непокътнат. След това обаче някой го отваря в Acrobat и панелът за отметки е празен. Документ от 400 страници без навигация е технически завършен, но практически неизползваем, а до версия 2.347.0 HotPDF можеше да чете, променя и изтрива отметки в заредени документи, но не и да ги създава. Тази празнота вече е запълнена и статията разглежда новия набор от прилодни програмни интерфейсове (API) и една особеност на PDF структурата – ключа /Count, който ще ви създаде проблеми, ако го третирате като обикновен брояч на записи
Какво всъщност представлява контурното дърво (outline tree)
Контурът на PDF (outline) е двойно свързано дърво от речници, а не плосък списък, и тази структура обяснява всеки параметър в API. Стандартът ISO 32000-1 §12.3.3 го дефинира: каталогът на документа сочи към корен /Outlines, коренът сочи към първото и последното си дете съответно чрез /First и /Last, а съседните елементи (siblings) се свързват чрез /Next и /Prev, докато всеки запис сочи обратно нагоре чрез /Parent. Всеки запис носи низ /Title и обикновено /Dest, който указва къде щракването върху него трябва да отведе читателя
Дестинациите биват два вида и ISO 32000-1 §12.3.2 ги държи умишлено разделени. Явната дестинация е вграден масив от типа [page /XYZ x y zoom]: директна препратка към обекта на страницата плюс спецификация на изгледа. Наименуваната дестинация вместо това съхранява низ с име, а реалната цел живее в каталога на документа под дървото с имена /Names /Dests – едно ниво на косвеност (indirection), което позволява на много връзки да споделят една цел и позволява на целта да се мести, без да се променят връзките. HotPDF записва явни /XYZ дестинации при създаване на отметки и предоставя отделни извиквания за четене и разширяване на дървото с имена
Как се добавят отметки към съществуващ PDF в Delphi?
Извикайте AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) след LoadFromFile: предайте ParentIndex = -1 за запис от най-горно ниво или нулево-базирания индекс на съществуващ запис от най-горно ниво, за да го вградите под него. Функцията създава корена /Outlines, ако документът никога не е имал такъв, изгражда речника на записа, записва масива /Dest [page /XYZ x y zoom], сочещ към нулево-базирания DestPageIndex, и свързва новия запис във веригата от съседни елементи. Връща 0 при успех и -1 при неуспех (например когато DestPageIndex е извън диапазона)
Една подробност при свързването е важна за подредбата: новият запис се вмъква в началото на веригата от деца на своя родител, като нов /First. Така че, ако добавите „Chapter 1“ и след това „Chapter 2“ как записи на най-горно ниво, „Chapter 2“ ще се появи над „Chapter 1“ в страничната лента, а „Chapter 1“ се измества на индекс 1. Практическият шаблон е приложим – да добавяте записи в обратен ред на четене или да добавяте всеки родител и веднага да попълвате неговите деца, докато той все още се намира на индекс 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Add chapters in reverse reading order: each new
// top-level entry becomes /First (index 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 is now top-level index 0; nest sections
// under it (children also prepend, so reverse order).
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
Параметрите X, Y и Zoom се съпоставят директно с дестинацията /XYZ и всички са 0 по подразбиране. PDF поставя координатното начало в долния ляв ъгъл, така че Y = 792 сочи към горната част на страница с формат US Letter, а според ISO 32000-1 нулевата стойност в слот за /XYZ казва на визуализатора да запази текущата си настройка за мащабиране, което е точно това, което искате в почти всеки случай. Всичко се случва върху заредения обектен граф в паметта; нищо не докосва диска, докато не се изпълни SaveLoadedDocument – същият модел „редактирай и след това запиши“, който API-то за заредени документи използва за пренаписване на заглавие, автор и XMP метаданни в заредени PDF файлове
Пренасочване на отметки: прескачане към страници спрямо URI действия
Съществуващите отметки могат да бъдат пренасочвани без да се пресъздават, като HotPDF предоставя две различни извиквания за двата случая, тъй като PDF структурите под тях са напълно различни. Методът SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) заменя /Dest на записа от най-горно ниво при Index с нов масив /XYZ – инструмент за класическия проблем след сливане, при който всяка отметка все още сочи към номерата на страниците на оригиналния файл преди сливането. Методът SetLoadedOutlineURI(Index, URI) прави нещо структурно различно: той прикачва речник за действие /A с /S /URI, превръщайки отметката в уеб връзка вместо в прескачане в рамките на документа
// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Дръжте двата механизма ясно разделени, когато проектирате дървото. /Dest е чиста навигация вътре в документа; URI действието излиза изцяло извън документа и ISO 32000-1 изисква записът в контура да навигира чрез единия или другия механизъм, но не и чрез двата едновременно. Така че запазете SetLoadedOutlineURI за записи, които вече не носят дестинация на страница – например връзки от тип „Начална страница на продукта“ или „Докладване на проблем“ в края на дървото, а не за преобразувани заглавия на глави. Записаното тук URI действие е същата структура /S /URI, използвана за анотации за връзки върху съдържанието на страниците, разгледана по-подробно в ръководството за създаване на хипервръзки в PDF документи с HotPDF
Как работят наименуваните дестинации в зареден PDF?
Наименуваните дестинации са механизмът за стабилни котви (stable anchors) на ISO 32000-1 §12.3.2.3: вместо да вгражда препратка към страница във всяка връзка, документът поддържа карта от име към дестинация в дървото с имена /Names /Dests на каталога, а връзките препращат към записите по име. ResolveLoadedNamedDestination(Name) търси име в това дърво и връща нулево-базирания индекс на страницата, към която то сочи, или -1, ако името липсва. Резолверът обработва и двете форми на съхранение, които спецификацията позволява за съпоставената стойност: чист дестинационен масив като [page /XYZ x y zoom] и формата на речник, която обвива масива под ключ /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) работи по обратния начин: регистрира нова котва в дървото /Names /Dests, създавайки самото дърво, когато документът няма такова, и връща True при успех. Заедно тази двойка методи покрива работния процес, при който обединен документ трябва да зачита котвите, записани от предходни инструменти, и да публикува нови, към които да сочат следващите връзки
var
PageIdx: Integer;
begin
// Honor an anchor another tool wrote into /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publish a new anchor for other links to target.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Практическите ограничения: дестинациите, които HotPDF записва – както за отметки, така е и за наименувани котви – са /XYZ дестинации. Другите явни типове, дефинирани от спецификацията (/Fit, /FitH, /FitB и подобни), не се поддържат от тези извиквания, въпреки че /XYZ с нулево мащабиране покрива преобладаващата част от случаите. Методът GetLoadedBookmarkPageIndex(Title), удобното търсене, което взема заглавие на отметка и връща целевата страница, сканира само най-горното ниво на контурното дърво, така че го използвайте за проверка на записите на ниво глави, а не за дълбоко вградени листа
Капанът /Count: той брои наследниците, а не записите
Ключът /Count в корена на контура не означава това, което повечето разработчици приемат, и погрешното предположение води до грешки при тестове, които изглеждат като повреда на данни. Стандартът ISO 32000-1 §12.3.3 дефинира /Count като общия брой на видимите наследници на всички нива, а не броя на записите от най-горно ниво. При отделен запис знакът също носи смисъл: положителна стойност означава, че записът е отворен и толкова наследници се показват в страничната лента, докато отрицателна стойност означава, че записът е свит и |N| наследници са скрити в него
Това беше открито в собствения тестов пакет за регресия на HotPDF по време на разработването на API-тата за отметки в заредени документи. Тестов документ съдържаше три отметки на най-горно ниво, една от които с дете; след изтриване на един запис от най-горно ниво, GetLoadedOutlineCount, който чете корена /Count, върна 1 вместо очакваните 2. Нищо не беше повредено: първоначалното 3 никога не е било „три записа на най-горно ниво“ и правилното преизчисляване трябва да обходи веригата на най-горното ниво, натрупвайки всеки възел плюс неговия положителен /Count, като пропуска наследниците на свитите възли. Урокът за вашия собствен код е директен: никога не разчитайте на промени в /Count след структурни редакции, тъй като стойността се движи нелинейно с броя на видимите наследници. Вместо това проверявайте структурата по съдържание
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Къде се вписва това в набора от инструменти за заредени документи
Създаването на отметки и наименувани дестинации допълва картината, която API-то за заредени документи изгражда от известно време: същият цикъл „зареди-редактирай-запиши“ вече пренаписва метаданните на документа, добавя анотации за връзки и прехвърля двупосочно анотации чрез импорт и експорт на XFDF. Моделът е унифициран: LoadFromFile, последователност от *Loaded* извиквания спрямо обектовия граф в паметта, и след това едно извикване на SaveLoadedDocument, което улеснява вграждането на изграждането на контура в съществуващ процес за сливане или маркиране като още една стъпка преди записа
Приложните програмни интерфейсове (API), обсъдени тук – AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination и GetLoadedBookmarkPageIndex, се доставят в стандартния компонент HotPDF Component за Delphi и C++Builder, без външни зависимости и без необходимост от отделен визуализатор при изпълнение