Teknisk artikel

Slet PDF-sider i Delphi uden hængende referencer

HotPDF Delphi Component sletter en side fra en loadet PDF gennem THotPDF.DeletePage, og siden version 2.751.0 beskærer kaldet også hver dokumentniveau-reference, der stadig peger på siden: named destinations i /Names-/Dests-træet, den gamle catalog-/Dests-dictionary, bookmark-/GoTo-actions, structure elements under /StructTreeRoot, ParentTree, OBJR-entries for annotationer og link-annotationer på overlevende sider. Sidetræet genbygges sidst, efter intet andet kan nå det slettede objekt

Fejlen, dette forhindrer, er let at reproducere og svær at diagnosticere. Slet forsiden af en tagget rapport, gem, og åbn resultatet: Acrobat viser det rigtige sideantal, men "Contents"-bookmarket lander nu ingen steder, accessibility checkeren rapporterer et structure element uden side, og en streng validator lister en reference til et frit objekt. Intet i sidetræet er forkert. Problemet er, at en PDF-side ikke kun er et blad af /Pages; den er et target, som halvdelen af catalog peger på, og at fjerne bladet efterlader hver eneste af de pointere hængende

Hvorfor er det ikke nok at fjerne en side fra /Kids?

Fordi ISO 32000-1 lader mindst syv uafhængige strukturer holde en reference til et sideobjekt, og kun én af dem er sidetræet. At droppe siden fra /Kids og dekrementere /Count tilfredsstiller §7.7.3, og alle andre referencer bliver en pointer til et objekt, der enten er frigjort i xref'en eller simpelthen fraværende fra den omskrevne fil. En viewer, der følger én af de pointere, får null, og hvad den gør ved det null, er op til viewer

  • Navnetræet under /Names /Dests (§7.7.4, §12.3.2.3) mapper navne til destinationsarrays, hvis første element er siden
  • Pre-1.2-/Dests-dictionaryen direkte i catalog holder samme slags arrays nøglet efter navn
  • Outline items (§12.3.3) når en side enten gennem en inline /Dest eller gennem en /A-action med /S /GoTo og et /D-array
  • Structure elements (§14.7.2) bærer en /Pg-nøgle, der navngiver den side, deres marked content bor på, og deres /K-kids kan være marked-content references og object references (§14.7.4.3) knyttet til den side
  • ParentTree (§14.7.4.4) mapper side- og annotations-/StructParents-numre tilbage til structure elements, og et element kan bo dér uden slet at optræde på /K-kæden fra roden
  • Link-annotationer på andre sider (§12.5.6.5) bærer en /Dest eller /GoTo-action, der target'er siden, og catalog'ens /OpenAction kan gøre det samme
Hvorfor det ikke er nok at fjerne en HotPDF-side fra /Kids: ISO 32000-1 lader /Names-/Dests-navnetræet, den gamle catalog-/Dests-dictionary, outline items, structure elements med /Pg, ParentTree, link-annotationer og /OpenAction alle holde en reference til det samme sideobjekt, og kun sidetræet genbygges
En PDF-side er et target, som halvdelen af catalog peger på: at droppe bladet tilfredsstiller sidetræet, mens alle andre pointere resolver til null, så en beskåret rapport mister sit Contents-bookmark og stryger ved sit accessibility-tjek

Hvad rydder THotPDF.DeletePage op, før den rører sidetræet?

THotPDF.DeletePage(PageIndex) på et loadet dokument kører hele reference-sweepet først, markerer derefter sideobjektet som slettet med DeleteObj, løsner eventuelle widget-annotationer fra AcroForm-felttræet, forskubber det interne side-array og kalder til sidst RebuildLoadedPageTree for at omskrive /Kids, /Count og hver overlevende sides /Parent. Sweepet besøger catalog i fast rækkefølge: /Names-/Dests-navnetræet, den gammeldags /Dests-dictionary, /OpenAction, outline-træet, /StructTreeRoot med sin ParentTree og til sidst /Annots-arraysene på hver side, der bliver. Hvert trin beslutter, om en reference fjernes, retarget'es eller efterlades i fred, efter hvad specifikationen tillader den struktur at gøre uden siden. To værn gælder, før noget af det kører: DeletePage raise'r Invalid page number for et indeks uden for området og nægter at fjerne den sidste side, for en /Pages-knude med nul kids er ikke en gyldig PDF, mens DeletePages tager samme en-baserede "1,3-5,7-"-notation som de andre loaded-document-sideoperationer og itererer fra det højeste valgte indeks og ned, så de indekser, du skrev, forbliver gyldige, mens den arbejder

