Techninis straipsnis

PDF puslapių trynimas Delphi be kabančių nuorodų

HotPDF Delphi Component trina puslapį iš įkelto PDF per THotPDF.DeletePage, ir nuo 2.751.0 versijos tas kvietimas taip pat išvalo kiekvieną dokumento lygio nuorodą, kuri vis dar rodo į tą puslapį: vardinius tikslus /Names /Dests medyje, senąjį katalogo /Dests žodyną, žymių /GoTo veiksmus, struktūros elementus po /StructTreeRoot, ParentTree, OBJR įrašus anotacijoms ir nuorodų anotacijas likusiuose puslapiuose. Puslapių medis perstatomas paskutinis, po to, kai niekas kitas nebegali pasiekti ištrinto objekto

Gedimą, kurio tai išvengia, lengva atkartoti ir sunku diagnozuoti. Ištrinkite žymėto ataskaitos dokumento viršelio puslapį, išsaugokite ir atidarykite rezultatą: Acrobat rodo teisingą puslapių skaičių, bet žymė „Contents“ dabar nusileidžia niekur, prieinamumo tikrintuvas praneša struktūros elementą be puslapio, o griežtas validuotojas išvardija nuorodą į atlaisvintą objektą. Puslapių medyje nieko neteisingo nėra. Problema ta, kad PDF puslapis yra ne tik /Pages lapas; jis yra taikinys, į kurį rodo pusė katalogo, ir to lapo pašalinimas palieka kiekvieną iš tų rodyklių kabančią

Kodėl puslapio pašalinimo iš /Kids nepakanka?

Nes ISO 32000-1 leidžia bent septynioms nepriklausomoms struktūroms laikyti nuorodą į puslapio objektą, ir tik viena iš jų yra puslapių medis. Puslapio išmetimas iš /Kids ir /Count sumažinimas patenkina §7.7.3, o kiekviena kita nuoroda tampa rodykle į objektą, kuris arba atlaisvintas xref lentelėje, arba tiesiog neegzistuoja perrašytame faile. Peržiūros programa, sekanti vieną iš tų rodyklių, gauna null, ir ką ji su tuo null padarys, priklauso nuo pačios peržiūros programos

  • Vardų medis po /Names /Dests (§7.7.4, §12.3.2.3) susieja vardus su tikslo masyvais, kurių pirmasis elementas yra puslapis
  • Iki 1.2 versijos /Dests žodynas tiesiogiai kataloge laiko tuos pačius masyvus, raktuotus pagal vardą
  • Kontūrų elementai (§12.3.3) pasiekia puslapį arba per tiesioginį /Dest, arba per /A veiksmą su /S /GoTo ir /D masyvu
  • Struktūros elementai (§14.7.2) nešasi /Pg raktą, įvardijantį puslapį, kuriame gyvena jų žymėtas turinys, o jų /K vaikai gali būti žymėto turinio nuorodos ir objekto nuorodos (§14.7.4.3), susietos su tuo puslapiu
  • ParentTree (§14.7.4.4) susieja puslapio ir anotacijų /StructParents numerius atgal su struktūros elementais, ir elementas gali gyventi ten, net neatsirasdamas /K grandinėje nuo šaknies
  • Kitų puslapių nuorodų anotacijos (§12.5.6.5) nešasi /Dest arba /GoTo veiksmą, nukreiptą į tą puslapį, ir katalogo /OpenAction gali daryti tą patį
Kodėl HotPDF puslapio pašalinimo iš /Kids nepakanka: ISO 32000-1 leidžia /Names /Dests vardų medžiui, senajam katalogo /Dests žodynui, kontūrų elementams, struktūros elementams su /Pg, ParentTree, nuorodų anotacijoms ir /OpenAction visiems laikyti nuorodą į tą patį puslapio objektą, o perstatomas tik puslapių medis
PDF puslapis yra taikinys, į kurį rodo pusė katalogo: lapo išmetimas patenkina puslapių medį, o kiekviena kita rodyklė išsisprendžia į null, tad apkarpyta ataskaita praranda žymę Contents ir krinta prieinamumo patikrinime

