Vyjměte sedm stránek z příručky o 200 stránkách a každá záložka dopadne někde špatně. Oprava nestojí v znovuvybudování osnovy z plochého seznamu titulů. PDFiumPas vystavuje TPdfOutlineEditor, který načte skutečný strom osnovy, dovolí přesouvat a přecílit položky, pak spustí ApplyPageMap, aby posunul každou explicitní destinaci vaším plánem stránek
Proč mazání stránek rozbije každou záložku?
Protože položka osnovy neukládá číslo stránky. Ukládá referenci na objekt stránky a když se objekty stránek změní, reference buď ukazuje na stránku, která se přesunula, nebo na nic. ISO 32000-1 §12.3.2.2 definuje explicitní destinaci jako pole, jehož první prvek je nepřímá reference na slovník stránky, následovaná názvem fit jako /Fit nebo /XYZ. Smažte stránku a zbude vám visící reference; přeuspořádejte stránky a reference je stále platná, ale teď popisuje jinou kapitolu. PDFiumPas vyřeší to pole zpět na číslo stránky při načtení, takže TPdfOutlineItem.PageNumber dává index stránky od 1, který odpovídá veřejnému API TPdf, nikoli číslo objektu. To je celá pointa abstrakce: vaše logika přemapování pracuje ve stejném souřadném systému jako plán stránek, který jste už postavili, když jste dokument rozdělili, přeuspořádali nebo imponovali. Stavíte-li ten plán, totéž podjedničkové konvence probíhá dělením dokumentů PDF do více souborů i n-up impozicí a přeuspořádáním stránek
Osnova je dvakrát řetězený strom, nikoli seznam
Důvod, proč nemůžete prostě serializovat ploché pole titulů, je, že ISO 32000-1 §12.3.3 zapojuje každou položku osnovy do pěti oddělených odkazů: /Parent, /Prev, /Next, /First a /Last. Přesun jediného podstromu proto přepíše starého rodiče, nového rodiče, oba sousední sourozence na každé straně řezu i místa vložení a ukazatel rodiče přesouvaného uzlu samého. Uděláte-li chybu v jednom z nich, konformní čtenáři ukáží useknutý strom nebo se zacyklí. PDFiumPas drží stav editace jako depth-first pole záznamů TPdfOutlineItem se stabilním celočíselným Id, takže podstrom je souvislý řez a řetězec sourozenců je odvozený, nikdy ručně udržovaný. TPdfOutlineEditor.Move zvedne ten řez, znovu jej vloží pod nového rodiče na požadovaném indexu sourozence a přiřadí znovu jen kořen bloku. Odmitne též dva přesuny, které by graf zkorumpovaly: přesun položky do jejího vlastního podstromu a pojmenování rodiče, který neexistuje
Proč je /Count se znaménkem?
Protože znaménko nese rozvinutý stav, ne velikost. Kladné /Count znamená, že položka je otevřená a číslo je, kolik potomků je právě viditelných; záporné /Count znamená, že položka je sbalená. PDFiumPas zapíše počet potomků pro každou položku, která má děti, a zneguje jej, když je IsOpen False, a při načtení načte stav zpět jako IsOpen := HasCount and (CountValue > 0). Toto je nejčastější ručně válcovaná chyba v zapisovačích osnov: vydání počtu bez znaménka a tiché vynucení celého stromu otevřeného
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 se druhým dítětem kořene
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 zvládne oba tvary, které specifikace dovoluje. Předejte DestinationInAction jako False a PDFiumPas zapíše přímé pole /Dest; předejte True a zapíše akci Go-To, /A << /S /GoTo /D [ page ref suffix ] >>, dle ISO 32000-1 §12.6.4.2. V obou případech nejprve oloupe z položky jakékoli existující /Dest a /A, aby obě nemohly koexistovat a rozcházet se. Přípona má výchozí /Fit a musí začínat názvem PDF, proto prázdná nebo deformovaná přípona vyvolá okamžitě, místo aby vytvořila pole destinace, které žádný čtenář nenaparsuje
Jak ApplyPageMap konzumuje plán stránek?
ApplyPageMap bere přesně to pole, které váš plán stránek už validoval: NewPageNumbers, indexované starou stránkou minus jedna, držící nové číslo stránky od 1, nebo nulu, když ta stránka nepřežila. Projde pole položek pozpátku, takže mazání podstromu nikdy nezneplatní index, který ještě nenavštívilo, a hlásí, co udělalo, přes 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 == tato stránka byla upuštěna
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: smaž celý visící podstrom. False: podrž položku, oloupi její cíl
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;
Příznak DeleteDangling rozhoduje politiku pro destinaci, která se mapovala na nulu, a obě větve jsou záměrné. S True PDFiumPas smaže položku a její celý podstrom, protože uzel osnovy, jehož cíl zmizel, obvykle stojí v čele kapitoly, která zmizela s ním. S False položka přežije s titulem a hierarchií nedotčenými, ale bez svých /Dest a /A, což chcete, když ji člověk bude přecilovat v revizi. Skutečně deformovaný vstup stále selhává nahlas, místo aby se opravil: záporná položka nebo destinace ukazující za konec dodané mapy vrátí False s IssueKind nastaveným na poviInvalidPageMap
Neprůhledné položky a poctivý kompromis
Ne každá položka osnovy má číslo stránky, o kterém PDFiumPas může uvažovat. Tři druhy se přenáší nedotčeny: pojmenované destinace, akce, které nejsou /S /GoTo, a neznámé klíče slovníku přidané tím, co soubor vytvořilo. Ty se načtou s PageNumber rovným nule, podrží své původní bajty v položce a zapíší se zpět doslova, pokud explicitně nezavoláte Retarget na ně
- Pojmenovaná destinace je klíč do stromu názvů dokumentu, takže její správné přemapování znamená vyřešit strom a přepsat cílovou položku, ne hádat na úrovni osnovy
- Akce
/URI,/Launchnebo JavaScript nemá vůbec žádnou sémantiku stránek a nesmí být tiše převedena na Go-To - Klíče specifické pro dodavatele a destinace struktur se podrží, protože upouštění toho, čemu nerozumíte, je způsob, jakým zpáteční cesty ztrácejí data
Cena je reálná a stojí za jasné vyjádření: ApplyPageMap tyto položky přeskočí úplně, takže dokument, jehož záložky všechny používají pojmenované destinace, projde mazáním stránek se svou osnovou strukturálně platnou a sémanticky zastaralou. To je záměrná volba — zastaralý odkaz, který revidující chytí, porazí sebejistě špatný, kterého si nikdo nevšimne. Tříďte-li příchozí soubory před úpravou, inventární průchod v PDF intake review workbench vám řekne, které dokumenty spadají do tého kbelíku
Ukládání: inkrementální revize a pak nezávislé znovunačtení
TPdfOutlineEditor.SaveIncremental připojí řídkou inkrementální revizi, místo aby přepisoval soubor. Položky, které byly načteny, podrží svou původní nepřímou referenci objektu včetně přesné generace, takže stávající křížové reference zůstávají platné; jen položky, které jste přidali, vyberou čerstvé číslo, alokované od jedné za maximálním číslem objektu revize. Katalog se aktualizuje ve stejné revizi a chybějící položka /Outlines se mu přidá, když zdroj neměl vůbec žádnou osnovu
Co se stane po zápisu je část, kterou stojí za zkopírování. PDFiumPas znovuotevře cílový proud zcela nezávislým editorem a srovná znovunačtený strom s tím v paměti — počet položek, tituly, čísla stránek, přípony destinací, podobu akce versus přímé destinace, styly, rozvinutý stav a vztahy rodičů. Jakákoli neshoda nebo jakékoli selhání načtení vymaže cílový proud a vrátí poviVerificationFailure místo podání souboru vypadajícího věrohodně. Šifrované zdroje se odmítnou rovnou s poviEncryptedInput, protože nové tituly a destinace vytvářejí obsah řetězců, který se nedá vytvořit zkopírováním traileru /Encrypt dopředu
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;
Zacházejte s osnovou jako s tím, čím je — řetězeným grafem objektů s vlastními invariantami — a mazání stránek přestane být katastrofou záložek a stane se mapou stránek, kterou podáte jednomu volání metody. TPdfOutlineEditor, ApplyPageMap a ověřený inkrementální zapisovač se dodávají v PDFiumPas od v3.98.0 pro Delphi, C++Builder a Lazarus; plné API si můžete prohlédnout a zkušební verzi stáhnout na produktové stránce PDFium Delphi Component