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