Ką THotPDF.DeletePage sutvarko, dar nepaliestas puslapių medžio?

THotPDF.DeletePage(PageIndex) įkeltame dokumente pirmiausia atlieka visą nuorodų šlavimą, tada pažymi puslapio objektą ištrintu su DeleteObj, atkabina visas valdiklių anotacijas nuo AcroForm laukų medžio, perstumia vidinį puslapių masyvą ir galiausiai iškviečia RebuildLoadedPageTree, kad perrašytų /Kids, /Count bei kiekvieno likusio puslapio /Parent. Šlavimas aplanko katalogą fiksuota tvarka: /Names /Dests vardų medį, seno stiliaus /Dests žodyną, /OpenAction, kontūrų medį, /StructTreeRoot su savo ParentTree ir paskiausiai kiekvieno liekančio puslapio /Annots masyvus. Kiekvienas žingsnis nusprendžia, ar nuoroda pašalinama, peradresuojama ar paliekama, pagal tai, ką specifikacija leidžia tai struktūrai daryti be to puslapio. Prieš bet ką iš to paleidžiant galioja dvi sargybos: DeletePage kelia Invalid page number už intervalo ribų esančiam indeksui ir atsisako šalinti paskutinį puslapį, nes /Pages mazgas be vaikų nėra galiojantis PDF, o DeletePages priima tą pačią vienetu pagrįstą "1,3-5,7-" žymėjimo formą kaip ir kitos įkelto dokumento puslapių operacijos ir iteruoja nuo didžiausio pasirinkto indekso žemyn, kad jūsų parašyti indeksai liktų galiojantys jam dirbant

Fiksuotas nuorodų šlavimas, kurį THotPDF.DeletePage atlieka dar nepaliestas puslapių medžio: sargybos atmeta už intervalo ribų esantį indeksą arba paskutinį puslapį, tada /Names /Dests ir senasis /Dests išvalomi, /OpenAction išmetamas, kontūrai peradresuojami į NearestRetainedPage, StructTreeRoot ir ParentTree išvalomi, likusių puslapių nuorodos pašalinamos, o RebuildLoadedPageTree paleidžiamas paskutinis
Kiekviena struktūra gauna tai, ką specifikacija leidžia: vardai dingsta, žymės nusileidžia artimiausiame likusiame puslapyje, struktūros elementai praranda /Pg arba išnyksta, o /Kids perrašymas įvyksta tik tada, kai niekas kitas nebegali pasiekti ištrinto objekto
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
    begin
      // Nuliu pagrįsta: išmetamas viršelio puslapis. Vardinius
      // tikslus, žymes, struktūros medį, ParentTree ir nuorodų
      // anotacijas, kurios rodė į jį, išvalomos dar prieš
      // perstatinėjant /Pages medį.
      Pdf.DeletePage(0);
      // Vienetu pagrįsta intervalų sintaksė partijoms, viduje
      // nuo didžiausio indekso, kad ankstesni indeksai liktų galiojantys.
      Pdf.DeletePages('3-4,9');
      Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Kaip vardiniai tikslai ir žymės apdorojami skirtingai?

Vardiniai tikslai pašalinami, o žymės peradresuojamos, nes vardas, kurio nebeliko, yra priimtinas rezultatas, o žymė be tikslo yra matomas defektas. /Names /Dests medyje HotPDF pereina kiekvieną mazgą, tikrina kiekvieną tikslą – tiek nuogos masyvo formos, tiek žodyno formos su /D raktu – prieš ištrintą puslapį ir pašalina vardo bei reikšmės porą, kai pirmasis masyvo elementas yra tas puslapis. Mazgas, kurio /Names ir /Kids abu lieka tušti, pažymimas ištrintu ir atkabinamas nuo savo tėvo, tad medis niekada nelaiko tuščiavidurių lapų. Tas pats patikrinimas atliekamas seno stiliaus katalogo /Dests žodynui, o katalogo /OpenAction tiesiog išmetamas, jei jis atidarydavo ištrintą puslapį. Viena riba čia: kai vardų medžio mazgas praranda įrašus, HotPDF ištrina to mazgo /Limits porą, o ne perskaičiuoja naujus žemiausią ir aukščiausią raktus, ir nors peržiūros programos vardus išsprendžia puikiai ir be jos, griežtas atitikties tikrintuvas, skaitantis ISO 32000-1 §7.9.6, gali pažymėti ne šakninį mazgą, kuriam trūksta /Limits

