Patche um array de leaf para adicionar um nome e a entrada ainda pode ser lida como inexistente. O PDFium Component responde a isso com TPdfTreeEditor, um editor geral para name trees e number trees da ISO 32000: ele carrega a árvore com validação estrita, edita entradas em memória, reconstrói um conjunto balanceado de nós com /Limits corretos, e escreve uma revisão incremental verificada
A falha que motivou essa unidade é do tipo que você só diagnostica lendo bytes. Uma ferramenta acrescenta uma chave e um valor ao array /Names de uma leaf, escreve uma revisão nova, e todo hex dump confirma que a chave está no arquivo. O Acrobat continua dizendo que o anexo não existe. Nada está corrompido no sentido ingênuo. A árvore simplesmente deixou de ser uma árvore que o algoritmo de lookup consegue descer
Por que um array de leaf patcheado quebra o lookup?
Porque o lookup é uma descida binária pelos /Limits, não uma varredura das leaves. A ISO 32000-1 §7.9.6 define uma name tree como uma raiz cujos /Kids descem até leaves que seguram /Names, um array de chave e valor alternados, com as chaves ordenadas por comparação de bytes na árvore inteira, e todo nó não-raiz declarando /Limits como a menor e a maior chave próprias. Um reader conforme compara a chave desejada com cada faixa de kid e abre exatamente um filho. Acrescente uma chave a uma leaf e deixe os /Limits antigos intocados, e a descida passa reto pelo nó que de fato a segura
Number trees (§7.9.7) têm a mesma forma com /Nums e chaves inteiras, e carregam tráfego de verdade: o /PageLabels da §12.4.2 é uma number tree, e as categorias /Names do catálogo da §7.7.4 te dão /Dests, /EmbeddedFiles e /JavaScript como name trees. Então quatro invariantes têm que se sustentar juntas, não uma de cada vez. As chaves precisam ser estritamente crescentes na ordem da árvore, todo nível precisa publicar limits que batam com a faixa real de chaves, faixas de irmãos não podem se sobrepor, e o número e a geração do objeto raiz precisam ficar exatamente onde estavam, porque o catálogo e qualquer outro referenciador apontam para essa raiz por referência indireta. Nossa nota sobre trabalhar com anexos de PDF em Delphi mostra o lado consumidor da mesma estrutura
O que o TPdfTreeEditor se recusa a carregar?
Mais do que você esperaria, e de propósito. O TPdfTreeEditor.TryLoad percorre o grafo de nós pelo TPdfSparseDictionaryReader, resolvendo cada filho com ReadDictionary na geração exata nomeada pela referência, em vez de aceitar qualquer revisão que esteja ativa. Uma árvore que não pode ser provada bem formada é rejeitada antes de uma única edição ser permitida, o que impede um input quebrado de virar um output quebrado de aparência plausível
- Ciclos e referências repetidas, ambos reportados como
ptviCycle, então uma entrada de/Kidsapontando de volta na cadeia termina em vez de recursar - Formas de nó misturadas: um dicionário que carrega
/Kidsjunto com/Namesou/NumséptviInvalidNodeShape, assim como uma leaf vazia que não é raiz - Chaves desordenadas ou duplicadas, reportadas como
ptviUnsortedKeyeptviDuplicateKey, verificadas na travessia inteira em vez de dentro de uma leaf só - Limits que discordam das chaves realmente presentes, reportados como
ptviInvalidLimits - Um valor raw que não é exatamente um objeto PDF completo, reportado como
ptviInvalidValue - Estouros de budget:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimiteptviDeadlineExceeded, limitados tanto peloTPdfTreeEditOptionsquanto pelo budget central do parser
O TPdfTreeEditOptions.Default define esses limites locais da árvore como 1.000.000 de entradas, 100.000 nós, profundidade 64, 64 entradas por leaf e 32 kids por nó. Os números de leaf e kid não são só guards; são o fan-out usado depois quando a árvore é reconstruída, e é por isso que o record valida que ambos são pelo menos dois
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
{ o valor são os bytes brutos de um objeto PDF completo }
Editor.UpsertName('invoice-2026-09',
'<< /Type /Filespec /F (invoice.pdf) /EF << /F 42 0 R >> >>');
Editor.RemoveName('draft-2025-11');
finally
{ Editor.Free após o salvamento abaixo }
end;
end;
Valores de entrada ficam como bytes raw de objeto PDF
Um valor de entrada é guardado verbatim, e essa é a única decisão de design que torna o editor seguro para apontar para arquivos que você não produziu. O TPdfTreeEntry segura NameKey, NumberKey e RawValue, e o RawValue é a sequência raw de bytes de um objeto PDF completo: um dicionário, um array, uma string, uma referência de stream ou uma referência indireta pura. Nada é decodificado num variant do Delphi, num TStrings ou num record tipado. O ganho é round-trip sem perda de valores que a unidade não conhece nada. Um /Filespec carregando uma extensão de vendor, um array de destination com um fit type incomum, um dicionário de action /JavaScript com chaves privadas, tudo sobrevive a uma reconstrução byte por byte, porque o editor nunca teve opinião sobre o conteúdo deles. Isso também encolhe a API de modificação para algo provável: UpsertName e UpsertNumber só precisam verificar que o valor fornecido faz parse como exatamente um objeto completo, e RemoveName, RemoveNumber e Clear só tocam a lista de entradas
Como uma árvore balanceada é reconstruída sem renumerar a raiz?
Em duas passadas com um ponto fixo. O BuildBalancedObjects primeiro corta a lista ordenada de entradas em leaves de no máximo MaxLeafEntries, depois dobra cada nível em pais de no máximo MaxKidsPerNode e repete até sobrar um único nó. Os limits são calculados de baixo para cima: uma leaf toma a primeira e a última chave, um nó interno toma o limite inferior do primeiro filho e o limite superior do último, então faixas de irmãos sobrepostas não podem ocorrer por construção
O ponto fixo é a identidade. A raiz final sempre recebe o número de objeto e a geração com que a árvore foi carregada, então a referência do catálogo continua válida e nada mais no documento precisa ser reescrito. Todo outro nó reutiliza a referência de um nó previamente carregado só quando o tipo de nó bate e a faixa completa de chaves bate, e caso contrário toma um número novo alocado do número máximo de objeto ativo mais um. Essa combinação mantém subárvores inalteradas apontando para seus objetos existentes, ao mesmo tempo que garante que qualquer nó cujo conteúdo mudou seja escrito como objeto novo em vez de reinterpretado silenciosamente. A mesma disciplina de reuso de referência dirige o editor de outline e seu remapeamento de páginas na unidade irmã
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber vem da revisão de origem }
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;
>
Escreva a revisão, depois leia o resultado de volta
Um save só termina quando o output foi reaberto e comparado. O SaveIncremental lê os fatos da revisão de origem, recusa uma raiz que seja o próprio catálogo do documento, serializa o conjunto balanceado de nós mais o objeto do catálogo, e entrega tudo ao AppendPdfSparseUpdate, que acrescenta uma revisão nova conforme a §7.5.6 preservando a referência /Info existente e o array /ID do documento
Aí vem a parte que vale copiar para seus próprios writers. O editor chama TPdfTreeEditor.TryLoad de novo, agora contra o stream de destino, com a mesma referência de raiz e as mesmas options, e compara a contagem de entradas recarregada e toda chave e RawValue contra o que pretendia escrever. Se o reload falhar ou qualquer comparação diferir, o stream de destino é resetado em vez de devolvido, então uma revisão parcialmente escrita nunca volta como sucesso. A verificação lê o output pelo mesmo caminho sparse limitado descrito no nosso artigo sobre o índice de objetos PDF sparse lazy, que é o que mantém o check barato em arquivos grandes
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
{ destino já reiniciado, nada escrito pela metade foi mantido }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
e>
Fontes criptografadas são recusadas, e isso é honesto
A implementação atual rejeita uma fonte criptografada de cara. O motivo é específico, não defensivo: uma reconstrução de árvore introduz objetos de string novos e pode introduzir referências de stream novas, e a §7.6 exige que elas sejam criptografadas por objeto com a chave do documento e o número de objeto e a geração donos. Copiar o trailer /Encrypt para frente enquanto acrescenta strings em plaintext produziria um arquivo que abre e depois lê como lixo exatamente onde estão suas chaves novas, o que é pior do que uma recusa clara. Dois limites a mais valem ser ditos sem rodeio. O editor trabalha num snapshot do stream de origem, então a fonte não pode ser modificada enquanto um editor estiver vivo sobre ela, e ele se recusa a tratar o catálogo do documento como raiz de árvore, porque o catálogo é um dicionário que por acaso contém árvores, não um nó de árvore em si. Se você mantém código Delphi, C++Builder ou Lazarus que escreve /EmbeddedFiles, /Dests, /JavaScript ou /PageLabels e vem patcheando arrays de leaf na mão, o editor de árvores e seu save incremental verificado vêm no componente PDFium Delphi PDFiumPas