Remende um array de folha para adicionar um nome e a entrada ainda pode ler-se como ausente. O PDFium Component responde com o TPdfTreeEditor, um editor geral para name trees e number trees da ISO 32000: carrega a árvore com validação estrita, edita as entradas em memória, reconstrói um conjunto de nós balanceado com /Limits corretos e escreve uma revisão incremental verificada
A falha que motivou esta unit é do tipo que só se diagnostica a ler bytes. Uma ferramenta anexa uma chave e um valor a um array /Names de uma folha, escreve uma revisão nova, e cada hex dump confirma que a chave está no ficheiro. O Acrobat continua a dizer que o anexo não existe. Nada está corrompido no sentido ingénuo. A árvore simplesmente já não é uma árvore que o algoritmo de lookup consiga descer
Porque é que um array de folha remendado parte o lookup?
Porque o lookup é uma descida binária através de /Limits, não uma varredura das folhas. A ISO 32000-1 §7.9.6 define uma name tree como uma raiz cujos /Kids descem até folhas que guardam /Names, um array de chave e valor alternados, com as chaves ordenadas por comparação de bytes em toda a árvore, e cada nó não-raiz a declarar /Limits como a sua própria chave mínima e máxima. Um reader conforme compara a chave pretendida com cada intervalo de kid e abre exatamente um filho. Anexe uma chave a uma folha, deixe o /Limits antigo intocado, e a descida passa direto pelo nó que realmente a guarda
As number trees (§7.9.7) têm a mesma forma com /Nums e chaves inteiras, e carregam tráfego real: o /PageLabels em §12.4.2 é uma number tree, e as categorias de /Names do catálogo de §7.7.4 dão-lhe /Dests, /EmbeddedFiles e /JavaScript como name trees. Assim, quatro invariantes têm de se aguentar em conjunto, não uma de cada vez. As chaves têm de estar estritamente crescentes em ordem de árvore, cada nível tem de publicar limites que batam certo com o seu intervalo real de chaves, os intervalos de irmãos não se podem sobrepor, e o object number e a geração da raiz têm de ficar exatamente onde estavam, porque o catálogo e qualquer outro referenciador apontam para essa raiz por referência indireta. A nossa nota sobre trabalhar com anexos de PDF em Delphi mostra o lado consumidor da mesma estrutura
O que é que o TPdfTreeEditor recusa carregar?
Mais do que pode esperar, e de propósito. O TPdfTreeEditor.TryLoad percorre o grafo de nós através do TPdfSparseDictionaryReader, resolvendo cada filho com ReadDictionary na geração exata nomeada pela referência em vez de aceitar a revisão que estiver ativa. Uma árvore que não possa ser provada bem formada é rejeitada antes de uma única edição ser permitida, o que impede um input partido de se tornar um output partido com ar plausível
- Ciclos e referências repetidas, ambos reportados como
ptviCycle, pelo que uma entrada/Kidsa apontar de volta pela cadeia termina em vez de recursar - Formas de nó misturadas: um dicionário que carrega
/Kidsjuntamente com/Namesou/NumséptviInvalidNodeShape, tal como uma folha não-raiz vazia - Chaves desordenadas ou duplicadas, reportadas como
ptviUnsortedKeyeptviDuplicateKey, verificadas ao longo de todo o percurso em vez de dentro de uma folha - Limits que discordam das chaves realmente presentes, reportados como
ptviInvalidLimits - Um valor cru que não seja exatamente um objeto PDF completo, reportado como
ptviInvalidValue - Estouros de budget:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimiteptviDeadlineExceeded, limitados tanto peloTPdfTreeEditOptionscomo pelo budget central do parser
O TPdfTreeEditOptions.Default define esses limites locais à árvore em 1.000.000 de entradas, 100.000 nós, profundidade 64, 64 entradas por folha e 32 kids por nó. Os valores de folha e kid não são só guards; são o fan-out usado mais tarde quando a árvore é reconstruída, que é a razão pela qual 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; { catálogo /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 crus 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 depois do save abaixo }
end;
end;
Os valores de entrada ficam como bytes crus de objeto PDF
Um valor de entrada é guardado verbatim, e esta é a única decisão de design que torna o editor seguro de apontar a ficheiros que não produziu. O TPdfTreeEntry guarda NameKey, NumberKey e RawValue, e o RawValue é a sequência de bytes crus de um objeto PDF completo: um dicionário, um array, uma string, uma referência a stream ou uma referência indireta nua. Nada é descodificado num variant de Delphi, num TStrings ou num record tipado. O payoff é um round-trip lossless de valores que a unit não conhece. Um /Filespec com uma extensão de vendor, um array de destinos com um fit type invulgar, um dicionário de ação /JavaScript com chaves privadas, tudo sobrevive a um rebuild byte a byte, porque o editor nunca teve opinião sobre os conteúdos. Também encolhe a API de modificação para algo provável: o UpsertName e o UpsertNumber só têm de verificar que o valor fornecido se analisa como exatamente um objeto completo, e o RemoveName, o RemoveNumber e o Clear só tocam na lista de entradas
Como é que 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 folhas 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 folha a tomar a sua primeira e última chave, um nó interno a tomar o limite inferior do seu primeiro filho e o limite superior do último, pelo que intervalos sobrepostos entre irmãos não podem ocorrer por construção
O ponto fixo é a identidade. A raiz final recebe sempre o object number e a geração com que a árvore foi carregada, pelo que a referência do catálogo se mantém válida e nada mais no documento precisa de ser reescrito. Cada outro nó reutiliza a referência de um nó previamente carregado apenas quando o tipo de nó coincide e o intervalo completo de chaves coincide, e de outro modo toma um número novo alocado a partir do object number máximo ativo mais um. Essa combinação mantém subárvores inalteradas a apontar para os seus objetos existentes, garantindo ao mesmo tempo que qualquer nó cujo conteúdo se mexeu é escrito como objeto novo em vez de re-interpretado em silêncio. A mesma disciplina de reutilização de referências conduz o editor de outlines e o seu remap de páginas na unit 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 e depois leia o resultado de volta
Um save só está acabado depois de o output ter sido reaberto e comparado. O SaveIncremental lê os factos 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 anexa uma revisão nova segundo §7.5.6 preservando a referência /Info existente e o array /ID do documento
Depois vem a parte que vale a pena copiar para os seus próprios writers. O editor chama o TPdfTreeEditor.TryLoad de novo, desta vez contra o stream de destino, com a mesma referência de raiz e as mesmas opções, e compara a contagem de entradas recarregada e cada chave e RawValue com o que pretendia escrever. Se o reload falhar ou alguma comparação diferir, o stream de destino é reiniciado em vez de devolvido, pelo que uma revisão parcialmente escrita nunca é entregue como sucesso. A verificação lê o output pelo mesmo path sparse com limites descrito no nosso artigo sobre o índice de objetos PDF sparse e lazy, que é o que mantém a verificação acessível em ficheiros 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 de meio escrito foi mantido }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Fontes encriptadas são recusadas, e isso é honesto
A implementação atual rejeita uma fonte encriptada sem rodeios. A razão é específica e não defensiva: um rebuild de árvore introduz objetos de string novos e pode introduzir referências a streams novas, e a §7.6 exige que esses sejam encriptados por objeto com a chave do documento e o object number e a geração donos. Copiar o trailer /Encrypt para a frente enquanto anexa strings em plaintext produziria um ficheiro que abre e depois se lê como lixo exatamente onde as suas chaves novas estão, que é pior do que uma recusa clara. Mais dois limites valem a pena ser ditos claramente. O editor trabalha sobre um snapshot do stream de origem, pelo que a origem não pode ser modificada enquanto um editor estiver vivo sobre ela, e recusa tratar o catálogo do documento como raiz de árvore, já que o catálogo é um dicionário que por acaso contém árvores e não um nó de árvore em si. Se mantém código Delphi, C++Builder ou Lazarus que escreve /EmbeddedFiles, /Dests, /JavaScript ou /PageLabels e tem remendado arrays de folha à mão, o editor de árvores e o seu save incremental verificado vêm no componente PDFiumPas Delphi PDFium