HotPDF Delphi Component изтрива страница от зареден PDF през THotPDF.DeletePage, и от версия 2.751.0 насам това извикване кастри и всяка документна справка, която все още сочи към страницата: именувани дестинации в /Names /Dests дървото, legacy каталожния речник /Dests, bookmark /GoTo действия, structure елементи под /StructTreeRoot, ParentTree, OBJR записи за анотации и линк анотации на оцелелите страници. Page tree-то се пренаписва последно, след като нищо друго вече не може да достигне изтрития обект
Провалът, който това предотвратява, е лесен за възпроизвеждане и труден за диагностициране. Изтрийте корицата на тагнат доклад, запишете и отворете резултата: Acrobat показва правилния брой страници, но отметката „Contents" вече води никъде, accessibility проверката докладва structure елемент без страница, а строг валидатор изброява справка към свободен обект. Нищо в page tree-то не е грешно. Проблемът е, че PDF страница не е само листо на /Pages; тя е цел, към която сочи половината каталог, и махането на листото оставя всеки един от тези пойнтери да виси
Защо махането на страница от /Kids не е достатъчно?
Защото ISO 32000-1 позволява поне седем независими структури да държат справка към page обект, и само една от тях е page tree-то. Махането на страницата от /Kids и декрементирането на /Count задоволява §7.7.3, а всяка друга справка става пойнтер към обект, който или е освободен в xref, или просто липсва в пренаписания файл. Viewer, който последва един от тези пойнтери, получава null, а какво прави с този null е работа на viewer-а
- Name tree-то под
/Names/Dests(§7.7.4, §12.3.2.3) мапва имена към destination масиви, чийто първи елемент е страницата - Pre-1.2 речникът
/Destsнаправо в каталога държи същия вид масиви, ключовани по име - Outline елементите (§12.3.3) достигат страница или чрез inline
/Dest, или чрез/Aдействие с/S /GoToи/Dмасив - Structure елементите (§14.7.2) носят
/Pgключ, назоваващ страницата, на която живее техният marked content, а техните/Kдеца могат да бъдат marked-content справки и object справки (§14.7.4.3), завързани за тази страница ParentTree(§14.7.4.4) мапва/StructParentsномерата на страници и анотации обратно към structure елементи, и един елемент може да живее там, без изобщо да се появява на/Kверигата от корена- Линк анотациите на други страници (§12.5.6.5) носят
/Destили/GoToдействие към страницата, а каталожният/OpenActionможе да прави същото
Какво почиства THotPDF.DeletePage, преди да пипне page tree-то?
THotPDF.DeletePage(PageIndex) върху зареден документ пуска първо цялата справочна чистка, после маркира page обекта като изтрит с DeleteObj, откача каквито и да е widget анотации от AcroForm field дървото, измества вътрешния масив от страници и най-накрая вика RebuildLoadedPageTree, за да пренапише /Kids, /Count и /Parent на всяка оцеляла страница. Чистката обхожда каталога в фиксиран ред: /Names /Dests name tree, стария стил /Dests речник, /OpenAction, outline дървото, /StructTreeRoot с неговия ParentTree и последно /Annots масивите на всяка страница, която остава. Всяка стъпка решава дали справка се маха, пренасочва или се оставя на мира според това какво позволява спецификацията на тази структура без страницата. Два guard-а важат преди всичко друго: DeletePage вдига Invalid page number за извън-диапазонен индекс и отказва да махне последната страница, защото /Pages възел с нула деца не е валиден PDF, докато DeletePages приема същата one-based нотация "1,3-5,7-" като другите операции върху страници на зареден документ и итерира от най-високия избран индекс надолу, така че индексите, които сте написали, остават валидни, докато работи
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Zero-based: маха корицата. Именувани дестинации,
// отметки, structure tree, ParentTree и линк анотации,
// сочещи към нея, се кастрят, преди /Pages дървото
// да бъде пренаписано.
Pdf.DeletePage(0);
// One-based range синтаксис за партиди, най-високият индекс
// първо вътрешно, така че по-ранните индекси си остават валидни.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Как се третират по различен начин именуваните дестинации и отметките?
Именуваните дестинации се махат, а отметките се пренасочват, защото име, което вече не съществува, е приемлив изход, докато отметка без дестинация е видим дефект. В /Names /Dests дървото HotPDF обхожда всеки възел, тества всяка дестинация — и в голия масивен вид, и в речниковия вид с /D ключ — срещу изтритата страница и маха двойката име/стойност, когато първият елемент на масива е тази страница. Възел, чиито /Names и /Kids свършват празни, се маркира изтрит и се отвързва от родителя си, така че дървото никога не пази кухи листа. Същият тест минава и по стария стил каталожен речник /Dests, а каталожният /OpenAction просто се маха, ако е отварял изтритата страница. Една граница тук: когато name tree възел загуби записи, HotPDF изтрива /Limits двойката на възела вместо да преизчисли новите най-нисък и най-висок ключ, и докато viewer-ите resolve-ват имена добре и без нея, строг conformance проверител, четящ ISO 32000-1 §7.9.6, може да маркира не-коренен възел, на който липсва /Limits
Outline елементите отиват в другата посока. RetargetOutlineDestinations обхожда /First и /Next от outline корена, с visited списък и дълбочинна граница от 128, така че развалено циклично дърво не може да закачи извикването, и за всеки /Dest масив или /GoTo действие с /D масив, целящи страницата, заменя първия елемент с NearestRetainedPage: страницата, следвала изтритата, или страницата преди нея, когато изтритата е била последна. View параметрите след справката към страницата остават каквито са били. Отметка, сочеща към изтрито начало на глава, затова каца на първата страница от останалото, вместо да изчезне от страничната лента, което е поведението, което ревюторите очакват от отрязан документ. Дестинационният тест пасва обаче само изрични масиви: outline елемент, чийто /Dest е name string, който е resolve-вал към изтритата страница, не се пренасочва, защото name tree записът го няма и справката сега resolve-ва до нищо, а не до освободен обект, така че viewer-ът го третира като мъртва отметка. Механиката на самото outline дърво — /First, /Next и неочевидните /Count семантики — е покрита в ръководството за добавяне на отметки и именувани дестинации върху зареден PDF
// Провери прочистването вместо да му вярваш.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Отметка, сочеща към корицата, сега resolve-ва към
// страницата, следвала я (zero-based индекс 0 след изтриването).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Какво се случва със structure tree-то и ParentTree?
Structure елементи, съществуващи само заради изтритата страница, се махат, а елементи, обхващащи няколко страници, губят /Pg ключа си, но запазват децата си. PruneStructureElement слиза по /K веригата от /StructTreeRoot до дълбочина 128, обработвайки и масивния вид, и единичния речников вид на /K, който §14.7.2 позволява. За всеки елемент първо кастри децата, после оценява самия елемент: ако кастренето е изпразнило /K, елементът се маркира изтрит и родителят го маха. Ако собственият /Pg на елемента назовава изтритата страница и елементът все още има деца плюс /P родител, маха се само /Pg, защото /Pg върху елемент е страницата по подразбиране за неговите marked-content деца, а тези деца могат да сочат други страници изрично. Само елемент, чийто /Pg е изтритата страница и който няма нищо останало под себе си, се маха изцяло
ParentTree получава същото третиране, и причината е тази, която ухапе по време на разработката: structure елемент може да бъде достижим от ParentTree и никъде другаде. Number tree-то мапва /StructParents цели числа към един елемент или масив от елементи, а PruneParentTreeNode пуска PruneStructureElement върху всяка стойност, която намери, маха стойности, които са били кастрени, изтрива /Nums двойка, когато нейният масив е празен, и отвързва възел, чиито /Nums и /Kids и двете ги няма. Кастренето само на потомците на /K би оставило тези осиротели елементи да сочат освободена страница през /Pg и освободени marked-content справки през техните /MCR деца. Ако извличате текст в structure ред, това има пряка тежест: извличането на текст в structure ред обхожда точно тези дървета, а елемент с null /Pg е параграф, който тихо изпада от реда на четене
Кои линк анотации на оцелелите страници се махат?
Всяка линк анотация на запазена страница, чийто /Dest масив или /GoTo действие сочи изтритата страница, се маха заедно със собствеността си в structure tree-то. RemoveRetainedPageDestinationAnnotations обхожда /Annots масива на всяка страница освен целевата, прилага същия дестинационен тест, използван за outline-ите, маркира пасваща анотация изтрита, маха я от масива и после вика PruneAnnotationReferencesInStructureTree, така че OBJR речникът, чийто /Obj назоваваше тази анотация, се маха от нейния structure елемент, а самият елемент се маха, ако OBJR-ът е бил единственото му дете. Оставеният OBJR би нарушил §14.7.4.3, който изисква /Obj да сочи съществуващ обект, и би се показал в PDF/UA проверка като тагнат линк без анотация зад него. Забележете асиметрията с отметките: линковете се махат, не се пренасочват. Кръстосана справка в текста, казваща „виж страница 3", е грешна щом страница 3 я няма, а насочването ѝ към страница 4 би било лъжа по начин, по който отметка, кацнала на най-близката глава, не е, така че ако вашият работен процес се нуждае от тези линкове запазени, пренасочете ги сами, преди да викате DeletePage
Защо махнат /MCR или /OBJR никога не се регистрира като свободен?
Защото marked-content справките и object справките обикновено са директни речници вътре в /K масива на родителския си елемент, а incremental change регистърът resolve-ва директен обект към най-близкия indirect обект, който го съдържа. Когато RemoveArrayItem маха дете от /K масив, той освобождава in-memory обекта само ако той е бил THPDFLink или не-indirect стойност, а MarkRemovedObject регистрира обект за free list-а само когато номерът му е по-голям от нула. Първата версия на тази чистка не правеше това разграничение, а ефектът при инкрементален запис беше точно това, за което регистърът е проектиран: RegisterIncrementalChange вървеше от директния /MCR нагоре до graph transaction корена му, който беше запазеният structure елемент, притежаващ го, и записваше този елемент като null. Документ, загубил една страница, се връщаше с тагнато съдържание на другите страници, тихо детагнато. Единственият правилен ход за директно дете е да маркира контейнера си dirty чрез TouchContainer, така че контейнерът да бъде пренаписан, и да остави free list-а на мира
// Инкрементална актуализация: само пипнатите контейнери и
// освободеният page обект отиват в добавената секция.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Запазени structure елементи, чието /K е загубило директен
// /MCR, се презаписват на място, никога не се записват като null.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Същата предпазливост оформя това, което DeletePage нарочно не освобождава върху зареден документ. Content stream-овете, XObject-ите и не-widget анотациите на изтритата страница се оставят като обекти, защото зареден файл може да споделя някой от тях с страница, която остава, и няма евтин начин да се докаже обратното в момента на изтриване. Махането на справката от page tree-то е достатъчно за коректност; байтовете, които тези обекти все още заемат, са отделен въпрос, а object dependency графата с анализа на задържани байтове е инструментът за измерване на това какво отрязан документ все още носи
DeletePage срещу DeleteLoadedPage: кое да извикате?
Викайте DeletePage за всяко видимо за потребителя махане на страница, а запазете DeleteLoadedPage за случая, в който целият документ се преоразмерява и нито една документна справка не си заслужава да се пази. THotPDF.DeleteLoadedPage(PageIndex), добавен във версия 2.508.0, е лекият вариант: измества вътрешния масив от страници, вика RebuildLoadedKidsArray, за да пренапише /Kids и /Count, инвалидира rendered-page cache-а и палит OnLoadedDocumentModified. Той не обхожда name tree, outline-ите, structure tree-то или анотациите на други страници, и не маркира page обекта изтрит. Това е правилният инструмент вътре в N-up imposition, където HotPDF добавя прясно компонирани листове и после маха всяка оригинална страница с DeleteLoadedPage(0): източник страниците се заменят изцяло, а съдържанието на листовете се позовава на техните ресурси, а не на page обектите. За обикновената задача „махни страница 7 от този договор" DeletePage е единственото извикване, което оставя тагнат, с отметки и кръстосани линкове документ достатъчно консистентен, за да мине валидатор — и при пълно пренаписване през SaveLoadedDocument, и при инкрементална актуализация през SaveIncrementalUpdate. И двата метода се доставят в HotPDF Delphi Component за Delphi и C++Builder, без изискване за външен viewer runtime или друга зависимост