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
/BaseStateda configuração, em que/ONe/Unchangedcontam como visível e só/OFFesconde - Aplique o array
/ONda 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
/Intentnão contém nem/Viewnem/Allcomo visível, já que ele não participa da visibilidade de intent de view - Por fim rode o array
/ASda configuração selecionada, cujas entradas para o evento correspondente setam o estado dos grupos que listam
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
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
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