Artículo técnico

Explicación de metadatos, esquemas y anotaciones de PDF

El mismo PDF puede reportar dos autores distintos. El diálogo de propiedades de un visor de escritorio muestra un nombre, un sistema de gestión de activos que indexa XMP muestra otro, y ambos programas están leyendo el archivo correctamente. PDF guarda los metadatos del documento dos veces, y mantiene marcadores, enlaces y notas en estructuras que jamás tocan el contenido de las páginas, así que cualquiera de ellos puede estar mal mientras cada página renderiza perfecta

El modelo de objetos de abajo (objetos indirectos, la tabla de referencias cruzadas, el trailer) está cubierto en el recorrido de la estructura de documentos PDF. Acá el foco se queda en lo que cuelga del catálogo del documento

Cuatro entradas en el catálogo, cuatro subsistemas independientes

Todo lo que se discute abajo se alcanza desde el catálogo del documento, el diccionario al que apunta la entrada /Root del trailer (ISO 32000-1 §7.7.2). Un catálogo que cablea todo se ve así:

1 0 obj
<< /Type /Catalog
   /Pages 2 0 R
   /Outlines 3 0 R
   /Names << /EmbeddedFiles 4 0 R /Dests 6 0 R >>
   /Metadata 5 0 R
>>
endobj
Diagrama PDF de un catálogo de documento PDF enlazando al árbol de páginas, la raíz del esquema, el diccionario de nombres y el stream de metadatos XMP como cuatro subsistemas opcionales
El catálogo del documento cablea cuatro subsistemas independientes, cada uno alcanzable por su propia entrada. Cualquiera puede estar ausente mientras las páginas siguen renderizando normal

/Pages es el documento visible, y el único de los cuatro que el catálogo exige. /Outlines lleva al árbol de marcadores, /Metadata al stream XMP, y /Names al diccionario de nombres, que guarda archivos incrustados bajo /EmbeddedFiles y destinos con nombre bajo /Dests. Un reader que no encuentre ninguno de los tres opcionales igual muestra cada página, razón por la cual esta capa es la primera en degradarse cuando un archivo pasa por herramientas que solo entienden páginas. Nada dentro de un objeto de página apunta de vuelta al esquema, al árbol /Dests ni al stream XMP. Copie las páginas solas a un archivo nuevo y las tres cosas se pierden

¿Por qué un PDF lleva dos almacenes de metadatos?

Un PDF lleva dos almacenes de metadatos porque el formato creció un segundo sin retirar el primero. El mecanismo original es el diccionario de información del documento (ISO 32000-1 §14.3.3, Tabla 317), referenciado por /Info en el trailer: entradas de texto planas para /Title, /Author, /Subject, /Keywords, /Creator, /Producer, más /CreationDate, /ModDate y /Trapped. ISO 32000-2 declara deprecated el diccionario entero salvo las dos entradas de fecha, y apunta a los writers hacia el stream de metadatos XMP

Diagrama PDF que compara el diccionario Info de PDF y el stream de metadatos XMP, dos almacenes con campos que se traslapan que deben mantenerse sincronizados con XMP como fuente de verdad
El diccionario Info y el stream XMP responden las mismas preguntas en paralelo. Manténgalos sincronizados regenerando ambos desde un solo juego de valores

XMP (ISO 32000-1 §14.3.2) es un documento XML escrito en RDF, guardado en un stream cuyo diccionario lleva /Type /Metadata /Subtype /XML. A diferencia del diccionario Info, un paquete XMP está diseñado para ser encontrado y parseado por herramientas que no saben nada de la sintaxis PDF:

5 0 obj
<< /Type /Metadata /Subtype /XML /Length 1235 >>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
  <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description rdf:about=""
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:xmp="http://ns.adobe.com/xap/1.0/"
        xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
      <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report</rdf:li></rdf:Alt></dc:title>
      <dc:creator><rdf:Seq><rdf:li>A. Author</rdf:li></rdf:Seq></dc:creator>
      <xmp:CreateDate>2026-06-16T10:46:27+08:00</xmp:CreateDate>
      <xmp:CreatorTool>Reporting Service 4.2</xmp:CreatorTool>
      <pdf:Producer>losLab PDF Library</pdf:Producer>
    </rdf:Description>
  </rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
