Tehnični članak

Brisanje strani PDF v Delphiju brez visečih referenc

HotPDF Delphi Component iz naloženega PDF izbriše stran s klicem THotPDF.DeletePage, od različice 2.751.0 pa ta klic počisti tudi vsako referenco na ravni dokumenta, ki še kaže na to stran: imenovane cilje v drevesu /Names /Dests, slovar /Dests iz starega kataloga, akcije /GoTo zaznamkov, elemente strukture pod /StructTreeRoot, ParentTree, vnose OBJR za opombe in opombe povezav na preživelih straneh. Drevo strani se obnovi nazadnje, ko nič drugega ne more več doseči izbrisanega objekta

Napako, ki jo to prepreči, je lahko ponoviti in težko diagnosticirati. Izbrišite naslovno stran označenega poročila, shranite in odprite rezultat: Acrobat pokaže pravo število strani, a zaznamek »Contents« zdaj ne pristane nikjer, pregledovalnik dostopnosti poroča element strukture brez strani, strogi preverjevalnik veljavnosti pa navede referenco na sproščen objekt. V drevesu strani ni nič napačno. Težava je v tem, da stran PDF ni le list drevesa /Pages; je cilj, na katerega kaže polovica kataloga, in odstranitev lista pusti vsak od teh kazalcev viseč

Zakaj odstranitev strani iz /Kids ni dovolj?

Ker ISO 32000-1 dopušča, da vsaj sedem neodvisnih struktur hrani referenco na objekt strani, in le ena od njih je drevo strani. Odstranitev strani iz /Kids in zmanjšanje /Count zadosti §7.7.3, vsaka druga referenca pa postane kazalec na objekt, ki je v xref bodisi sproščen bodisi preprosto odsoten iz prepisane datoteke. Pregledovalnik, ki sledi enemu od teh kazalcev, dobi null, in kaj stori s tem null, je stvar pregledovalnika

  • Drevo imen pod /Names /Dests (§7.7.4, §12.3.2.3) preslika imena v nize ciljev, katerih prvi element je stran
  • Slovar /Dests iz obdobja pred 1.2 neposredno v katalogu hrani enake vrste nizov, oprtih na ime
  • Elementi obrisa (§12.3.3) dosežejo stran bodisi prek vgrajenega /Dest bodisi prek akcije /A s /S /GoTo in nizom /D
  • Elementi strukture (§14.7.2) nosijo ključ /Pg, ki navaja stran, na kateri živi njihova označena vsebina, njihovi otroci /K pa so lahko reference na označeno vsebino in reference na objekte (§14.7.4.3), vezane na to stran
  • ParentTree (§14.7.4.4) preslika številke /StructParents strani in opomb nazaj v elemente strukture, element pa lahko živi tam, ne da bi se sploh pojavil na verigi /K od korena
  • Opombe povezav na drugih straneh (§12.5.6.5) nosijo /Dest ali akcijo /GoTo, ki cilja to stran, isto pa lahko stori /OpenAction kataloga
Zakaj odstranitev strani HotPDF iz /Kids ni dovolj: ISO 32000-1 dopušča, da drevo imen /Names /Dests, slovar /Dests iz starega kataloga, elementi obrisa, elementi strukture s /Pg, ParentTree, opombe povezav in /OpenAction vsi hranijo referenco na isti objekt strani, obnovi pa se le drevo strani
Stran PDF je cilj, na katerega kaže polovica kataloga: odstranitev lista zadosti drevesu strani, vsak drug kazalec pa se razreši v null, zato obrezano poročilo izgubi zaznamek Contents in pade na preverjanju dostopnosti

Kaj THotPDF.DeletePage počisti, preden se dotakne drevesa strani?

THotPDF.DeletePage(PageIndex) na naloženem dokumentu najprej opravi celoten prehod po referencah, nato objekt strani označi kot izbrisan z DeleteObj, odklopi morebitne opombe gradnikov iz drevesa polj AcroForm, premakne notranji niz strani in nazadnje pokliče RebuildLoadedPageTree, da prepiše /Kids, /Count in /Parent vsake preživele strani. Prehod obišče katalog v fiksnem vrstnem redu: drevo imen /Names /Dests, starinski slovar /Dests, /OpenAction, drevo obrisa, /StructTreeRoot z njegovim ParentTree in nazadnje nize /Annots vsake strani, ki ostane. Vsak korak se odloči, ali se referenca odstrani, preusmeri ali pusti pri miru, glede na to, kaj specifikacija tej strukturi dovoli brez strani. Preden se karkoli od tega zažene, veljata dve varovali: DeletePage ob indeksu zunaj obsega sproži Invalid page number in zavrne brisanje zadnje strani, ker vozlišče /Pages brez otrok ni veljaven PDF, DeletePages pa sprejme isti zapis z osnovo ena, "1,3-5,7-", kot druge operacije s stranmi naloženega dokumenta in ponavlja od najvišjega izbranega indeksa navzdol, da indeksi, ki ste jih zapisali, med delom ostanejo veljavni

