Um conjunto de desenhos chega com as cotas dele em uma camada própria, e o jeito óbvio de escondê-las é mover a referência daquele grupo do array /ON da configuração padrão para o /OFF. O arquivo salva e reabre, e as linhas de cota continuam impressas por cima da cartela de título. A causa fica um objeto adiante, dentro do dicionário de grupo: /Usage << /View << /ViewState /ON >> >>, um estado explícito que o renderizador lê antes dos arrays de configuração
O estado de uma camada pode viver em vários lugares em um PDF, e uma edição em /OCProperties toca apenas um deles. O motor PDFium da Google renderiza optional content corretamente, mas a API C pública dele não tem função para enumerar grupos de optional content (OCGs), escolher uma configuração ou ligar e desligar um grupo. Código Delphi que dirige camadas precisa reescrever os objetos por conta própria e reproduzir exatamente a ordem de decisão do renderizador, ou a lista de camadas dele reporta um estado enquanto a página mostra outro
Por que editar /ON e /OFF não muda o que o PDFium renderiza?
Os arrays /ON e /OFF são apenas o segundo de cinco passos, e dois passos posteriores podem sobrepujá-los. A ISO 32000-1 §8.11.4.3 define o dicionário de configuração de optional content com /BaseState, /ON, /OFF, /Locked, /RBGroups e /AS, enquanto a §8.11.4.4 define o dicionário Usage que um OCG pode carregar. Na especificação, Usage descreve para que um grupo serve, e as entradas de auto-estado no /AS transformam essa descrição em ON ou OFF. O PDFium pega um caminho mais curto: quando renderiza para um propósito, o estado explícito do próprio grupo para aquele propósito vence. Dobrados juntos, a ordem fica assim:
- Parta do
/BaseStateda configuração, em que/ONe/Unchangedcontam como visível e apenas/OFFesconde - Aplique o array
/ON, depois o array/OFF, então um grupo listado nos dois acaba escondido - Aplique o estado Usage explícito do grupo para o propósito 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 - Rode o array
/ASda configuração selecionada por último, deixando entradas para o evento correspondente definirem o estado dos grupos que listam
O passo três explica o arquivo da abertura: alguns exportadores gravam /ViewState /ON em todo grupo que criam, e daí em diante nenhuma quantidade de edição de arrays esconde essas camadas. Impressão e exportação seguem a mesma regra com uma torção: o renderizador lê /PrintState ou /ExportState primeiro e volta para o /ViewState quando a entrada específica falta. Um /ViewState /ON solitário portanto prende a camada no papel também. O passo quatro é mais discreto. Um grupo marcado com /Intent /Design está fora da visibilidade de intent de view por completo e pinta independentemente dos arrays, enquanto um grupo sem chave /Intent é tratado como /View, que é o padrão que a especificação dá
Listando camadas e o estado renderizado delas com InspectOptionalContent
O PDFium Component reproduz essa ordem no TPdf.InspectOptionalContent. O método preenche um TPdfOptionalContentInventory e retorna True em caso de sucesso; o parâmetro Usage dele tem padrão ocuView e o parâmetro ConfigurationIndex tem padrão -1, que seleciona o índice 0. O índice de configuração 0 é o dicionário /D padrão, e as entradas de /Configs seguem a partir do índice 1 na ordem do arquivo. Antes de escanear, o método faz o PDFium salvar o documento aberto na memória e expande quaisquer object streams, então edições feitas antes na sessão aparecem no resultado. Um arquivo sem /OCProperties, ou um índice de configuração fora da faixa disponível, faz a chamada retornar False com a razão no ErrorMessage; nenhuma exceção é levantada para nenhum dos casos
procedure TFormMain.ListLayers;
var
Inv: TPdfOptionalContentInventory;
Cfg: TPdfOptionalContentConfiguration;
G: TPdfOptionalContentGroup;
begin
// Usage tem padrão ocuView; -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;
Cfg := Inv.Configurations[Inv.SelectedConfigurationIndex];
Memo1.Lines.Add(Format('Configuration %d "%s", %d groups, /AS present: %s',
[Inv.SelectedConfigurationIndex, string(Cfg.Name), Length(Inv.Groups),
BoolToStr(Cfg.HasAutomaticState, True)]));
for G in Inv.Groups do
Memo1.Lines.Add(Format('obj %d %-24s visible=%s locked=%s radio=%d',
[G.ObjectNumber, string(G.Name),
BoolToStr(G.EffectiveVisible, True),
BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;
Cada TPdfOptionalContentGroup carrega o número de objeto dele, nome, Intents e os três estados explícitos ViewState, PrintState e ExportState, cada um deles ocsUnspecified, ocsOn ou ocsOff. Quando um estado lê ocsOn e o grupo está no array /OFF, você encontrou a armadilha do parágrafo de abertura. Locked e RadioGroupIndex vêm da configuração selecionada, e o RadioGroupIndex é -1 para um grupo que não pertence a nenhum conjunto de /RBGroups
O resto do dicionário Usage é reportado como o arquivo o declara: Language e LanguagePreferred, UserType e UserNames, PageElementSubtype, e MinimumZoom e MaximumZoom atrás de uma flag HasZoomRange. O EffectiveVisible não depende de nenhum deles. Exibição dependente de zoom é tarefa do seu viewer. Cada TPdfOptionalContentConfiguration expõe o nome dele, BaseState, conteúdo dos arrays e RadioGroups, mais OrderGroupObjectNumbers, que achata o /Order em referências de grupo e pula os rótulos de texto de arrays de rótulos aninhados
Impressão e exportação podem discordar da tela
O argumento Usage muda a resposta, e essa é a razão de existir. Sob ocuPrint o inventário lê o /PrintState com o fallback dele para o /ViewState, e apenas entradas /AS cujo /Event é /Print participam. Uma camada de marca d'água construída para aparecer apenas no papel portanto reporta escondida sob ocuView e visível sob ocuPrint. Comparar as duas passadas encontra essas camadas antes que uma impressão surpreenda alguém
procedure TFormMain.ReportPrintOnlyLayers;
var
OnScreen, OnPaper: TPdfOptionalContentInventory;
I: Integer;
begin
if not Pdf.InspectOptionalContent(OnScreen, ocuView) or
not Pdf.InspectOptionalContent(OnPaper, ocuPrint) then
Exit;
// As duas passadas leem o mesmo array /OCGs, então a ordem dos grupos casa
for I := 0 to High(OnScreen.Groups) do
if OnScreen.Groups[I].EffectiveVisible <> OnPaper.Groups[I].EffectiveVisible then
Memo1.Lines.Add(Format('%s: screen=%s, print=%s',
[string(OnScreen.Groups[I].Name),
BoolToStr(OnScreen.Groups[I].EffectiveVisible, True),
BoolToStr(OnPaper.Groups[I].EffectiveVisible, True)]));
end;
Como expressões de visibilidade OCMD são avaliadas?
Um dicionário de membership de optional content (OCMD, ISO 32000-1 §8.11.2.2) liga um pedaço de conteúdo a vários grupos por uma política /P, ou por uma expressão de visibilidade /VE que combina grupos com /And, /Or e /Not. A especificação dá ao /P um padrão de /AnyOn, deixa o /VE tomar precedência sobre /OCGs e /P quando presente, e diz que um OCMD sem grupos não tem efeito na visibilidade
O array Memberships do inventário segue essas regras contra os resultados por grupo da mesma passada, então o usage que você pediu atravessa. Cada membership reporta os números de objeto dos grupos dele, Policy (ocmpAnyOn, ocmpAllOn, ocmpAnyOff, ocmpAllOff, ou ocmpUnknown para um nome fora desses quatro), o texto bruto de VisibilityExpression e o EffectiveVisible dele próprio. Um OCMD sem grupos conta como visível. Uma referência a um número de objeto que não é um OCG conhecido também conta como visível, então uma referência pendurada não esconde tudo ao redor. A avaliação de /VE para em uma profundidade de aninhamento de 32 e trata qualquer coisa mais funda como escondida, então uma expressão autorreferente não consegue estourar a pilha. O scan lista todo OCMD guardado como objeto indireto com /Type /OCMD, object streams incluídos
Gravando um novo estado de camada com SaveAsOptionalContentConfigured
O TPdf.SaveAsOptionalContentConfigured recebe um array de records TPdfOptionalContentStateChange, cada um segurando um GroupObjectNumber e uma flag Visible. Ele grava uma cópia na qual a configuração selecionada produz exatamente aquele estado sob a ordem de cinco passos. Grupos que você não menciona mantêm a visibilidade que o inventário calculou para eles
procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer;
ForPrint: Boolean);
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;
Options := TPdfOptionalContentConfigureOptions.Default;
if ForPrint then
begin
Options.Usage := ocuPrint; // grava /Print << /PrintState ... >>
Options.ConfigurationIndex := 1; // primeira entrada de /Configs, o /D fica como estava
end;
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;
A saída mantém o documento salvo do próprio PDFium como um prefixo byte a byte e anexa apenas o owner de configuração reescrito e os objetos OCG que carregam dicionários Usage, seguidos por uma nova seção de xref e um trailer. Antes de qualquer coisa chegar ao seu destino, o resultado é reaberto em um TPdf separado sob a política de carga estrita, e o método falha se essa carga não tiver sucesso ou se a tabela de referências cruzadas não validar. O overload de arquivo grava em um arquivo temporário ao lado do alvo e substitui o alvo apenas depois da verificação, então uma atualização rejeitada deixa o desenho anterior intocado. É a mesma abordagem de revisão incremental verificada usada pelo editor de name tree e number tree de PDF no PDFium Component. Para confirmar um preset de impressão, recarregue a saída e chame InspectOptionalContent(Inv, ocuPrint, 1); os grupos devem reportar os estados que você passou
O que o save configurado se recusa a fazer?
O save configurado recusa qualquer mudança que o documento proíba ou não consiga carregar com segurança, e toda recusa acontece antes de o destino ser gravado. As checagens rodam em uma ordem fixa. Um documento assinado levanta EPdfError antes de qualquer outra coisa acontecer, a menos que você passe AllowSignedDocument = True. Um documento criptografado é rejeitado em seguida, porque objetos incrementais em texto plano não conseguem carregar o security handler ativo. Então cada mudança pedida é checada: um número de objeto que não está no /OCGs falha de plano, e uma mudança em um grupo listado no array /Locked da configuração falha, embora reafirmar o valor atual dele passe. Por último, com EnforceRadioGroups ligado, um conjunto de /RBGroups que acabaria com mais de um membro visível é rejeitado. Toda recusa exceto a de assinatura retorna False com a razão no Report.ErrorMessage
Dois desses portões vão além do que a especificação estritamente exige. A §8.11.4.3 descreve o /Locked como proteção contra mudanças pela interface de usuário do viewer e descreve grupos de radio button como desligando os outros membros quando um é ligado. O PDFium Component aplica os dois a mudanças programáticas também, e para radio groups ele recusa em vez de escolher membros para desligar. Pedidos dois membros de um conjunto mutuamente exclusivo, a biblioteca não pode saber qual você quis, e um chute contradiria o seu pedido. Desligue os outros no mesmo array Changes e o save passa
O portão de documento assinado existe porque a revisão anexada fica por cima do re-save completo do PDFium, que é uma nova sequência de bytes; a byte range que uma assinatura existente cobre não pode ser presumida de sobreviver. Passe AllowSignedDocument = True apenas quando perder aquela assinatura for aceitável
Três efeitos colaterais importam em um job em lote. A reescrita normaliza a configuração selecionada para /BaseState /ON, então uma linha de base /Unchanged ou /OFF de um autor vira arrays explícitos com a mesma visibilidade resultante. Descartar o /AS também remove truques apenas de impressão, como uma camada de marca d'água que aparece apenas no papel; defina DisableAutomaticState como False para manter essas regras e aceitar que elas podem sobrepujar o estado pedido por você para o evento delas. E os arrays /ON e /OFF são gravados a partir dos estados calculados para o Options.Usage, então um preset de impressão salvo na configuração 0 também muda o que a tela mostra para todo grupo que não tem dicionário Usage. Mantenha presets de impressão em uma entrada de /Configs e deixe o /D para a tela. Do lado da conformidade, PDF/A-2 (ISO 19005-2 cláusula 6.9) e PDF/UA (ISO 14289-1 cláusula 7.10) proíbem /AS em dicionários de configuração, então a saída padrão limpa uma pendência que a sua validação de preflight PDF/A com o PDFium Component reportaria de outro jeito
Aplicando mudanças de camada dentro de um viewer Delphi
Em um viewer, controle de camadas é uma checklist construída a partir do inventário, seguida de um save na memória e uma recarga. Preencha a checklist a partir de Groups, desabilite entradas cuja flag Locked está definida, trate membros que compartilham um RadioGroupIndex como mutuamente exclusivos, e ao aplicar grave para um TMemoryStream pelo overload de stream. O TPdf.LoadDocument espera um componente inativo, então desligue-o primeiro; a carga em buffer copia os bytes, o que deixa você liberar o stream direto
procedure TFormMain.ApplyLayerChecklist(
const Changes: TPdfOptionalContentStateChanges);
var
Report: TPdfOptionalContentConfigureReport;
Output: TMemoryStream;
KeepPage: Integer;
begin
Output := TMemoryStream.Create;
try
try
if not Pdf.SaveAsOptionalContentConfigured(Output, Changes,
TPdfOptionalContentConfigureOptions.Default, Report) then
begin
ShowMessage('Layer update rejected: ' + Report.ErrorMessage);
Exit;
end;
except
on E: EPdfError do
begin
ShowMessage(E.Message); // arquivo assinado: Output continua vazio
Exit;
end;
end;
KeepPage := Pdf.PageNumber;
Pdf.Active := False;
Pdf.LoadDocument(Output); // Buffered tem padrão True
Pdf.PageNumber := KeepPage;
finally
Output.Free;
end;
end;
Depois da recarga, o PDFium pinta a partir da configuração nova, então a tela e a próxima chamada de InspectOptionalContent concordam. A fiação entre TPdf e TPdfView é coberta em construir um viewer de PDF cheio de recursos com PDFium VCL no Delphi. Quando um documento ainda não tem camadas e você precisa adicioná-las, criar camadas PDF com OCG e OCMD no Delphi percorre o SaveAsOptionalContentCreated
Precificação, o build de teste e os outros recursos de documento do componente estão listados na página de produto do PDFium Component para Delphi