endstream
endobj

Tres detalles de ese bloque deciden si el paquete sobrevive al tooling real. Las instrucciones de procesamiento xpacket enmarcan el paquete para que un scanner pueda ubicarlo dentro de un stream de bytes arbitrario; quite el <?xpacket end="w"?> de cierre y el archivo igual abre, pero los scanners pierden su marca de fin. Los tipos de valor también importan: dc:title es una alternativa de lenguaje y necesita rdf:Alt, mientras que dc:creator es una lista ordenada y necesita rdf:Seq. Escribir cualquiera como un nodo de texto desnudo es un error fácil de cometer. Los prefijos, en cambio, son solo convención; un parser se guía por el URI del namespace, así que un dc: atado a otro URI ya no es Dublin Core

Las claves Info y sus contrapartes XMP se emparejan como sigue. PDF/A-1 (ISO 19005-1 §6.7.3) declara el emparejamiento como regla de conformidad: cuando ambos almacenes están presentes, los valores deben ser equivalentes

Info dictionaryXMP property
/Titledc:title (rdf:Alt)
/Authordc:creator (rdf:Seq)
/Subjectdc:description (rdf:Alt)
/Keywordspdf:Keywords
/Creatorxmp:CreatorTool
/Producerpdf:Producer
/CreationDate, /ModDatexmp:CreateDate, xmp:ModifyDate

Las ediciones a mano y las fusiones son donde los dos se desvían. Una fusión puede heredar el /Info de una entrada y el paquete XMP de otra. La regla más segura es tratar XMP como la fuente de verdad, conservar el /Info para los consumidores más viejos, y regenerar ambos desde un solo juego de valores cada vez que cualquiera cambie

Escribir ambos almacenes desde un solo juego de propiedades

HotPDF construye ambos almacenes desde las mismas propiedades del documento. Title, Author, Subject y Keywords van al /Info; cuando EnableXMPMetadata está en True, EndDoc también escribe un paquete XMP desde esos mismos valores, con dc:title como rdf:Alt y dc:creator como rdf:Seq, y lo enlaza desde el /Metadata del catálogo. EnableXMPMetadata queda en False por defecto, y vale la pena activarlo para cualquier cosa que salga de sus manos: un archivo con solo /Info es invisible para toda herramienta que lea únicamente XMP. CustomXMP reemplaza el paquete generado por su propio texto cuando usted necesita esquemas extra

uses
  System.SysUtils, Vcl.Graphics, HPDFDoc;

procedure BuildReport(const FileName: string);
var
  PDF: THotPDF;
  Chapter: THPDFDocOutlineObject;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;
    PDF.FileName := FileName;
    // Un solo juego de valores alimenta al /Info y al paquete XMP
    PDF.Title := 'Quarterly Report';
    PDF.Author := 'A. Author';
    PDF.Subject := 'Results for the third quarter';
    PDF.Keywords := 'quarterly, results';
    PDF.EnableXMPMetadata := True;
    PDF.BeginDoc;

    PDF.CurrentPage.SetFont('Arial', [fsBold], 20);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Chapter 1: Results');
    Chapter := PDF.OutlineRoot.AddChild('Chapter 1: Results', 0, 50);
    Chapter.Opened := True;   // se guarda como un /Count positivo

    PDF.AddPage;
    PDF.CurrentPage.SetFont('Arial', [fsBold], 16);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Introduction');
    Chapter.AddChild('Introduction', 0, 50);

    PDF.AddPage;
    PDF.CurrentPage.SetFont('Arial', [fsBold], 16);
    PDF.CurrentPage.TextOut(50, 50, 0, 'Methodology');
    Chapter.AddChild('Methodology', 0, 50);

    PDF.EndDoc;
  finally
    PDF.Free;
  end;
end;

Editar un archivo existente es distinto. Sobre un documento cargado, SetLoadedTitle, SetLoadedAuthor, SetLoadedCreator y sus hermanos escriben la entrada del diccionario Info, y el stream XMP es un objeto aparte que usted reemplaza con SetLoadedXMPMetadata. Cambie el autor con una llamada y no con la otra, y SaveLoadedDocument produce exactamente el archivo de dos autores del párrafo inicial

El árbol de esquema detrás del panel de marcadores

