Smid syv sider ud af en håndbog på 200 sider, og hvert bogmærke lander et sted forkert. Løsningen er ikke at genopbygge dispositionen ud fra en flad titelliste. PDFiumPas eksponerer TPdfOutlineEditor, som indlæser det rigtige dispositionstræ, lader dig flytte elementer og give dem nye mål og derefter kører ApplyPageMap for at flytte hver eksplicit destination gennem din sideplan
Hvorfor ødelægger sletning af sider hvert bogmærke?
Fordi et dispositionselement ikke gemmer et sidetal. Det gemmer en reference til et sideobjekt, og når sideobjekterne ændres, peger referencen enten på en side, der er flyttet, eller på ingenting. ISO 32000-1 §12.3.2.2 definerer en eksplicit destination som et array, hvis første element er en indirekte reference til en sideordbog, efterfulgt af et fit-navn som /Fit eller /XYZ. Slet siden, og du står tilbage med en hængende reference; omarrangér siderne, og referencen er stadig gyldig, men beskriver nu et andet kapitel. PDFiumPas opløser det array tilbage til et sidetal ved indlæsning, så TPdfOutlineItem.PageNumber giver dig et én-baseret sideindeks, der matcher den offentlige TPdf-API frem for et objektnummer. Det er hele pointen med abstraktionen: din omtilknytningslogik arbejder i samme koordinatsystem som den sideplan, du allerede byggede, da du opdelte, omarrangerede eller udlagde dokumentet. Hvis du er ved at bygge den plan, løber samme én-baserede konvention gennem opdeling af PDF-dokumenter i flere filer og gennem n-up-imposition og sideomarrangering
Dispositionen er et dobbelt linket træ, ikke en liste
Grunden til, at du ikke blot kan serialisere et fladt array af titler, er, at ISO 32000-1 §12.3.3 forbinder hvert dispositionselement med fem separate links: /Parent, /Prev, /Next, /First og /Last. At flytte et enkelt deltræ omskriver derfor den gamle forælder, den nye forælder, begge nabo-søskende på hver side af snittet og indsættelsespunktet samt den flyttede knudes forældrepeger. Tag en af dem forkert, og standardoverholdende læsere viser et afkortet træ eller looper. PDFiumPas holder redigeringstilstanden som et depth-first-array af TPdfOutlineItem-records med et stabilt heltals-Id, så et deltræ er et sammenhængende udsnit, og søskendekæden er afledt, aldrig håndvedligeholdt. TPdfOutlineEditor.Move løfter det udsnit, genindsætter det under den nye forælder ved det ønskede søskendeindeks og tildeler kun blokkens rod om. Den nægter også de to flyt, der ville ødelægge grafen: at flytte et element ind i dets eget deltræ og at nævne en forælder, der ikke findes
Hvorfor er /Count fortegnet?
Fordi tegnet bærer den udfoldede tilstand, ikke størrelsen. En positiv /Count betyder, at elementet er åbent, og tallet er, hvor mange efterkommere der i øjeblikket er synlige; en negativ /Count betyder, at elementet er sammenklappet. PDFiumPas skriver efterkommerantallet for hvert element, der har børn, og negerer det, når IsOpen er False, og ved indlæsning læser den tilstanden tilbage som IsOpen := HasCount and (CountValue > 0). Dette er den mest almindelige håndrullede fejl i dispositionsskrivere: at udsende et utegnet antal og stille tvinge hele træet åbent
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // bliver andet barn af roden
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // skriver en negativ /Count
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget håndterer begge former, specifikationen tillader. Giv DestinationInAction som False, og PDFiumPas skriver et direkte /Dest-array; giv True, og den skriver en Go-To-handling, /A << /S /GoTo /D [ page ref suffix ] >>, efter ISO 32000-1 §12.6.4.2. Uanset hvad fjerner den først enhver eksisterende /Dest og /A fra elementet, så de to ikke kan eksistere samtidig og være uenige. Suffikset er /Fit som standard og skal begynde med et PDF-navn, hvilket er derfor, et tomt eller misdannet suffiks rejser straks i stedet for at frembringe et destinationsarray, ingen læser kan parse
Hvordan indtager ApplyPageMap en sideplan?
ApplyPageMap tager præcis det array, din sideplan allerede har valideret: NewPageNumbers, indekseret efter gammel side minus én, holdende det nye én-baserede sidetal eller nul, når siden ikke overlevede. Den går elementarrayet baglæns, så sletning af et deltræ aldrig invaliderer et indeks, den endnu ikke har besøgt, og den rapporterer, hvad den gjorde, gennem RemappedDestinationCount og RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Én post pr. side af det ORIGINALE dokument
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == denne side blev droppet
NewPageNumbers[0] := 1; // gammel side 1 -> ny side 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // gammel side 10 -> ny side 3
// True: slet hele det hængende deltræ. False: behold elementet, fjern dets mål
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
Flaget DeleteDangling beslutter politikken for en destination, der kortlagde til nul, og begge grene er bevidste. Med True sletter PDFiumPas elementet og hele dets deltræ, for en dispositionsknude, hvis mål forsvandt, leder normalt et kapitel, der forsvandt med det. Med False overlever elementet med sin titel og hierarki intakt, men med /Dest og /A fjernet, hvilket er det, du vil have, når et menneske skal give det et nyt mål i gennemgangen. Genuint misdannet input fejler stadig højlydt frem for at blive lappet: en negativ post eller en destination, der peger forbi slutningen af det leverede map, returnerer False med IssueKind sat til poviInvalidPageMap
Ugennemsigtige poster og den ærlige afvejning
Ikke hvert dispositionselement har et sidetal, PDFiumPas kan ræsonnere om. Tre slags bæres igennem urørte: navngivne destinationer, handlinger, der ikke er /S /GoTo, og ukendte ordbogsnøgler tilføjet af det, der frembragte filen. Disse indlæses med PageNumber lig nul, beholder deres originale bytes i elementet og skrives tilbage ordret, medmindre du eksplicit kalder Retarget på dem
- En navngiven destination er en nøgle ind i dokumentets navnetræ, så at kortlægge den korrekt betyder at opløse træet og omskrive måleposten, ikke at gætte på dispositionsniveau
- En
/URI-,/Launch- eller JavaScript-handling har slet ingen sidesemantik og må ikke stille konverteres til en Go-To - Leverandørspecifikke nøgler og strukturerdestinationer bevares, for at droppe det, du ikke forstår, er sådan rundture taber data
Prisen er reel og værd at slå fast rent ud: ApplyPageMap springer disse elementer helt over, så et dokument, hvis bogmærker alle bruger navngivne destinationer, kommer gennem en sidesletning med sin disposition strukturelt gyldig og semantisk forældet. Det er det bevidste valg — et forældet link, en anmelder kan fange, slår et selvsikkert forkert, ingen bemærker. Hvis du triagerer indkommende filer, før du redigerer dem, vil et inventargennemløb i en PDF-intake-review-workbench fortælle dig, hvilke dokumenter der falder i den spand
Gemning: inkrementel revision, derefter en uafhængig genindlæsning
TPdfOutlineEditor.SaveIncremental tilføjer en sparsom inkrementel revision frem for at omskrive filen. Elementer, der blev indlæst, beholder deres originale indirekte objektreference inklusive den nøjagtige generation, så eksisterende krydsreferencer forbliver gyldige; kun elementer, du tilføjede, trækker et nyt nummer, allokeret fra én forbi revisionens maksimale objektnummer. Kataloget opdateres i samme revision, og en manglende /Outlines-post tilføjes til det, når kilden slet ingen disposition havde
Det, der sker efter skrivningen, er delen værd at kopiere. PDFiumPas genåbner destinationsstreamen med en fuldstændig uafhængig editor og sammenligner det genindlæste træ med det i hukommelsen — elementantal, titler, sidetal, destinationssuffikser, handling-versus-direkte-destination-form, stilarter, udfoldelsestilstand og forældrerelationer. Enhver uoverensstemmelse eller enhver indlæsningsfejl rydder destinationsstreamen og returnerer poviVerificationFailure i stedet for at række dig en plausibelt udseende fil. Krypterede kilder afvises på forhånd med poviEncryptedInput, da nye titler og destinationer frembringer strengindhold, der ikke kan frembringes ved at kopiere /Encrypt-traileren frem
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Behandl dispositionen som det, den er — en linket objektgraf med sine egne invarianter — og sletning af sider holder op med at være en bogmærkekatastrofe og bliver et page map, du rækker til ét metodekald. TPdfOutlineEditor, ApplyPageMap og den verificerede inkrementelle skriver følger med i PDFiumPas fra v3.98.0 til Delphi, C++Builder og Lazarus; du kan gennemse den fulde API og downloade en prøve på PDFium Delphi Component-produktsiden