Artigo Técnico

Alternar camadas de optional content no Delphi com PDFium

O PDFium Component controla camadas de optional content de PDF (OCGs) no Delphi por meio de dois métodos do TPdf: o InspectOptionalContent lista toda camada junto com a visibilidade que o PDFium de fato vai renderizar, e o SaveAsOptionalContentConfigured grava uma cópia verificada em que as camadas que você escolher ficam ligadas ou desligadas. O segundo método também neutraliza as regras Usage e /AS que de outro modo desfariam sua edição em silêncio. Os dois trabalham no documento já aberto no TPdf, então não há um segundo parser para manter em sincronia com o que o viewer mostra

O pedido costuma chegar de um escritório de CAD ou GIS: o conjunto de desenhos sai com cotas, anotações e um carimbo em camadas separadas, e o cliente quer uma cópia com as cotas escondidas antes de ir para um fornecedor. O PDFium renderiza optional content corretamente, mas a ABI pública dele não tem função nenhuma para enumerar OCGs, escolher uma configuração ou virar o estado de uma camada. Então você desce ao nível de objeto, edita /OCProperties, salva, recarrega, e a camada continua lá. A razão é a lógica de visibilidade do PDFium, e vale entendê-la antes de tocar em qualquer byte

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

Editar os arrays /ON e /OFF do dicionário de configuração não basta, porque o PDFium deixa um estado explícito dentro do dicionário /Usage do próprio OCG vencer esses arrays, e uma regra de auto-state /AS pode então sobrepor ambos. A ISO 32000-1 §8.11.4 descreve configurações e dicionários de uso como mecanismos separados; o renderer do PDFium os dobra numa única decisão, e o InspectOptionalContent a reproduz nesta ordem:

  • Parta do /BaseState da configuração, em que /ON e /Unchanged contam como visível e só /OFF esconde
  • Aplique o array /ON da configuração, depois o /OFF, então um grupo listado nos dois acaba escondido
  • Aplique o estado Usage explícito do grupo para o uso pedido, como /Usage << /View << /ViewState /OFF >> >>, que sobrepuja tudo acima
  • Trate um grupo cujo /Intent não contém nem /View nem /All como visível, já que ele não participa da visibilidade de intent de view
  • Por fim rode o array /AS da configuração selecionada, cujas entradas para o evento correspondente setam o estado dos grupos que listam
A decisão de visibilidade de cinco passos que o PDFium Component repassa para cada grupo de optional content de PDF no Delphi: o BaseState define o começo, os arrays ON e OFF da configuração se aplicam em ordem, uma entrada Usage explícita ViewState ou PrintState sobrepuja ambos, a não participação de Intent conta como visível, e o array AS roda por último
Editar os arrays ON e OFF não basta porque o PDFium dobra BaseState, os dois arrays, o estado Usage do grupo e por fim as regras de auto-state do AS num único veredito que o InspectOptionalContent reproduz passo a passo

O terceiro passo é o que queima a galera. Um arquivo salvo por uma ferramenta de layout costuma carregar /ViewState /ON em todo OCG, e o PDFium então ignora o array /OFF que você editou com tanto cuidado: o save tem sucesso, o arquivo reabre limpo, e a camada continua pintando. Para Print e Export, o OcExplicitUsageState lê PrintState ou ExportState primeiro e cai para ViewState quando a entrada específica está ausente, então um ViewState /ON solitário prende a camada para impressão também. Marked content que referencia um OCMD (§8.11.2.2) é então resolvido contra esses resultados por grupo, pela política /P ou, quando presente, pela expressão de visibilidade /VE

Como listar as camadas que o PDFium vai de fato mostrar?

O TPdf.InspectOptionalContent devolve um TPdfOptionalContentInventory cujo array Groups carrega o número de objeto de cada OCG, nome, intents, os três estados Usage, idioma, faixa de zoom, flag Locked, índice de radio-group e o EffectiveVisible calculado. O método primeiro faz o PDFium salvar o documento em memória corrente, expande object streams e varre o resultado, então edições feitas antes na sessão aparecem. O índice de configuração 0 é sempre o dicionário padrão /D e as entradas de /Configs seguem a partir do índice 1; o argumento padrão de -1 seleciona o índice 0. Um documento sem /OCProperties faz o método devolver False com o motivo em ErrorMessage em vez de disparar exceção

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage tem ocuView como padrã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;

O array Memberships reporta todo OCMD com o Policy dele (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff), o texto cru de VisibilityExpression e o EffectiveVisible próprio. Algumas regras de borda são deliberadas. /P tem /AnyOn como padrão, e um OCMD sem grupos conta como visível. Uma referência a um número de objeto que não é um OCG conhecido é tratada como visível em vez de derrubar a expressão inteira. A avaliação de /VE para numa profundidade de aninhamento de 32 e trata tudo mais fundo como escondido, o que impede uma expressão hostil ou autorreferente de transformar a inspeção num stack overflow