Fiksni prehod po referencah, ki ga THotPDF.DeletePage opravi, preden se dotakne drevesa strani: varovali zavrneta indeks zunaj obsega ali zadnjo stran, nato se počistita /Names /Dests in stari /Dests, /OpenAction se opusti, obrisi se preusmerijo na NearestRetainedPage, StructTreeRoot in ParentTree se počistita, povezave na preživelih straneh se odstranijo, RebuildLoadedPageTree pa teče nazadnje
Vsaka struktura dobi obravnavo, ki jo specifikacija dopušča: imena izginejo, zaznamki pristanejo na najbližji ohranjeni strani, elementi strukture izgubijo /Pg ali izginejo, prepis /Kids pa se zgodi šele, ko nič drugega ne more doseči izbrisanega objekta
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
    begin
      // Z osnovo nič: odstrani naslovno stran. Imenovani cilji,
      // zaznamki, drevo strukture, ParentTree in opombe
      // povezav, ki so kazali nanjo, so počiščeni, preden
      // se obnovi drevo /Pages.
      Pdf.DeletePage(0);
      // Zaporedna sintaksa z osnovo ena za serije, najvišji indeks
      // gre znotraj prvi, da prejšnji indeksi ostanejo veljavni.
      Pdf.DeletePages('3-4,9');
      Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Kako se imenovani cilji in zaznamki obravnavajo različno?

Imenovani cilji se odstranijo, zaznamki pa preusmerijo, ker je ime, ki ne obstaja več, sprejemljiv izid, medtem ko je zaznamek brez cilja viden defekt. V drevesu /Names /Dests HotPDF prehodi vsako vozlišče, vsak cilj preizkusi, tako v goli nizovni obliki kot v slovarjevski obliki s ključem /D, proti izbrisani strani in odstrani par ime/vrednost, kadar je prvi element niza ta stran. Vozlišče, katerega /Names in /Kids na koncu ostaneta prazna, se označi kot izbrisano in odklopi od nadrejenega, tako da drevo nikoli ne obdrži votlih listov. Isti preizkus teče po starinskem slovarju /Dests v katalogu, /OpenAction kataloga pa se preprosto opusti, če se je odpiral na izbrisani strani. Ena meja je tu: ko vozlišče drevesa imen izgubi vnose, HotPDF izbriše par /Limits tega vozlišča, namesto da bi znova izračunal najnižji in najvišji ključ, in čeprav pregledovalniki imena brez tega razrešujejo lepo, lahko strogi preverjevalnik skladnosti ob branju ISO 32000-1 §7.9.6 opozori na vozlišče, ki ni koren in mu manjka /Limits

Elementi obrisa gredo v drugo smer. RetargetOutlineDestinations prehodi /First in /Next od korena obrisa, s seznamom obiskanih in omejitvijo globine 128, da pokvarjeno ciklično drevo ne more obesiti klica, in za vsak niz /Dest ali niz /D akcije /GoTo, usmerjen v to stran, prvi element nadomesti z NearestRetainedPage: s stranjo, ki je sledila izbrisani, ali s tisto pred njo, kadar je bila izbrisana stran zadnja. Parametri pogleda za referenco strani ostanejo takšni, kot so bili. Zaznamek, ki je kazal na izbrisano naslovnico poglavja, zato pristane na prvi strani tega, kar ostane, in ne izgine iz stranskega stolpca, kar je vedenje, ki ga pregledovalci od obrezanega dokumenta pričakujejo. Preizkus cilja pa se ujema le z izrecnimi nizi: element obrisa, katerega /Dest je ime niza, ki je nekoč razreševalo na izbrisano stran, se ne preusmeri, ker vnosa v drevesu imen ni več in referenca zdaj ne razreši v nič, namesto v sproščen objekt, zato jo pregledovalnik obravnava kot mrtv zaznamek. Mehanika samega drevesa obrisa, /First, /Next in ne tako očitna semantika /Count, je opisana v vodniku po dodajanju zaznamkov in imenovanih ciljev v naložen PDF