Kontūrų elementai eina priešinga kryptimi. RetargetOutlineDestinations pereina /First ir /Next nuo kontūrų šaknies su aplankytų sąrašu ir 128 gylio limitu, kad sugadintas ciklinis medis negalėtų pakabinti kvietimo, ir kiekvienam į tą puslapį nukreiptam /Dest masyvui ar /GoTo veiksmo /D masyvui jis pakeičia pirmąjį elementą į NearestRetainedPage: puslapį, kuris ėjo po ištrintojo, arba prieš jį buvusį, kai ištrintasis buvo paskutinis. Rodinio parametrai po puslapio nuorodos paliekami tokie, kokie buvo. Žymė, rodžiusi į ištrintą skyriaus pradžią, todėl nusileidžia pirmame likusio turinio puslapyje, o ne dingsta iš šoninės juostos – būtent tokio elgesio recenzentai ir tikisi iš apkarpyto dokumento. Tikslo patikrinimas vis dėlto sutampa tik su tiesioginiais masyvais: kontūro elementas, kurio /Dest yra vardo eilutė, anksčiau skyrusi į ištrintą puslapį, neperadresuojamas, nes vardų medžio įrašo nebėra, o nuoroda dabar išsisprendžia į nieką, o ne į atlaisvintą objektą, tad peržiūros programa ją traktuoja kaip negyvą žymę. Pačio kontūrų medžio mechanika, /First, /Next ir neakivaizdi /Count semantika, aprašyta žymių ir vardinių tikslų pridėjimo į įkeltą PDF vadove

// Patikrinkite šlavimą, o ne juo pasitikėkite.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
  ShowMessage('Named destination "cover" was pruned');
// Žymė, kuri taikėsi į viršelį, dabar išsisprendžia į
// puslapį, kuris ėjo po jo (nuliu pagrįstas indeksas 0 po trynimo).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
  ShowMessage('Bookmark retargeted to the nearest retained page');

Kas nutinka struktūros medžiui ir ParentTree?

Struktūros elementai, kurie egzistuoja tik dėl ištrinto puslapio, pašalinami, o elementai, apimantys kelis puslapius, praranda savo /Pg raktą, bet išlaiko vaikus. PruneStructureElement leidžiasi /K grandine nuo /StructTreeRoot iki 128 gylio, apdorodamas ir masyvo formą, ir vieno žodyno formą /K, kurią leidžia §14.7.2. Kiekvienam elementui jis pirmiausia išvalo vaikus, tada įvertina patį elementą: jei išvalymas ištuštino jo /K, elementas pažymimas ištrintu, ir jo tėvas jį išmeta. Jei paties elemento /Pg įvardija ištrintą puslapį, o elementas vis dar turi vaikų ir /P tėvą, pašalinamas tik /Pg, nes elemento /Pg yra numatytasis puslapis jo žymėto turinio vaikams, o tie vaikai gali aiškiai nurodyti kitus puslapius. Tiesiog ištrinamas tik elementas, kurio /Pg yra ištrintas puslapis ir po kuriuo nieko nebeliko