El panel de marcadores se reconstruye desde un árbol de diccionarios doblemente enlazado, el esquema del documento (ISO 32000-1 §12.3.3, Tablas 152 y 153). No existe ningún arreglo de marcadores en el archivo; el visor reconstruye el panel siguiendo referencias desde el /Outlines del catálogo

Diagrama PDF del árbol de esquema de PDF mostrando los enlaces Parent, First, Last, Prev, Next y Count que reconstruyen el panel de marcadores del visor
El panel de marcadores se reconstruye desde un árbol de esquema doblemente enlazado hilado por Parent, First, Last, Prev, Next y Count. Un enlace vencido esconde una rama entera sin error alguno
8 0 obj                                    % la raíz del esquema
<< /Type /Outlines /Count 3 /First 9 0 R /Last 9 0 R >>
endobj
9 0 obj                                    % nivel superior: un capítulo
<< /Title (Chapter 1: Results)
   /Parent 8 0 R /Count 2
   /First 12 0 R /Last 15 0 R
   /Dest [3 0 R /XYZ 0 792 0] >>
endobj
12 0 obj                                   % primer hijo
<< /Title (Introduction)
   /Parent 9 0 R /Next 15 0 R
   /Dest [4 0 R /XYZ 72 720 0] >>
endobj
15 0 obj                                   % segundo hijo, último hermano
<< /Title (Methodology)
   /Parent 9 0 R /Prev 12 0 R
   /Dest [5 0 R /Fit] >>
endobj

Las invariantes se leen directo de los enlaces. Cada item apunta a su /Parent. Los hermanos se encadenan por /Prev y /Next; el primer hijo no tiene /Prev, el último no tiene /Next. Un padre nombra sus dos extremos por /First y /Last, y todo lo intermedio existe solo como un paso en el recorrido de hermanos. Rompa un enlace y los visores muestran lo que todavía alcancen: un /Next vencido corta un capítulo a la mitad, mientras que un /Last que discrepa del final de la cadena deja a distintos visores dibujando árboles distintos

/Count sorprende a la gente porque guarda estado del panel. En la raíz es el número de items visibles en todos los niveles. En un item, un valor positivo significa que el item está abierto y da el número de descendientes visibles; un valor negativo significa que está cerrado, y su magnitud es cuántos descendientes aparecerían al expandirlo. En el ejemplo la raíz lleva 3: un capítulo más sus dos hijos abiertos. Un generador que escribe cada /Count como un total positivo reabre en silencio cada rama que el autor colapsó

En HotPDF, OutlineRoot es un THPDFDocOutlineObject, y AddChild(Title, X, Y) agrega un item, cablea sus enlaces /Parent, /Prev, /Next, /First y /Last, y lo apunta a la página actual como [page /XYZ left top 0]. La propiedad Opened decide el signo, y el writer recalcula cada /Count desde el árbol terminado en EndDoc, así que fijar Opened después de agregar los hijos está bien. InsertBefore, InsertAfter y MoveTo reestructuran el árbol con la misma contabilidad

Destinos: dónde aterriza realmente un clic

Marcadores y anotaciones de enlace se resuelven ambos a un destino (ISO 32000-1 §12.3.2, Tabla 151), y un destino es más que un número de página. Es un arreglo que nombra un objeto de página y después un tipo de ajuste que le dice al visor cómo enmarcarla. El caballo de batalla es [page /XYZ left top zoom]. Cualquiera de los tres operandos puede ser null, que significa "conservar lo que el reader tenía", y un zoom de 0 significa lo mismo que null. Así que [page /XYZ null null null] cambia la página y nada más, que suele ser lo que un enlace "ir a página" debería hacer

La familia de fit intercambia posicionamiento exacto por resiliencia. [page /Fit] escala la página entera a la ventana, [page /FitH top] ajusta el ancho de la página con un borde superior dado, y [page /FitR left bottom right top] hace zoom a un rectángulo hasta llenar la vista. Estas computan la escala desde la geometría de la página al momento del clic; un /XYZ con un zoom horneado lo conserva aun después de que la página cambie de tamaño. Para un índice de contenidos, /FitH en el borde superior de la sección es el mejor valor por defecto

