Vytiahnite zo 200-stranovej príručky sedem stránok a každá záložka dopadne niekde zle. Opravou nie je znovu vybudovať osnovu z plochého zoznamu titulov. PDFiumPas vystavuje TPdfOutlineEditor, ktorý načíta skutočný strom osnovy, dovolí položky presúvať a precieľovať a potom spustí ApplyPageMap, aby posunul každú explicitnú destináciu podľa vášho plánu stránok
Prečo mazanie stránok rozbije každú záložku?
Pretože položka osnovy neukladá číslo stránky. Ukladá odkaz na objekt stránky a keď sa objekty stránok zmenia, odkaz buď ukazuje na stránku, ktorá sa presunula, alebo na nič. ISO 32000-1 §12.3.2.2 definuje explicitnú destináciu ako pole, ktorého prvý prvok je nepriamy odkaz na slovník stránky, nasledovaný fit názvom ako /Fit alebo /XYZ. Zmazaním stránky vám zostane visiaci odkaz; preusporiadaním stránok odkaz ostane platný, ale teraz opisuje inú kapitolu. PDFiumPas toto pole pri načítaní vyrieši späť na číslo stránky, takže TPdfOutlineItem.PageNumber vám dá index stránky od jednej, ktorý sedí s verejným API TPdf namiesto čísla objektu. V tom je celý zmysel abstrakcie: vaša remapovacia logika pracuje v tom istom súradnicovom systéme ako plán stránok, ktorý ste už vybudovali, keď ste dokument delili, preusporiadali alebo sadali. Ak tento plán stavite, tá istá konvencia od jednej prechádza delením PDF dokumentov do viacerých súborov aj n-up sadením a preusporiadaním stránok
Osnova je obojsmerne zreťazený strom, nie zoznam
Dôvod, prečo nemôžete jednoducho serializovať ploché pole titulov, je, že ISO 32000-1 §12.3.3 zapája každú položku osnovy do piatich samostatných odkazov: /Parent, /Prev, /Next, /First a /Last. Presun jediného podstromu preto prepíše starého rodiča, nového rodiča, oboch susediacich súrodencov na každej strane rezu a bodu vloženia a ukazovateľ na rodiča presúvaného uzla samého. Ak jedno z nich pokazíte, konformné prehliadače zobrazia skrátený strom alebo sa zacyklia. PDFiumPas drží stav úprav ako pole záznamov TPdfOutlineItem usporiadané do hĺbky so stabilným celočíselným Id, takže podstrom je súvislý rez a reťazec súrodencov je odvodený, nikdy ručne udržiavaný. TPdfOutlineEditor.Move tento rez zdvihne, znovu vloží pod nového rodiča na požadovanom indexe súrodencov a prerozdelí len koreň bloku. Odmietne aj tie dva presuny, ktoré by graf pokazili: presun položky do jej vlastného podstromu a pomenovanie rodiča, ktorý neexistuje
Prečo je /Count podpísané?
Pretože znamienko nesie stav rozbalenia, nie veľkosť. Kladné /Count znamená, že položka je otvorená a číslo hovorí, koľko potomkov je práve viditeľných; záporné /Count znamená, že položka je zbalená. PDFiumPas zapisuje počet potomkov pre každú položku s deťmi a neguje ho, keď IsOpen je False, a pri načítaní prečíta stav späť ako IsOpen := HasCount and (CountValue > 0). Toto je najbežnejší ručne robený bug v zapisovačoch osnov: vypustiť nepodpísaný počet a ticho vynútiť celý strom rozbalený
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // stane sa druhým dieťaťom koreňa
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // zapíše záporné /Count
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget ošetrí oba tvary, ktoré špecifikácia dovoľuje. Prejdete DestinationInAction ako False a PDFiumPas zapíše priame pole /Dest; prejdete True a zapíše akciu Go-To, /A << /S /GoTo /D [ page ref suffix ] >> podľa ISO 32000-1 §12.6.4.2. V oboch prípadoch najprv zoškrabne existujúce /Dest a /A z položky, aby tieto dva nemohli koexistovať a rozchádzať sa. Prípona má predvolené /Fit a musí začínať PDF názvom, a preto prázdna alebo zdeformovaná prípona okamžite vyvolá výnimku namiesto vyrobenia poľa destinácie, ktoré nedokáže žiadny prehliadač vyparsovať
Ako ApplyPageMap spracováva plán stránok?
ApplyPageMap berie presne to pole, ktoré váš plán stránok už overil: NewPageNumbers, indexované starou stránkou mínus jedna, nesúce nové číslo stránky od jednej alebo nulu, keď tá stránka neprežila. Pole položiek prechádza odzadu, takže mazanie podstromu nikdy nezneplatní index, ktorý ešte má navštíviť, a to, čo urobila, hlási cez RemappedDestinationCount a RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Jedna položka na stránku PÔVODNÉHO dokumentu
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == táto stránka bola zahodená
NewPageNumbers[0] := 1; // stará stránka 1 -> nová stránka 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // stará stránka 10 -> nová stránka 3
// True: zmazať celý visiaci podstrom. False: ponechať položku, odstrihnúť jej cieľ
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
Príznak DeleteDangling rozhoduje politiku pre destináciu namapovanú na nulu a obe vetvy sú zámerné. Pri True PDFiumPas zmaže položku aj celý jej podstrom, pretože uzol osnovy, ktorého cieľ zmizol, zvykne čeliť kapitole, ktorá zmizla s ním. Pri False položka prežije s titulom a hierarchiou nedotknutými, ale so svojím /Dest a /A odstránenými — presne to chcete, keď ju človek bude v recenzii precieľovať. Skutočne zdeformovaný vstup stále zlyhá nahlas namiesto záplatovania: záporná položka alebo destinácia ukazujúca za koniec dodanej mapy vráti False s IssueKind nastaveným na poviInvalidPageMap
Nepriehľadné položky a úprimný kompromis
Nie každá položka osnovy má číslo stránky, o ktorom PDFiumPas dokáže uvažovať. Tri druhy sa prenášajú nedotknuté: pomenované destinácie, akcie, ktoré nie sú /S /GoTo, a neznáme kľúče slovníka pridané tým, čo súbor vyrobil. Tieto sa načítajú s PageNumber rovným nule, držia v položke svoje pôvodné bajty a zapisujú sa späť doslovne, pokiaľ na nich explicitne nezavoláte Retarget
- Pomenovaná destinácia je kľúč do stromu mien dokumentu, takže jej správne remapovanie znamená vyriešiť strom a prepísať cieľovú položku, nie hádať na úrovni osnovy
- Akcia
/URI,/Launchalebo JavaScript nemá žiadnu sémantiku stránky a nesmie byť ticho prevedená na Go-To - Dodávateľské kľúče a štruktúrne destinácie sa zachovávajú, pretože zahadzovanie toho, čomu nerozumiete, je spôsob, akým round-tripy strácajú dáta
Cena je reálna a stojí za to povedať ju napriamo: ApplyPageMap tieto položky úplne preskočí, takže dokument, ktorého záložky všetky používajú pomenované destinácie, prejde mazaním stránok so štrukturálne platnou a sémanticky zastaranou osnovou. To je zámerná voľba — zastaraný odkaz, ktorý recenzent chytí, je lepší než sebavedomo nesprávny, ktorého si nikto nevšimne. Ak triážete prichádzajúce súbory skôr, než ich upravíte, inventárny priechod v PDF intake review workbenchi vám povie, ktoré dokumenty do tej skupiny padajú
Ukladanie: prírastková revízia a potom nezávislé znovunačítanie
TPdfOutlineEditor.SaveIncremental pripojí riedku prírastkovú revíziu namiesto prepisu súboru. Položky, ktoré boli načítané, držia svoj pôvodný nepriamy odkaz na objekt vrátane presnej generácie, takže existujúce krížové odkazy zostávajú platné; len položky, ktoré ste pridali, si vyžrebovajú čerstvé číslo, alokované od jednej za maximálnym číslom objektu revízie. Katalóg sa aktualizuje v tej istej revízii a chýbajúca položka /Outlines sa doň pridá, keď zdroj nemal žiadnu osnovu
Čo sa stane po zápise, je časť hodná napodobnenia. PDFiumPas znovuotvorí cieľový prúd s úplne nezávislým editorom a porovná znovunačítaný strom s tým v pamäti — počet položiek, tituly, čísla stránok, prípony destinácií, formu akcia verzus priama destinácia, štýly, stav rozbalenia a vzťahy rodičovstva. Akákoľvek nezhoda alebo akékoľvek zlyhanie načítania vyčistí cieľový prúd a vráti poviVerificationFailure namiesto toho, aby vám podstrčil súbor, ktorý len vyzerá uveriteľne. Šifrované zdroje sa odmietajú hneď na začiatku s poviEncryptedInput, pretože nové tituly a destinácie vytvárajú reťazcový obsah, ktorý nedá vyrobiť skopírovaním traileru /Encrypt dopredu
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Berte osnovu ako to, čím je — zreťazený graf objektov s vlastnými invariantmi — a mazanie stránok prestane byť katastrofou záložiek a stane sa mapou stránok, ktorú odovzdáte jednému volaniu metódy. TPdfOutlineEditor, ApplyPageMap a overený prírastkový zapisovač sa dodávajú v PDFiumPas od v3.98.0 pre Delphi, C++Builder a Lazarus; celé API si môžete pozrieť a stiahnuť skúšobnú verziu na produktovej stránke PDFium Delphi Component