Conteúdo marcado é o mecanismo que o ISO 32000-1 §14.6 define para marcar conteúdo de página, e o PDF marcado e o PDF/UA estão ambos construídos sobre ele. O PDFium Component expõe-no diretamente: PageObjectMarks lê todas as etiquetas BDC e as suas listas de propriedades de um objeto de página, AddPageObjectMark escreve uma, RemovePageObjectMark elimina uma, e PageObjectMarkedContentID reporta o MCID que liga o conteúdo à árvore de estrutura
Até a árvore de estrutura poder ser ligada ao conteúdo que descreve, as ferramentas de acessibilidade são adivinhação. A árvore de estrutura diz "isto é um título"; o MCID diz que marcas em que página esse título realmente é. Ambas as metades têm de ser legíveis antes que uma aplicação consiga verificar, reparar ou reportar marcação
O que é uma marca, em bytes?
Um operador BDC com um nome de marca e uma lista de propriedades opcional, fechado por EMC. No stream de conteúdo parece-se com /P <</MCID 3>> BDC ... EMC: a marca /P nomeia o papel, o dicionário transporta propriedades, e tudo entre os operadores é o conteúdo marcado. Um objeto de página dentro desse transporta a marca, que é o que o PDFium devolve e o que o PDFium Component transforma num registo
TPdfContentMark contém um manípulo, a marca Name, e uma matriz de TPdfContentMarkParam. Cada parâmetro tem uma Key, um Kind e um campo de valor significativo selecionado por esse tipo: pmpInt, pmpFloat, pmpString ou pmpBlob. O tipo vem do próprio relatório de tipo do PDFium em vez de qualquer getter que tenha tido sucesso, que é a diferença entre ler uma lista de propriedades e adivinhar uma
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;
Porque razão pmpUnknown significa duas coisas diferentes
pmpUnknown é devolvido quando o PDFium reporta FPDF_OBJECT_UNKNOWN, e o PDFium também devolve isso para uma chave que não existe. Os dois casos não conseguem ser distinguidos a este nível, e fingir o contrário seria pior do que dizê-lo
A consequência prática para o seu código: trate pmpUnknown como "nenhum valor utilizável aqui" em vez de como um tipo que pode descodificar à mesma. Se uma propriedade importa para o seu fluxo de trabalho, verifique que está presente com um tipo que reconheça, e não infira ausência a partir de um desconhecido — uma marca cuja lista de propriedades não consegue ler é uma marca sobre a qual deve reportar, não uma que deve aceitar silenciosamente
Um registo de marca é um instantâneo, não um manípulo que possui
O campo Handle pertence à biblioteca. Fica obsoleto no momento em que a marca é removida, o objeto de página é destruído ou a página é descarregada, pelo que o registo é um instantâneo só de leitura com vida curta. Faça cache dele através de uma mudança de página e está a segurar um ponteiro para memória que o motor já reclamou
Esta é a mesma disciplina que se aplica a manípulos de objetos de página em geral no PDFium, e apanha as pessoas no mesmo sítio: um controlo de lista povoado com registos de marca, um utilizador a navegar para outra página, e um bloqueio que parece não relacionado com a navegação. Copie os valores de que precisa — o nome, as chaves, os números — e largue o manípulo. As notas sobre manípulos de objetos de página que ficam obsoletos depois de uma transformação cobrem a regra geral e como morde noutros sítios
Adicionar uma marca, e o passo de gravação que é fácil perder
AddPageObjectMark recebe o índice do objeto de página, um nome de marca e um conjunto completo de parâmetros. Os parâmetros são escritos como um conjunto em vez de remendados uma chave de cada vez, que é o porquê de TPdfContentMarkParam não ter sentinelas Has* — o caso "atualizar um campo de um registo existente" que elas guardariariam não surge
A parte que vale a pena afirmar explicitamente: adicionar uma marca reconstrói o stream de conteúdo da página para que a etiqueta sobreviva a uma gravação. Isto teve de ser explícito porque SaveAs não regenera conteúdo por si — uma mudança que vivesse apenas no modelo de objetos seria descartada, e o ficheiro gravado parecer-se-ia exatamente com aquele com que começou. Se alguma vez adicionou algo a uma página PDFium e descobriu que faltava na saída, é normalmente por isto
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;
O que isto faz e não faz a um documento
As marcas por si só não fazem um PDF marcado. Um documento marcado conforme precisa de uma árvore de estrutura cujos elementos referenciem estes MCIDs, uma entrada /MarkInfo a declarar o documento marcado, e nomes de papel que signifiquem o que a norma diz que significam. Escrever uma marca /P com um MCID a que nenhum elemento de estrutura aponta dá-lhe conteúdo que reclama ser marcado e uma árvore de estrutura que nunca o menciona
Onde o conteúdo marcado ganha genuinamente o seu valor a este nível é na inspeção e reparação: auditar que objetos de página estão marcados, encontrar artefactos que deviam ter sido marcados como tal, ou corresponder MCIDs com uma árvore de estrutura para encontrar os órfãos. Para a metade árvore de estrutura desse trabalho, veja o guia de validação de árvore de estrutura PDF/UA, e para a experiência de leitura para a qual as etiquetas afinal servem, as notas sobre construir um leitor de PDF acessível em Delphi
O PDFium Component dá a aplicações Delphi, C++Builder e Lazarus uma API VCL de alto nível sobre o motor PDFium, com conteúdo marcado, árvores de estrutura e validação de acessibilidade alcançáveis a partir de código Pascal comum — veja a página do produto PDFium Component para a superfície de API completa