Artigo Técnico

Camadas de conteúdo opcional PDF: alternar com o PDFium

O PDFium Component controla camadas de conteúdo opcional PDF (OCGs) em Delphi através de dois métodos do TPdf: o InspectOptionalContent lista todas as camadas juntamente com a visibilidade que o PDFium realmente vai renderizar, e o SaveAsOptionalContentConfigured escreve uma cópia verificada em que as camadas que escolher ficam ligadas ou desligadas. O segundo método também neutraliza as regras Usage e /AS que de outra forma desfariam a sua edição em silêncio. Ambos trabalham sobre o documento já aberto no TPdf, pelo que não há um segundo parser a manter em sincronia com o que o visualizador mostra

O pedido costuma chegar de uma casa de CAD ou GIS: o conjunto de desenhos sai com cotas, anotações e um bloco de título em camadas separadas, e o cliente quer uma cópia com as cotas escondidas antes de ir para o fornecedor. O PDFium renderiza conteúdo opcional corretamente, mas o seu ABI público não tem função nenhuma para enumerar OCGs, escolher uma configuração ou virar o estado de uma camada. Então desce ao nível dos objetos, edita o /OCProperties, grava, recarrega, e a camada continua lá. A razão é a lógica de visibilidade do PDFium, e vale a pena percebê-la antes de tocar em bytes quaisquer

Porque é que editar /ON e /OFF não muda o que o PDFium renderiza?

Editar as matrizes /ON e /OFF do dicionário de configuração não chega, porque o PDFium deixa um estado explícito dentro do dicionário /Usage do próprio OCG ganhar sobre essas matrizes, e uma regra de auto-estado /AS pode depois sobrepor-se a ambas. A ISO 32000-1 §8.11.4 descreve configurações e dicionários de usage como mecanismos separados; o renderizador do PDFium funde-os numa decisão única, e o InspectOptionalContent reproduz essa decisão nesta ordem:

  • Parta do /BaseState da configuração, onde /ON e /Unchanged contam ambos como visível e só o /OFF esconde
  • Aplique a matriz /ON da configuração, depois a /OFF, pelo que um grupo listado em ambas acaba escondido
  • Aplique o estado Usage explícito do grupo para o usage pedido, como /Usage << /View << /ViewState /OFF >> >>, que se sobrepõe a tudo o que está acima
  • Trate um grupo cujo /Intent não contém nem /View nem /All como visível, já que não participa na visibilidade de intent de vista
  • Por fim corra a matriz /AS da configuração selecionada, cujas entradas para o evento correspondente põem o estado dos grupos que listam
A decisão de visibilidade em cinco passos que o PDFium Component reprime para cada grupo de conteúdo opcional PDF em Delphi: o BaseState fixa o começo, as matrizes ON e OFF da configuração aplicam-se por ordem, uma entrada Usage ViewState ou PrintState explícita se sobrepõe a ambas, a não participação de Intent conta como visível, e a matriz AS corre por último
Editar as matrizes ON e OFF não chega porque o PDFium funde o BaseState, ambas as matrizes, o estado Usage do grupo e por fim as regras de auto-estado AS num único veredicto que o InspectOptionalContent reproduz passo a passo

O terceiro passo é o que queima as pessoas. Um ficheiro gravado por uma ferramenta de layout costuma transportar /ViewState /ON em todos os OCGs, e o PDFium ignora então a sua matriz /OFF cuidadosamente editada: a gravação tem êxito, o ficheiro reabre limpo, e a camada continua a pintar. Para Print e Export, o OcExplicitUsageState lê primeiro o PrintState ou o ExportState e recua para o ViewState quando a entrada específica está ausente, pelo que um ViewState /ON solitário fixa a camada também para impressão. O marked content que referencia um OCMD (§8.11.2.2) é depois resolvido contra estes resultados por grupo, através da política /P ou, quando presente, da expressão de visibilidade /VE

Como listar as camadas que o PDFium realmente vai mostrar?

O TPdf.InspectOptionalContent devolve um TPdfOptionalContentInventory cuja matriz Groups transporta o número de objeto de cada OCG, o nome, os intents, os três estados Usage, a língua, o intervalo de zoom, a flag Locked, o índice de radio-group e o EffectiveVisible calculado. O método primeiro manda o PDFium gravar o documento atual em memória, expande os object streams, e analisa o resultado, pelo que as edições feitas mais cedo na sessão ficam refletidas. O índice de configuração 0 é sempre o dicionário predefinido /D e as entradas de /Configs seguem a partir do índice 1; o argumento predefinido de -1 seleciona o índice 0. Um documento sem /OCProperties faz o método devolver False com a razão em ErrorMessage em vez de disparar

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage assume ocuView por predefinição; -1 seleciona a configuração 0, o dicionário /D
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

A matriz Memberships reporta todos os OCMD com a sua Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), o texto bruto da VisibilityExpression e o seu próprio EffectiveVisible. Algumas regras de limite são deliberadas. O /P assume /AnyOn por predefinição, e um OCMD sem grupos conta como visível. Uma referência a um número de objeto que não seja um OCG conhecido é tratada como visível em vez de fazer falhar a expressão inteira. A avaliação do /VE pára a uma profundidade de aninhamento de 32 e trata tudo o que for mais fundo como escondido, o que impede uma expressão hostil ou autorreferente de transformar a inspeção num stack overflow