Las coordenadas son donde la gente se quema. Los números de destino están en espacio de usuario por defecto, origen abajo a la izquierda y y creciendo hacia arriba (§8.3.2.3), mientras que los hábitos de maquetación en pantalla miden desde arriba. Las llamadas estilo canvas de HotPDF (TextOut, rectángulos de enlace, la X y la Y de AddChild) toman coordenadas de arriba hacia abajo y las convierten por usted. RegisterNamedDestination escribe su Left y su Top en el arreglo de destino tal como se los dan, así que deben venir ya en espacio de usuario de PDF:

// Llame entre BeginDoc y EndDoc, cuando las páginas 0..2 ya existan
procedure AddContentsLinks(PDF: THotPDF);
var
  R: TRect;
begin
  // Destino con nombre 50 pt debajo del tope de la página 1.
  // Top está en espacio de usuario de PDF, medido hacia arriba desde el borde inferior
  PDF.CurrentPageNumber := 1;
  PDF.RegisterNamedDestination('chapter-1', 1, dfmFitH,
    0, PDF.CurrentPage.Height - 50, 0, 0, 0);

  // Entradas del índice en la página 0; los rectángulos usan coordenadas de arriba hacia abajo
  PDF.CurrentPageNumber := 0;
  R := Rect(50, 175, 545, 209);
  PDF.CurrentPage.AddNamedDestinationLink(R, 'chapter-1', 'Chapter 1: Results');

  // YPos = -1 escribe [page /XYZ null null null]: cambia la página, conserva la vista
  R := Rect(50, 225, 545, 259);
  PDF.CurrentPage.AddGoToLink(R, 2, -1, 'Methodology');

  R := Rect(50, 275, 545, 309);
  PDF.CurrentPage.AddURILink(R, 'https://www.example.com', 'Project website');
end;

Pásele el valor estilo pantalla 50 como Top y el enlace aterriza 50 puntos encima del borde inferior, mostrando el pie de página en lugar del encabezado. Un destino con nombre vive en el árbol de nombres /Dests del catálogo y cada enlace se resuelve a través del nombre, así que con docenas de enlaces hacia los mismos capítulos es la llamada correcta. Mueva una sección y usted edita una entrada en /Dests; cada enlace que la nombra la sigue. El orden de páginas del que dependen esos destinos está cubierto en por qué importa el orden del árbol de páginas

Anotaciones: objetos interactivos fuera del content stream

Una anotación se sienta sobre una página sin ser parte de su content stream (ISO 32000-1 §12.5). Enlaces, notas adhesivas, resaltados, widgets de formulario, íconos de adjuntos y sellos viven todos en el arreglo /Annots de su página. Quite una entrada y la anotación desaparece, mientras el content stream queda byte a byte igual

Toda anotación comparte un núcleo chico (Tabla 164): /Subtype nombra la clase, /Rect da el bounding box en espacio de usuario por defecto, y /Contents guarda texto que hace de descripción alternativa que la tecnología asistiva lee en voz alta. Las anotaciones de enlace (§12.5.6.5, Tabla 173) vienen en dos formas:

20 0 obj                                    % link to a destination
<< /Type /Annot /Subtype /Link
   /Rect [100 200 300 250]
   /Border [0 0 0]
   /Dest [5 0 R /XYZ null null null] >>
