Um topógrafo abre uma planta de implantação e pretende ocultar as curvas de nível enquanto as redes de infraestruturas permanecem ativas. Um revisor quer as anotações de revisão visíveis no ecrã e removidas da impressão. Uma folha de especificações de produto é disponibilizada em três idiomas num único ficheiro, e o leitor escolhe que idioma mostrar. Todos os três cenários usam a mesma funcionalidade de PDF, e o painel que os controla no Acrobat designa-se Camadas. A funcionalidade por trás desse painel é o conteúdo opcional, que permite que uma única página contenha vários estratos visuais independentes que o visualizador pode ativar e desativar.
O conteúdo opcional está especificado na norma ISO 32000-1, secção 8.11. A unidade de visibilidade é um grupo de conteúdo opcional, um OCG (Optional Content Group), que é um dicionário do tipo /OCG com um nome. O conteúdo marcado numa página está associado a um grupo, e o visualizador decide se esse grupo é mostrado atualmente. Uma estrutura relacionada, o dicionário de pertença a conteúdo opcional ou OCMD (Optional Content Membership Dictionary), 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 a representar uma única camada. O documento une todo o mecanismo através de uma única entrada no catálogo, /OCProperties, descrita a seguir.
O que o catálogo deve conter
Um OCG por si só é inerte. Para que um visualizador liste uma camada e recorde o seu estado, o catálogo do documento necessita de um dicionário /OCProperties, e a secção 8.11.4 detalha exatamente o que deve constar nele. Existe uma matriz /OCGs que identifica cada grupo no ficheiro e uma entrada /D que contém a configuração predefinida. A configuração predefinida é a parte que um leitor aplica quando o ficheiro é aberto pela primeira vez. Regista quais os grupos que começam ativos e os que começam inativos, quais as entradas bloqueadas para impedir o utilizador de as alternar e, através de uma matriz /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 tem de ser desenhado na página e também registado numa estrutura ao nível do catálogo que não existia anteriormente. PDFlibPas faz ambas as coisas por si. A primeira chamada que cria um grupo adiciona a entrada /OCProperties ao catálogo e inicia a configuração predefinida, para que a camada seja tanto desenhada como listada sem necessidade de registos adicionais do seu lado.
Por que razão um modo de conformidade pode reter a funcionalidade
Antes de qualquer código de camadas ser executado, o objetivo de conformidade do documento decide se o conteúdo opcional é sequer legal. O PDF/A-1, o perfil de arquivo definido na norma ISO 19005-1, proíbe liminarmente a entrada /OCProperties na secção 6.1.13. O raciocínio ajusta-se ao objetivo do formato. Um ficheiro de arquivo deve ser renderizado de forma idêntica para todos os leitores no futuro, e um conteúdo cuja visibilidade pode ser alterada pelo visualizador é um conteúdo cuja aparência não está fixada. Assim, o perfil proíbe esta estrutura em vez de permitir um arquivo ambíguo. O PDF/A-2 e o PDF/A-3, definidos nas normas ISO 19005-2 e ISO 19005-3, adotam a perspetiva oposta na sua secção 6.9 e permitem conteúdo opcional, com regras sobre a visibilidade predefinida.
Essa diferença reflete-se diretamente na API. Quando o documento está num modo PDF/A-1, o método NewOptionalContentGroup recusa-se a criar o grupo e devolve zero, porque atender ao pedido produziria um ficheiro que falharia na sua própria conformidade declarada. No modo PDF/A-2 ou PDF/A-3, e num PDF comum sem restrições, a mesma chamada é bem-sucedida e devolve um ID de grupo diferente de zero. Um resultado zero não é, portanto, uma falha genérica a inspecionar mais tarde; é a biblioteca a indicar que o nível de conformidade ativo não tem espaço para essa funcionalidade.
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 apenas um
Uma camada não é simplesmente visível ou invisível. A configuração predefinida regista o seu estado no ecrã e um estado de impressão separado, porque a secção 8.11.4 distingue o que um visualizador apresenta do que um fluxo de impressão emite. Os dois são independentes de propósito. Uma marca de água de rascunho pode ser mostrada no ecrã e omitida no papel, e uma camada de linhas de corte pode estar oculta no ecrã mas ser enviada para uma plotter. Unificar os dois forçaria um a seguir o outro, perdendo-se precisamente o controlo para o qual a funcionalidade existe.
O PDFlibPas expõe o par através de dois métodos de escrita (setters). O SetOptionalContentGroupVisible recebe o ID do grupo e uma sinalização (flag), onde um significa visível e zero significa oculto, e controla o estado inicial no ecrã. O SetOptionalContentGroupPrintable recebe o ID do grupo e uma sinalização que define se a camada é emitida ao imprimir o documento. Os métodos de leitura (getters) correspondentes, GetOptionalContentGroupVisible e GetOptionalContentGroupPrintable, devolvem cada um um ou zero, para que possa ler a disposição de ecrã e de impressão de uma camada separadamente, em vez de deduzir uma a partir da outra.
Criar duas camadas numa página
A criação de uma camada e o seu preenchimento seguem uma ordem fixa. Desenha o conteúdo da camada na página atual e, em seguida, chama SetContentStreamOptional com o ID do grupo, o que envolve o fluxo de conteúdo atual da página para que tudo o que foi desenhado até ao momento pertença a esse grupo. Como a chamada captura o que quer que esteja no fluxo naquele instante, a regra é desenhar as marcas de uma camada, atribuí-las e só depois iniciar a camada seguinte. O exemplo abaixo coloca redes na primeira página e anotações de revisão numa segunda página, define o estado de ecrã e de impressão de cada camada e guarda o ficheiro.
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 revisão é o caso digno de nota. É apresentada no ecrã para que um revisor veja a nota, mas a sua sinalização de impressão é zero para que uma cópia impressa do mesmo ficheiro não contenha texto de revisão. Esta assimetria é o principal motivo para manter os dois estados separados.
Ler a configuração de volta
A leitura de camadas é um percurso diferente através da mesma estrutura. Após o carregamento de um ficheiro, o método GetOptionalContentConfigCount indica quantos dicionários de configuração o documento contém; a primeira configuração predefinida corresponde ao ID de configuração 1. Dentro de uma configuração, o GetOptionalContentConfigOrderCount fornece o número de entradas na árvore de ordenação, as quais são indexadas a partir de 1. Para cada entrada, o GetOptionalContentConfigOrderItemLabel devolve o seu texto de exibição e o GetOptionalContentConfigOrderItemLevel devolve a sua profundidade de aninhamento, permitindo reconstruir fielmente uma estrutura de painel com subcamadas indentadas sob cabeçalhos.
Cada entrada possui também um tipo. O método GetOptionalContentConfigOrderItemType distingue um grupo de conteúdo opcional real de uma etiqueta de texto simples que serve apenas como cabeçalho de uma secção da árvore. Esta distinção é importante porque as consultas de estado por grupo apenas fazem sentido para grupos reais. Para uma entrada de grupo, o GetOptionalContentConfigState informa se a configuração o inicia ativo, inativo ou se o mantém inalterado, e o GetOptionalContentConfigLocked informa se o utilizador está impedido de o alternar. O ciclo (loop) seguinte renderiza a árvore de ordenação com o estado de cada grupo e o respetivo estado de bloqueio, aplicando indentação por 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 garantem a correção deste ciclo. O índice de ordenação baseia-se em um, de 1 até à contagem total, correspondendo à forma como a biblioteca numera internamente a árvore. Além disso, as chamadas por grupo só são executadas quando o tipo de item é um grupo, uma vez que uma etiqueta de texto é um cabeçalho com nome e nível, mas sem estado ativo, inativo ou bloqueado para consultar. Omitir essa proteção resultará na tentativa de consultar numa etiqueta um estado que ela não possui.
Onde isto se enquadra
As camadas são um mecanismo de apresentação, pelo que o motor de processamento deve honrá-las em todos os caminhos que renderizam uma página, tema que é coberto no nosso guia de renderização multimotor em Delphi. Intersetam-se também com a estrutura do documento, visto que o nome de uma camada é texto visível ao autor e o leitor beneficia de uma estrutura de camadas organizada, o que se liga ao trabalho descrito no nosso artigo sobre PDF etiquetado e estrutura de acessibilidade. Ambos se conjugam com as APIs de conteúdo opcional descritas aqui, fornecidas como parte da Delphi PDF Library juntamente com as funcionalidades de página, texto, tipo de letra e conformidade abordadas noutros locais deste blogue.