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 la borra, y PageObjectMarkedContentID informa del MCID que enlaza el contenido con el árbol de estructura
Hasta que el árbol de estructura se pueda reunir con el contenido que describe, las herramientas de accesibilidad son conjetura. El árbol de estructura dice «esto es un titular»; el MCID dice qué marcas en qué página es de verdad ese titular. Ambas mitades tienen que ser legibles antes de que una aplicación pueda comprobar, reparar o informar 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 porta las propiedades, y todo lo que hay entre los operadores es el contenido marcado. Un objeto de página dentro de ese tramo porta la marca, que es lo que PDFium devuelve y lo que PDFium Component convierte en un registro
TPdfContentMark aloja un handle, la etiqueta Name, y un array 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 informe de tipos de PDFium en lugar del getter que haya tenido éxito, lo que es la diferencia entre leer una lista de propiedades y conjeturarla
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 reconocerlo
La consecuencia práctica para tu código: trata pmpUnknown como «aquí no hay ningún valor utilizable» en lugar de como un tipo que podrías decodificar de todos modos. Si una propiedad importa a tu flujo, verifica que está presente con un tipo que reconozcas, y no infieras la ausencia a partir de un desconocido —una marca cuya lista de propiedades no puedes leer es una marca sobre la que deberías informar, no una que deberías aceptar en silencio—
Un registro de marca es una instantánea, no un handle del que eres dueño
El campo Handle pertenece a la librería. Queda obsoleto en el instante 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 cacheas a través de un cambio de página y estás sujetando un puntero a memoria que el motor ya ha reclamado
Esta es la misma disciplina que aplica a los handles de objeto de página en general en PDFium, y atrapa a la gente en el mismo sitio: un control de lista poblado con registros de marca, un usuario que navega a otra página, y un cuelgue que parece no tener relación con la navegación. Copia los valores que necesites —el nombre, las claves, los números— y suelta el handle. Las notas sobre los handles de objeto de página que caducan tras una transformación cubren la regla general y cómo muerde en otros sitios
Añadir una marca, y el paso de guardado fácil de 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 lugar de parchearse clave a clave, por eso TPdfContentMarkParam no tiene centinelas Has* —el caso de «actualizar un campo de un registro existente» que esos protegerían no se da—
La parte que vale la pena decir explícitamente: añadir una marca reconstruye el flujo de contenido de la página de modo 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 que el de partida—. Si alguna vez añadiste algo a una página PDFium y lo echaste en falta en la salida, suele ser por esto
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 convierten un PDF en etiquetado. Un documento etiquetado conforme necesita un árbol de estructura cuyos elementos referencien estos MCIDs, una entrada /MarkInfo que declare el documento como 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 de verdad se gana su sitio a este nivel es en 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 casar MCIDs con un árbol de estructura para encontrar los huérfanos. Para la mitad de árbol de estructura de ese trabajo, consulta el recorrido por la validación del árbol de estructura PDF/UA, y para la experiencia de lectura para la que al final están las etiquetas, las notas sobre cómo construir un lector PDF accesible en Delphi
PDFium Component da a las aplicaciones 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 accesibles desde código Pascal ordinario; consulta la página del producto PDFium Component para la superficie completa de la API