endobj
21 0 obj                                    % link that runs an action
<< /Type /Annot /Subtype /Link
   /Rect [50 50 200 100]
   /Border [0 0 0]
   /Contents (Project website)
   /A << /Type /Action /S /URI /URI (https://www.example.com) >> >>
endobj

El /Border [0 0 0] está ahí por una razón: el borde por defecto es [0 0 1], un marco de un punto que muchos visores dibujan alrededor de cada enlace. /Dest y /A son mutuamente excluyentes, y el /S de la acción elige el comportamiento: /GoTo dentro de este archivo (§12.6.4.2), /GoToR para una página de otro PDF (§12.6.4.3), /URI para una dirección web (§12.6.4.7), /Launch para abrir una aplicación o archivo externo (§12.6.4.5). Deje /Launch fuera de los documentos que distribuya: los visores suelen bloquearlo o ponerle un prompt de seguridad delante, y PDF/A lo prohíbe sin más (ISO 19005-1 §6.6.1). El argumento Description de las llamadas de enlace de arriba llena el /Contents, y con PDFUACompliance de HotPDF activado una descripción vacía lanza una excepción, ya que PDF/UA-1 exige esa entrada en cada enlace

Las anotaciones de marcado como resaltados y notas, y las formas como /Square, agregan un giro: su tipo no fija su apariencia. Sin un stream de apariencia (la entrada /AP, §12.5.5), cada visor dibuja su propia versión. ISO 32000-2 hace /AP obligatorio para la mayoría de los tipos de anotación, con Popup y Link entre las excepciones. Los adjuntos reutilizan la misma maquinaria: un stream de archivo incrustado envuelto en una especificación de archivo (§7.11), presentado como una anotación /FileAttachment en una página o a través del árbol de nombres /EmbeddedFiles bajo el /Names del catálogo

¿Cómo revisa esta capa antes de que un archivo se distribuya?

Dos verificaciones atrapan la mayor parte: recorra a clics el panel de marcadores y una muestra de enlaces en un visor real, y después relea los metadatos con un programa, porque ningún número de clics revela un /Info que discrepa del XMP. Las llamadas de documento cargado de HotPDF cubren las dos mitades. GetLoadedOutlineTitlesDeep recorre la cadena /First y /Next en profundidad, el mismo recorrido que hace un visor, así que una rama cortada por un enlace vencido falta también en su resultado. FindLoadedOutlineByTitle resuelve la página objetivo de un marcador y reporta -1 cuando el objetivo es un destino con nombre o no se resuelve a una página

// StripXmlTags es un helper que usted agrega; no es una llamada de la biblioteca.
// GetLoadedXMPValue devuelve el markup interno del elemento, así que dc:creator
// vuelve envuelto en tags rdf:Seq y rdf:li
function StripXmlTags(const S: AnsiString): AnsiString;
var
  I: Integer;
  InTag: Boolean;
begin
  Result := '';
  InTag := False;
  for I := 1 to Length(S) do
    if S[I] = '<' then InTag := True
    else if S[I] = '>' then InTag := False
    else if not InTag then Result := Result + S[I];
  Result := AnsiString(Trim(string(Result)));
end;

procedure CheckNavigationLayer(const FileName: string);
var
  PDF: THotPDF;
  Title, Author, Subject, Keywords, Creator, Producer, XmpAuthor: AnsiString;
  Titles: THPDFAnsiStringArray;
  I, PageIndex: Integer;
begin
  PDF := THotPDF.Create(nil);
  try
    PDF.AutoLaunch := False;
    if PDF.LoadFromFile(FileName) <= 0 then
      raise Exception.Create('No readable pages');

    if PDF.GetLoadedDocumentInfo(Title, Author, Subject, Keywords,
      Creator, Producer) then
    begin
      XmpAuthor := StripXmlTags(PDF.GetLoadedXMPValue('dc', 'creator'));
      if (XmpAuthor <> '') and (XmpAuthor <> Author) then
        Writeln('Author mismatch: /Info="', Author, '" XMP="', XmpAuthor, '"');
    end;

    Titles := PDF.GetLoadedOutlineTitlesDeep;
    Writeln(Length(Titles), ' bookmarks reachable');
    for I := 0 to High(Titles) do
      if PDF.FindLoadedOutlineByTitle(Titles[I], PageIndex, False, True)
        and (PageIndex < 0) then
        Writeln('No direct page target: ', Titles[I]);
  finally
    PDF.Free;
  end;
end;

Un conteo de marcadores por debajo de lo que usted escribió apunta a una cadena de hermanos rota. Un -1 en un marcador que usa un destino con nombre es lo esperado, y FindLoadedOutlineByTitle se detiene en la primera coincidencia, así que títulos duplicados reportan el objetivo del primero. GetLoadedXMPValue es una búsqueda de texto plano sobre el paquete, y la comparación es byte a byte, así que con nombres no ASCII mire cómo vuelven ambos valores antes de confiar en una discrepancia

Todo en esta capa es contabilidad de referencias, y la contabilidad es la parte que vale la pena entregarle a una biblioteca. El HotPDF Delphi Component para Delphi y C++Builder escribe ambos almacenes de metadatos desde un solo juego de propiedades, hila los enlaces del esquema mientras usted agrega items, y adjunta anotaciones de enlace con sus acciones y descripciones; su API de documento cargado relea las mismas estructuras para verificar