HotPDF Delphi Component poistaa sivun ladatusta PDF:stä THotPDF.DeletePage-kutsulla, ja versiosta 2.751.0 lähtien tuo kutsu karsii myös jokaisen asiakirjatason viittauksen, joka osoittaa yhä poistettuun sivuun: nimetut kohteet /Names /Dests -puussa, perinteisen luettelon /Dests-sanakirjan, kirjanmerkkien /GoTo-toiminnot, rakennealkiot /StructTreeRoot-juuren alla, ParentTree-puun, huomautusten OBJR-merkinnät sekä jäljelle jäävien sivujen linkkihuomautukset. Sivupuu rakennetaan uudelleen viimeisenä, sen jälkeen kun mikään muu ei voi enää tavoittaa poistettua objektia
Vika, jonka tämä estää, on helppo toistaa ja vaikea diagnosoida. Poista merkityn raportin kansisivu, tallenna ja avaa tulos: Acrobat näyttää oikean sivumäärän, mutta ”Contents”-kirjanmerkki ei enää osu mihinkään, saavutettavuustarkistaja ilmoittaa rakennealkiosta jolla ei ole sivua, ja tiukka validaattori luettelee viittauksen vapaaseen objektiin. Sivupuussa ei ole mitään vialla. Ongelma on se, että PDF-sivu ei ole vain /Pages-puun lehti; se on kohde, johon puolet luettelosta osoittaa, ja lehden poistaminen jättää jokaisen noista osoittimista roikkumaan
Miksi sivun poistaminen /Kids-listasta ei riitä?
Koska ISO 32000-1 sallii vähintään seitsemän riippumattoman rakenteen pitää viittausta sivun objektiin, ja vain yksi niistä on sivupuu. Sivun pudottaminen /Kids-listasta ja /Count-arvon vähentäminen tyydyttää §7.7.3:n, ja jokaisesta muusta viittauksesta tulee osoitin objektiin, joka on joko vapautettu xrefissä tai yksinkertaisesti poissa uudelleenkirjoitetusta tiedostosta. Katselin, joka seuraa yhtä noista osoittimista, saa null-arvon, ja se mitä se tekee tuolla nullilla, on katselimen asia
- Nimipuu
/Names/Dests-polun alla (§7.7.4, §12.3.2.3) kuvaa nimet kohdetaulukoiksi, joiden ensimmäinen alkio on sivu - Suoraan luettelossa oleva esiversion 1.2
/Dests-sanakirja sisältää samanlaisia taulukoita nimen mukaan avaimistettuna - Jäsennyspuun alkiot (§12.3.3) tavoittavat sivun joko inline-
/Dest-arvon kautta tai/A-toiminnon kautta, jossa on/S /GoToja/D-taulukko - Rakennealkiot (§14.7.2) kuljettavat
/Pg-avainta, joka nimeää sivun, jolla niiden merkitty sisältö elää, ja niiden/K-lapset voivat olla merkittyyn sisältöön liittyviä viittauksia ja objektiviittauksia (§14.7.4.3), jotka on sidottu tuohon sivuun ParentTree(§14.7.4.4) kuvaa sivujen ja huomautusten/StructParents-numerot takaisin rakennealkioiksi, ja alkio voi elää siellä esiintymättä lainkaan juuresta alkavassa/K-ketjussa- Muiden sivujen linkkihuomautukset (§12.5.6.5) kuljettavat
/Dest-arvon tai/GoTo-toiminnon, joka kohdistuu tuohon sivuun, ja luettelon/OpenActionvoi tehdä samoin
Mitä THotPDF.DeletePage siivoaa ennen kuin se koskee sivupuuhun?
THotPDF.DeletePage(PageIndex) ajaa ladatulle asiakirjalle ensin koko viittausläpikäynnin, merkitsee sitten sivuobjektin poistetuksi DeleteObj-kutsulla, irrottaa mahdolliset widget-huomautukset AcroForm-kenttäpuusta, siirtää sisäistä sivutaulukkoa ja kutsuu lopuksi RebuildLoadedPageTree-metodia kirjoittamaan uudelleen /Kids-, /Count- ja jokaisen henkiin jäävän sivun /Parent-arvon. Läpikäynti käy luettelon läpi kiinteässä järjestyksessä: /Names /Dests -nimipuu, vanhan mallin /Dests-sanakirja, /OpenAction, jäsennyspuu, /StructTreeRoot ja sen ParentTree sekä viimeisenä jokaisen paikalleen jäävän sivun /Annots-taulukot. Jokainen vaihe päättää, poistetaanko viittaus, kohdistetaanko se uudelleen vai jätetäänkö se rauhaan sen mukaan, mitä spesifikaatio sallii tuon rakenteen tehdä ilman sivua. Ennen mitään tästä pätevät kaksi vartijaa: DeletePage nostaa Invalid page number -virheen alueen ulkopuolisesta indeksistä ja kieltäytyy poistamasta viimeistä sivua, koska /Pages-solmu ilman yhtään lasta ei ole kelvollinen PDF, kun taas DeletePages ottaa saman 1-pohjaisen "1,3-5,7-" -merkinnän kuin muut ladatun asiakirjan sivutoiminnot ja etenee korkeimmasta valitusta indeksistä alaspäin, jotta kirjoittamasi indeksit pysyvät voimassa työn aikana
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
begin
// Nollapohjainen: pudota kansisivu. Nimetut kohteet,
// kirjanmerkit, jäsennyspuu, ParentTree ja siihen
// osoittaneet linkkihuomautukset karsitaan ennen
// kuin /Pages-puu rakennetaan uudelleen.
Pdf.DeletePage(0);
// 1-pohjainen vaihtelumerkintä erille, korkein indeksi
// ensin sisäisesti, jotta aiemmat indeksit pysyvät voimassa.
Pdf.DeletePages('3-4,9');
Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
end;
finally
Pdf.Free;
end;
end;
Miten nimettyjä kohteita ja kirjanmerkkejä kohdellaan eri tavoin?
Nimetut kohteet poistetaan ja kirjanmerkit kohdistetaan uudelleen, koska nimi, jota ei enää ole, on hyväksyttävä lopputulos, kun taas kirjanmerkki ilman kohdetta on näkyvä virhe. /Names /Dests -puussa HotPDF käy läpi jokaisen solmun, testaa jokaisen kohteen sekä paljasta taulukkamuotoa että /D-avaimellista sanakirjamuotoa vasten poistettua sivua ja poistaa nimi–arvo-parin, kun taulukon ensimmäinen alkio on tuo sivu. Solmu, jonka sekä /Names että /Kids jäävät tyhjiksi, merkitään poistetuksi ja irrotetaan vanhemmastaan, joten puuhun ei koskaan jää onttoja lehtiä. Sama testi ajetaan vanhan mallin luettelo-/Dests-sanakirjalle, ja luettelon /OpenAction pudotetaan yksinkertaisesti, jos se avasi poistetulle sivulle. Yksi rajaus tässä: kun nimipuun solmu menettää merkintöjä, HotPDF poistaa tuon solmun /Limits-parin sen sijaan että laskisi uudet alimman ja ylimmän avaimen, ja vaikka katselimet ratkaisevat nimet mainiosti ilman sitä, tiukka vaatimustenmukaisuustarkistaja voi ISO 32000-1 §7.9.6:tä lukiessaan liputtaa ei-juurisolmun, jolta puuttuu /Limits
Jäsennyspuun alkiot menevät toiseen suuntaan. RetargetOutlineDestinations kulkee /First- ja /Next-kenttien läpi jäsennyspuun juuresta, käytetyllä listalla ja 128:n syvyysrajalla, jotta vioittunut syklinen puu ei voi jumittaa kutsua, ja jokaiselle sivuun kohdistuvalle /Dest-taulukolle tai /GoTo-toiminnon /D-taulukolle se korvaa ensimmäisen alkion NearestRetainedPage-arvolla: sivulla, joka seurasi poistettua, tai sitä edeltävällä sivulla, kun poistettu sivu oli viimeinen. Siviviittauksen jälkeiset näkymäparametrit jätetään sellaisiksi kuin ne olivat. Kirjanmerkki, joka osoitti poistettuun luvun avaussivuun, laskeutuu siksi jäljelle jäävän ensimmäiselle sivulle sen sijaan että se katoaisi sivupalkista, ja juuri sitä käyttäjät odottavat karsitulta asiakirjalta. Kohdetesti täsmää kuitenkin vain nimenomaisiin taulukoihin: jäsennyspuun alkiota, jonka /Dest on nimen merkkijono, joka ennen ratkesi poistettuun sivuun, ei kohdisteta uudelleen, koska nimipuun merkintä on poissa ja viittaus ratkeaa nyt ei-mihinkään eikä vapautettuun objektiin, joten katselin kohtelee sitä kuolleena kirjanmerkkinä. Jäsennyspuun oma mekaniikka, /First, /Next ja /Count-arvon epäitsestään selvä semantiikka, on käsitelty oppaassa kirjanmerkkien ja nimettyjen kohteiden lisäämisestä ladattuun PDF:ään
// Varmista läpikäynti sen sijaan että luottaisit siihen.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
ShowMessage('Named destination "cover" was pruned');
// Kansisivuun kohdistunut kirjanmerkki ratkeaa nyt sitä
// seuranneeseen sivuun (nollapohjainen indeksi 0 poiston jälkeen).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
ShowMessage('Bookmark retargeted to the nearest retained page');
Mitä jäsennyspuulle ja ParentTreelle tapahtuu?
Rakennealkiot, joita on olemassa vain poistetun sivun takia, poistetaan, ja usealle sivulle ulottuvat alkiot menettävät /Pg-avaimensa mutta pitävät lapsensa. PruneStructureElement laskeutuu /K-ketjua pitkin /StructTreeRoot-juuresta 128:n syvyyteen ja käsittelee sekä taulukkemuodon että §14.7.2:n salliman /K:n yksittäisen sanakirjamuodon. Jokaiselle alkiolle se karsii ensin lapset ja arvioi sitten alkion itse: jos karsinta tyhjensi sen /K-kokoelman, alkio merkitään poistetuksi ja sen vanhempi pudottaa sen. Jos alkion oma /Pg nimeää poistetun sivun ja alkiolla on yhä lapsia sekä /P-vanhempi, vain /Pg poistetaan, koska alkion /Pg on sen merkittyä sisältöä kantavien lasten oletussivu ja ne lapset voivat viitata nimenomaisesti muihin sivuihin. Vain alkio, jonka /Pg on poistettu sivu ja jonka alla ei ole mitään jäljellä, poistetaan kokonaan
ParentTree saa saman kohtelun, ja syy on se, joka puri kehityksen aikana: rakennealkio voi olla tavoitettavissa ParentTree-puusta eikä mistään muualta. Numeropuu kuvaa /StructParents-kokonaisluvut joko yksittäiseksi alkioksi tai alkiotaulukoksi, ja PruneParentTreeNode ajaa PruneStructureElement-kutsun jokaiselle löytämälleen arvolle, poistaa arvot jotka karsittiin pois, poistaa /Nums-parin kun sen arvotaulukko on tyhjä ja irrottaa solmun, jonka sekä /Nums että /Kids ovat poissa. Vain /K-ketjun jälkeläisten karsiminen olisi jättänyt nuo orvoiksi jääneet alkiot osoittamaan vapautettuun sivuun /Pg-avaimen kautta ja vapautettuihin merkittyä sisältöä koskeviin viittauksiin niiden /MCR-lasten kautta. Jos poimit tekstiä jäsennysjärjestyksessä, sillä on suoraa merkitystä: tekstinpoiminta jäsennysjärjestyksessä kulkee täsmälleen näiden puiden läpi, ja alkio, jonka /Pg on null, on kappale joka putoaa hiljaa pois lukujärjestyksestä
Mitkä linkkihuomautukset jäljelle jäävillä sivuilla poistetaan?
Jokainen jäljelle jäävällä sivulla oleva linkkihuomautus, jonka /Dest-taulukko tai /GoTo-toiminto osoittaa poistettuun sivuun, poistetaan yhdessä sen jäsennyspuussa olevan omistajuuden kanssa. RemoveRetainedPageDestinationAnnotations käy läpi jokaisen muun kuin kohdesivun /Annots-taulukon, soveltaa samaa kohdetestiä kuin jäsennyspuulle, merkitsee täsmäävän huomautuksen poistetuksi, pudottaa sen taulukosta ja kutsuu sitten PruneAnnotationReferencesInStructureTree-metodia, jotta OBJR-sanakirja, jonka /Obj nimesi tuon huomautuksen, poistetaan rakennealkiostaan, ja itse alkiokin poistetaan, jos OBJR oli sen ainoa lapsi. OBJR:n jättäminen paikalleen rikkoisi §14.7.4.3:a, joka vaatii /Obj:n viittaavan olemassa olevaan objektiin, ja näkyisi PDF/UA-tarkistuksessa merkittynä linkkinä, jonka takana ei ole huomautusta. Huomaa epäsymmetria kirjanmerkkeihin: linkit poistetaan, niitä ei kohdisteta uudelleen. Leipätekstin ristiviittaus, joka sanoi ”katso sivu 3”, on väärä kun sivu 3 on poistunut, ja sen osoittaminen sivulle 4 olisi valhe sillä tavalla jolla kirjanmerkin laskeutuminen lähimpään lukuun ei ole, joten jos työnkulkusi tarvitsee nuo linkit säilytettävinä, kohdista ne itse uudelleen ennen DeletePage-kutsua
Miksi poistettua /MCR- tai /OBJR-merkintää ei saa koskaan rekisteröidä vapaaksi?
Koska merkittyä sisältöä koskevat viittaukset ja objektiviittaukset ovat yleensä suoria sanakirjoja vanhempialkionsa /K-taulukon sisällä, ja inkrementaalisten muutosten rekisteri ratkaisee suoran objektin lähimpään epäsuoraan objektiin, joka sisältää sen. Kun RemoveArrayItem pudottaa lapsen /K-taulukosta, se vapauttaa muistissa olevan objektin vain jos se oli THPDFLink tai ei-epäsuora arvo, ja MarkRemovedObject rekisteröi objektin vapaaseen listaan vain kun sen objektinumero on suurempi kuin nolla. Tämän läpikäynnin ensimmäinen versio ei tehnyt tuota erottelua, ja vaikutus inkrementaalisessa tallennuksessa oli täsmälleen se, mihin rekisteri on suunniteltu: RegisterIncrementalChange kulki suorasta /MCR-merkinnästä ylös graafinsa transaktiojuureen, joka oli sen omistanut paikalleen jäävä rakennealkio, ja kirjoitti tuon alkion ulos null-arvona. Asiakirja, joka menetti yhden sivun, palasi takaisin muiden sivujen merkityn sisällön ollessa hiljaa merkitsemätöntä. Ainoa oikea siirto suoralle lapselle on merkitä sen säiliö likaiseksi TouchContainer-kutsulla, jotta säiliö kirjoitetaan uudelleen, ja jättää vapaa lista rauhaan
// Inkrementaalinen päivitys: vain kosketetut säiliöt ja
// vapautettu sivuobjekti päätyvät liitettyyn osioon.
Pdf := THotPDF.Create(nil);
try
Pdf.BeginIncrementalUpdate('tagged-report.pdf');
Pdf.DeletePage(0);
// Paikalleen jäävät rakennealkiot, joiden /K menetti suoran
// /MCR:n, kirjoitetaan paikan päällä, ei koskaan nullina.
Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
Pdf.Free;
end;
Sama varovaisuus muovaa sitä, mitä DeletePage jättää tarkoituksella vapauttamatta ladatussa asiakirjassa. Sisältöstreamit, XObjectit ja poistetun sivun muut kuin widget-huomautukset jätetään objekteiksi, koska ladattu tiedosto voi jakaa minkä tahansa niistä paikalleen jäävän sivun kanssa eikä poistohetkellä ole halpaa tapaa todistaa päinvastaista. Sivupuun viittauksen poistaminen riittää oikeellisuuteen; tavut, jotka nuo objektit yhä varaavat, ovat erillinen kysymys, ja objektien riippuvuusgraafi ja pidätettyjen tavujen analyysi on työkalu sen mittaamiseen, mitä karsittu asiakirja yhä kantaa
DeletePage vastaan DeleteLoadedPage: kumpaa pitäisi kutsua?
Kutsu DeletePage-metodia kaikkeen käyttäjälle näkyvään sivunpoistoon ja säästä DeleteLoadedPage tapaukseen, jossa koko asiakirja virtautetaan uudelleen eikä yksikään asiakirjatason viittaus ole säilyttämisen arvoinen. THotPDF.DeleteLoadedPage(PageIndex), joka lisättiin versiossa 2.508.0, on kevyt variantti: se siirtää sisäistä sivutaulukkoa, kutsuu RebuildLoadedKidsArray-metodia kirjoittamaan /Kids- ja /Count-arvot uudelleen, mitätöi renderöityjen sivujen välimuistin ja laukaisee OnLoadedDocumentModified-tapahtuman. Se ei käy läpi nimipuuta, jäsennyspuuta, rakennealkioita eikä muiden sivujen huomautuksia, eikä se merkitse sivuobjektia poistetuksi. Se on oikea työkalu N-up-arkitusasettelun sisällä, jossa HotPDF liittää tuoreita koottuja arkkeja ja pudottaa sitten jokaisen alkuperäisen sivun DeleteLoadedPage(0)-kutsulla: lähdesivut korvataan kokonaan, ja arkin sisältö viittaa niiden resursseihin eikä sivuobjekteihin. Tavalliseen ”poista sivu 7 tästä sopimuksesta” -työhön DeletePage on ainoa kutsu, joka jättää merkityn, kirjanmerkitetyn ja ristiviitteistetyn asiakirjan riittävän yhtenäiseksi validaattorin läpäisemiseksi, niin täydessä uudelleenkirjoituksessa SaveLoadedDocument-kutsun kautta kuin inkrementaalisessa päivityksessä SaveIncrementalUpdate-kutsun kautta. Molemmat metodit toimitetaan HotPDF Delphi Componentissa Delphille ja C++Builderille ilman ulkoista katselinaikaa tai riippuvuutta