ParentTree gauna tą patį apdorojimą, ir priežastis yra ta, kuri įkando kūrimo metu: struktūros elementas gali būti pasiekiamas iš ParentTree ir iš niekur kitur. Skaičių medis susieja /StructParents sveikuosius skaičius arba su vienu elementu, arba su elementų masyvu, o PruneParentTreeNode paleidžia PruneStructureElement kiekvienai rastai reikšmei, pašalina reikšmes, kurios buvo išvalytos, ištrina /Nums porą, kai jos reikšmių masyvas tuščias, ir atkabina mazgą, kurio /Nums ir /Kids abu dingę. Išvalymas tik /K palikuonių būtų palikęs tuos našlaičius elementus rodančius į atlaisvintą puslapį per /Pg ir į atlaisvintas žymėto turinio nuorodas per savo /MCR vaikus. Jei ištraukiate tekstą struktūros tvarka, tai tiesiogiai svarbu: teksto ištraukimas struktūros tvarka eina būtent per šiuos medžius, o elementas su nuliniu /Pg yra pastraipa, kuri tyliai iškrinta iš skaitymo tvarkos

Kurios nuorodų anotacijos likusiuose puslapiuose pašalinamos?

Bet kuri nuorodos anotacija likusiame puslapyje, kurios /Dest masyvas ar /GoTo veiksmas nurodo į ištrintą puslapį, pašalinama kartu su savo nuosavybe struktūros medyje. RemoveRetainedPageDestinationAnnotations pereina kiekvieno puslapio, išskyrus tikslinį, /Annots masyvą, pritaiko tą patį tikslo patikrinimą, naudotą kontūrams, pažymi sutampančią anotaciją ištrinta, išmeta ją iš masyvo ir tada iškviečia PruneAnnotationReferencesInStructureTree, kad OBJR žodynas, kurio /Obj įvardijo tą anotaciją, būtų pašalintas iš savo struktūros elemento, o pats elementas būtų pašalintas, jei OBJR buvo jo vienintelis vaikas. Palikus OBJR vietoje būtų pažeistas §14.7.4.3, kuris reikalauja, kad /Obj nurodytų į egzistuojantį objektą, ir tai PDF/UA patikrinime pasirodytų kaip žymėta nuoroda be jokios anotacijos už jos. Atkreipkite dėmesį į asimetriją su žymėmis: nuorodos pašalinamos, o ne peradresuojamos. Kryžminė nuoroda pagrindiniame tekste, kuri sakė „žr. 3 puslapį“, yra neteisinga, kai 3 puslapio nebeliko, o jos nukreipimas į 4 puslapį būtų melas taip, kaip žymė, nusileidžianti artimiausiame skyriuje, nėra, tad jei jūsų darbo eigai reikia tas nuorodas išsaugoti, peradresuokite jas patys prieš kviesdami DeletePage

Kodėl pašalintas /MCR ar /OBJR niekada neturi būti registruojamas kaip atlaisvintas?

Nes žymėto turinio nuorodos ir objekto nuorodos paprastai yra tiesioginiai žodynai savo tėvinio elemento /K masyve, o inkrementinių pakeitimų registras tiesioginį objektą išsprendžia į artimiausią netiesioginį objektą, kuris jį talpina. Kai RemoveArrayItem išmeta vaiką iš /K masyvo, jis atlaisvina atmintyje esantį objektą tik tada, jei tai buvo THPDFLink arba ne netiesioginė reikšmė, o MarkRemovedObject registruoja objektą atlaisvinimo sąrašui tik tada, kai jo objekto numeris didesnis už nulį. Pirmoji šio šlavimo versija to skirtumo nedarė, ir poveikis inkrementiniame išsaugojime buvo būtent toks, kokiam registras ir skirtas: RegisterIncrementalChange nuo tiesioginio /MCR kilo iki savo grafo transakcijos šaknies, kuri buvo jį valdęs likęs struktūros elementas, ir tą elementą išrašė kaip null. Dokumentas, praradęs vieną puslapį, grįždavo su tyliai nežymėtu turiniu kituose puslapiuose. Vienintelis teisingas žingsnis tiesioginiam vaikui yra per TouchContainer pažymėti jo konteinerį nešvariu, kad konteineris būtų perrašytas, ir palikti atlaisvinimo sąrašą ramybėje

