Um topógrafo abre uma planta baixa e deseja que as curvas de nível fiquem ocultas enquanto as redes de serviços públicos permanecem visíveis. Um revisor quer que as anotações de marcação fiquem visíveis na tela e sumam da impressão. Uma folha de especificações de produto é distribuída em três idiomas a partir de um único arquivo, e o leitor escolhe qual idioma exibir. Todas essas três situações utilizam o mesmo recurso de PDF, e o painel que as gerencia no Acrobat é chamado de Camadas. O recurso por trás desse painel é o conteúdo opcional, que permite que uma única página carregue várias camadas visuais independentes que o leitor ativa e desativa.
O conteúdo opcional é especificado na ISO 32000-1 §8.11. A unidade de visibilidade é um grupo de conteúdo opcional, um OCG (Optional Content Group), um dicionário do tipo /OCG que carrega um nome. O conteúdo marcado em uma página é associado a um grupo, e o visualizador decide se esse grupo é exibido no momento. Um construto relacionado, o dicionário de associação de conteúdo opcional ou OCMD, permite que a visibilidade dependa de uma combinação booleana de vários grupos, mas o caso do dia a dia é um único grupo nomeado representando uma única camada. O documento une todo esse mecanismo por meio de uma entrada de catálogo, /OCProperties, descrita a seguir.
O que o catálogo precisa conter
Um OCG por si só é inerte. Para que um visualizador liste uma camada e lembre seu estado, o catálogo do documento precisa de um dicionário /OCProperties, e a seção §8.11.4 estabelece exatamente o que vai nele. Há um array /OCGs nomeando cada grupo no arquivo, e há uma entrada /D contendo a configuração padrão. A configuração padrão é a parte que o leitor aplica quando o arquivo é aberto pela primeira vez. Ela registra quais grupos começam ativados e quais começam desativados, quais entradas são bloqueadas contra a alternância do usuário e, por meio de um array /Order, como os nomes das camadas são organizados e aninhados no painel.
A consequência prática é que criar uma camada nunca é um ato puramente local. O grupo precisa ser desenhado na página e também deve ser registrado em uma estrutura no nível do catálogo que não existia anteriormente. O PDFlibPas faz ambos para você. A primeira chamada que cria um grupo adiciona a entrada /OCProperties ao catálogo e inicia a configuração padrão, de modo que a camada é tanto desenhada quanto listada sem a necessidade de controle separado de sua parte.
Por que um modo de conformidade pode omitir o recurso
Antes que qualquer código de camada seja executado, a meta de conformidade do documento decide se o conteúdo opcional é permitido. O formato PDF/A-1, o perfil de arquivamento definido na ISO 19005-1, proíbe expressamente a entrada /OCProperties em seu §6.1.13. Essa regra atende ao propósito do formato. Um arquivo de arquivamento deve ser renderizado de forma idêntica para todos os leitores no futuro, e um conteúdo cuja visibilidade o visualizador pode alterar é um conteúdo cuja aparência não é fixa, logo o perfil proíbe essa estrutura em vez de permitir um arquivo ambíguo. O PDF/A-2 e o PDF/A-3, definidos na ISO 19005-2 e ISO 19005-3, adotam a visão oposta em seus respectivos parágrafos §6.9 e permitem o conteúdo opcional, com regras sobre a visibilidade padrão.
Essa diferença aparece diretamente na API. Quando o documento está em um modo PDF/A-1, NewOptionalContentGroup recusa-se a criar o grupo e retorna zero, pois atender à solicitação geraria um arquivo que falharia em sua própria conformidade declarada. No modo PDF/A-2 ou PDF/A-3, e em PDFs comuns sem restrições, a mesma chamada é bem-sucedida e retorna um ID de grupo diferente de zero. Um resultado zero, portanto, não é uma falha genérica a ser inspecionada depois; é a biblioteca informando que o nível de conformidade ativo não tem espaço para o recurso.
var
Pdf: TPDFlib;
LayerID: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1); // PDF/A-1a: OCProperties forbidden
LayerID := Pdf.NewOptionalContentGroup('Utilities');
if LayerID = 0 then
// refused under PDF/A-1; not a transient error, the mode bans layers
ShowMessage('Optional content is not available in PDF/A-1 mode.');
finally
Pdf.Free;
end;
end;
Dois estados por camada, não um
Uma camada não é simplesmente visível ou invisível. A configuração padrão registra seu estado na tela e um estado de impressão separado, porque a seção §8.11.4 distingue o que um visualizador exibe do que um fluxo de impressão emite. Os dois são independentes de propósito. Uma marca d'água de rascunho pode ser exibida na tela e omitida do papel, e uma camada de linhas de corte pode ser oculta na tela e enviada a uma impressora plotter. Unificar os dois forçaria um a seguir o outro, perdendo exatamente o controle para o qual o recurso existe.
O PDFlibPas expõe o par por meio de dois métodos de gravação. SetOptionalContentGroupVisible recebe o ID do grupo e um sinalizador, onde um significa visível e zero significa oculto, e gerencia o estado na tela padrão. SetOptionalContentGroupPrintable recebe o ID do grupo e um sinalizador para definir se a camada é emitida quando o documento é impresso. Os métodos de leitura correspondentes, GetOptionalContentGroupVisible e GetOptionalContentGroupPrintable, retornam um ou zero, permitindo ler a disposição da camada em tela e impressão separadamente, em vez de deduzir uma com base na outra.
Construindo duas camadas em uma página
A criação e o preenchimento de uma camada seguem uma ordem fixa. Você desenha o conteúdo para a camada na página atual e depois chama SetContentStreamOptional com o ID do grupo, o que envolve o fluxo de conteúdo atual da página para que tudo desenhado até o momento pertença a esse grupo. Como a chamada captura o que quer que esteja no fluxo naquele instante, a regra é aplicar as marcas de uma camada, atribuí-las e só então iniciar a próxima camada. O exemplo abaixo coloca redes de serviços na primeira página e marcações de um revisor na segunda página, define os estados de tela e impressão de cada camada e salva o arquivo.
var
Pdf: TPDFlib;
FontID, UtilLayer, RedlineLayer: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument; // unconstrained PDF: layers allowed
Pdf.SetPageDimensions(595, 842); // A4 in points
FontID := Pdf.AddStandardFont(0); // Helvetica
Pdf.SelectFont(FontID);
// Layer 1: utilities, drawn then assigned to its own group
Pdf.SetTextColor(0.10, 0.30, 0.65);
Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
Pdf.SetContentStreamOptional(UtilLayer);
Pdf.SetOptionalContentGroupVisible(UtilLayer, 1); // shown on screen
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper
// Layer 2: reviewer redline on a fresh page
Pdf.InsertPages(2, 1); // append one page after page 1
Pdf.SetTextColor(0.80, 0.10, 0.10);
Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
Pdf.SetContentStreamOptional(RedlineLayer);
Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1); // visible while reviewing
Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0); // never printed
Pdf.SaveToFile('SitePlan_Layers.pdf');
finally
Pdf.Free;
end;
end;
A camada de marcação é o caso que merece destaque. Ela é exibida na tela para que o revisor veja a nota, e seu sinalizador de impressão é zero para que a impressão do mesmo arquivo não contenha o texto de revisão. Essa assimetria é toda a razão de manter os dois estados separados.
Lendo a configuração de volta
A leitura de camadas é um processo diferente na mesma estrutura. Depois que um arquivo é carregado, GetOptionalContentConfigCount informa quantos dicionários de configuração o documento contém; a primeira configuração padrão recebe o ID de configuração 1. Dentro de uma configuração, GetOptionalContentConfigOrderCount fornece o número de entradas na árvore de ordenação, e você as indexa a partir de 1. Para cada entrada, GetOptionalContentConfigOrderItemLabel retorna o texto de exibição e GetOptionalContentConfigOrderItemLevel retorna o nível de aninhamento, permitindo reconstruir fielmente um painel de tópicos com subcamadas recuadas sob os títulos.
Cada entrada também possui um tipo. GetOptionalContentConfigOrderItemType distingue um grupo de conteúdo opcional real de um rótulo de texto simples que existe apenas para encabeçar uma seção da árvore. Essa distinção é importante porque as consultas de estado por grupo só fazem sentido para grupos reais. Para uma entrada de grupo, GetOptionalContentConfigState informa se a configuração o inicia ativado, desativado ou se o deixa inalterado, e GetOptionalContentConfigLocked relata se o usuário está impedido de alterná-lo. O loop abaixo renderiza a árvore de ordenação com o estado de cada grupo e seu status de bloqueio, recuando de acordo com o nível.
var
Pdf: TPDFlib;
Cfg, Count, I, ItemType, GroupID, Indent: Integer;
Line: string;
begin
Pdf := TPDFlib.Create(nil);
try
if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
if Pdf.GetOptionalContentConfigCount = 0 then Exit;
Cfg := 1; // the default configuration
Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
for I := 1 to Count do
begin
Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
Line := StringOfChar(' ', Indent * 2)
+ Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);
ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
if ItemType = 1 then // 1 = optional content group
begin
GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
1: Line := Line + ' [on]';
2: Line := Line + ' [off]';
3: Line := Line + ' [unchanged]';
end;
if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
Line := Line + ' (locked)';
end;
// ItemType = 2 is a text label heading; it has no per-group state
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Dois detalhes mantêm este loop correto. O índice de ordenação é baseado em um, de 1 até o total, correspondendo a como a biblioteca numera a árvore internamente. E as chamadas específicas de grupo são executadas apenas quando o tipo de item é um grupo, já que um rótulo de texto é um título com um nome e um nível, mas sem estado de ativado, desativado ou bloqueado para consultar. Pule essa verificação e você consultará um rótulo sobre um estado que ele não possui.
Onde isso se aplica
As camadas são um mecanismo de apresentação, portanto o motor deve respeitá-las em qualquer caminho que renderize uma página, e o lado da renderização é detalhado em nosso passo a passo sobre renderização multimotor no Delphi. Elas também se cruzam com a estrutura do documento, pois o nome de uma camada é um texto voltado ao autor e o leitor se beneficia de uma estrutura de camadas organizada, o que se conecta ao trabalho em nosso artigo sobre PDF etiquetado e estrutura de acessibilidade. Ambos se integram com las APIs de conteúdo opcional descritas aqui, fornecidas como parte da Delphi PDF Library junto com os recursos de página, texto, fonte e conformidade discutidos em outras partes deste blog.