„HotPDF“ prideda žymes (bookmarks) prie esamo PDF failo Delphi aplinkoje per „AddLoadedOutline“, kuri sukuria žymių įrašus bet kuriame lizdavimo lygyje įkeltame dokumente, bei skaito ir rašo vardines paskirties vietas (named destinations) per „ResolveLoadedNamedDestination“ ir „AddLoadedNamedDestination“. Įkelkite failą, sukurkite medį, iškvieskite „SaveLoadedDocument“, ir skaitytuvo šoninė juosta, kuri anksčiau buvo tuščia, dabar turės veikiantį turinį
Scenarijus, kuris paskatina visa tai, yra slegiančiai dažnas. Sujungiate tuziną sutarčių į vieną peržiūros paketą arba sujungsite tris produktų vadovus į vieną platinamą failą, ir rezultatas struktūriškai yra geras: visi puslapiai yra, visi šriftai nepažeisti. Tada kas nors atidaro jį „Acrobat“ programoje, o žymių skydelis yra tuščias. 400 puslapių dokumentas be naršymo yra techniškai pilnas, bet praktiškai nenaudingas. Iki 2.347.0 versijos „HotPDF“ galėjo skaityti, keisti ir trinti žymes įkeltuose dokumentuose, bet niekada negalėjo jų sukurti. Ši spraga dabar užpildyta, ir šiame straipsnyje apžvelgiama nauja API sąsaja bei viena PDF paslapčių dalis — raktas „/Count“, kuris sukels klaidų, jei jį traktuosite kaip paprastą įrašų skaitiklį
Kas iš tikrųjų yra žymių medis
PDF žymių medis (outline) yra dvigubai susietas žodynų medis, o ne plokščias sąrašas, ir ši struktūra paaiškina kiekvieną API parametrą. ISO 32000-1 §12.3.3 tai apibrėžia: dokumento katalogas rodo į „/Outlines“ šaknį, šaknis rodo į savo pirmąjį ir paskutinį vaiką per „/First“ ir „/Last“, o broliniai elementai jungiasi per „/Next“ ir „/Prev“, tuo tarpu kiekvienas įrašas rodo atgal į tėvą per „/Parent“. Kiekvienas įrašas turi „/Title“ eilutę ir, paprastai, „/Dest“ savybę, kuri nurodo, kur skaitytojas turi būti nukreiptas jį spustelėjus
Paskirties vietos yra dviejų rūšių, ir ISO 32000-1 §12.3.2 jas sąmoningai laiko atskirai. Aiški paskirties vieta (explicit destination) yra įterptas masyvas, pvz., „[page /XYZ x y zoom]“ — tiesioginė nuoroda į puslapio objektą plius vaizdo specifikacija. Vardinė paskirties vieta (named destination) vietoj to saugo pavadinimo eilutę, o tikrasis taikinys gyvena dokumento kataloge po „/Names /Dests“ pavadinimų medžiu — tai vienas netiesioginio kreipimosi lygis, leidžiantis daugeliui nuorodų dalytis vienu taikiniu ir leisti taikiniui judėti neliečiant pačių nuorodų. „HotPDF“ rašo aiškias „/XYZ“ paskirties vietas, kai kuria žymes, ir pateikia atskirus iškvietimus pavadinimų medžio skaitymui bei išplėtimui
Kaip pridėti žymes prie esamo PDF failo Delphi aplinkoje?
Iškvieskite „AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom)“ po „LoadFromFile“: perduokite „ParentIndex = -1“ aukščiausio lygio įrašui arba esamo aukščiausio lygio įrašo indeksą (pradedant nuo 0), kad įterptumėte po juo. Funkcija sukuria „/Outlines“ šaknį, jei dokumentas jos neturėjo, sukuria įrašo žodyną, įrašo „/Dest [page /XYZ x y zoom]“ masyvą, rodantį į puslapio indeksą „DestPageIndex“ (pradedant nuo 0), ir susieja naują įrašą į brolišką grandinę. Sėkmės atveju grąžina 0, o nesėkmės atveju — -1, pavyzdžiui, kai „DestPageIndex“ yra už ribų
Viena susiejimo detalė yra svarbi tvarkai: naujas įrašas įterpiamas jo tėvo vaikų grandinės pradžioje kaip naujas „/First“. Taigi, jei pridedate „1 skyrių“, o po to „2 skyrių“ kaip aukščiausio lygio įrašus, „2 skyrius“ šoninėje juostoje atsiras virš „1 skyriaus“, o „1 skyrius“ pasislinks į aukščiausio lygio indeksą 1. Praktinis šablonas yra arba pridėti įrašus atvirkštine skaitymo tvarka, arba pridėti kiekvieną tėvą ir iškart užpildyti jo vaikus, kol jis vis dar yra indekse 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;
Parametrai „X“, „Y“ ir „Zoom“ tiesiogiai susiejami su „/XYZ“ paskirties vieta, o jų numatytoji reikšmė yra 0. PDF koordinačių pradžią padeda apatiniame kairiajame kampe, todėl „Y = 792“ nukreipia į „US Letter“ puslapio viršų, o pagal ISO 32000-1 nulinė reikšmė „/XYZ“ lauke nurodo skaitytuvui išlaikyti esamus nustatymus, kas beveik visada yra būtent tai, ko norite iš mastelio keitimo. Viskas vyksta atmintyje įkeltame objektų grafe; niekas neliečia disko, kol nepaleidžiamas „SaveLoadedDocument“ — tai tas pats modelis „redaguok, tada išsaugok“, kurį įkeltų dokumentų API naudoja įkeltų PDF failų pavadinimo, autoriaus ir XMP metaduomenų perrašymui
Žymių nukreipimas iš naujo: puslapių šuoliai prieš URI veiksmus
Esamas žymes galima perorientuoti jų nesukuriant iš naujo, o „HotPDF“ šiems dviems atvejams pateikia du skirtingus iškvietimus, nes po jais esančios PDF struktūros iš tikrųjų skiriasi. „SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom)“ pakeičia „Index“ nurodyto aukščiausio lygio įrašo „/Dest“ nauju „/XYZ“ masyvu — tai įrankis klasikinei sujungimo klaidai ištaisyti, kai kiekviena žymė vis dar rodo į originalaus failo puslapių numerius. „SetLoadedOutlineURI(Index, URI)“ daro kažką struktūriškai skirtingo: prideda „/A“ veiksmo žodyną su „/S /URI“, paversdama žymę internetine nuoroda, o ne šuoliu dokumento viduje
// 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');
Kurdami medį aiškiai atskirkite šiuos du mechanizmus. „/Dest“ yra grynas naršymas dokumento viduje; URI veiksmas visiškai palieka dokumentą, o ISO 32000-1 tikisi, kad žymės įrašas naršys vienu ar kitu mechanizmu, bet ne abiem kartu. Todėl rezervuokite „SetLoadedOutlineURI“ įrašams, kurie dar neturi puslapio paskirties, pavyzdžiui, „Produkto svetainė“ arba „Pranešti apie klaidą“ lapui medžio pabaigoje, o ne konvertuotai skyriaus antraštei. Čia įrašomas URI veiksmas yra ta pati „/S /URI“ struktūra, naudojama nuorodų anotacijoms puslapio turinyje, kas išsamiau aprašyta nuorodų kūrimo PDF dokumentuose su HotPDF vadove
Kaip veikia vardinės paskirties vietos įkeltame PDF faile?
Vardinės paskirties vietos yra stabilių inkarų (stable-anchor) mechanizmas pagal ISO 32000-1 §12.3.2.3: užuot įterpus puslapio nuorodą į kiekvieną nuorodą, dokumentas palaiko pavadinimų-paskirties vietų susiejimą katalogo „/Names /Dests“ pavadinimų medyje, o nuorodos nurodo įrašus pagal pavadinimą. „ResolveLoadedNamedDestination(Name)“ suranda pavadinimą tame medyje ir grąžina puslapio indeksą (pradedant nuo 0), į kurį jis rodo, arba -1, jei pavadinimo nėra. Apdorojimo programa apdoroja abi saugojimo formas, kurias specifikacija leidžia susietai reikšmei: gryną paskirties masyvą, pvz., „[page /XYZ x y zoom]“, ir žodyno formą, kuri apgaubia masyvą po „/D“ raktu
„AddLoadedNamedDestination(Name, DestPageIndex, X, Y)“ veikia priešinga kryptimi: užregistruoja naują inkarą „/Names /Dests“ medyje, sukurdama patį medį, jei dokumentas jo neturi, ir grąžina „True“ sėkmės atveju. Ši pora apima darbo eigą, kai sujungtas dokumentas turi gerbti inkarus, kuriuos įrašė ankstesni įrankiai, ir paskelbti naujus inkarus vėlesnėms nuorodoms
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;
Tikrosios ribos: paskirties vietos, kurias rašo „HotPDF“ žymėms ir vardiniams inkarams, yra „/XYZ“ paskirtys. Kitų aiškių tipų, kuriuos apibrėžia specifikacija („/Fit“, „/FitH“, „/FitB“ ir kt.), šie iškvietimai neišmeta, nors „/XYZ“ su nuliniu masteliu apima pagrindinį naudojimo atvejį „eik į šią vietą, išlaikyk esamą mastelį“. O „GetLoadedBookmarkPageIndex(Title)“ — patogi paieška, kuri priima žymės pavadinimą ir grąžina jos paskirties puslapį, nuskaito tik viršutinį žymių medžio lygį, todėl naudokite jį skyriaus lygio įrašų tikrinimui, o ne giliai įterptiems lapams
„/Count“ spąstai: skaičiuojami palikuonys, o ne įrašai
Raktas „/Count“ žymių šaknyje nereiškia to, kai mano daugelis programuotojų, o klaidinga prielaida sukelia testavimo nesėkmes, kurios atrodo kaip duomenų sugadinimas. ISO 32000-1 §12.3.3 apibrėžia „/Count“ kaip bendrą matomų palikuonių (visible descendants) skaičių visuose lygiuose, o ne aukščiausio lygio įrašų skaičių. Atskirame įraše ženklas taip pat turi reikšmę: teigiama reikšmė reiškia, kad įrašas yra atvertas ir šoninėje juostoje rodoma tiek palikuonių, o neigiama reikšmė reiškia, kad įrašas yra suskleistas ir po juo paslėpta |N| palikuonių
Tai paaiškėjo iš paties „HotPDF“ regresijos testų rinkinio, kai buvo kuriami įkeltų dokumentų žymių API. Testo dokumente buvo trys aukščiausio lygio žymės, viena iš jų su vaiku; ištrynus vieną aukščiausio lygio įrašą, „GetLoadedOutlineCount“, kuri skaito šaknies „/Count“, pranešė apie 1, o ne apie tikėtiną 2. Nieko nebuvo sugadinta: pirminis 3 niekada nebuvo „trys aukščiausio lygio įrašai“, o teisingas perskaičiavimas turi eiti per aukščiausio lygio grandinę, sudedant kiekvieną mazgą plius jo teigiamą „/Count“ ir praleidžiant suskleistų mazgų palikuonis. Pamoka jūsų kodui yra tiesioginė: niekada nedarykite griežtų prielaidų (assert) apie „/Count“ pokyčius po struktūrinių redagavimų, nes reikšmė keičiasi netiesiškai su matomais palikuonimis. Vietoj to tikrinkite struktūrą pagal turinį
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Kur tai tinka įkeltų dokumentų įrankių rinkinyje
Žymių ir vardinių paskirties vietų kūrimas papildo vaizdą, kurį jau kurį laiką pildė įkeltų dokumentų API: tas pats ciklas „įkelk-redaguok-išsaugok“ jau perrašo dokumento metaduomenis, prideda nuorodų anotacijas bei perkelia anotacijas per XFDF importą ir eksportą. Šablonas yra vienodas: „LoadFromFile“, eilė „*Loaded*“ iškvietimų atmintyje esančiam objektų grafui ir tada vienas „SaveLoadedDocument“ iškvietimas, kas leidžia lengvai įtraukti turinio kūrimą į esamą sujungimo ar spaudų dėjimo procesą kaip dar vieną žingsnį prieš išsaugojimą
Čia parodyti API („AddLoadedOutline“, „SetLoadedOutlineDestination“, „SetLoadedOutlineURI“, „ResolveLoadedNamedDestination“, „AddLoadedNamedDestination“ ir „GetLoadedBookmarkPageIndex“) tiekiama standartinėje „HotPDF Component“ Delphi ir C++Builder aplinkoms be jokių išorinių priklausomybių ir be atskiros peržiūros programos vykdymo aplinkos