// Prehod raje preverite, kot da mu zaupate.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
  ShowMessage('Named destination "cover" was pruned');
// Zaznamek, ki je ciljal naslovnico, se zdaj razreši v
// stran, ki ji je sledila (indeks 0 z osnovo nič po brisanju).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
  ShowMessage('Bookmark retargeted to the nearest retained page');

Kaj se zgodi z drevesom strukture in z ParentTree?

Elementi strukture, ki obstajajo le zaradi izbrisane strani, se odstranijo, elementi, ki se raztezajo čez več strani, pa izgubijo ključ /Pg, otroke pa obdržijo. PruneStructureElement se spusti po verigi /K od /StructTreeRoot do globine 128 in obravnava tako nizovno obliko kot obliko enega samega slovarja polja /K, ki jo §14.7.2 dopušča. Za vsak element najprej počisti otroke, nato oceni element sam: če je čiščenje izpraznilo njegov /K, se element označi kot izbrisan in njegov nadrejeni ga izpusti. Če /Pg elementa sam navaja izbrisano stran in ima element še otroke ter nadrejeni /P, se odstrani le /Pg, ker je /Pg na elementu privzeta stran za njegove otroke označene vsebine in lahko ti otroci izrecno navajajo druge strani. Povsem se odstrani le element, katerega /Pg je izbrisana stran in pod katerim ni ostalo nič

ParentTree dobi enako obravnavo, razlog pa je tisti, ki je pičil med razvojem: element strukture je lahko dosegljiv iz ParentTree in od nikoder drugod. Drevo števil preslika cela števila /StructParents bodisi v en sam element bodisi v niz elementov, PruneParentTreeNode pa požene PruneStructureElement po vsaki vrednosti, ki jo najde, odstrani vrednosti, ki so bile počiščene, izbriše par /Nums, kadar je njegov niz vrednosti prazen, in odklopi vozlišče, ki mu manjkata tako /Nums kot /Kids. Čiščenje le potomcev /K bi te osirotele elemente pustilo kazati na sproščeno stran skozi /Pg in na sproščene reference označene vsebine skozi njihove otroke /MCR. Če besedilo izvlečete v vrstnem redu strukture, je to neposredno pomembno: izvleček besedila v vrstnem redu strukture hodi natanko po teh drevesih in element z ničelnim /Pg je odstavek, ki tiho izpade iz bralnega vrstnega reda

Katere opombe povezav na preživelih straneh se odstranijo?

Vsaka opomba povezave na ohranjeni strani, katere niz /Dest ali akcija /GoTo kaže na izbrisano stran, se odstrani skupaj s pripadnostjo v drevesu strukture. RemoveRetainedPageDestinationAnnotations prehodi niz /Annots vsake strani razen ciljne, uporabi isti preizkus cilja, kot je bil uporabljen za obrise, ujemajočo se opombo označi kot izbrisano, jo izpusti iz niza in nato pokliče PruneAnnotationReferencesInStructureTree, da se slovar OBJR, katerega /Obj je navajal to opombo, odstrani iz svojega elementa strukture, element sam pa se odstrani, če je bil OBJR njegov edini otrok. Če bi OBJR pustili na mestu, bi to kršilo §14.7.4.3, ki zahteva, da /Obj kaže na obstoječi objekt, in bi se v pregledu PDF/UA pokazalo kot označena povezava brez opombe za sabo. Upoštevajte asimetrijo z zaznamki: povezave se odstranijo in ne preusmerijo. Navzkrižno sklicevanje v besedilu, ki je reklo »glej stran 3«, je napačno, ko strani 3 ni več, in če bi ga usmerili na stran 4, bi bila to laž, kot pristanek zaznamka na najbližjem poglavju ni, zato, če vaš potek dela te povezave potrebuje ohranjene, jih preusmerite sami, preden pokličete DeletePage

Zakaj odstranjeni /MCR ali /OBJR ne sme biti nikoli registriran kot sproščen?