Escrever um novo estado de camada com SaveAsOptionalContentConfigured

O TPdf.SaveAsOptionalContentConfigured recebe uma matriz de registos TPdfOptionalContentStateChange (número de objeto do grupo mais Visible) e escreve um documento em que a configuração selecionada produz exatamente esse estado. A configuração selecionada recebe /BaseState /ON mais matrizes /ON e /OFF completas cobrindo todos os grupos, e cada OCG que já tenha um dicionário Usage recebe um ViewState explícito (ou PrintState / ExportState, seguindo Options.Usage) a condizer com o seu novo estado. Com TPdfOptionalContentConfigureOptions.Default, a chave /AS da configuração selecionada é removida para que um evento de abertura, impressão ou exportação não volte a virar as camadas

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // Configuração 0, ocuView, DisableAutomaticState e EnforceRadioGroups a True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

O caminho de escrita mantém a saída gravada do próprio PDFium como prefixo byte a byte e acrescenta apenas o dono da configuração reescrito e os objetos OCG que transportam dicionários Usage, seguidos de uma nova secção xref e trailer. Antes de um único byte chegar ao seu destino, o resultado é reaberto num TPdf separado sob a política de carregamento estrita, e o método falha se a tabela de referências cruzadas não validar. A sobrecarga para ficheiro vai um passo além: escreve para um ficheiro temporário ao lado do destino e só substitui o destino depois de a verificação ter êxito, pelo que uma atualização recusada nunca deixa para trás um desenho meio escrito. É a mesma abordagem de revisão incremental verificada usada pelo editor de name tree e number tree PDF no PDFium Component

Como o SaveAsOptionalContentConfigured no PDFium Component escreve um PDF Delphi com camadas alteradas: as mudanças de estado e as opções entram, a configuração selecionada é reescrita com matrizes ON e OFF completas e estados Usage, a revisão incremental verificada é acrescentada, e uma reabertura estrita tem de validar antes de qualquer coisa ser escrita
A gravação configurada mantém a regravação do próprio PDFium como prefixo de bytes, acrescenta o dono da configuração reescrito mais uma nova secção xref, e reabre o resultado num TPdf separado antes de o destino ser tocado

O que se recusa a fazer a gravação configurada?

A gravação configurada recusa qualquer mudança que o próprio documento proíba ou não consiga representar em segurança, e cada recusa acontece antes de o destino ser tocado. Um número de objeto que não esteja em /OCGs falha de imediato. Mudar um grupo listado na matriz /Locked da configuração falha, embora reenunciar o seu valor atual seja permitido. Com EnforceRadioGroups ativo, qualquer conjunto /RBGroups que acabe com mais do que um membro visível é recusado em vez de desligar os outros em silêncio. Documentos encriptados são recusados porque objetos incrementais em plaintext não conseguem transportar o security handler ativo. Documentos assinados disparam EPdfError a menos que passe AllowSignedDocument = True, já que mudar o que uma página mostra pode partir a cobertura da assinatura ou uma política de certificação

Os portões de recusa que o SaveAsOptionalContentConfigured aplica no PDFium Component antes de escrever um PDF Delphi configurado: um número de objeto fora dos OCGs falha, grupos bloqueados falham, conjuntos RBGroups com mais do que um membro visível são recusados, documentos encriptados não conseguem transportar objetos incrementais em plaintext, e ficheiros assinados exigem AllowSignedDocument
Cada recusa acontece antes de o destino ser tocado, e a razão da falha cai em Report.ErrorMessage em vez de deixar para trás um desenho meio escrito
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // escreve /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // primeira entrada de /Configs, não /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument fica False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // ficheiro assinado: nada escrito no Target
      Result := False;
    end;
  end;
end;

Conheça as contrapartidas antes de ligar isto a um job em lote. A revisão acrescentada assenta por cima da regravação completa do PDFium, não dos bytes do seu ficheiro original, o que é exatamente a razão pela qual a entrada assinada precisa de consentimento explícito. A reescrita também normaliza a configuração selecionada para /BaseState /ON, pelo que uma linha de base /Unchanged ou /OFF do autor é substituída por matrizes explícitas com a mesma visibilidade resultante. Largar o /AS remove truques só de impressão, como uma camada de marca de água que só aparece no papel; ponha DisableAutomaticState a False para conservar essas regras, aceitando que podem sobrepor-se ao estado que pediu para esse evento. Do lado positivo, o PDF/A-2 (ISO 19005-2 cláusula 6.9) e o PDF/UA (ISO 14289-1 cláusula 7.10) proíbem ambos /AS nos dicionários de configuração, pelo que a saída predefinida remove um problema que a sua validação preflight PDF/A com o PDFium Component de outra forma reportaria

Onde encaixa o controlo de camadas num visualizador PDF em Delphi

Num visualizador, o controlo de camadas é uma checklist alimentada pelo inventário mais um recarregamento do resultado gravado. Preencha a checklist a partir de Groups, desative as entradas que estão Locked, trate os membros que partilham um RadioGroupIndex como mutuamente exclusivos, e ao aplicar escreva para um TMemoryStream e carregue esse stream de volta no TPdf para que a vista pinte o novo estado. A ligação entre o TPdf e o TPdfView está coberta em construir um visualizador PDF rico em funcionalidades com PDFium VCL em Delphi. Licenciamento, downloads de avaliação e o resto do conjunto de funcionalidades estão na página do produto PDFium Component for Delphi