HotPDF Delphi Component maže stránku z načteného PDF přes THotPDF.DeletePage a od verze 2.751.0 to volání taky prořezává každou referenci na úrovni dokumentu, která na stránku pořád míří: pojmenované destinace ve stromu /Names /Dests, starý slovník /Dests v catalogu, /GoTo akce záložek, strukturní elementy pod /StructTreeRoot, ParentTree, OBJR položky anotací a link anotace na přežívajících stránkách. Strom stránek se přestavuje naposled, až už se k smazanému objektu nic jiného nedostane
Selhání, kterému to brání, se snadno reprodukuje a těžko diagnostikuje. Smažte obálkovou stránku tagovaného reportu, uložte a otevřete výsledek: Acrobat ukazuje správný počet stránek, ale záložka „Contents" teď přistává nikde, accessibility checker hlásí strukturní element bez stránky a přísný validátor vypíše referenci na free objekt. Ve stromu stránek není nic špatně. Problém je v tom, že PDF stránka není jen list /Pages; je to cíl, na který míří půlka catalogu, a odstranění listu nechává každý z těch pointerů viset
Proč odebrání stránky z /Kids nestačí?
Protože ISO 32000-1 dovoluje aspoň sedmi nezávislým strukturám držet referenci na objekt stránky a jen jedna z nich je strom stránek. Vypuštění stránky z /Kids a snížení /Count uspokojí §7.7.3 a každá jiná reference se stane pointerem na objekt, který je buď uvolněný v xrefu, nebo prostě chybí v přepsaném souboru. Prohlížeč, který jeden z těch pointerů sleduje, dostane null a co s tím null udělá, je na něm
- Strom jmen pod
/Names/Dests(§7.7.4, §12.3.2.3) mapuje jména na destination pole, jejichž první prvek je stránka - Slovník
/Destsz před-1.2 přímo v catalogu drží stejný druh polí klíčovaných jménem - Položky outline (§12.3.3) se ke stránce dostávají buď přes inline
/Dest, nebo přes akci/As/S /GoToa polem/D - Strukturní elementy (§14.7.2) nesou klíč
/Pgpojmenovávající stránku, na které jejich marked content žije, a jejich děti/Kmohou být marked-content reference a object reference (§14.7.4.3) svázané s tou stránkou ParentTree(§14.7.4.4) mapuje čísla/StructParentsstránek a anotací zpátky na strukturní elementy a element tam může bydlet, aniž by se na řetězci/Kod kořene vůbec objevil- Link anotace na jiných stránkách (§12.5.6.5) nesou
/Destnebo akci/GoTomířící na stránku a catalog/OpenActionmůže dělat totéž
Co THotPDF.DeletePage uklidí, než sahne ke stromu stránek?
THotPDF.DeletePage(PageIndex) na načteném dokumentu nejdřív protáhne celý referenční sweep, pak označí objekt stránky jako smazaný přes DeleteObj, odpojí případné widget anotace ze stromu polí AcroForm, posune interní pole stránek a nakonec zavolá RebuildLoadedPageTree k přepsání /Kids, /Count a /Parent každé přežívající stránky. Sweep navštěvuje catalog v pevném pořadí: strom jmen /Names /Dests, starý slovník /Dests, /OpenAction, strom outline, /StructTreeRoot s jeho ParentTree a naposled pole /Annots každé stránky, která zůstává. Každý krok rozhoduje, zda se reference odstraní, přecílí, nebo nechá na pokoji, podle toho, co spec dovoluje dané struktuře udělat bez stránky. Dvě hlídky platí, než se cokoli z toho spustí: DeletePage hodí Invalid page number pro index mimo rozsah a odmítá odstranit poslední stránku, protože uzel /Pages s nulou dětí není validní PDF, zatímco DeletePages bere stejnou one-based notaci "1,3-5,7-" jako ostatní operace stránek načteného dokumentu a iteruje od nejvyššího vybraného indexu dolů, takže indexy, které jste napsali, zůstávají validní, zatímco pracuje
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Zero-based: zahoď obálkovou stránku. Pojmenované destinace,
// záložky, structure tree, ParentTree a link anotace,
// které na ni mířily, se prořežou, než se
// přestaví strom /Pages.
Pdf.DeletePage(0);
// One-based syntax rozsahu pro dávky, nejvyšší index první
// interně, takže dřívější indexy zůstávají validní.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Jak se pojmenované destinace a záložky obsluhují rozdílně?
Pojmenované destinace se odstraňují a záložky se přecílují, protože jméno, které už neexistuje, je přijatelný výsledek, kdežto záložka bez destinace je viditelná vada. Ve stromu /Names /Dests HotPDF projde každý uzel, otestuje každou destinaci, v podobě holého pole i ve slovníkové podobě s klíčem /D, proti smazané stránce a odstraní pár jméno/hodnota, když první prvek pole je ta stránka. Uzel, jehož /Names i /Kids skončí prázdné, se označí jako smazaný a odpojí se od rodiče, takže strom nikdy nedrží duté listy. Tentýž test běží přes starý slovník /Dests v catalogu a catalog /OpenAction se prostě zahodí, pokud otevíral smazanou stránku. Jedna hranice tady: když uzel stromu jmen ztratí položky, HotPDF maže pár /Limits toho uzlu místo přepočítání nových nejnižších a nejvyšších klíčů, a zatímco prohlížeče rozřešují jména bez něj dobře, přísný conformance checker čtoucí ISO 32000-1 §7.9.6 může označit nerootový uzel, kterému /Limits chybí
Položky outline jdou opačnou cestou. RetargetOutlineDestinations prochází /First a /Next od kořene outline, se seznamem navštívených a limitem hloubky 128, takže poškozený cyklický strom nemůže volání zablokovat, a pro každé pole /Dest nebo pole /D akce /GoTo mířící na stránku vymění první prvek za NearestRetainedPage: stránku, která následovala po smazané, nebo stránku před ní, když byla smazaná poslední. View parametry za referencí stránky se nechávají, jak byly. Záložka, která mířila na smazaný úvod kapitoly, proto přistane na první stránce toho, co zbývá, místo aby zmizela ze sidebaru, což je chování, které recensenti od ořezaného dokumentu čekají. Test destinace ale matchuje jen explicitní pole: položka outline, jejíž /Dest je name string, který se dřív rozřešoval na smazanou stránku, se nepřecíluje, protože položka stromu jmen je pryč a reference se teď rozřeší na nic místo na uvolněný objekt, takže prohlížeč ji bere jako mrtvou záložku. Mechanika samotného stromu outline, /First, /Next a ne intuitivní sémantika /Count, je rozebraná v průvodci přidáváním záložek a pojmenovaných destinací na načtené PDF
// Ověřte sweep místo toho, abyste mu věřili.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Záložka mířící na obálku se teď rozřeší na
// stránku, která po ní následovala (zero-based index 0 po smazání).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Co se stane se stromem struktur a ParentTree?
Strukturní elementy, které existují jen kvůli smazané stránce, se odstraňují a elementy přesahující několik stránek ztratí svůj klíč /Pg, ale nechají si děti. PruneStructureElement sestupuje řetězcem /K z /StructTreeRoot do hloubky 128 a obsluhuje jak podobu pole, tak jedno-slovníkovou podobu /K, kterou §14.7.2 dovoluje. U každého elementu nejdřív prořeže děti a pak vyhodnotí element samotný: pokud prořezání vyprázdnilo jeho /K, element se označí jako smazaný a rodič ho upustí. Pokud vlastní /Pg elementu pojmenovává smazanou stránku a element má pořád děti plus rodiče /P, odstraňuje se jen /Pg, protože /Pg na elementu je defaultní stránka pro jeho marked-content děti a ta děti mohou reference jiné stránky explicitně. Jen element, jehož /Pg je smazaná stránka a pod kterým už nic nezbývá, se odstraňuje rovnou
ParentTree dostane stejnou péči a důvod je ten, který kousl během vývoje: strukturní element může být dosažitelný z ParentTree a nikde jinde. Číselný strom mapuje celá čísla /StructParents na jediný element nebo pole elementů a PruneParentTreeNode protáhne PruneStructureElement přes každou hodnotu, kterou najde, odstraní hodnoty, které byly prořezané pryč, maže pár /Nums, když je jeho pole hodnot prázdné, a odpojí uzel, jehož /Nums i /Kids zmizely. Prořezat jen potomky /K by zanechalo ty osiřelé elementy mířící přes /Pg na uvolněnou stránku a přes jejich děti /MCR na uvolněné marked-content reference. Pokud extrahujete text v pořadí struktur, dotýká se to vás přímo: extrakce textu v pořadí struktur prochází přesně tyhle stromy a element s null /Pg je odstavec, který potichu vypadne z reading orderu
Které link anotace na přežívajících stránkách se odstraňují?
Jakákoli link anotace na přežívající stránce, jejíž pole /Dest nebo akce /GoTo míří na smazanou stránku, se odstraňuje spolu s jejím vlastnictvím ve stromu struktur. RemoveRetainedPageDestinationAnnotations projde pole /Annots každé stránky jiné než cíl, aplikuje tentýž destination test jako u outline, označí odpovídající anotaci jako smazanou, vyhodí ji z pole a pak zavolá PruneAnnotationReferencesInStructureTree, takže slovník OBJR, jehož /Obj pojmenovával tu anotaci, se odstraní z jejího strukturního elementu, přičemž samotný element se odstraní, pokud byl OBJR jeho jediným dítětem. Nechat OBJR na místě by porušilo §14.7.4.3, který vyžaduje, aby /Obj referencovalo existující objekt, a ukázalo by se v kontrole PDF/UA jako tagovaný link bez anotace za ním. Všimněte si asymetrie se záložkami: linky se odstraňují, nepřecílí se. Cross-reference v textu, která říkala „viz stránku 3", je špatně, jakmile stránka 3 zmizí, a namířit ji na stránku 4 by byla lež způsobem, jakým záložka přistávající na nejbližší kapitole lež není, takže pokud váš workflow potřebuje ty linky zachovat, přecílte je sami před voláním DeletePage
Proč se odstraněné /MCR nebo /OBJR nesmí nikdy registrovat jako free?
Protože marked-content reference a object reference jsou obvykle přímé slovníky uvnitř pole /K jejich rodičovského elementu a registr inkrementálních změn rozřešuje přímý objekt na nejbližší nepřímý objekt, který ho obsahuje. Když RemoveArrayItem vyhazuje dítě z pole /K, uvolní in-memory objekt jen tehdy, pokud to byl THPDFLink nebo non-indirect hodnota, a MarkRemovedObject registruje objekt do free listy jen tehdy, když jeho číslo objektu je větší než nula. První verze tohohle sweepu neudělala to rozlišení a efekt v inkrementálním uložení byl přesně to, k čemu je registr stavěný: RegisterIncrementalChange došel z přímého /MCR nahoru ke svému graph transaction rootu, který byl přežívající strukturní element, který ho vlastnil, a zapsal ten element jako null. Dokument, který ztratil jednu stránku, se vrátil s obsahem na ostatních stránkách, který potichu přišel o tagy. Jediný správný tah pro přímé dítě je označit jeho kontejner dirty přes TouchContainer, aby se kontejner přepsal, a nechat free listu na pokoji
// Inkrementální update: jen dotčené kontejnery a
// uvolněný objekt stránky přistávají v připojované sekci.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Přežívající strukturní elementy, jejichž /K ztratilo přímé /MCR,
// se přepíší na místě, nikdy se nezapisují jako null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Stejná opatrnost tvaruje to, co DeletePage záměrně neuvolňuje na načteném dokumentu. Content streamy, XObjects a non-widget anotace smazané stránky se nechávají jako objekty, protože načtený soubor může se stránkou, která zůstává, sdílet kterýkoli z nich a neexistuje levný způsob, jak to v momentě mazání dokázat opačně. Odstranění reference stromu stránek stačí pro správnost; bajty, které ty objekty pořád obsazují, jsou oddělená otázka a graf závislostí objektů a analýza retained bytes je nástroj na měření toho, co ořezaný dokument pořád nese
DeletePage versus DeleteLoadedPage: které volat?
DeletePage volejte pro jakékoli uživatelsky viditelné mazání stránky a DeleteLoadedPage si nechejte pro případ, kdy se celý dokument přeformovává a žádná reference na úrovni dokumentu nestojí za držení. THotPDF.DeleteLoadedPage(PageIndex), přidaná ve verzi 2.508.0, je lehká varianta: posune interní pole stránek, zavolá RebuildLoadedKidsArray k přepsání /Kids a /Count, invaliduje cache vykreslených stránek a odpálí OnLoadedDocumentModified. Neprochází strom jmen, outline, strom struktur ani anotace jiných stránek a neoznačuje objekt stránky jako smazaný. To je správný nástroj uvnitř N-up imposition, kde HotPDF přidává čerstvě skládané listy a pak zahazuje každou původní stránku přes DeleteLoadedPage(0): zdrojové stránky se vyměňují celostně a obsah listu referencuje jejich zdroje, ne objekty stránek. Pro obyčejný úkol „odeberte stránku 7 z téhle smlouvy" je DeletePage jediné volání, které zanechá tagovaný, se záložkami a cross-linky provázaný dokument dostatečně konzistentní na to, aby prošel validátorem, jak v plném přepisu přes SaveLoadedDocument, tak v inkrementálním updatu přes SaveIncrementalUpdate. Obě metody dodává HotPDF Delphi Component pro Delphi a C++Builder, bez potřeby externího viewer runtimeu nebo závislosti