Fai una patch all'array di una foglia per aggiungere un nome e la voce può comunque risultare assente. La PDFium Component risponde con TPdfTreeEditor, un editor generico per i name tree e i number tree di ISO 32000: carica l'albero con validazione rigorosa, modifica le voci in memoria, ricostruisce un set di nodi bilanciato con i /Limits corretti e scrive una revisione incrementale verificata
Il fallimento che ha motivato questa unit è del tipo che diagnostichi solo leggendo i byte. Uno strumento append una chiave e un valore a un array /Names di una foglia, scrive una nuova revisione, e ogni hex dump conferma che la chiave è nel file. Acrobat continua a dire che l'allegato non esiste. Nulla è corrotto nel senso naif. L'albero semplicemente non è più un albero che l'algoritmo di lookup può scendere
Perché un array di foglia patchato rompe il lookup?
Perché il lookup è una discesa binaria attraverso i /Limits, non una scansione delle foglie. ISO 32000-1 §7.9.6 definisce un name tree come una radice i cui /Kids portano giù alle foglie che tengono i /Names, un array di chiave e valore alternati, con le chiavi ordinate per confronto di byte su tutto l'albero, e ogni nodo non radice che dichiara i /Limits come la propria chiave minima e massima. Un reader conforme confronta la chiave cercata con ogni intervallo dei kid e apre esattamente un figlio. Append una chiave a una foglia, lascia i vecchi /Limits intatti e la discesa passa dritto davanti al nodo che in realtà la contiene
I number tree (§7.9.7) hanno la stessa forma con i /Nums e chiavi intere, e portano traffico vero: i /PageLabels di §12.4.2 sono un number tree, e le categorie /Names del catalogo di §7.7.4 ti danno /Dests, /EmbeddedFiles e /JavaScript come name tree. Quindi quattro invarianti devono reggere insieme, non uno alla volta. Le chiavi devono essere strettamente crescenti nell'ordine dell'albero, ogni livello deve pubblicare dei limiti che combaciano col suo intervallo di chiavi reale, gli intervalli tra fratelli non devono sovrapporsi, e l'object number e la generazione della radice devono restare esattamente dove erano, perché il catalogo e ogni altro referenziatore puntano a quella radice con riferimento indiretto. La nostra nota su lavorare con gli allegati PDF in Delphi mostra il lato consumer della stessa struttura
Cosa si rifiuta di caricare TPdfTreeEditor?
Più di quanto ti aspetteresti, e di proposito. TPdfTreeEditor.TryLoad cammina sul grafo dei nodi tramite TPdfSparseDictionaryReader, risolvendo ogni figlio con ReadDictionary alla generazione esatta indicata dal riferimento invece di accettare qualunque revisione sia attiva. Un albero di cui non si può dimostrare la buona formazione viene rifiutato prima che sia permessa una singola modifica, così un input rotto non diventa un output rotto dall'aria plausibile
- Cicli e riferimenti ripetuti, entrambi segnalati come
ptviCycle, così una voce/Kidsche punta indietro su per la catena termina invece di ricorrere - Forme di nodo miste: un dizionario che porta
/Kidsinsieme a/Nameso/NumsèptviInvalidNodeShape, come una foglia non radice vuota - Chiavi non ordinate o duplicate, segnalate come
ptviUnsortedKeyeptviDuplicateKey, controllate su tutta la traversata e non dentro una singola foglia - Limits che non concordano con le chiavi davvero presenti, segnalati come
ptviInvalidLimits - Un valore grezzo che non è esattamente un oggetto PDF completo, segnalato come
ptviInvalidValue - Superamenti del budget:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimiteptviDeadlineExceeded, delimitati sia daTPdfTreeEditOptionssia dal budget centrale del parser
TPdfTreeEditOptions.Default imposta quei limiti locali dell'albero a 1.000.000 di voci, 100.000 nodi, profondità 64, 64 voci per foglia e 32 kid per nodo. I valori di foglia e kid non sono solo guardie; sono il fan-out usato più avanti quando l'albero viene ricostruito, ed è per questo che il record valida che entrambi siano almeno due
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
{ il valore sono i byte grezzi di un intero oggetto PDF }
Editor.UpsertName('invoice-2026-09',
'<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
Editor.RemoveName('draft-2025-11');
finally
{ Editor.Free dopo il salvataggio qui sotto }
end;
end;
I valori delle voci restano byte grezzi di oggetto PDF
Un valore di voce è memorizzato alla lettera, ed è questa la singola decisione di design che rende l'editor sicuro da puntare su file che non hai prodotto tu. TPdfTreeEntry tiene NameKey, NumberKey e RawValue, e RawValue è la sequenza di byte grezzi di un oggetto PDF completo: un dizionario, un array, una stringa, un riferimento a stream o un riferimento indiretto nudo. Nulla viene decodificato in un variant Delphi, in un TStrings o in un record tipizzato. Il tornaconto è il round-trip senza perdite di valori di cui l'unit non sa nulla. Un /Filespec con un'estensione del vendor, un array di destinazione con un fit type insolito, un dizionario di azione /JavaScript con chiavi private: tutto sopravvive a una ricostruzione byte per byte, perché l'editor non ha mai avuto un'opinione sui loro contenuti. Inoltre restringe l'API di modifica a qualcosa di dimostrabile: UpsertName e UpsertNumber devono solo verificare che il valore fornito si parsi come esattamente un oggetto completo, e RemoveName, RemoveNumber e Clear toccano solo la lista delle voci
Come si ricostruisce un albero bilanciato senza rinumerare la radice?
In due passate con un punto fisso. BuildBalancedObjects prima taglia la lista di voci ordinata in foglie di al massimo MaxLeafEntries, poi piega ogni livello in genitori di al massimo MaxKidsPerNode e ripete finché resta un nodo solo. I Limits sono calcolati dal basso verso l'alto, con una foglia che prende la sua prima e ultima chiave e un nodo interno che prende il limite inferiore del suo primo figlio e il limite superiore dell'ultimo, così gli intervalli tra fratelli non possono sovrapporsi per costruzione
Il punto fisso è l'identità. La radice finale riceve sempre l'object number e la generazione con cui l'albero è stato caricato, quindi il riferimento del catalogo resta valido e nient'altro nel documento va riscritto. Ogni altro nodo riusa il riferimento di un nodo caricato prima solo quando il tipo di nodo combacia e l'intero intervallo di chiavi combacia, altrimenti prende un numero nuovo allocato a partire dall'object number massimo attivo più uno. Questa combinazione tiene i sottoalberi invariati a puntare ai loro oggetti esistenti, garantendo al tempo stesso che ogni nodo il cui contenuto si è spostato venga scritto come oggetto nuovo invece che reinterpretato in silenzio. La stessa disciplina di riuso dei riferimenti guida l'outline editor e il suo page remapping nell'unit sorella
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber proviene dalla revisione sorgente }
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;
Scrivi la revisione, poi rileggi il risultato
Un salvataggio è finito solo quando l'output è stato riaperto e confrontato. SaveIncremental legge i fatti della revisione sorgente, rifiuta una radice che è il catalogo del documento stesso, serializza il set di nodi bilanciato più l'oggetto catalogo, e consegna tutto a AppendPdfSparseUpdate, che append una nuova revisione secondo §7.5.6 preservando il riferimento /Info esistente e l'array /ID del documento
Poi arriva la parte che vale la pena copiare nei tuoi writer. L'editor chiama di nuovo TPdfTreeEditor.TryLoad, stavolta sullo stream di destinazione, con lo stesso riferimento di radice e le stesse opzioni, e confronta il conteggio delle voci ricaricato e ogni chiave e RawValue con ciò che intendeva scrivere. Se il reload fallisce o un confronto differisce, lo stream di destinazione viene resettato invece di essere restituito, così una revisione scritta a metà non viene mai consegnata come successo. La verifica legge l'output attraverso lo stesso percorso sparse limitato descritto nel nostro articolo sull'indice sparse lazy degli oggetti PDF, ed è questo che tiene il controllo a buon mercato sui file grandi
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
{ destinazione già azzerata, nulla di scritto a metà è stato conservato }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Le sorgenti criptate vengono rifiutate, ed è onesto
L'implementazione attuale rifiuta a priori una sorgente criptata. Il motivo è specifico, non difensivo: una ricostruzione dell'albero introduce nuovi oggetti stringa e può introdurre nuovi riferimenti a stream, e §7.6 richiede che siano criptati per oggetto con la chiave del documento e l'object number e la generazione proprietari. Copiare avanti il trailer /Encrypt appendendo stringhe in chiaro produrrebbe un file che si apre e poi legge spazzatura esattamente dove stanno le tue nuove chiavi, il che è peggio di un rifiuto netto. Altri due limiti vale la pena dirli chiaro. L'editor lavora su uno snapshot dello stream sorgente, quindi la sorgente non va modificata finché un editor è vivo sopra di essa, e si rifiuta di trattare il catalogo del documento come radice dell'albero, perché il catalogo è un dizionario che per caso contiene alberi, non un nodo d'albero esso stesso. Se mantieni codice Delphi, C++Builder o Lazarus che scrive /EmbeddedFiles, /Dests, /JavaScript o /PageLabels e hai patchato array di foglie a mano, l'editor di alberi e il suo salvataggio incrementale verificato sono inclusi nella PDFiumPas Delphi PDFium component