Artigo Técnico

Lendo e Escrevendo Marked Content de PDF em Delphi

Marked content é o mecanismo que a ISO 32000-1 §14.6 define para marcar conteúdo de página, e tagged PDF e PDF/UA são ambos construídos sobre ele. O PDFium Component o expõe diretamente: PageObjectMarks lê toda tag BDC e sua lista de propriedades de um objeto de página, AddPageObjectMark escreve uma, RemovePageObjectMark exclui uma, e PageObjectMarkedContentID informa o MCID que vincula o conteúdo à árvore de estrutura

Até que a árvore de estrutura possa ser ligada de volta ao conteúdo que descreve, ferramentas de acessibilidade são adivinhação. A árvore de estrutura diz "isto é um título"; o MCID diz quais marcas em qual página esse título de fato é. As duas metades precisam ser legíveis antes que uma aplicação possa verificar, reparar ou relatar marcação

O que é uma marca, em bytes?

Um operador BDC com um nome de tag e uma lista de propriedades opcional, fechado por EMC. No content stream parece com /P <</MCID 3>> BDC ... EMC: a tag /P nomeia o papel, o dicionário carrega propriedades, e tudo entre os operadores é o conteúdo marcado. Um objeto de página dentro desse trecho carrega a marca, que é o que o PDFium devolve e o que o PDFium Component transforma em um registro

TPdfContentMark segura um handle, a tag Name, e um array 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 por acaso 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;

Por que pmpUnknown significa duas coisas diferentes

pmpUnknown é devolvido quando o PDFium relata FPDF_OBJECT_UNKNOWN, e o PDFium também devolve isso para uma chave que não existe. Os dois casos não podem ser distinguidos nessa camada, e fingir o contrário seria pior do que dizer isso

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 você poderia decodificar assim mesmo. Se uma propriedade importa para o seu fluxo, verifique se ela está presente com um tipo que você reconheça, e não infira ausência a partir de um desconhecido — uma marca cuja lista de propriedades você não consegue ler é uma marca que você deveria relatar, não uma que você deveria aceitar silenciosamente

Um registro de marca é um snapshot, não um handle que você detém

O campo Handle pertence à biblioteca. Ele fica obsoleto no momento em que a marca é removida, o objeto de página é destruído ou a página é descarregada, então o registro é um snapshot somente leitura com vida curta. Armazene-o em cache por uma troca de página e você estará segurando um ponteiro para memória que o motor já reclamou

Essa é a mesma disciplina que se aplica a handles de objeto de página em geral no PDFium, e ela pega as pessoas no mesmo lugar: um controle de lista populado com registros de marca, um usuário navegando para outra página, e uma queda que parece não relacionada à navegação. Copie para fora os valores de que você precisa — o nome, as chaves, os números — e solte o handle. As notas sobre handles de objeto de página ficando obsoletos após uma transformação cobrem a regra geral e como ela morde em outros lugares

Adicionando uma marca, e a etapa de salvamento que é fácil perder

AddPageObjectMark recebe o índice do objeto de página, um nome de tag e um conjunto completo de parâmetros. Parâmetros são escritos como um conjunto em vez de remendados uma chave por vez, que é por que TPdfContentMarkParam não tem sentinelas Has* — o caso "atualizar um campo de um registro existente" que essas protegeriam não ocorre

A parte que vale dizer explicitamente: adicionar uma marca reconstrói o content stream da página para que a tag sobreviva a um salvamento. Isso precisava ser explícito porque SaveAs não regenera conteúdo por conta própria — uma mudança que vivesse apenas no modelo de objetos seria descartada, e o arquivo salvo pareceria exatamente como aquele com o qual você começou. Se você já adicionou algo a uma página do PDFium e o encontrou ausente da saída, geralmente é por isso

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 isso faz e não faz de um documento

Marcas por si só não fazem um tagged PDF. Um documento tagged compatível precisa de uma árvore de estrutura cujos elementos referenciem esses MCIDs, uma entrada /MarkInfo declarando o documento marcado, e nomes de papel que signifiquem o que o padrão diz que significam. Escrever uma marca /P com um MCID para o qual nenhum elemento de estrutura aponta dá a você conteúdo que alega ser tagged e uma árvore de estrutura que nunca o menciona

Onde marked content de fato ganha seu valor nesse nível é inspeção e reparo: auditar quais objetos de página estão marcados, encontrar artefatos que deveriam ter sido marcados como tal, ou corresponder MCIDs contra uma árvore de estrutura para encontrar os órfãos. Para a metade da árvore de estrutura desse trabalho, veja o guia sobre validação de árvore de estrutura PDF/UA, e para a experiência de leitura para a qual as tags afinal servem, as notas sobre a construção de um leitor de PDF acessível em Delphi

O PDFium Component dá às aplicações Delphi, C++Builder e Lazarus uma API VCL de alto nível sobre o motor PDFium, com marked content, á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 completa da API