Ker so reference na označeno vsebino in reference na objekte običajno neposredni slovarji znotraj niza /K svojega nadrejenega elementa, register inkrementalnih sprememb pa neposredni objekt razreši na najbližji posredni objekt, ki ga vsebuje. Ko RemoveArrayItem izpusti otroka iz niza /K, sprosti objekt v pomnilniku le, če je bil THPDFLink ali neposredna vrednost, MarkRemovedObject pa objekt uvrsti na seznam sproščenih le, kadar je njegova številka objekta večja od nič. Prva različica tega prehoda te razlike ni delala in učinek pri inkrementalnem shranjevanju je bil natanko to, za kar je register zasnovan: RegisterIncrementalChange je od neposrednega /MCR šel navzgor do svojega korena transakcije grafa, ki je bil ohranjeni element strukture, ki ga je imel v lasti, in ta element zapisal kot null. Dokument, ki je izgubil eno stran, se je vrnil z označeno vsebino na drugih straneh tiho neoznačeno. Edina pravilna poteza za neposrednega otroka je, da se njegov vsebnik označi kot umazan prek TouchContainer, tako da se vsebnik prepiše, seznama sproščenih pa se ne dotaknemo

Zakaj odstranjeni otrok /MCR ali /OBJR v HotPDF ne sme biti nikoli registriran kot sproščen: register inkrementalnih sprememb razreši neposredni slovar na najbližji posredni vsebnik, zato je prva različica ohranjeni element strukture zapisala kot null in preživele strani tiho razoznačila, TouchContainer pa zdaj prepiše vsebnik in seznama sproščenih se ne dotakne
Sproščanje otroka v pomnilniku je pridržano za THPDFLink ali neposredne vrednosti in za številke objektov, večje od nič, zato inkrementalno shranjevanje priloži le dotaknjene vsebnike in sproščeni objekt strani
// Inkrementalna posodobitev: v priloženi odsek pristaneta le
// dotaknjeni vsebniki in sproščeni objekt strani.
Pdf := THotPDF.Create(nil);
try
  Pdf.BeginIncrementalUpdate('tagged-report.pdf');
  Pdf.DeletePage(0);
  // Ohranjeni elementi strukture, katerih /K je izgubil neposredni
  // /MCR, se prepišejo na mestu, nikoli zapisani kot null.
  Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
  Pdf.Free;
end;

Ista previdnost oblikuje tudi to, česar DeletePage na naloženem dokumentu namenoma ne sprosti. Tokovi vsebine, objekti XObject in opombe izbrisane strani, ki niso gradniki, ostanejo kot objekti, ker si lahko naložena datoteka katerega od njih deli s stranjo, ki ostane, in ob brisanju ni poceni načina, da bi dokazali nasprotno. Odstranitev reference iz drevesa strani zadostuje za pravilnost; bajti, ki jih ti objekti še zasedajo, so ločeno vprašanje, in graf odvisnosti objektov in analiza zadržanih bajtov je orodje za merjenje tega, kar obrezan dokument še nosi

DeletePage proti DeleteLoadedPage: katerega poklicati?

DeletePage pokličite za vsako brisanje strani, ki ga vidi uporabnik, DeleteLoadedPage pa prihranite za primere, ko se celoten dokument preliva znova in nobena referenca na ravni dokumenta ni vredna ohranitve. THotPDF.DeleteLoadedPage(PageIndex), dodan v različici 2.508.0, je lahka različica: premakne notranji niz strani, pokliče RebuildLoadedKidsArray, da prepiše /Kids in /Count, razveljavi predpomnilnik upodobljenih strani in sproži OnLoadedDocumentModified. Ne hodi po drevesu imen, po obrisih, po drevesu strukture ali po opombah drugih strani in objekta strani ne označi kot izbrisanega. To je pravo orodje znotraj postavitve N-up, kjer HotPDF priloži na novo sestavljene pole in nato spusti vsako izvirno stran z DeleteLoadedPage(0): izvorne strani se zamenjajo v celoti in vsebina pole se sklicuje na njihove vire in ne na objekte strani. Za običajno opravilo »odstrani stran 7 iz te pogodbe« je DeletePage edini klic, po katerem označen, zaznamkovan in navzkrižno povezan dokument ostane dovolj skladen, da prestane preverjevalnik veljavnosti, tako pri polnem prepisu skozi SaveLoadedDocument kot pri inkrementalni posodobitvi skozi SaveIncrementalUpdate. Obe metodi se dobavljata v HotPDF Delphi Component za Delphi in C++Builder, brez zunanjega izvajalnega okolja pregledovalnika ali odvisnosti