Patch een leaf array om één naam toe te voegen en de entry kan nog steeds als ontbrekend worden gelezen. PDFium Component beantwoordt dat met TPdfTreeEditor, een algemene editor voor ISO 32000 name trees en number trees: hij laadt de boom met strikte validatie, bewerkt entries in het geheugen, herbouwt een gebalanceerde nodeset met correcte /Limits, en schrijft een geverifieerde incrementele revisie
De mislukking die deze unit motiveerde is van het soort dat je alleen diagnosticeert door bytes te lezen. Een tool voegt een key en value toe aan een leaf-/Names-array, schrijft een nieuwe revisie, en elke hex dump bevestigt dat de key in het bestand staat. Acrobat zegt nog steeds dat de bijlage niet bestaat. Niets is corrupt in de naïeve zin. De boom is simpelweg geen boom meer waar het lookup-algoritme in kan afdalen
Waarom breekt een gepatchte leaf array de lookup?
Omdat lookup een binaire afdaling door /Limits is, geen scan van de leaves. ISO 32000-1 §7.9.6 definieert een name tree als een root waarvan de /Kids afdalen naar leaves die /Names bevatten, een array van afwisselend key en value, met keys geordend per byte-vergelijking over de hele boom, en elke niet-root-node die /Limits declareert als zijn eigen kleinste en grootste key. Een conforme reader vergelijkt de gezochte key met elk kid-bereik en opent precies één child. Voeg een key toe aan een leaf en laat de oude /Limits ongemoeid, en de afdaling loopt recht langs de node die hem eigenlijk bevat
Number trees (§7.9.7) hebben dezelfde vorm met /Nums en integer keys, en ze dragen echt verkeer: /PageLabels in §12.4.2 is een number tree, en de catalogus-/Names-categorieën van §7.7.4 geven je /Dests, /EmbeddedFiles en /JavaScript als name trees. Dus vier invarianten moeten samen standhouden, niet één voor één. Keys moeten strikt oplopend zijn in boomvolgorde, elk niveau moet limits publiceren die overeenkomen met zijn werkelijke key-bereik, sibling-bereiken mogen niet overlappen, en het objectnummer en de generatie van de root moeten precies blijven waar ze waren, omdat de catalogus en elke andere verwijzer met een indirecte referentie naar die root wijzen. Onze notitie over werken met PDF-bijlagen in Delphi toont de consumer-kant van dezelfde structuur
Wat weigert TPdfTreeEditor te laden?
Meer dan je zou verwachten, en met opzet. TPdfTreeEditor.TryLoad wandelt door de node-graaf via TPdfSparseDictionaryReader, en lost elk child op met ReadDictionary op de exacte generatie die de referentie noemt in plaats van welke revisie er toevallig actief is. Een boom die niet als welgevormd kan worden bewezen wordt afgewezen voordat er één edit wordt toegestaan, wat voorkomt dat een kapotte input een aannemelijk ogende kapotte output wordt
- Cycles en herhaalde referenties, beide gemeld als
ptviCycle, zodat een/Kids-entry die terug omhoog in de keten wijst eindigt in plaats van te recurseren - Gemengde node-vormen: een dictionary die
/Kidssamen met/Namesof/Numsdraagt isptviInvalidNodeShape, net als een lege niet-root-leaf - Ongesorteerde of dubbele keys, gemeld als
ptviUnsortedKeyenptviDuplicateKey, gecheckt over de hele traversal in plaats van binnen één leaf - Limits die het oneens zijn met de werkelijk aanwezige keys, gemeld als
ptviInvalidLimits - Een ruwe waarde die niet precies één compleet PDF-object is, gemeld als
ptviInvalidValue - Budgetoverschrijdingen:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimitenptviDeadlineExceeded, begrensd door zowelTPdfTreeEditOptionsals het centrale parser-budget
TPdfTreeEditOptions.Default zet die boom-lokale grenzen op 1.000.000 entries, 100.000 nodes, diepte 64, 64 entries per leaf en 32 kids per node. De leaf- en kid-getallen zijn niet alleen guards; het zijn de fan-out die later wordt gebruikt wanneer de boom wordt herbouwd, en daarom valideert de record dat beide minstens twee zijn
uses
FPdfCompress;
var
Options: TPdfTreeEditOptions;
RootRef: TPdfTreeObjectReference;
Editor: TPdfTreeEditor;
Report: TPdfTreeValidationReport;
begin
Options := TPdfTreeEditOptions.Default;
RootRef.ObjectNumber := EmbeddedFilesRootObject; { catalog /Names /EmbeddedFiles }
RootRef.Generation := EmbeddedFilesRootGeneration;
if not TPdfTreeEditor.TryLoad(Source, ptkName, RootRef, Options,
Editor, Report) then
begin
Writeln('tree rejected: ', Report.ErrorMessage);
Exit;
end;
try
{ de waarde is de ruwe bytes van één compleet PDF-object }
Editor.UpsertName('invoice-2026-09',
'<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
Editor.RemoveName('draft-2025-11');
finally
{ Editor.Free na de save hieronder }
end;
end;
Entry-waarden blijven als ruwe PDF-objectbytes
Een entry-waarde wordt verbatim opgeslagen, en dit is de enige ontwerpbeslissing die de editor veilig maakt om op bestanden te richten die jij niet hebt geproduceerd. TPdfTreeEntry houdt NameKey, NumberKey en RawValue vast, en RawValue is de ruwe byte-reeks van één compleet PDF-object: een dictionary, een array, een string, een stream-referentie of een kale indirecte referentie. Er wordt niets gedecodeerd naar een Delphi-variant, een TStrings of een getypeerde record. De beloning is lossless round-tripping van waarden waar de unit niets van weet. Een /Filespec met een vendor-extensie, een destination-array met een ongebruikelijk fit type, een /JavaScript-actiedictionary met private keys, alles overleeft een rebuild byte voor byte, omdat de editor nooit een mening over hun inhoud heeft gehad. Het krimpt ook de wijzigings-API tot iets bewijsbaars: UpsertName en UpsertNumber hoeven alleen te verifiëren dat de aangeleverde waarde als precies één compleet object parst, en RemoveName, RemoveNumber en Clear raken alleen de entry-lijst aan
Hoe wordt een gebalanceerde boom herbouwd zonder de root te hernummeren?
In twee passes met één vast punt. BuildBalancedObjects snijdt eerst de gesorteerde entry-lijst in leaves van hoogstens MaxLeafEntries, vouwt dan elk niveau in parents van hoogstens MaxKidsPerNode en herhaalt tot er één node overblijft. Limits worden bottom-up berekend, een leaf neemt zijn eerste en laatste key, een interne node neemt de ondergrens van zijn eerste child en de bovengrens van zijn laatste, dus overlappende sibling-bereiken kunnen door constructie niet voorkomen
Het vaste punt is identiteit. De uiteindelijke root krijgt altijd het objectnummer en de generatie waarmee de boom is geladen, dus de catalogus-referentie blijft geldig en niets anders in het document hoeft herschreven te worden. Elke andere node hergebruikt de referentie van een eerder geladen node alleen wanneer het node-type overeenkomt en het volledige key-bereik overeenkomt, en neemt anders een vers nummer dat is toegewezen uit het actieve maximum objectnummer plus één. Die combinatie houdt ongewijzigde subtrees naar hun bestaande objecten wijzen, terwijl gegarandeerd wordt dat elke node waarvan de inhoud is verplaatst als nieuw object wordt geschreven in plaats van stilzwijgend geherinterpreteerd. Dezelfde referentie-hergebruikdiscipline drijft de outline editor en zijn page remapping in de zusterunit
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber komt uit de bronrevisie }
if Editor.BuildBalancedObjects(MaximumObjectNumber + 1,
Serialized, Report) then
begin
Writeln('nodes ', Report.NodeCount, ' depth ', Report.MaximumDepth);
for I := 0 to High(Serialized) do
Writeln(Serialized[I].Reference.ObjectNumber, ' ',
Serialized[I].Reference.Generation, ' obj');
end
else
for I := 0 to High(Report.Issues) do
Writeln('issue ', Ord(Report.Issues[I].Kind), ' at obj ',
Report.Issues[I].ObjectNumber, ': ', Report.Issues[I].Message);
end;
Schrijf de revisie, en lees het resultaat dan terug
Een save is pas afgerond zodra de output opnieuw is geopend en vergeleken. SaveIncremental leest de feiten van de bronrevisie, weigert een root die de documentcatalogus zelf is, serialiseert de gebalanceerde nodeset plus het catalogusobject, en geeft alles door aan AppendPdfSparseUpdate, die een nieuwe revisie toevoegt volgens §7.5.6 met behoud van de bestaande /Info-referentie en de /ID-array van het document
Dan komt het deel dat het waard is om naar je eigen writers over te nemen. De editor roept TPdfTreeEditor.TryLoad opnieuw aan, dit keer tegen de destination stream, met dezelfde root-referentie en dezelfde opties, en vergelijkt het herladen entry-aantal en elke key en RawValue met wat hij van plan was te schrijven. Als het herladen faalt of een vergelijking verschilt, wordt de destination stream gereset in plaats van geretourneerd, zodat een gedeeltelijk geschreven revisie nooit als succes wordt teruggegeven. Verificatie leest de output door hetzelfde begrensde sparse-pad dat in onze write-up over de sparse lazy PDF object index wordt beschreven, en dat is wat de check betaalbaar houdt op grote bestanden
var
Dest: TFileStream;
begin
Dest := TFileStream.Create(OutputName, fmCreate);
try
if Editor.SaveIncremental(Source, Dest, Report) then
Writeln('wrote ', Report.NodeCount, ' nodes, ',
Report.EntryCount, ' entries')
else
{ destination al gereset, er is niets halfgeschreven bewaard }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Versleutelde bronnen worden geweigerd, en dat is eerlijk
De huidige implementatie wijst een versleutelde bron ronduit af. De reden is specifiek in plaats van defensief: een tree-rebuild introduceert nieuwe string-objecten en kan nieuwe stream-referenties introduceren, en §7.6 vereist dat die per object worden versleuteld met de documentkey en het bezittende objectnummer en de generatie. De /Encrypt-trailer naar voren kopiëren terwijl plaintext-strings worden toegevoegd, zou een bestand opleveren dat opent en dan precies daar waar je nieuwe keys zitten als rommel leest, wat erger is dan een duidelijke weigering. Twee verdere grenzen verdienen het om ronduit te worden gesteld. De editor werkt op een snapshot van de bronstream, dus de bron mag niet worden gewijzigd zolang er een editor overheen leeft, en hij weigert de documentcatalogus als tree-root te behandelen, want de catalogus is een dictionary die toevallig bomen bevat, geen tree-node zelf. Als je Delphi-, C++Builder- of Lazarus-code onderhoudt die /EmbeddedFiles, /Dests, /JavaScript of /PageLabels schrijft en je hebt leaf arrays met de hand gepatcht, zitten de tree editor en zijn geverifieerde incrementele save in de PDFiumPas Delphi PDFium component