Gooi zeven pagina's uit een handboek van 200 pagina's en elke bladwijzer landt ergens verkeerd. De oplossing is niet de outline herbouwen vanuit een platte titellijst. PDFiumPas stelt TPdfOutlineEditor beschikbaar, die de echte outline-boom laadt, u items laat verplaatsen en herdoelen, en daarna ApplyPageMap draait om elke expliciete destination door uw paginaplan te schuiven
Waarom breekt paginaverwijdering elke bladwijzer?
Omdat een outline-item geen paginanummer opslaat. Het slaat een referentie naar een paginaobject op, en wanneer de paginaobjecten veranderen wijst de referentie of naar een pagina die is verplaatst of naar helemaal niets. ISO 32000-1 §12.3.2.2 definieert een expliciete destination als een array waarvan het eerste element een indirecte referentie naar een page dictionary is, gevolgd door een fit name zoals /Fit of /XYZ. Verwijder de pagina en u houdt een bungelende referentie over; herorden de pagina's en de referentie is nog geldig maar beschrijft nu een ander hoofdstuk. PDFiumPas resolveert die array bij het laden terug naar een paginanummer, dus TPdfOutlineItem.PageNumber geeft u een paginaindex vanaf één die matcht met de publieke TPdf-API in plaats van een objectnummer. Dat is het hele punt van de abstractie: uw hertoewijzingslogica werkt in hetzelfde coördinatensysteem als het paginaplan dat u al bouwde toen u het document splitste, herordende of imposeerde. Bent u dat plan aan het bouwen, dan loopt dezelfde vanaf-één-conventie door PDF-documenten opsplitsen in meerdere bestanden en door n-up imposition en pagina-herordening
De outline is een dubbel gelinkte boom, geen lijst
De reden dat u niet simpelweg een platte array van titels kunt serialiseren is dat ISO 32000-1 §12.3.3 elk outline-item in vijf aparte links verwerkt: /Parent, /Prev, /Next, /First en /Last. Eén subtree verplaatsen herschrijft daardoor de oude parent, de nieuwe parent, beide buur-siblings aan elke kant van de snede en het invoegpunt, en de parent-pointer van het verplaatste knooppunt zelf. Eén daarvan verkeerd en conforme readers tonen een afgekapte boom, of lopen vast in een lus. PDFiumPas houdt de editstatus bij als een depth-first array van TPdfOutlineItem-records met een stabiele integer-Id, zodat een subtree een aaneengesloten slice is en de sibling-keten wordt afgeleid, nooit met de hand onderhouden. TPdfOutlineEditor.Move tilt die slice op, zet hem opnieuw onder de nieuwe parent op de gevraagde sibling-index en wijst alleen de root van het blok opnieuw toe. Hij weigert ook de twee verplaatsingen die de grafiek zouden corrumperen: een item in zijn eigen subtree verplaatsen, en een parent noemen die niet bestaat
Waarom is /Count ondertekend?
Omdat het teken de opengeklapte status draagt, niet de grootte. Een positieve /Count betekent dat het item open is en het getal is hoeveel nakomelingen er nu zichtbaar zijn; een negatieve /Count betekent dat het item is ingeklapt. PDFiumPas schrijft het nakomelingenaantal voor elk item met kinderen en maakt het negatief als IsOpen op False staat, en leest bij het laden de status terug als IsOpen := HasCount and (CountValue > 0). Dit is de meest voorkomende handgemaakte bug in outline-schrijvers: een unsigned count uitzenden en de hele boom geruisloos open forceren
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); // wordt tweede kind van root
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // schrijft een negatieve /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 behandelt beide vormen die de specificatie toestaat. Geef DestinationInAction als False door en PDFiumPas schrijft een directe /Dest-array; geef True door en hij schrijft een Go-To action, /A << /S /GoTo /D [ page ref suffix ] >>, volgens ISO 32000-1 §12.6.4.2. In beide gevallen stript hij eerst elke bestaande /Dest en /A van het item zodat de twee niet naast elkaar kunnen bestaan en tegenspreken. Het suffix is standaard /Fit en moet beginnen met een PDF name, en daarom gooit een leeg of malformed suffix onmiddellijk een exception in plaats van een destination-array te produceren die geen reader kan parsen
Hoe gebruikt ApplyPageMap een paginaplan?
ApplyPageMap neemt exact de array die uw paginaplan al heeft gevalideerd: NewPageNumbers, geïndexeerd op oude pagina min één, met het nieuwe paginanummer vanaf één, of nul als die pagina het niet heeft overleefd. Hij loopt de itemarray achterstevoren door zodat het verwijderen van een subtree nooit een index ongeldig maakt die hij nog moet bezoeken, en hij rapporteert wat hij deed via RemappedDestinationCount en RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Eén entry per pagina van het ORIGINELE document
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == deze pagina is weggevallen
NewPageNumbers[0] := 1; // oude pagina 1 -> nieuwe pagina 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // oude pagina 10 -> nieuwe pagina 3
// True: de hele bungelende subtree verwijderen. False: item houden, target strippen
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;
De vlag DeleteDangling beslist het beleid voor een destination die naar nul mapte, en beide takken zijn bewust gekozen. Bij True verwijdert PDFiumPas het item en zijn hele subtree, want een outline-knooppunt waarvan de target verdween leidt meestal een hoofdstuk dat mee verdween. Bij False overleeft het item met titel en hiërarchie intact maar zonder /Dest en /A, en dat is precies wat u wilt wanneer een mens hem in review opnieuw gaat herdoelen. Echt malformede invoer faalt nog steeds luid in plaats van gepatcht te worden: een negatieve entry of een destination die voorbij het einde van de meegegeven map wijst geeft False terug met IssueKind op poviInvalidPageMap
Ondoorzichtige entries, en de eerlijke afweging
Niet elk outline-item heeft een paginanummer waar PDFiumPas over kan redeneren. Drie soorten worden onaangeroerd meegevoerd: named destinations, actions die geen /S /GoTo zijn, en onbekende dictionary keys toegevoegd door wie het bestand ook produceerde. Deze laden met PageNumber gelijk aan nul, houden hun originele bytes in het item en worden letterlijk teruggeschreven tenzij u expliciet Retarget op ze aanroept
- Een named destination is een sleutel in de document name tree, dus hem correct hermappen betekent de boom resolven en de target-entry herschrijven, niet gokken op outlineniveau
- Een
/URI-,/Launch- of JavaScript-action heeft helemaal geen paginasemantiek en mag niet geruisloos in een Go-To worden omgezet - Leveranciersspecifieke keys en structure destinations worden behouden, want weggooien wat u niet begrijpt is hoe round-trips data verliezen
De prijs is echt en de moeite waard om kaal te benoemen: ApplyPageMap slaat die items volledig over, dus een document waarvan alle bladwijzers named destinations gebruiken komt door een paginaverwijdering met een structureel geldige maar semantisch verouderde outline. Dat is de bewuste keuze — een verouderde link die een reviewer kan vangen verslaat een zelfverzekerd verkeerde die niemand opmerkt. Bent u inkomende bestanden aan het triageren voordat u ze bewerkt, dan vertelt een inventarisatiepas in een PDF intake review workbench u welke documenten in dat hokje vallen
Opslaan: incrementele revisie, daarna onafhankelijk herladen
TPdfOutlineEditor.SaveIncremental plakt een sparse incrementele revisie aan in plaats van het bestand te herschrijven. Items die zijn geladen houden hun originele indirecte objectreferentie inclusief de exacte generation, dus bestaande kruisverwijzingen blijven geldig; alleen items die u toevoegt trekken een fris nummer, toegewezen vanaf één voorbij het maximale objectnummer van de revisie. De catalogue wordt in dezelfde revisie bijgewerkt, en ontbreekt een /Outlines-entry dan wordt die eraan toegevoegd als de bron helemaal geen outline had
Wat er na het schrijven gebeurt is het deel om over te nemen. PDFiumPas opent de destinationstream opnieuw met een volledig onafhankelijke editor en vergelijkt de herladen boom met die in het geheugen — itemaantal, titels, paginanummers, destination-suffixen, action-versus-directe destination-vorm, stijlen, uitgeklapte status en ouderrelaties. Elke mismatch, of elke laadfaling, maakt de destinationstream leeg en geeft poviVerificationFailure terug in plaats van u een plausibel ogend bestand te geven. Versleutelde bronnen worden vooraf geweigerd met poviEncryptedInput, want nieuwe titels en destinations maken stringcontent die niet door het vooruitkopiëren van de /Encrypt-trailer kan worden geproduceerd
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;
Behandel de outline als wat hij is — een gelinkte objectgrafiek met zijn eigen invarianten — en paginaverwijdering stopt met een bladwijzerramp te zijn en wordt een page map die u aan één method-aanroep meegeeft. TPdfOutlineEditor, ApplyPageMap en de geverifieerde incrementele writer zitten in PDFiumPas vanaf v3.98.0 voor Delphi, C++Builder en Lazarus; u kunt de volledige API bekijken en een proefversie downloaden op de productpagina van PDFium Delphi Component