Kodėl pašalintas /MCR ar OBJR vaikas HotPDF niekada neturi būti registruojamas kaip atlaisvintas: inkrementinių pakeitimų registras tiesioginį žodyną išsprendžia į artimiausią netiesioginį konteinerį, tad pirmoji versija likusį struktūros elementą išrašė kaip null ir tyliai nužymėjo likusius puslapius, o TouchContainer dabar perrašo konteinerį ir palieka atlaisvinimo sąrašą ramybėje
Atlaisvinti atmintyje esantį vaiką skiriama tik THPDFLink arba ne netiesioginėms reikšmėms ir objekto numeriams, didesniems už nulį, tad inkrementinis išsaugojimas prideda tik paliestus konteinerius ir atlaisvintą puslapio objektą
// Inkrementinis atnaujinimas: tik paliesti konteineriai ir
// atlaisvintas puslapio objektas patenka į pridėtą dalį.
Pdf := THotPDF.Create(nil);
try
  Pdf.BeginIncrementalUpdate('tagged-report.pdf');
  Pdf.DeletePage(0);
  // Likę struktūros elementai, kurių /K prarado tiesioginį /MCR,
  // perrašomi vietoje ir niekada neišrašomi kaip null.
  Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
  Pdf.Free;
end;

Tas pats atsargumas formuoja ir tai, ko DeletePage įkeltame dokumente sąmoningai neatlaisvina. Turinio srautai, XObject objektai ir ne valdiklių anotacijos ištrinto puslapio paliekami kaip objektai, nes įkeltas failas gali bet kurį iš jų dalintis su puslapiu, kuris lieka, ir nėra pigaus būdo trynimo metu įrodyti priešingai. Puslapių medžio nuorodos pašalinimo korektiškumui pakanka; baitai, kuriuos tie objektai vis dar užima, yra atskiras klausimas, o objektų priklausomybių grafas ir pasilaikomų baitų analizė yra įrankis išmatuoti, ką apkarpytas dokumentas vis dar nešasi

DeletePage ar DeleteLoadedPage: kurį reikia kviesti?

Kvieskite DeletePage bet kokiam naudotojui matomam puslapio pašalinimui, o DeleteLoadedPage pasilikite tam atvejui, kai pertekstinamas visas dokumentas ir jokia dokumento lygio nuoroda neverta išsaugoti. THotPDF.DeleteLoadedPage(PageIndex), pridėtas 2.508.0 versijoje, yra lengvasis variantas: jis perstumia vidinį puslapių masyvą, iškviečia RebuildLoadedKidsArray, kad perrašytų /Kids ir /Count, invaliduoja atvaizduotų puslapių podėlį ir sukelia OnLoadedDocumentModified. Jis nepereina vardų medžio, kontūrų, struktūros medžio ar kitų puslapių anotacijų ir nepažymi puslapio objekto ištrintu. Tai teisingas įrankis N-up maketavimo viduje, kur HotPDF prideda ką tik sudėliotus lapus ir tada išmeta kiekvieną originalų puslapį su DeleteLoadedPage(0): šaltinio puslapiai keičiami visiškai, o lapo turinys nurodo į jų resursus, o ne į puslapių objektus. Įprastam darbui „pašalinti 7 puslapį iš šios sutarties“ DeletePage yra vienintelis kvietimas, paliekantis žymėtą, su žymėmis ir kryžminėmis nuorodomis dokumentą pakankamai nuoseklų, kad praeitų validuotoją – tiek pilname perrašyme per SaveLoadedDocument, tiek inkrementiniame atnaujinime per SaveIncrementalUpdate. Abu metodai platinami HotPDF Delphi Component, skirtoje Delphi ir C++Builder, nereikalaujant jokios išorinės peržiūros programos vykdymo aplinkos ar priklausomybės