HotPDF Delphi Component verwijdert een pagina uit een geladen PDF via THotPDF.DeletePage, en sinds versie 2.751.0 snoeit die aanroep ook elke verwijzing op documentniveau weg die nog naar de pagina wijst: named destinations in de /Names /Dests-tree, de oude catalog /Dests-dictionary, bookmark-/GoTo-acties, structuurelementen onder /StructTreeRoot, de ParentTree, OBJR-entries voor annotaties, en linkannotaties op pagina's die blijven. De paginaboom wordt als laatste herbouwd, nadat niets anders het verwijderde object nog kan bereiken
De fout die dit voorkomt is makkelijk te reproduceren en lastig te diagnosticeren. Verwijder de omslagpagina van een getagd rapport, sla op en open het resultaat: Acrobat toont het juiste aantal pagina's, maar het bookmark "Contents" komt nu nergens uit, de toegankelijkheidschecker meldt een structuurelement zonder pagina, en een strenge validator somt een verwijzing naar een vrij object op. Niets aan de paginaboom is fout. Het probleem is dat een PDF-pagina niet alleen een blad van /Pages is; het is een doel waar de halve catalog naar wijst, en het blad weghalen laat al die pointers dangling achter
Waarom is een pagina uit /Kids halen niet genoeg?
Omdat ISO 32000-1 minstens zeven onafhankelijke structuren toestaat een verwijzing naar een paginaobject te bevatten, en slechts één daarvan is de paginaboom. De pagina uit /Kids halen en /Count verlagen voldoet aan §7.7.3, en elke andere verwijzing wordt een pointer naar een object dat ofwel in de xref is vrijgegeven ofwel simpelweg ontbreekt in het herschreven bestand. Een viewer die een van die pointers volgt krijgt null, en wat hij met die null doet is aan de viewer
- De name tree onder
/Names/Dests(§7.7.4, §12.3.2.3) mapt namen op destinationarrays waarvan het eerste element de pagina is - De
/Dests-dictionary van vóór 1.2, direct in de catalog, bevat hetzelfde soort arrays met een naam als key - Outline-items (§12.3.3) bereiken een pagina via een inline
/Destof via een/A-actie met/S /GoToen een/D-array - Structuurelementen (§14.7.2) dragen een
/Pg-key die de pagina noemt waar hun marked content op staat, en hun/K-kinderen kunnen marked-content-references en object references (§14.7.4.3) zijn die aan die pagina vastzitten - De
ParentTree(§14.7.4.4) mapt/StructParents-nummers van pagina's en annotaties terug naar structuurelementen, en een element kan daar leven zonder ooit op de/K-keten vanaf de root te verschijnen - Linkannotaties op andere pagina's (§12.5.6.5) dragen een
/Destof een/GoTo-actie die op de pagina mikt, en de catalog/OpenActionkan hetzelfde doen
Wat ruimt THotPDF.DeletePage op voordat hij de paginaboom aanraakt?
THotPDF.DeletePage(PageIndex) op een geladen document draait eerst de hele verwijzingsronde, markeert dan het paginaobject als verwijderd met DeleteObj, koppelt eventuele widgetannotaties los van de AcroForm-veldboom, schuift de interne paginalijst op, en roept ten slotte RebuildLoadedPageTree aan om /Kids, /Count en het /Parent van elke overlevende pagina te herschrijven. De ronde bezoekt de catalog in een vaste orde: de /Names /Dests-name tree, de ouderwetse /Dests-dictionary, /OpenAction, de outline-tree, /StructTreeRoot met zijn ParentTree, en als laatste de /Annots-arrays van elke pagina die blijft. Elke stap beslist of een verwijzing wordt weggehaald, omgeleid of met rust gelaten, volgens wat de spec die structuur toestaat zonder de pagina. Twee bewakingen gelden voordat daar iets van loopt: DeletePage gooit Invalid page number bij een index buiten bereik en weigert de laatste pagina te verwijderen, want een /Pages-knooppunt met nul kinderen is geen geldige PDF, terwijl DeletePages dezelfde 1-based notatie "1,3-5,7-" neemt als de andere paginabewerkingen op een geladen document en van de hoogste geselecteerde index naar beneden itereert, zodat de indices die u schreef geldig blijven terwijl hij werkt
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Zero-based: laat de omslagpagina vallen. Named destinations,
// bookmarks, structure tree, ParentTree en linkannotaties
// die erop wezen worden gesnoeid voordat de /Pages-boom
// opnieuw wordt opgebouwd.
Pdf.DeletePage(0);
// 1-based bereiknotatie voor batches, intern van de hoogste
// index naar beneden zodat eerdere indices geldig blijven.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Hoe worden named destinations en bookmarks verschillend behandeld?
Named destinations worden verwijderd en bookmarks worden omgeleid, want een naam die niet meer bestaat is een acceptabele uitkomst terwijl een bookmark zonder bestemming een zichtbaar defect is. In de /Names /Dests-tree loopt HotPDF elk knooppunt langs, toetst elke destination, zowel in de kale arrayvorm als in de dictionaryvorm met een /D-key, tegen de verwijderde pagina, en haalt het naam/waarde-paar weg wanneer het eerste element van de array die pagina is. Een knooppunt waarvan /Names en /Kids beide leeg eindigen wordt als verwijderd gemarkeerd en van zijn parent losgekoppeld, zodat de tree nooit holle bladeren houdt. Dezelfde toets loopt over de ouderwetse catalog /Dests-dictionary, en de catalog /OpenAction wordt simpelweg weggehaald als die opende op de verwijderde pagina. Eén grens hier: wanneer een name-tree-knooppunt entries verliest, verwijdert HotPDF het /Limits-paar van dat knooppunt in plaats van de nieuwe laagste en hoogste keys te herberekenen, en terwijl viewers namen prima oplossen zonder dat paar, kan een strenge conformiteitschecker die ISO 32000-1 §7.9.6 leest een niet-root-knooppunt zonder /Limits markeren
Outline-items gaan de andere kant op. RetargetOutlineDestinations loopt /First en /Next af vanaf de outline-root, met een bezochtlijst en een dieptelimiet van 128 zodat een corrupte cyclische tree de aanroep niet kan laten hangen, en voor elke /Dest-array of /GoTo-actie-/D-array die op de pagina mikt vervangt hij het eerste element door NearestRetainedPage: de pagina die op de verwijderde volgde, of de pagina ervoor wanneer de verwijderde pagina de laatste was. De view-parameters na de paginareferentie blijven zoals ze waren. Een bookmark die naar een verwijderde hoofdstukopener wees komt dus uit op de eerste pagina van wat overblijft in plaats van uit de zijbalk te verdwijnen, en dat is het gedrag dat reviewers van een uitgedund document verwachten. De destinationtoets matcht wel alleen expliciete arrays: een outline-item waarvan het /Dest een naamstring is die vroeger naar de verwijderde pagina oploste wordt niet omgeleid, want de name-tree-entry is weg en de verwijzing lost nu naar niets op in plaats van naar een vrijgegeven object, dus behandelt de viewer hem als een dode bookmark. De mechanica van de outline-tree zelf, /First, /Next en de niet voor de hand liggende /Count-semantiek, staat in de gids over het toevoegen van bookmarks en named destinations aan een geladen PDF
// Controleer de ronde in plaats van erop te vertrouwen.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Een bookmark die op de omslag mikte lost nu op naar de
// pagina die erop volgde (zero-based index 0 na de delete).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Wat gebeurt er met de structure tree en de ParentTree?
Structuurelementen die alleen bestaan vanwege de verwijderde pagina worden verwijderd, en elementen die meerdere pagina's overspannen verliezen hun /Pg-key maar houden hun kinderen. PruneStructureElement daalt af door de /K-keten vanaf /StructTreeRoot tot een diepte van 128, en behandelt zowel de arrayvorm als de enkele-dictionaryvorm van /K die §14.7.2 toestaat. Voor elk element snoeit hij eerst de kinderen en beoordeelt dan het element zelf: als het snoeien zijn /K leegmaakte, wordt het element als verwijderd gemarkeerd en laat zijn parent het vallen. Als het /Pg van het element zelf de verwijderde pagina noemt en het element heeft nog kinderen plus een /P-parent, dan wordt alleen /Pg verwijderd, want een /Pg op een element is de standaardpagina voor zijn marked-content-kinderen en die kinderen kunnen expliciet naar andere pagina's verwijzen. Alleen een element waarvan het /Pg de verwijderde pagina is en waar niets meer onder hangt wordt helemaal verwijderd
De ParentTree krijgt dezelfde behandeling, en de reden is degene die tijdens de ontwikkeling beet: een structuurelement kan bereikbaar zijn vanuit de ParentTree en nergens anders. De number tree mapt /StructParents-integers op ofwel één element ofwel een array van elementen, en PruneParentTreeNode draait PruneStructureElement over elke waarde die hij vindt, haalt waarden weg die weggesnoeid zijn, verwijdert een /Nums-paar wanneer zijn waardearray leeg is, en koppelt een knooppunt waarvan /Nums en /Kids beide weg zijn los. Alleen de afstammelingen van /K snoeien zou die verweesde elementen hebben achtergelaten met een /Pg naar een vrijgegeven pagina en met /MCR-kinderen naar vrijgegeven marked-content-references. Als u tekst in structuurorde extraheert maakt dat direct uit: tekstextractie in structuurorde loopt precies door deze trees, en een element met een null /Pg is een alinea die stilzwijgend uit de leesorde valt
Welke linkannotaties op overlevende pagina's worden verwijderd?
Elke linkannotatie op een pagina die blijft, waarvan de /Dest-array of /GoTo-actie naar de verwijderde pagina wijst, wordt verwijderd samen met het eigendom in de structure tree. RemoveRetainedPageDestinationAnnotations loopt de /Annots-array af van elke pagina behalve het doel, past dezelfde destinationtoets toe als bij outlines, markeert een passende annotatie als verwijderd, haalt hem uit de array, en roept dan PruneAnnotationReferencesInStructureTree aan zodat de OBJR-dictionary waarvan /Obj die annotatie noemde uit zijn structuurelement wordt verwijderd, waarbij het element zelf verdwijnt als de OBJR zijn enige kind was. De OBJR laten staan zou §14.7.4.3 schenden, die eist dat /Obj naar een bestaand object verwijst, en zou in een PDF/UA-check opduiken als een getagde link zonder annotatie erachter. Let op de asymmetrie met bookmarks: links worden verwijderd, niet omgeleid. Een kruisverwijzing in de broodtekst die "see page 3" zei is fout zodra pagina 3 weg is, en hem op pagina 4 richten zou een leugen zijn op een manier die een bookmark die op het dichtstbijzijnde hoofdstuk landt niet is, dus als uw workflow die links wil behouden, moet u ze zelf omleiden voordat u DeletePage aanroept
Waarom mag een verwijderde /MCR of /OBJR nooit als vrij geregistreerd worden?
Omdat marked-content-references en object references meestal directe dictionaries zijn binnen de /K-array van hun parent-element, en het register voor incrementele wijzigingen een direct object oplost naar het dichtstbijzijnde indirecte object dat het bevat. Wanneer RemoveArrayItem een kind uit een /K-array haalt, geeft hij het in-memory object alleen vrij als het een THPDFLink of een niet-indirecte waarde was, en MarkRemovedObject registreert een object alleen voor de free list wanneer zijn objectnummer groter dan nul is. De eerste versie van deze ronde maakte dat onderscheid niet, en het effect in een incrementele save was precies wat het register hoort te doen: RegisterIncrementalChange liep van de directe /MCR omhoog naar zijn graaftransactiewortel, het structuurelement dat hem bezat, en schreef dat element als null uit. Een document dat één pagina kwijtraakte kwam terug met de getagde inhoud op de andere pagina's stilzwijgend ontdaan van tags. De enige juiste zet voor een direct kind is zijn container dirty markeren via TouchContainer zodat de container wordt herschreven, en de free list met rust te laten
// Incrementale update: alleen de aangeraakte containers en het
// vrijgegeven paginaobject komen in de aangehangen sectie terecht.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Structuurelementen die blijven en waarvan /K een directe /MCR
// kwijtraakte worden ter plekke herschreven, nooit als null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Dezelfde voorzichtigheid bepaalt wat DeletePage op een geladen document bewust niet vrijgeeft. Contentstreams, XObjects en de niet-widget-annotaties van de verwijderde pagina blijven als objecten staan, want een geladen bestand kan ze allemaal delen met een pagina die blijft en er is geen goedkope manier om op het moment van verwijderen het tegendeel te bewijzen. De paginaboomverwijzing weghalen is genoeg voor correctheid; de bytes die die objecten nog innemen zijn een aparte vraag, en de object dependency graph en retained-bytes-analyse is het gereedschap om te meten wat een uitgedund document nog draagt
DeletePage versus DeleteLoadedPage: welke moet u aanroepen?
Roep DeletePage aan voor elke paginaverwijdering die de gebruiker ziet, en houd DeleteLoadedPage voor het geval waarin het hele document opnieuw wordt opgemaakt en geen enkele verwijzing op documentniveau het bewaren waard is. THotPDF.DeleteLoadedPage(PageIndex), toegevoegd in versie 2.508.0, is de lichtgewicht variant: hij schuift de interne paginalijst op, roept RebuildLoadedKidsArray aan om /Kids en /Count te herschrijven, maakt de gerenderde paginacache ongeldig en vuurt OnLoadedDocumentModified af. Hij loopt niet door de name tree, de outlines, de structure tree of de annotaties van andere pagina's, en markeert het paginaobject niet als verwijderd. Dat is het juiste gereedschap binnen N-up imposition, waar HotPDF vers opgemaakte vellen toevoegt en daarna elke originele pagina laat vallen met DeleteLoadedPage(0): de bronpagina's worden in hun geheel vervangen, en de velinhoud verwijst naar hun resources in plaats van naar de paginaobjecten. Voor het gewone klusje "pagina 7 uit dit contract halen" is DeletePage de enige aanroep die een getagd, van bookmarks voorzien en onderling gelinkt document consistent genoeg achterlaat om een validator te halen, zowel bij een volledige herschrijving via SaveLoadedDocument als bij een incrementele update via SaveIncrementalUpdate. Beide methoden zitten in de HotPDF Delphi Component voor Delphi en C++Builder, zonder externe viewer-runtime of afhankelijkheid