Gravando um novo estado de camada com SaveAsOptionalContentConfigured

O TPdf.SaveAsOptionalContentConfigured recebe um array de records TPdfOptionalContentStateChange (número de objeto do grupo mais Visible) e grava um documento em que a configuração selecionada produz exatamente esse estado. A configuração selecionada ganha /BaseState /ON mais arrays /ON e /OFF completos cobrindo todo grupo, e cada OCG que já tem dicionário Usage recebe um ViewState explícito (ou PrintState / ExportState, seguindo Options.Usage) casando com o novo estado dele. Com TPdfOptionalContentConfigureOptions.Default, a chave /AS da configuração selecionada é removida para que um evento de abertura, impressão ou export não vire as camadas de volta

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 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 gravação mantém a saída salva do próprio PDFium como prefixo byte a byte e anexa apenas o owner de configuração reescrito e os objetos OCG que carregam dicionários Usage, seguidos de uma nova seção xref e trailer. Antes de um único byte chegar ao destino, o resultado é reaberto num TPdf separado sob a política de carga estrita, e o método falha se a tabela de referências cruzadas não validar. A sobrecarga de arquivo vai um passo além: grava num arquivo temporário ao lado do alvo e só substitui o alvo depois que a verificação tem sucesso, então uma atualização rejeitada nunca deixa um desenho pela metade para trás. É a mesma abordagem de revisão incremental verificada usada pelo editor de name tree e number tree de PDF no PDFium Component

Como o SaveAsOptionalContentConfigured no PDFium Component grava um PDF Delphi com camadas alternadas: mudanças de estado e opções entram, a configuração selecionada é reescrita com arrays ON e OFF completos e estados Usage, a revisão incremental verificada é anexada, e uma reabertura estrita precisa validar antes que qualquer coisa seja gravada
O save configurado mantém o re-save do próprio PDFium como prefixo de bytes, anexa o owner de configuração reescrito mais uma nova seção xref, e reabre o resultado num TPdf separado antes que o destino seja tocado

O que o save configurado se recusa a fazer?

O save configurado recusa qualquer mudança que o próprio documento proíba ou não consiga representar com segurança, e toda recusa acontece antes de o destino ser tocado. Um número de objeto que não está em /OCGs falha de cara. Mudar um grupo listado no array /Locked da configuração falha, embora reafirmar o valor atual dele seja permitido. Com EnforceRadioGroups ligado, qualquer conjunto /RBGroups que acabaria com mais de um membro visível é rejeitado em vez de desligar os outros em silêncio. Documentos criptografados são rejeitados porque objetos incrementais em plaintext não podem carregar o security handler ativo. Documentos assinados disparam EPdfError a menos que você passe AllowSignedDocument = True, já que mudar o que uma página mostra pode quebrar 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 gravar um PDF Delphi configurado: um número de objeto fora dos OCGs falha, grupos travados falham, conjuntos RBGroups com mais de um membro visível são rejeitados, documentos criptografados não podem carregar objetos incrementais em plaintext, e arquivos assinados exigem AllowSignedDocument
Toda recusa acontece antes de o destino ser tocado, e o motivo da falha vai para o Report.ErrorMessage em vez de deixar um desenho pela metade para trás
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // grava /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // primeira entrada de /Configs, não /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // AllowSignedDocument permanece False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // arquivo assinado: nada escrito no Target
      Result := False;
    end;
  end;
end;

Conheça os trade-offs antes de ligar isso a um job em lote. A revisão anexada fica em cima do re-save completo do PDFium, não dos bytes originais do seu arquivo, e é exatamente por isso que entrada assinada precisa de consentimento explícito. A reescrita também normaliza a configuração selecionada para /BaseState /ON, então uma baseline /Unchanged ou /OFF do autor é substituída por arrays explícitos com a mesma visibilidade resultante. Soltar o /AS remove truques só de impressão, como uma camada de marca d'água que aparece só no papel; sete DisableAutomaticState para False para manter essas regras, aceitando que elas podem sobrepor o estado pedido para aquele evento. Do lado positivo, PDF/A-2 (ISO 19005-2 clause 6.9) e PDF/UA (ISO 14289-1 clause 7.10) proíbem /AS em dicionários de configuração, então a saída padrão remove um problema que a sua validação preflight PDF/A com PDFium Component de outro modo reportaria

Onde o controle de camadas cabe num viewer de PDF em Delphi

Num viewer, o controle de camadas é um checklist alimentado pelo inventário mais uma recarga do resultado salvo. Popule o checklist a partir de Groups, desabilite as entradas Locked, trate membros que compartilham um RadioGroupIndex como mutuamente exclusivos, e ao aplicar grave num TMemoryStream e carregue esse stream de volta no TPdf para que a view pinte o novo estado. A fiação entre TPdf e TPdfView está coberta em construir um viewer de PDF cheio de recursos com PDFium VCL no Delphi. Licenciamento, downloads trial e o resto do conjunto de recursos estão na página do produto PDFium Component para Delphi