Patch et leaf-array for at tilføje ét navn, og indgangen kan stadig læses som manglende. PDFium Component svarer på det med TPdfTreeEditor, en generel editor til ISO 32000 name trees og number trees: den indlæser træet med streng validering, redigerer indgange i hukommelsen, genopbygger et balanceret node-sæt med korrekte /Limits og skriver en verificeret inkrementel revision
Den fejl der motiverede denne unit, er af den slags du kun diagnosticerer ved at læse bytes. Et værktøj tilføjer en nøgle og en værdi til et leaf-/Names-array, skriver en ny revision, og hver hex dump bekræfter at nøglen er i filen. Acrobat siger stadig at vedhæftningen ikke findes. Intet er korrupt i den naive forstand. Træet er simpelthen ikke længere et træ opslagsalgoritmen kan dykke ned i
Hvorfor ødelægger et patchet leaf-array opslaget?
Fordi opslag er en binær nedstigning gennem /Limits, ikke en scanning af leaf'ene. ISO 32000-1 §7.9.6 definerer et name tree som en rod hvis /Kids fører ned til leaves der holder /Names, et array med skiftende nøgle og værdi, hvor nøglerne er ordnet efter byte-sammenligning på tværs af hele træet, og hvor enhver ikke-rod-node erklærer /Limits som sin egen mindste og største nøgle. En konform læser sammenligner den ønskede nøgle mod hvert kid-interval og åbner præcis ét barn. Tilføjer du en nøgle til et leaf og lader de gamle /Limits stå urørt, går nedstigningen lige forbi den node der faktisk holder den
Number trees (§7.9.7) har samme form med /Nums og heltals-nøgler, og de bærer ægte trafik: /PageLabels i §12.4.2 er et number tree, og katalogets /Names-kategorier fra §7.7.4 giver dig /Dests, /EmbeddedFiles og /JavaScript som name trees. Så fire invarianter skal holde sammen, ikke én ad gangen. Nøgler skal være strengt stigende i træ-rækkefølgen, hvert niveau skal offentliggøre limits der matcher dets faktiske nøgle-interval, søskende-intervaller må ikke overlappe, og rod-objektets nummer og generation skal blive præcis hvor de var, fordi kataloget og enhver anden referent peger på den rod via indirekte reference. Vores note om at arbejde med PDF-vedhæftninger i Delphi viser forbruger-siden af samme struktur
Hvad nægter TPdfTreeEditor at indlæse?
Mere end du måske forventer, og det er bevidst. TPdfTreeEditor.TryLoad gennemgår node-grafen gennem TPdfSparseDictionaryReader og opløser hvert barn med ReadDictionary ved præcis den generation referencen nævner, i stedet for at acceptere den revision der tilfældigvis er aktiv. Et træ der ikke kan bevises velformet, afvises før en eneste redigering tillades, hvilket forhindrer at et ødelagt input bliver til et plausibelt-udseende ødelagt output
- Cykler og gentagne referencer, begge rapporteret som
ptviCycle, så en/Kids-indgang der peger tilbage op i kæden terminerer i stedet for at rekursere - Blandede node-former: en dictionary der bærer
/Kidssammen med/Nameseller/NumserptviInvalidNodeShape, ligesom et tomt ikke-rod-leaf - Usorterede eller duplikerede nøgler, rapporteret som
ptviUnsortedKeyogptviDuplicateKey, tjekket på tværs af hele gennemløbet snarere end inde i ét leaf - Limits der er uenige med de nøgler der faktisk er til stede, rapporteret som
ptviInvalidLimits - En rå værdi der ikke er præcis ét komplet PDF-objekt, rapporteret som
ptviInvalidValue - Budget-overskridelser:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimitogptviDeadlineExceeded, afgrænset af bådeTPdfTreeEditOptionsog det centrale parser-budget
TPdfTreeEditOptions.Default sætter de træ-lokale grænser til 1.000.000 indgange, 100.000 noder, dybde 64, 64 indgange per leaf og 32 kids per node. Leaf- og kid-tallene er ikke kun vagter; de er den fan-out der bruges senere når træet genopbygges, hvilket er grunden til at record'en validerer at begge er mindst to
uses
FPdfCompress;
var
Options: TPdfTreeEditOptions;
RootRef: TPdfTreeObjectReference;
Editor: TPdfTreeEditor;
Report: TPdfTreeValidationReport;
begin
Options := TPdfTreeEditOptions.Default;
RootRef.ObjectNumber := EmbeddedFilesRootObject; { katalog /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
{ værdien er de rå bytes af ét komplet PDF-objekt }
Editor.UpsertName('invoice-2026-09',
'<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
Editor.RemoveName('draft-2025-11');
finally
{ Editor.Free efter save'et nedenfor }
end;
end;
Indgangsværdier forbliver som rå PDF-objekt-bytes
En indgangsværdi gemmes ordret, og det er den ene designbeslutning der gør editoren sikker at pege på filer du ikke selv har produceret. TPdfTreeEntry holder NameKey, NumberKey og RawValue, og RawValue er det rå byte-løb af ét komplet PDF-objekt: en dictionary, et array, en streng, en stream-reference eller en bar indirekte reference. Intet afkodes til en Delphi-variant, en TStrings eller en typer record. Gevinsten er lossless round-tripping af værdier unit'en intet kender til. En /Filespec med en vendor-extension, et destinations-array med en usædvanlig fit type, en /JavaScript-action-dictionary med private nøgler — alt sammen overlever en genopbygning byte for byte, fordi editoren aldrig havde en mening om deres indhold. Det krymper også modificerings-API'et til noget beviseligt: UpsertName og UpsertNumber skal kun verificere at den leverede værdi parser som præcis ét komplet objekt, og RemoveName, RemoveNumber og Clear rører kun indgangs-listen
Hvordan genopbygges et balanceret træ uden at rodnummeret ændres?
I to gennemløb med ét fast punkt. BuildBalancedObjects skærer først den sorterede indgangs-liste i leaves af højst MaxLeafEntries, folder så hvert niveau ind i forældre af højst MaxKidsPerNode og gentager indtil én enkelt node er tilbage. Limits beregnes nedefra og op: et leaf tager sin første og sidste nøgle, en intern node tager nedre limit fra sit første barn og øvre limit fra sit sidste, så overlappende søskende-intervaller ikke kan opstå ved konstruktion
Det faste punkt er identitet. Den endelige rod modtager altid det objektnummer og den generation træet blev indlæst med, så katalog-referencen forbliver gyldig og intet andet i dokumentet skal omskrives. Enhver anden node genbruger referencen fra en tidligere indlæst node kun når node-typen matcher og hele nøgle-intervallet matcher, og tager ellers et friskt nummer allokeret fra den aktive maksimale objekts-nummer plus én. Den kombination holder uændrede subtræer pegende på deres eksisterende objekter, mens den garanterer at enhver node hvis indhold flyttede sig skrives som et nyt objekt frem for lydløst at blive omfortolket. Den samme reference-genbrugs-disciplin driver outline-editoren og dens page-remapping i søster-unit'en
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber kommer fra kilderevisionen }
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;
Skriv revisionen, og læs så resultatet tilbage
Et save er først færdigt når outputtet er genåbnet og sammenlignet. SaveIncremental læser kilderevisionens fakta, nægter en rod der er selve dokumentkataloget, serialiserer det balancerede node-sæt plus katalog-objektet og overdrager alting til AppendPdfSparseUpdate, som tilføjer en ny revision per §7.5.6 mens den bevarer den eksisterende /Info-reference og dokumentets /ID-array
Så kommer delen der er værd at kopiere ind i dine egne writers. Editoren kalder TPdfTreeEditor.TryLoad igen, denne gang mod destinations-streamen, med samme rod-reference og samme options, og sammenligner den genindlæste indgangs-tæller samt hver nøgle og RawValue mod det den havde til hensigt at skrive. Fejler genindlæsningen, eller afviger nogen sammenligning, nulstilles destinations-streamen i stedet for at blive returneret, så en delvist skrevet revision aldrig udleveres som en succes. Verifikationen læser outputtet gennem den samme afgrænsede sparse-sti beskrevet i vores write-up om det sparse lazy PDF-objekt-indeks, hvilket er det der holder checken overkommelig på store filer
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
{ destinationen er allerede nulstillet, intet halvt skrevet blev beholdt }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Krypterede kilder afvises, og det er ærligt
Den nuværende implementering afviser en krypteret kilde blankt. Grunden er specifik snarere end defensiv: en træ-genopbygning introducerer nye streng-objekter og kan introducere nye stream-referencer, og §7.6 kræver at de krypteres per objekt med dokument-nøglen og det ejende objekts nummer og generation. At kopiere /Encrypt-traileren med fremad, mens man tilføjer plaintext-strenge, ville producere en fil der åbner og så læses som skrald præcis der hvor dine nye nøgler er, hvilket er værre end et klart afslag. To yderligere grænser er værd at nævne lige ud. Editoren arbejder på et snapshot af kilde-streamen, så kilden må ikke modificeres mens en editor lever over den, og den afslår at behandle dokumentkataloget som en træ-rod, da kataloget er en dictionary der tilfældigvis indeholder træer snarere end en træ-node selv. Vedligeholder du Delphi-, C++Builder- eller Lazarus-kode der skriver /EmbeddedFiles, /Dests, /JavaScript eller /PageLabels, og har du patchet leaf-arrays i hånden, leveres træ-editoren og dens verificerede inkrementelle save i PDFiumPas Delphi PDFium component