O PDFium Component cria novas camadas PDF a partir do Delphi através do TPdf.SaveAsOptionalContentCreated. Numa única gravação verificada o método acrescenta grupos de conteúdo opcional (OCGs), dicionários de associação de conteúdo opcional (OCMDs) e desenho fornecido pelo chamador embrulhado em Form XObjects cuja entrada /OC aponta para um grupo novo, pelo que o PDFium e outros visualizadores mostram ou escondem esse desenho quando a camada é alternada
O pedido raramente é abstrato. Uma equipa de engenharia de instalações quer dimensões métricas e imperiais na mesma folha de desenho, com exatamente um conjunto visível. Uma gráfica quer uma sobreposição de linha de corte que apareça no ecrã mas possa ser desligada antes da prova. Nenhum dos ficheiros tem ainda camadas, pelo que não há nada para alternar. Alternar camadas existentes é um trabalho diferente, coberto em alternar camadas de conteúdo opcional PDF existentes com o SaveAsOptionalContentConfigured; este artigo trata de criar as camadas em primeiro lugar
Por que não pode simplesmente definir /OC num objeto de página com o PDFium?
O PDFium não tem função pública para anexar conteúdo opcional a um objeto de página existente. Os cabeçalhos públicos não contêm FPDFPageObj_SetOC nem nada equivalente, pelo que a rota habitual de editar e depois FPDFPage_GenerateContent não consegue produzir conteúdo vedado. A vedação tem de ser escrita na estrutura do ficheiro, e a única questão é que estrutura o PDFium honrará ao renderizar
A ISO 32000-1 §8.11.3 dá três formas de tornar gráficos opcionais, e o PDFium trata-as de forma diferente:
- Secções de fluxo de conteúdo da página embrulhadas em conteúdo marcado
/OC /Name BDC...EMC, verificado por objeto de página durante a renderização - Form XObjects e image XObjects que transportam uma entrada
/OCno seu próprio dicionário, verificados quando o XObject é pintado - Anotações com uma entrada
/OCno dicionário de anotação, que o caminho de renderização do PDFium não consulta de todo, pelo que uma anotação vedada permanece visível independentemente do estado da camada
Embrulhar em conteúdo marcado significa reescrever fluxos de conteúdo existentes operador a operador, e anotações estão excluídas pelo renderizador. Isso deixa a rota do Form XObject, e é a que o PDFium Component usa: o seu desenho vai para um Form XObject novo com /OC a apontar para o grupo, o formulário é registado no dicionário /Resources /XObject da página, e um novo fluxo de conteúdo contendo um único /Name Do é anexado ao /Contents da página. O conteúdo existente da página nunca é analisado nem reescrito
Descrever grupos e formulários vedados no Delphi
O TPdf.SaveAsOptionalContentCreated recebe três matrizes e devolve um relatório. Ambos os overloads, um gravando num TStream e outro num nome de ficheiro, partilham a mesma lista de parâmetros: Groups: TPdfOptionalContentCreateGroups, Memberships: TPdfOptionalContentCreateMemberships, Forms: TPdfOptionalContentCreateForms, out Report: TPdfOptionalContentCreateReport, e um AllowSignedDocument: Boolean = False opcional. O método chegou no PDFium Component v3.154.0
Cada TPdfOptionalContentCreateGroup tem um Name, um Creator opcional, um Intent de pociView ou pociDesign, uma flag InitialStateVisible, uma flag Locked e um RadioTag. Grupos que partilhem o mesmo RadioTag aterram numa matriz /RBGroups única, que é como um visualizador sabe que ligar um desliga os outros. Cada TPdfOptionalContentCreateForm nomeia o seu grupo de vedação por GroupIndex (um índice de base zero na sua matriz Groups), um PageNumber de base 1, os bytes brutos do fluxo de conteúdo, uma caixa delimitadora, uma matriz opcional e um ResourceName opcional
var
Pdf: TPdf;
Groups: TPdfOptionalContentCreateGroups;
Forms: TPdfOptionalContentCreateForms;
Report: TPdfOptionalContentCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'sheet-a1.pdf';
Pdf.Active := True;
SetLength(Groups, 2); // SetLength enche os registos com zeros
Groups[0].Name := 'Dimensions (mm)';
Groups[0].Intent := pociView;
Groups[0].InitialStateVisible := True;
Groups[0].RadioTag := 'Units';
Groups[1].Name := 'Dimensions (in)';
Groups[1].Intent := pociView;
Groups[1].InitialStateVisible := False;
Groups[1].RadioTag := 'Units';
SetLength(Forms, 2);
Forms[0].GroupIndex := 0;
Forms[0].PageNumber := 1;
Forms[0].Content := TEncoding.ASCII.GetBytes(
'0 0 1 RG 1.5 w 72 120 m 540 120 l S');
Forms[0].BBoxRight := 612;
Forms[0].BBoxTop := 792;
Forms[0].ResourceName := 'DimMetric';
Forms[1] := Forms[0];
Forms[1].GroupIndex := 1;
Forms[1].Content := TEncoding.ASCII.GetBytes(
'1 0 0 RG 1.5 w 72 140 m 540 140 l S');
Forms[1].ResourceName := 'DimImperial';
if not Pdf.SaveAsOptionalContentCreated('sheet-a1-layers.pdf',
Groups, nil, Forms, Report) then
raise Exception.Create(Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Note que o conteúdo do formulário é desenho vetorial puro. O componente escreve apenas um dicionário /Resources vazio em cada formulário, pelo que o fluxo de conteúdo tem de ser autossuficiente: cores de dispositivo, caminhos, recortes e transformações funcionam, mas qualquer coisa que precise de um recurso nomeado (uma fonte para Tf, uma imagem para Do, um ExtGState para gs) não tem nada contra que resolver. Se deixar o ResourceName vazio, os formulários chamam-se FmOc1, FmOc2 e assim por diante; um nome personalizado só pode conter letras, dígitos, sublinhado, menos e ponto
O que acrescenta um OCMD sobre um grupo simples?
Um dicionário de associação de conteúdo opcional faz a visibilidade depender de vários grupos ao mesmo tempo. A ISO 32000-1 §8.11.2.2 define duas formas: uma lista /OCs com uma política /P como /AnyOn ou /AllOn, e uma expressão de visibilidade /VE construída com /And, /Or e /Not na especificação. O PDFium Component recebe um TPdfOptionalContentCreateMembership com MemberGroupIndices, uma Policy e uma Expression; quando Expression.GroupIndices não está vazio, a expressão ganha e o /VE é escrito ao lado do /OCs
procedure AddUnitMemberships(var Memberships: TPdfOptionalContentCreateMemberships);
begin
SetLength(Memberships, 2);
// Visível quando qualquer camada de unidades está ligada: /OCs [...] /P /AnyOn
SetLength(Memberships[0].MemberGroupIndices, 2);
Memberships[0].MemberGroupIndices[0] := 0;
Memberships[0].MemberGroupIndices[1] := 1;
Memberships[0].Policy := ocmpAnyOn;
// Visível só enquanto a camada imperial está desligada: /VE [ /Not ref ]
SetLength(Memberships[1].MemberGroupIndices, 1);
Memberships[1].MemberGroupIndices[0] := 1;
Memberships[1].Expression.Kind := poceNot;
SetLength(Memberships[1].Expression.GroupIndices, 1);
Memberships[1].Expression.GroupIndices[0] := 1;
end;
O subconjunto suportado é deliberadamente estreito. As políticas limitam-se a ocmpAllOn e ocmpAnyOn; ocmpAnyOff e ocmpAllOff são rejeitadas. As expressões têm um nível de profundidade, com poceAll, poceAny ou poceNot sobre grupos criados, e o /Not exige exatamente um índice de grupo. O poceAll serializa como /And, o poceAny como /Or e o poceNot como /Not, os únicos três operadores que a §8.11.2.2 define. Versões anteriores à 3.181.2 escreviam /All e /Any, que o PDFium avalia como oculto, pelo que se criou OCMDs com uma build mais antiga, regenere-os em vez de confiar na expressão. Toda a associação também precisa de pelo menos uma entrada em MemberGroupIndices, mesmo quando há uma expressão, porque essa lista torna-se a matriz /OCs. Os números de objeto dos novos dicionários regressam em Report.CreatedMembershipObjectNumbers
Dois comportamentos valem a pena conhecer antes de construir sobre OCMDs. Primeiro, os formulários vedados na mesma chamada referenciam só grupos: o TPdfOptionalContentCreateForm.GroupIndex indexa Groups, não Memberships, pelo que um formulário não pode ser vedado por um OCMD recém-criado através desta API. Segundo, o PDFium Component acrescenta cada novo OCMD à matriz /OCProperties /OCGs do catálogo, enquanto /ON, /OFF e /Order continuam a ser exclusivos de grupos. A ISO 32000-1 §8.11.4.2 descreve o /OCGs como a lista de grupos, mas sem esse registo o TPdf.InspectOptionalContent não enumera a associação de todo. O efeito colateral é que um inventário da saída lista cada OCMD como uma entrada extra sem nome em Inventory.Groups, por isso case grupos por Name em vez de por posição
Como remenda o componente um /OCProperties existente?
O TPdf.SaveAsOptionalContentCreated ou constrói o /OCProperties do zero ou estende o que já está no catálogo, e estende acrescentando, nunca reconstruindo. Um documento sem camadas recebe um dicionário novo com /OCGs e uma configuração predefinida /D transportando /ON, /OFF e /Order, mais /Locked e /RBGroups quando algum grupo os pedir, como disposto na ISO 32000-1 §8.11.4.3
Para um documento que já tem camadas, cada matriz de destino é remendada inserindo as novas referências mesmo antes do parêntete reto de fecho. Isso parece minucioso, e está lá por uma razão. Uma matriz /Order pode conter matrizes aninhadas e rótulos de texto que moldam a árvore de camadas que um visualizador mostra, e uma abordagem anterior que achatava e reconstruía a matriz perdia esses rótulos. A inserção no fim mantém a árvore existente intacta e simplesmente lista os novos grupos depois dela. Matrizes que vivem nos seus próprios objetos indiretos são resolvidas e remendadas no lugar
Desde a v3.175.0 que a configuração predefinida em si também pode ser indireta. Alguns produtores escrevem /OCProperties << /OCGs [...] /D 12 0 R >>, e o PDFium Component agora segue essa referência e remenda o dicionário de destino em vez de falhar com um erro de entrada malformada. Dicionários /Resources de página diretos e indiretos, incluindo um subdicionário /XObject indireto, são tratados da mesma forma
Provar que as novas camadas realmente alternam
Uma camada que existe no /OCProperties mas não veda nada é fácil de produzir e difícil de notar. Durante o desenvolvimento a primeira build do escritor de formulários embrulhava um objeto de fluxo que já transportava o seu próprio cabeçalho N 0 obj e trailer endobj dentro de um segundo cabeçalho. O PDFium não conseguia analisar o objeto duplicado, e em vez de reportar um erro saltava silenciosamente o operador Do. O ficheiro abria, o painel de camadas parecia certo, e o desenho vedado nunca aparecia. O que o expôs foi uma sonda que alternava a camada e comparava píxeis renderizados: nenhuma diferença, portanto algo estava errado
É por isso que o método verifica antes de entregar. A saída é reaberta através de uma carga estrita que confere a tabela de referências cruzadas, e cada grupo pedido é lido de volta através do InspectOptionalContent para confirmar o seu nome, o seu estado inicial e o seu registo de bloqueio. Só então a saída é copiada para o seu fluxo, e o overload de ficheiro grava um ficheiro temporário e substitui o destino atomicamente. Pode correr a mesma verificação de alternância você mesmo reabrindo a saída, encontrando o grupo pelo nome e gravando uma cópia com o estado oposto
function SaveImperialPreset(const SourceName, TargetName: string): Boolean;
var
Pdf: TPdf;
Inventory: TPdfOptionalContentInventory;
Changes: TPdfOptionalContentStateChanges;
Options: TPdfOptionalContentConfigureOptions;
CfgReport: TPdfOptionalContentConfigureReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourceName;
Pdf.Active := True;
if not Pdf.InspectOptionalContent(Inventory) then
raise Exception.Create(Inventory.ErrorMessage);
for I := 0 to High(Inventory.Groups) do
if (Inventory.Groups[I].Name = 'Dimensions (mm)') or
(Inventory.Groups[I].Name = 'Dimensions (in)') then
begin
SetLength(Changes, Length(Changes) + 1);
Changes[High(Changes)].GroupObjectNumber := Inventory.Groups[I].ObjectNumber;
Changes[High(Changes)].Visible := Inventory.Groups[I].Name = 'Dimensions (in)';
end;
Options := TPdfOptionalContentConfigureOptions.Default;
Result := Pdf.SaveAsOptionalContentConfigured(TargetName, Changes,
Options, CfgReport);
finally
Pdf.Free;
end;
end;
Renderize a página um de ambos os ficheiros e as linhas azul e vermelha devem trocar. Se não trocarem, inspecione primeiro o valor EffectiveVisible do grupo no inventário; ele reflete o estado que o PDFium realmente renderizará depois de as matrizes de configuração e as regras de uso serem aplicadas
Limites e armadilhas para planear à volta
O TPdf.SaveAsOptionalContentCreated é um perfil delimitado, e a maioria das surpresas vem de tratá-lo como um editor de camadas geral. As restrições abaixo são impostas de início, pelo que uma chamada violadora falha com Report.ErrorMessage definido e não escreve saída:
- É exigido pelo menos um grupo; grupos, associações e formulários têm limite de 4096 entradas cada, os nomes de grupos têm de ser únicos e de 1 a 255 caracteres, e cada formulário aceita no máximo 16 MiB de bytes de conteúdo
- Documentos encriptados são rejeitados, e um documento assinado precisa de
AllowSignedDocument = True; a saída é uma reescrita completa, pelo que as assinaturas existentes deixarão de validar - Cada entrada
Formstorna-se o seu próprio Form XObject, pelo que pôr a mesma sobreposição em 200 páginas guarda o conteúdo 200 vezes em vez de uma - O novo fluxo
Doé anexado depois do conteúdo existente da página, pelo que corre no estado gráfico que os fluxos anteriores deixarem para trás; páginas bem formadas restauram o seu estado, mas uma página comq/Qdesequilibrados pode deslocar a sua sobreposição - O conteúdo existente da página nunca é movido para uma camada; só o desenho que passar em
Formsé vedado
O ponto da repetição é o que pesar para documentos grandes. Se a sobreposição for pesada e repetir em todas as páginas, a abordagem de objeto partilhado em carimbos de página reutilizáveis construídos sobre Form XObjects mantém uma cópia no ficheiro, ao custo de tratar a vedação de camada você mesmo
FAQ
Pode o PDFium Component pôr conteúdo de página existente numa camada nova?
Não. O TPdf.SaveAsOptionalContentCreated veda só o conteúdo que fornecer em Forms; não embrulha operadores existentes em conteúdo marcado nem os move para um XObject. Para camadar arte existente, regenere-a como conteúdo de formulário autossuficiente e remova o desenho original num passo separado
Por que as minhas anotações vedadas continuam visíveis quando a camada está desligada?
O PDFium não verifica a entrada /OC de dicionários de anotação no seu caminho de renderização, pelo que a vedação ao nível de anotação não tem efeito em visualizadores baseados em PDFium. Essa é a razão pela qual o PDFium Component anexa conteúdo de camada através de Form XObjects, que o PDFium veda
Que números de objeto devo usar ao alternar as novas camadas mais tarde?
O Report.CreatedGroupObjectNumbers guarda os números no ficheiro que acabou de ser escrito. Quando reabrir esse ficheiro e o configurar, tome o ObjectNumber do TPdf.InspectOptionalContent e case pelo Name, que permanece correto mesmo que uma gravação posterior renumere objetos
O PDFium Component viaja com SaveAsOptionalContentCreated, SaveAsOptionalContentConfigured e InspectOptionalContent como um único kit de conteúdo opcional para Delphi e C++Builder, construído sobre o renderizador PDFium. Detalhes e transferências estão na página do produto PDFium Component