HotPDF Delphi Component maže stránku z načítaného PDF cez THotPDF.DeletePage a od verzie 2.751.0 to volanie prerezáva aj každú referenciu na úrovni dokumentu, ktorá na stránku ešte ukazuje: named destinations v strome /Names /Dests, legacy slovník /Dests v katalógu, záložkové akcie /GoTo, structure elementy pod /StructTreeRoot, ParentTree, záznamy OBJR pre anotácie a link anotácie na prežívajúcich stránkach. Page tree sa prebuduje ako posledný, keď sa k zmazanému objektu už nič nemôže dostať
Zlyhanie, ktorému to zabraňuje, sa ľahko reprodukuje a ťažko diagnostikuje. Zmažte obálku tagovaného reportu, uložte a otvorte výsledok: Acrobat ukáže správny počet strán, ale záložka "Contents" teraz nevedie nikam, kontrola prístupnosti hlási structure element bez stránky a prísny validátor vypíše referenciu na uvoľnený objekt. V page tree nie je nič zlé. Problém je v tom, že PDF stránka nie je len list /Pages; je to cieľ, na ktorý ukazuje polovica katalógu, a odstránenie listu nechá každý z tých pointerov visieť
Prečo odstránenie stránky z /Kids nestačí?
Pretože ISO 32000-1 dovoľuje, aby na objekt stránky odkazovalo najmenej sedem nezávislých štruktúr, a len jedna z nich je page tree. Vyhodenie stránky z /Kids a zníženie /Count vyhovuje §7.7.3 a každá ďalšia referencia sa stane pointerom na objekt, ktorý je buď uvoľnený v xref, alebo jednoducho chýba v prepísanom súbore. Viewer, ktorý jeden z tých pointerov sleduje, dostane null, a čo s tým null-om urobí, je na ňom
- Strom mien pod
/Names/Dests(§7.7.4, §12.3.2.3) mapuje mená na polia destinácií, ktorých prvý element je stránka - Slovník
/Destsspred verzie 1.2 priamo v katalógu drží tie isté druhy polí kľúčovaných menom - Položky outline (§12.3.3) sa k stránke dostanú buď cez inline
/Dest, alebo cez akciu/As/S /GoToa poľom/D - Structure elementy (§14.7.2) nesú kľúč
/Pg, ktorý menuje stránku, na ktorej žije ich marked content, a ich potomkovia/Kmôžu byť marked-content referencie a object referencie (§14.7.4.3) viazané na tú stránku ParentTree(§14.7.4.4) mapuje čísla/StructParentsstránok a anotácií späť na structure elementy a element tam môže žiť bez toho, aby sa vôbec objavil na reťazci/Kod koreňa- Link anotácie na iných stránkach (§12.5.6.5) nesú
/Destalebo akciu/GoTomieriacu na tú stránku a to isté môže robiť/OpenActionv katalógu
Čo THotPDF.DeletePage upratuje, než sa dotkne page tree?
THotPDF.DeletePage(PageIndex) na načítanom dokumente najprv spustí celý priechod referencií, potom označí objekt stránky za zmazaný cez DeleteObj, odpojí prípadné widget anotácie zo stromu polí AcroForm, posunie interné pole stránok a nakoniec zavolá RebuildLoadedPageTree, aby prepísal /Kids, /Count a /Parent každej prežívajúcej stránky. Priechod navštevuje katalóg v pevnom poradí: strom mien /Names /Dests, starý slovník /Dests, /OpenAction, strom outline, /StructTreeRoot s jeho ParentTree a napokon polia /Annots každej stránky, ktorá zostáva. Každý krok rozhoduje, či sa referencia odstráni, presmeruje alebo nechá na pokoji, podľa toho, čo špecifikácia tej štruktúre dovoľuje robiť bez stránky. Pred tým všetkým platia dve stráže: DeletePage vyhodí Invalid page number pri indexe mimo rozsahu a odmietne odstrániť poslednú stránku, pretože uzol /Pages s nulou potomkov nie je platné PDF, kým DeletePages berie ten istý one-based zápis "1,3-5,7-" ako ostatné operácie so stránkami načítaného dokumentu a iteruje od najvyššieho vybraného indexu nadol, aby indexy, ktoré ste napísali, zostali počas práce platné
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Zero-based: vyhoď obálku. Named destinations,
// záložky, structure tree, ParentTree a link
// anotácie, ktoré na ňu ukazovali, sa prerežú skôr,
// než sa prebuduje strom /Pages.
Pdf.DeletePage(0);
// One-based syntax rozsahov pre dávky, interne od
// najvyššieho indexu, aby skoršie indexy zostali platné.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Ako sa named destinations a záložky spracúvajú odlišne?
Named destinations sa odstraňujú a záložky sa presmerujú, pretože meno, ktoré už neexistuje, je prijateľný výsledok, kým záložka bez cieľa je viditeľný defekt. V strome /Names /Dests HotPDF prejde každý uzol, otestuje každú destináciu, v holom tvare poľa aj v tvare slovníka s kľúčom /D, proti zmazanej stránke a dvojicu meno/hodnota odstráni, keď je prvým elementom poľa tá stránka. Uzol, ktorého /Names aj /Kids skončia prázdne, sa označí za zmazaný a odpojí od rodiča, takže strom nikdy nedrží duté listy. Ten istý test beží nad starým slovníkom /Dests v katalógu a /OpenAction v katalógu sa jednoducho vyhodí, ak sa otváral na zmazanej stránke. Jedna hranica tu je: keď uzol stromu mien stratí záznamy, HotPDF zmaže jeho dvojicu /Limits namiesto prepočítania nových najnižších a najvyšších kľúčov, a hoci viewery mená bez nej rozkladajú v poriadku, prísna kontrola konformity čítajúca ISO 32000-1 §7.9.6 môže označiť ne-koreňový uzol, ktorému /Limits chýba
Položky outline idú opačným smerom. RetargetOutlineDestinations prechádza /First a /Next od koreňa outline, s poľom navštívených a limitom hĺbky 128, aby pokazený cyklický strom nemohol volanie zablokovať, a pri každom poli /Dest alebo poli /D akcie /GoTo, ktoré mieri na tú stránku, nahradí prvý element hodnotou NearestRetainedPage: teda stránkou, ktorá nasledovala po zmazanej, alebo tou pred ňou, ak bola zmazaná stránka posledná. Parametre zobrazenia za referenciou na stránku zostávajú tak, ako boli. Záložka, ktorá ukazovala na zmazaný úvod kapitoly, tak pristane na prvej strane toho, čo zostalo, namiesto toho, aby zmizla z bočného panela, čo je správanie, aké recenzenti od orezaného dokumentu očakávajú. Test destinácie však páruje len explicitné polia: položka outline, ktorej /Dest je meno v reťazci, ktoré predtým odkazovalo na zmazanú stránku, sa nepresmeruje, pretože záznam v strome mien je preč a referencia sa teraz rozloží na nič a nie na uvoľnený objekt, takže ju viewer berie ako mŕtvu záložku. Mechanika samotného stromu outline, /First, /Next a nie celkom zjavná sémantika /Count, je pokrytá v návode na pridávanie záložiek a named destinations do načítaného PDF
// Priechod overujte, namiesto toho, aby ste mu verili.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Záložka, ktorá mierila na obálku, sa teraz rozloží na
// stránku, ktorá po nej nasledovala (zero-based index 0 po zmazaní).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Čo sa stane so structure tree a ParentTree?
Structure elementy, ktoré existujú len kvôli zmazanej stránke, sa odstránia a elementy, ktoré sa tiahnu cez niekoľko stránok, stratia svoj kľúč /Pg, ale ponechajú si potomkov. PruneStructureElement zostupuje reťazcom /K od /StructTreeRoot do hĺbky 128 a spracúva tvar poľa aj tvar jediného slovníka, ktoré §14.7.2 pre /K pripúšťa. Pre každý element najprv prerezá potomkov a potom zhodnotí samotný element: ak prerezanie vyprázdnilo jeho /K, element sa označí za zmazaný a jeho rodič ho vyhodí. Ak /Pg samotného elementu menuje zmazanú stránku a element má ešte potomkov plus rodiča /P, odstráni sa len /Pg, pretože /Pg na elemente je predvolená stránka pre jeho marked-content potomkov a tí môžu explicitne odkazovať na iné stránky. Rovno sa odstráni len element, ktorého /Pg je zmazaná stránka a pod ktorým už nič neostalo
ParentTree dostane to isté spracovanie a dôvod je ten, ktorý počas vývoja zabolel: structure element môže byť dosiahnuteľný z ParentTree a odnikiaľ inokadiaľ. Number tree mapuje celé čísla /StructParents buď na jediný element, alebo na pole elementov, a PruneParentTreeNode spustí PruneStructureElement nad každou hodnotou, ktorú nájde, odstráni hodnoty, ktoré boli prerezané, zmaže dvojicu /Nums, keď je jej pole hodnôt prázdne, a odpojí uzol, ktorému zmizli aj /Nums, aj /Kids. Prerezanie len potomkov /K by nechalo tie osirelé elementy ukazovať cez /Pg na uvoľnenú stránku a cez svojich potomkov /MCR na uvoľnené marked-content referencie. Ak extrahujete text v poradí štruktúry, to je priamo dôležité: extrakcia textu v poradí štruktúry prechádza presne tie stromy a element s nulovým /Pg je odsek, ktorý z poradia čítania potichu vypadne
Ktoré link anotácie na prežívajúcich stránkach sa odstraňujú?
Každá link anotácia na zachovanej stránke, ktorej pole /Dest alebo akcia /GoTo mieri na zmazanú stránku, sa odstráni spolu so svojím vlastníctvom v structure tree. RemoveRetainedPageDestinationAnnotations prejde pole /Annots každej stránky okrem cieľovej, nasadí ten istý test destinácie, aký sa používa pre outline, zhodnú anotáciu označí za zmazanú, vyhodí ju z poľa a potom zavolá PruneAnnotationReferencesInStructureTree, takže slovník OBJR, ktorého /Obj menoval tú anotáciu, sa odstráni z jeho structure elementu, a ak bol OBJR jeho jediným potomkom, odstráni sa aj element. Nechať OBJR na mieste by porušilo §14.7.4.3, ktorý vyžaduje, aby /Obj odkazoval na existujúci objekt, a v PDF/UA kontrole by sa to ukázalo ako tagovaný odkaz bez anotácie za ním. Všimnite si asymetriu so záložkami: linky sa odstraňujú, nepresmerúvajú. Krížový odkaz v texte, ktorý hovoril "pozri stranu 3", je po zmazaní strany 3 nesprávny, a nasmerovať ho na stranu 4 by bola lož iným spôsobom, než akým je záložka pristávajúca na najbližšej kapitole, takže ak váš workflow potrebuje tie linky zachovať, presmerujte ich sami pred volaním DeletePage
Prečo sa odstránený /MCR alebo /OBJR nikdy nesmie zaregistrovať ako uvoľnený?
Pretože marked-content referencie a object referencie sú zvyčajne priame slovníky v poli /K svojho rodičovského elementu a register inkrementálnych zmien rozkladá priamy objekt na najbližší nepriamy objekt, ktorý ho obsahuje. Keď RemoveArrayItem vyhodí potomka z poľa /K, uvoľní objekt v pamäti len vtedy, ak to bol THPDFLink alebo nepriama hodnota, a MarkRemovedObject zaregistruje objekt do free listu len vtedy, keď je jeho číslo objektu väčšie než nula. Prvá verzia tohto priechodu ten rozdiel nerobila a efektom pri inkrementálnom uložení bolo presne to, na čo je register navrhnutý: RegisterIncrementalChange prešiel od priameho /MCR nahor k svojmu graph transaction rootu, ktorým bol zachovaný structure element, ktorý ho vlastnil, a zapísal ten element ako null. Dokument, ktorý stratil jednu stránku, sa vrátil s tagovaným obsahom na ostatných stránkach potichu odtagovaným. Jediný správny krok pre priameho potomka je označiť jeho kontajner ako dirty cez TouchContainer, aby sa kontajner prepísal, a free list nechať na pokoji
// Inkrementálna aktualizácia: do pripojenej sekcie sa dostanú
// len dotknuté kontajnery a uvoľnený objekt stránky.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Zachované structure elementy, ktorých /K stratilo priamy /MCR,
// sa prepíšu na mieste, nikdy sa nezapíšu ako null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Tá istá opatrnosť formuje to, čo DeletePage na načítanom dokumente zámerne neuvoľňuje. Content streamy, XObjecty a ne-widget anotácie zmazanej stránky zostávajú ako objekty, pretože načítaný súbor môže ktorýkoľvek z nich zdieľať so stránkou, ktorá zostáva, a v čase mazania neexistuje lacný spôsob, ako dokázať opak. Odstránenie referencie z page tree stačí pre korektnosť; bajty, ktoré tie objekty ešte zaberajú, sú samostatná otázka a object dependency graph a analýza retained bytes je nástroj na meranie toho, čo orezaný dokument ešte nesie
DeletePage verzus DeleteLoadedPage: ktoré máte zavolať?
DeletePage volajte pri každom mazaní stránky smerom k používateľovi a DeleteLoadedPage si nechajte na prípad, keď sa celý dokument prekladá nanovo a žiadna referencia na úrovni dokumentu nestojí za zachovanie. THotPDF.DeleteLoadedPage(PageIndex), pridané vo verzii 2.508.0, je tá odľahčená varianta: posunie interné pole stránok, zavolá RebuildLoadedKidsArray, aby prepísal /Kids a /Count, zneplatní cache vykreslených stránok a spustí OnLoadedDocumentModified. Neprechádza strom mien, outline, structure tree ani anotácie iných stránok a objekt stránky neoznačí za zmazaný. To je správny nástroj vnútri N-up imposition, kde HotPDF pripojí nanovo poskladané hárky a potom vyhodí každú pôvodnú stránku cez DeleteLoadedPage(0): zdrojové stránky sa nahradzujú hromadne a obsah hárku odkazuje na ich zdroje, nie na objekty stránok. Pri bežnej úlohe "odstráň stranu 7 z tejto zmluvy" je DeletePage jediné volanie, ktoré nechá tagovaný, záložkami vybavený a krížovo prelinkovaný dokument dosť konzistentný na to, aby prešiel validátorom, a to pri úplnom prepise cez SaveLoadedDocument aj pri inkrementálnej aktualizácii cez SaveIncrementalUpdate. Obe metódy sú súčasťou HotPDF Delphi Component pre Delphi a C++Builder, bez potreby externého viewer runtime alebo inej závislosti