Artículo técnico

Leer y escribir contenido marcado de PDF en Delphi

El contenido marcado es el mecanismo que ISO 32000-1 §14.6 define para etiquetar el contenido de página, y tanto el PDF etiquetado como PDF/UA se construyen sobre él. PDFium Component lo expone directamente: PageObjectMarks lee cada etiqueta BDC y su lista de propiedades de un objeto de página, AddPageObjectMark escribe una, RemovePageObjectMark elimina una, y PageObjectMarkedContentID reporta el MCID que vincula el contenido con el árbol de estructura

Hasta que el árbol de estructura se pueda vincular de vuelta al contenido que describe, las herramientas de accesibilidad son conjetura. El árbol de estructura dice «esto es un encabezado»; el MCID dice qué marcas en qué página es realmente ese encabezado. Ambas mitades tienen que ser legibles antes de que una aplicación pueda verificar, reparar o reportar sobre el etiquetado

¿Qué es una marca, en bytes?

Un operador BDC con un nombre de etiqueta y una lista de propiedades opcional, cerrado por EMC. En el flujo de contenido se ve como /P <</MCID 3>> BDC ... EMC: la etiqueta /P nombra el rol, el diccionario lleva propiedades, y todo lo que está entre los operadores es el contenido marcado. Un objeto de página dentro de ese tramo lleva la marca, que es lo que PDFium devuelve y lo que PDFium Component convierte en un registro

TPdfContentMark lleva un identificador, la etiqueta Name, y un arreglo de TPdfContentMarkParam. Cada parámetro tiene una Key, un Kind y un campo de valor significativo seleccionado por ese tipo: pmpInt, pmpFloat, pmpString o pmpBlob. El tipo viene del propio reporte de tipos de PDFium en vez del primer getter que haya tenido éxito, que es la diferencia entre leer una lista de propiedades y adivinarla

var
  Marks: TPdfContentMarks;
  M: TPdfContentMark;
  P: TPdfContentMarkParam;
  I: Integer;
begin
  Pdf.PageNumber := 1;                    // PageNumber is 1-based
  for I := 0 to Pdf.ObjectCount - 1 do    // page object indexes are 0-based
  begin
    Marks := Pdf.PageObjectMarks(I);
    for M in Marks do
    begin
      Memo1.Lines.Add('mark ' + M.Name +
        ' (MCID ' + IntToStr(Pdf.PageObjectMarkedContentID(I)) + ')');
      for P in M.Params do
        case P.Kind of
          pmpInt:    Memo1.Lines.Add('  ' + P.Key + ' = ' + IntToStr(P.IntValue));
          pmpString: Memo1.Lines.Add('  ' + P.Key + ' = ' + P.StringValue);
          pmpFloat:  Memo1.Lines.Add('  ' + P.Key + ' = ' + FloatToStr(P.FloatValue));
          pmpBlob:   Memo1.Lines.Add('  ' + P.Key + ' = ' +
                       IntToStr(Length(P.BlobValue)) + ' bytes');
        end;
    end;
  end;
end;

Por qué pmpUnknown significa dos cosas distintas

pmpUnknown se devuelve cuando PDFium reporta FPDF_OBJECT_UNKNOWN, y PDFium también devuelve eso para una clave que no existe. Los dos casos no se pueden distinguir en esta capa, y pretender lo contrario sería peor que decirlo

La consecuencia práctica para tu código: trata pmpUnknown como «aquí no hay valor utilizable» en vez de un tipo que podrías decodificar de todos modos. Si una propiedad importa para tu flujo, verifica que esté presente con un tipo que reconozcas, y no infieras ausencia a partir de un desconocido — una marca cuya lista de propiedades no puedes leer es una marca que deberías reportar, no una que deberías aceptar en silencio

Un registro de marca es una instantánea, no un identificador que te pertenezca

El campo Handle pertenece a la biblioteca. Se vuelve obsoleto en el momento en que la marca se elimina, el objeto de página se destruye o la página se descarga, así que el registro es una instantánea de solo lectura con una vida corta. Lo guardas en caché a través de un cambio de página y estás sosteniendo un puntero a memoria que el motor ya reclamó

Esta es la misma disciplina que aplica a los identificadores de objetos de página en general en PDFium, y atrapa a la gente en el mismo lugar: un control de lista poblado con registros de marca, un usuario que navega a otra página, y un cuelgue que parece no relacionado con la navegación. Copia los valores que necesitas — el nombre, las claves, los números — y deja ir el identificador. Las notas sobre los identificadores de objetos de página que se vuelven obsoletos tras una transformación cubren la regla general y cómo muerde en otras partes

Agregar una marca, y el paso de guardado que es fácil pasar por alto

AddPageObjectMark toma el índice del objeto de página, un nombre de etiqueta y un conjunto completo de parámetros. Los parámetros se escriben como un conjunto en vez de parchearse una clave a la vez, razón por la cual TPdfContentMarkParam no tiene centinelas Has* — el caso de «actualizar un campo de un registro existente» que esos protegerían no se presenta

La parte que vale la pena declarar explícitamente: agregar una marca reconstruye el flujo de contenido de la página para que la etiqueta sobreviva a un guardado. Esto tuvo que ser explícito porque SaveAs no regenera el contenido por sí solo — un cambio que viviera solo en el modelo de objetos se descartaría, y el archivo guardado se vería exactamente igual al de partida. Si alguna vez agregaste algo a una página de PDFium y lo encontraste ausente de la salida, esta suele ser la razón

var
  Params: TPdfContentMarkParams;
begin
  SetLength(Params, 1);
  Params[0].Key := 'MCID';
  Params[0].Kind := pmpInt;
  Params[0].IntValue := NextMcid;
  Pdf.AddPageObjectMark(ObjectIndex, 'P', Params);   // rebuilds the content stream
  Pdf.UpdatePage;
  Pdf.SaveAs('tagged-out.pdf');
end;

Qué hace y qué no hace esto con un documento

Las marcas por sí solas no hacen un PDF etiquetado. Un documento etiquetado conforme necesita un árbol de estructura cuyos elementos referencien estos MCIDs, una entrada /MarkInfo que declare el documento marcado, y nombres de rol que signifiquen lo que la norma dice que significan. Escribir una marca /P con un MCID al que ningún elemento de estructura apunta te da contenido que afirma estar etiquetado y un árbol de estructura que nunca lo menciona

Donde el contenido marcado realmente se gana su valor a este nivel es la inspección y la reparación: auditar qué objetos de página están etiquetados, encontrar artefactos que deberían haberse marcado como tales, o cotejar MCIDs contra un árbol de estructura para encontrar los huérfanos. Para la mitad del trabajo que corresponde al árbol de estructura, consulta el recorrido por la validación del árbol de estructura PDF/UA, y para la experiencia de lectura para la que en última instancia son las etiquetas, las notas sobre cómo construir un lector de PDF accesible en Delphi

PDFium Component da a las aplicaciones de Delphi, C++Builder y Lazarus una API VCL de alto nivel sobre el motor PDFium, con contenido marcado, árboles de estructura y validación de accesibilidad alcanzables desde código Pascal ordinario — consulta la página del producto PDFium Component para la superficie completa de la API