Det faste reference-sweep, THotPDF.DeletePage kører, før sidetræet røres: værn afviser et indeks uden for området eller den sidste side, derefter beskæres /Names-/Dests og den gamle /Dests, /OpenAction droppes, outlines retarget'es til NearestRetainedPage, StructTreeRoot og ParentTree beskæres, links på beholdte sider fjernes, og RebuildLoadedPageTree kører sidst
Hver struktur får den behandling, specifikationen tillader: navne forsvinder, bookmarks lander på den nærmeste beholdte side, structure elements mister /Pg eller forsvinder, og /Kids-omskrivningen sker først, efter intet andet kan nå det slettede objekt
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('tagged-report.pdf', '') > 0 then
    begin
      // Nulbaseret: drop forsiden. Named destinations,
      // bookmarks, structure tree, ParentTree og link-
      // annotationer, der pegede på den, beskæres, før
      // /Pages-træet genbygges.
      Pdf.DeletePage(0);
      // En-baseret range-syntaks til batcher, højeste indeks først
      // internt, så tidligere indekser forbliver gyldige.
      Pdf.DeletePages('3-4,9');
      Pdf.SaveLoadedDocument('tagged-report-trimmed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Hvordan håndteres named destinations og bookmarks forskelligt?

Named destinations fjernes, og bookmarks retarget'es, for et navn, der ikke længere findes, er et acceptabelt udfald, mens et bookmark uden destination er en synlig defekt. I /Names-/Dests-træet gennemløber HotPDF hver knude, tester hver destination, både i den bare array-form og dictionary-formen med en /D-nøgle, op mod den slettede side og fjerner navne/værdi-paret, når arrayets første element er den side. En knude, hvis /Names og /Kids begge ender tomme, markeres slettet og aflinkes fra sin forælder, så træet aldrig beholder hule blade. Samme test kører over den gammeldags catalog-/Dests-dictionary, og catalog'ens /OpenAction droppes simpelthen, hvis den åbnede på den slettede side. Én grænse her: når en navnetræ-knude mister entries, sletter HotPDF den knudes /Limits-par i stedet for at genberegne de nye laveste og højeste nøgler, og mens viewers resolver navne fint uden den, kan en streng conformance checker, der læser ISO 32000-1 §7.9.6, flagge en ikke-rod-knude, der mangler /Limits

Outline items går den anden vej. RetargetOutlineDestinations traverserer /First og /Next fra outline-roden, med en besøgt-liste og en dybdegrænse på 128, så et korrupt cyklisk træ ikke kan hænge kaldet op, og for hvert /Dest-array eller /GoTo-action-/D-array, der sigter mod siden, erstatter den første element med NearestRetainedPage: siden, der fulgte efter den slettede, eller siden før den, når den slettede var den sidste. View-parametrene efter sidereferencen efterlades, som de var. Et bookmark, der pegede på et slettet kapitel-opslag, lander derfor på den første side af det, der er tilbage, i stedet for at forsvinde fra sidebjælken, hvilket er den adfærd, reviewers forventer af et beskåret dokument. Destinationstesten matcher kun eksplicitte arrays, dog: et outline item, hvis /Dest er en navnestreng, der plejede at resolve til den slettede side, retarget'es ikke, for navnetræ-entryet er væk, og referencen resolver nu til ingenting snarere end til et frigjort objekt, så viewer behandler det som et dødt bookmark. Mekanikken i outline-træet selv, /First, /Next og den ikke-indlysende /Count-semantik, er dækket i guiden til at tilføje bookmarks og named destinations på en loadet PDF

// Verificér sweepet i stedet for at stole på det.
Pdf.DeletePage(0);
if Pdf.ResolveLoadedNamedDestination('cover') = -1 then
  ShowMessage('Named destination "cover" was pruned');
// Et bookmark, der targetede forsiden, resolver nu til den
// side, der fulgte efter den (nulbaseret indeks 0 efter sletningen).
if Pdf.GetLoadedBookmarkPageIndex('Contents') = 0 then
  ShowMessage('Bookmark retargeted to the nearest retained page');

Hvad sker der med structure tree og ParentTree?

Structure elements, der kun findes på grund af den slettede side, fjernes, og elementer, der spænder over flere sider, mister deres /Pg-nøgle men beholder deres børn. PruneStructureElement går ned ad /K-kæden fra /StructTreeRoot til en dybde på 128 og håndterer både array-formen og enkelt-dictionary-formen af /K, som §14.7.2 tillader. For hvert element beskærer den først kidsene og evaluerer derefter elementet selv: hvis beskæringen tømte dets /K, markeres elementet slettet, og dets forælder dropper det. Navngiver elementets egen /Pg den slettede side, og har elementet stadig kids plus en /P-forælder, fjernes kun /Pg, for en /Pg på et element er default-siden for dets marked-content-kids, og de kids kan referere andre sider eksplicit. Kun et element, hvis /Pg er den slettede side, og som ikke har noget tilbage under sig, fjernes outright

ParentTree får samme behandling, og grunden er den, der bagede under udviklingen: et structure element kan være nåeligt fra ParentTree og ingen andre steder. Nummertræet mapper /StructParents-heltal til enten et enkelt element eller et array af elementer, og PruneParentTreeNode kører PruneStructureElement over hver værdi, den finder, fjerner værdier, der blev beskåret væk, sletter et /Nums-par, når dets værdiarray er tomt, og aflinker en knude, hvis /Nums og /Kids begge er væk. Kun at beskære efterkommerne af /K ville have efterladt de forældreløse elementer pegende på en frigjort side gennem /Pg og på frigjorte marked-content references gennem deres /MCR-kids. Ekstraherer du tekst i structure-orden, betyder det direkte noget: structure-order text extraction gennemløber præcis disse træer, og et element med en null /Pg er et afsnit, der lydløst falder ud af læserækkefølgen

Hvilke link-annotationer på overlevende sider fjernes?

Enhver link-annotation på en beholdt side, hvis /Dest-array eller /GoTo-action peger på den slettede side, fjernes sammen med sit structure tree-ejerskab. RemoveRetainedPageDestinationAnnotations gennemløber /Annots-arrayet på hver side undtagen targetet, anvender samme destinationstest som til outlines, markerer en matchende annotation som slettet, dropper den fra arrayet og kalder derefter PruneAnnotationReferencesInStructureTree, så den OBJR-dictionary, hvis /Obj navngav den annotation, fjernes fra sit structure element, med elementet selv fjernet, hvis OBJR'en var dets eneste kid. At lade OBJR'en blive ville krænke §14.7.4.3, som kræver, at /Obj refererer et eksisterende objekt, og ville vise sig i et PDF/UA-tjek som et tagget link uden annotation bag. Bemærk asymmetrien med bookmarks: links fjernes, ikke retarget'es. En krydsreference i brødteksten, der sagde "se side 3", er forkert, så snart side 3 er væk, og at pege den på side 4 ville være en løgn på en måde, et bookmark, der lander på nærmeste kapitel, ikke er, så hvis dit workflow har brug for de links bevaret, så retarget dem selv, før du kalder DeletePage

Hvorfor må en fjernet /MCR eller /OBJR aldrig registreres som free?

Fordi marked-content references og object references normalt er direkte dictionaries inde i deres parent-elements /K-array, og den inkrementelle ændringsregistrering resolver et direkte objekt til det nærmeste indirekte objekt, der indeholder det. Når RemoveArrayItem dropper en kid fra et /K-array, frigør den in-memory-objektet kun, hvis det var en THPDFLink eller en ikke-indirekte værdi, og MarkRemovedObject registrerer et objekt til fri-listen kun, når dets objektnummer er større end nul. Den første version af sweepet lavede ikke den skelnen, og effekten i en inkrementel gemning var præcis, hvad registreringen er designet til: RegisterIncrementalChange gik fra den direkte /MCR op til sin graph transaction-rod, som var det beholdne structure element, der ejede den, og skrev det element ud som null. Et dokument, der mistede én side, kom tilbage med tagget indhold på de andre sider lydløst utagget. Det eneste korrekte træk for en direkte kid er at markere sin container dirty gennem TouchContainer, så containeren omskrives, og at lade fri-listen være i fred

Hvorfor en fjernet /MCR- eller OBJR-kid aldrig må registreres som free i HotPDF: den inkrementelle ændringsregistrering resolver en direkte dictionary til den nærmeste indirekte container, så den første version skrev det beholdne structure element ud som null og utaggede overlevende sider lydløst, mens TouchContainer nu omskriver containeren og lader fri-listen være i fred
At frigøre in-memory-kidden er forbeholdt THPDFLink eller ikke-indirekte værdier og objektnumre større end nul, så en inkrementel gemning appender kun de berørte containere og det frigjorte sideobjekt
// Incremental update: kun de berørte containere og det
// frigjorte sideobjekt lander i den appendede sektion.
Pdf := THotPDF.Create(nil);
try
  Pdf.BeginIncrementalUpdate('tagged-report.pdf');
  Pdf.DeletePage(0);
  // Beholdte structure elements, hvis /K mistede en direkte /MCR,
  // omskrives in place, skrives aldrig som null.
  Pdf.SaveIncrementalUpdate('tagged-report-trimmed.pdf');
finally
  Pdf.Free;
end;

Samme forsigtighed former, hvad DeletePage bevidst ikke frigør på et loadet dokument. Content streams, XObjects og de ikke-widget-annotationer på den slettede side efterlades som objekter, for en loadet fil kan dele enhver af dem med en side, der bliver, og der er ingen billig måde at bevise det modsatte på ved slettetidspunkt. At fjerne sidetræ-referencen er nok til korrekthed; de bytes, de objekter stadig optager, er et separat spørgsmål, og objekt-afhængighedsgrafen og retained-bytes-analysen er værktøjet til at måle, hvad et beskåret dokument stadig bærer

DeletePage versus DeleteLoadedPage: hvilken skal du kalde?

Kald DeletePage til enhver brugerrettet sidesletning, og reserver DeleteLoadedPage til det tilfælde, hvor hele dokumentet flyder om, og ingen dokumentniveau-reference er værd at bevare. THotPDF.DeleteLoadedPage(PageIndex), tilføjet i version 2.508.0, er den lette variant: den forskubber det interne side-array, kalder RebuildLoadedKidsArray til at omskrive /Kids og /Count, ugyldiggør den renderede side-cache og affyrer OnLoadedDocumentModified. Den gennemløber ikke navnetræet, outlines, structure tree eller annotationerne på andre sider, og den markerer ikke sideobjektet som slettet. Det er det rigtige værktøj inde i N-up imposition, hvor HotPDF appender frisk sammensatte ark og derefter dropper hver original side med DeleteLoadedPage(0): kildesiderne erstattes wholesale, og ark-indholdet refererer deres ressourcer snarere end sideobjekterne. Til det almindelige "fjern side 7 fra denne kontrakt"-job er DeletePage det eneste kald, der efterlader et tagget, bookmarket, krydslinket dokument konsistent nok til at bestå en validator, både i en fuld omskrivning gennem SaveLoadedDocument og i en inkrementel opdatering gennem SaveIncrementalUpdate. Begge metoder følger med i HotPDF Delphi Component til Delphi og C++Builder, uden ekstern viewer-runtime eller afhængighed