Parchea un arreglo de hoja para agregar un nombre y la entrada igual puede leerse como inexistente. PDFium Component responde a eso con TPdfTreeEditor, un editor general para name trees y number trees de ISO 32000: carga el árbol con validación estricta, edita las entradas en memoria, reconstruye un conjunto de nodos balanceado con /Limits correctos y escribe una revisión incremental verificada
El fallo que motivó esta unidad es del tipo que solo se diagnostica leyendo bytes. Una herramienta agrega una clave y un valor a un arreglo /Names de hoja, escribe una revisión nueva, y cada hex dump confirma que la clave está en el archivo. Acrobat sigue diciendo que el adjunto no existe. Nada está corrupto en el sentido ingenuo. El árbol simplemente ya no es un árbol por el que el algoritmo de búsqueda pueda descender
¿Por qué un arreglo de hoja parcheado rompe la búsqueda?
Porque la búsqueda es un descenso binario a través de /Limits, no un escaneo de las hojas. ISO 32000-1 §7.9.6 define un name tree como una raíz cuyos /Kids bajan hasta hojas que contienen /Names, un arreglo de clave y valor alternados, con las claves ordenadas por comparación de bytes en todo el árbol, y cada nodo no raíz declarando /Limits como su clave menor y mayor propias. Un lector conforme compara la clave buscada contra el rango de cada kid y abre exactamente un hijo. Si agregas una clave a una hoja y dejas los /Limits viejos intactos, el descenso pasa de largo por el nodo que realmente la contiene
Los number trees (§7.9.7) tienen la misma forma con /Nums y claves enteras, y llevan tráfico real: /PageLabels en §12.4.2 es un number tree, y las categorías /Names del catálogo de §7.7.4 te dan /Dests, /EmbeddedFiles y /JavaScript como name trees. Así que cuatro invariantes tienen que cumplirse juntas, no una a la vez. Las claves deben crecer estrictamente en el orden del árbol, cada nivel debe publicar límites que coincidan con su rango real de claves, los rangos entre hermanos no deben solaparse, y el número de objeto y la generación de la raíz deben quedarse exactamente donde estaban, porque el catálogo y cualquier otro referenciador apuntan a esa raíz por referencia indirecta. Nuestra nota sobre trabajar con adjuntos PDF en Delphi muestra el lado consumidor de la misma estructura
¿Qué se niega a cargar TPdfTreeEditor?
Más de lo que esperarías, y a propósito. TPdfTreeEditor.TryLoad recorre el grafo de nodos a través de TPdfSparseDictionaryReader, resolviendo cada hijo con ReadDictionary en la generación exacta que nombra la referencia en lugar de aceptar la revisión que esté activa. Un árbol que no se pueda probar bien formado se rechaza antes de permitir una sola edición, lo que evita que una entrada rota se convierta en una salida rota de apariencia plausible
- Ciclos y referencias repetidas, ambos reportados como
ptviCycle, así que una entrada/Kidsque apunta hacia arriba en la cadena termina en lugar de recursar - Formas de nodo mezcladas: un diccionario que lleva
/Kidsjunto con/Nameso/NumsesptviInvalidNodeShape, igual que una hoja no raíz vacía - Claves desordenadas o duplicadas, reportadas como
ptviUnsortedKeyyptviDuplicateKey, verificadas en todo el recorrido en lugar de dentro de una sola hoja - Limits que no coinciden con las claves realmente presentes, reportados como
ptviInvalidLimits - Un valor crudo que no es exactamente un objeto PDF completo, reportado como
ptviInvalidValue - Excesos de presupuesto:
ptviDepthLimit,ptviNodeLimit,ptviEntryLimityptviDeadlineExceeded, acotados tanto porTPdfTreeEditOptionscomo por el presupuesto central del parser
TPdfTreeEditOptions.Default pone esos límites locales del árbol en 1,000,000 de entradas, 100,000 nodos, profundidad 64, 64 entradas por hoja y 32 kids por nodo. Las cifras de hoja y kid no son solo guardas; son el fan-out que se usa después cuando se reconstruye el árbol, que es por lo que el record valida que ambas sean al menos dos
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
{ el valor son los bytes en crudo de un 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 después del guardado de abajo }
end;
end;
Los valores de las entradas se quedan como bytes crudos de objeto PDF
Un valor de entrada se almacena textualmente, y esa es la única decisión de diseño que hace seguro apuntar el editor a archivos que no produjiste tú. TPdfTreeEntry tiene NameKey, NumberKey y RawValue, y RawValue es la corrida de bytes cruda de un objeto PDF completo: un diccionario, un arreglo, una cadena, una referencia a stream o una referencia indirecta pelada. Nada se decodifica a un variant de Delphi, a un TStrings o a un record tipado. La recompensa es un round trip sin pérdida de valores sobre los que la unidad no sabe nada. Un /Filespec con una extensión de vendor, un arreglo de destino con un tipo de fit inusual, un diccionario de acción /JavaScript con claves privadas, todos sobreviven una reconstrucción byte por byte, porque el editor nunca tuvo opinión sobre su contenido. También encoge la API de modificación a algo demostrable: UpsertName y UpsertNumber solo tienen que verificar que el valor suministrado se parsea como exactamente un objeto completo, y RemoveName, RemoveNumber y Clear solo tocan la lista de entradas
¿Cómo se reconstruye un árbol balanceado sin renumerar la raíz?
En dos pasadas con un punto fijo. BuildBalancedObjects primero corta la lista de entradas ordenadas en hojas de a lo más MaxLeafEntries, luego pliega cada nivel en padres de a lo más MaxKidsPerNode y repite hasta que quede un solo nodo. Los limits se calculan de abajo hacia arriba, una hoja tomando su primera y última clave, un nodo interno tomando el límite inferior de su primer hijo y el superior de su último, así que los rangos solapados entre hermanos no pueden ocurrir por construcción
El punto fijo es la identidad. La raíz final siempre recibe el número de objeto y la generación con los que se cargó el árbol, así que la referencia del catálogo sigue siendo válida y nada más del documento necesita reescribirse. Cualquier otro nodo reutiliza la referencia de un nodo cargado previamente solo cuando el tipo de nodo coincide y el rango completo de claves coincide, y en caso contrario toma un número fresco asignado desde el número de objeto máximo activo más uno. Esa combinación mantiene los subárboles sin cambios apuntando a sus objetos existentes y a la vez garantiza que cualquier nodo cuyo contenido se movió se escriba como objeto nuevo en lugar de reinterpretarse en silencio. La misma disciplina de reutilización de referencias impulsa el editor de outline y su re-mapeo de páginas en la unidad hermana
var
Serialized: TPdfTreeSerializedObjects;
I: Integer;
begin
{ FirstNewObjectNumber proviene de la revisión de origen }
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;
Escribe la revisión y luego vuelve a leer el resultado
Un guardado solo termina cuando la salida se ha vuelto a abrir y comparar. SaveIncremental lee los datos de la revisión fuente, rechaza una raíz que sea el propio catálogo del documento, serializa el conjunto de nodos balanceado más el objeto del catálogo, y se lo entrega todo a AppendPdfSparseUpdate, que agrega una revisión nueva según §7.5.6 preservando la referencia /Info existente y el arreglo /ID del documento
Luego viene la parte que vale la pena copiar en tus propios writers. El editor vuelve a llamar a TPdfTreeEditor.TryLoad, esta vez contra el stream de destino, con la misma referencia de raíz y las mismas opciones, y compara el conteo de entradas recargado y cada clave y RawValue contra lo que pretendía escribir. Si la recarga falla o alguna comparación difiere, el stream de destino se resetea en lugar de devolverse, así que una revisión parcialmente escrita nunca se entrega como éxito. La verificación lee la salida por el mismo camino sparse acotado descrito en nuestro artículo sobre el índice sparse perezoso de objetos PDF, que es lo que mantiene la comprobación accesible en archivos 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 ya reiniciado, no se conservó nada a medio escribir }
Writeln('save refused: ', Report.ErrorMessage);
finally
Dest.Free;
Editor.Free;
end;
end;
Las fuentes cifradas se rechazan, y eso es honesto
La implementación actual rechaza de plano una fuente cifrada. La razón es específica, no defensiva: una reconstrucción de árbol introduce objetos de cadena nuevos y puede introducir referencias a stream nuevas, y §7.6 exige que esos se cifren por objeto con la clave del documento y el número de objeto y la generación dueños. Copiar el trailer /Encrypt hacia adelante mientras se agregan cadenas en texto plano produciría un archivo que abre y luego se lee como basura exactamente donde están tus claves nuevas, que es peor que un rechazo claro. Dos límites más merecen decirse con claridad. El editor trabaja sobre una instantánea del stream fuente, así que la fuente no debe modificarse mientras un editor esté vivo sobre ella, y se niega a tratar el catálogo del documento como raíz de árbol, porque el catálogo es un diccionario que por casualidad contiene árboles, no un nodo de árbol en sí. Si mantienes código en Delphi, C++Builder o Lazarus que escriba /EmbeddedFiles, /Dests, /JavaScript o /PageLabels y has estado parcheando arreglos de hoja a mano, el editor de árboles y su guardado incremental verificado viajan en el componente PDFiumPas Delphi PDFium