A PDFlibPas implementa o PDF/VCR-1 como um perfil de estrutura marcada: a definição de substituição vive sob StructTreeRoot como um ramo filho direto, onde uma raiz de template transporta /O /GTS_Template para declarar o conjunto de campos e cada placeholder transporta /O /GTS_Replacement para ligar um campo de dados. Nada nisto é um dicionário ao nível da página
Essa frase única é a parte em que a maioria das implementações falha, e falhar custa mais do que uma validação rejeitada. Um trabalho de dados variáveis cujo plano de substituição é lido mal não rebenta. Imprime, em volume, com o campo errado na caixa errada, e a primeira pessoa a notar é o cliente a segurar cinquenta mil mailers
Onde vive realmente a definição de substituição
O PDF/VCR-1 coloca o seu modelo de substituição na hierarquia de estrutura lógica descrita na ISO 32000-1 §14.7, não num dicionário de página privado. A raiz do template é um elemento de estrutura pendurado diretamente no StructTreeRoot, distinguido por um attribute owner de GTS_Template, e declara todo o vocabulário de campos num único sítio através de GTS_Fields mais um GTS_Pages opcional que nomeia o campo que conduz a seleção de páginas
Os placeholders são elementos de estrutura aninhados dentro dessa raiz, com attribute owner GTS_Replacement. Cada um transporta GTS_Generator (que a PDFlibPas exige que seja PassThrough), um valor GTS_Data que nomeia exatamente um campo declarado e um GTS_BBox opcional. A ligação à página também não é uma anotação de página: o elemento aponta para a sua página através de /Pg e para a sequência de conteúdo marcado através de /K. Escolher estrutura em vez de um dicionário de página traz depois uma consequência fácil de afirmar e fácil de subestimar: a estrutura é um grafo geral alcançado a partir do catalog, pelo que qualquer coisa que consuma o perfil tem de ser um percorredor de árvore com todos os perigos que isso implica, em vez de um leitor de dicionários. Se nunca trabalhou com a estrutura antes, a mecânica está explicada em estrutura PDF marcada e acessibilidade
Porque é que verificar só o primeiro ramo de topo deixa passar ficheiros maus?
Porque o StructTreeRoot pode ter muitos filhos diretos, e um validador que para depois de localizar a raiz do template nunca vê o resto. Este é o falhanço específico que a PDFlibPas foi construída para evitar, e produz exatamente o tipo de aprovação falsa que é pior do que nenhuma verificação: um ficheiro que reporta conforme e contém uma segunda raiz de template, ou um placeholder sentado fora do ramo do template onde nenhum consumidor o vai honrar
Por isso o ValidatePDFVCRStructure na PDFlibPas corre duas passadas sobre a mesma árvore. A primeira passada enumera todos os filhos diretos do StructTreeRoot e conta os que trazem GTS_Template; qualquer coisa que não seja exatamente um é recusada antes de um único placeholder ser lido. A segunda passada desce por todos os ramos de topo, não só o do template, e rejeita duas coisas ao mesmo tempo: um elemento aninhado com GTS_Template que não seja a raiz, e qualquer elemento GTS_Replacement encontrado fora do ramo do template
O percurso é limitado a 64 níveis de profundidade e 1.000.000 de nós, e regista o número de objeto e de geração de cada elemento que visita, para que um ciclo ou uma referência repetida seja reportada em vez de seguida. Esses números não são afinação de desempenho. Uma estrutura é um grafo de objetos controlado por um atacante, e um validador que recorre sem teto é um stack overflow com uma extensão de ficheiro
Criar um template, e as restrições que a API se recusa a dobrar
A PDFlibPas expõe a criação através de uma API de bracketing que impõe as regras estruturais na altura da escrita em vez de as descobrir na altura de gravar. SetPDFVCRMode muda o documento para o perfil, o que internamente exige o modo base PDF/X-4 da ISO 15930-7 e liga o flag de mark-info, porque um ficheiro PDF/VCR-1 sem conteúdo marcado não tem nada para um placeholder apontar. A partir daí, BeginPDFVCRTemplate recebe a lista de campos e o campo de seleção de páginas, e é estrito de formas que vale a pena conhecer antes de desenhar o seu schema de dados. Os nomes de campos passam por PDFVCRValidFieldName, que aceita apenas letras ASCII, dígitos, underscore, hífen e ponto final, porque são esses os caracteres que se serializam num nome PDF sem ambiguidade de escaping. PDFVCRNormalizeFieldList mantém a lista sensível a maiúsculas e rejeita duplicados liminarmente, e o campo de seleção de páginas tem de aparecer ele próprio na lista declarada
var
Lib: TPDFlib;
Diag: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
if Lib.SetPDFVCRMode(1) <> 1 then
raise Exception.Create('PDF/VCR mode was refused');
Lib.NewPage;
if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
'AccountNo') <> 1 then
raise Exception.Create('Field list rejected');
// Um placeholder liga exatamente um campo declarado.
Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
Lib.DrawText(72, 694, 'SAMPLE NAME');
Lib.EndPDFVCRPlaceholder;
Lib.EndPDFVCRTemplate;
Diag := Lib.GetPDFVCRDiagnostics;
if Diag <> '' then
begin
Writeln('PDF/VCR blockers: ', Diag);
Exit;
end;
Lib.SaveToFile('campaign-template.pdf');
finally
Lib.Free;
end;
end;
Note o que o bracketing lhe compra. BeginPDFVCRPlaceholder recusa abrir a menos que um template esteja ativo e a stack de tags tenha exatamente um nível de profundidade, que é como a PDFlibPas garante que os placeholders são filhos diretos da raiz do template e nunca se aninham uns dentro dos outros. As coordenadas também são validadas, rejeitando NaN, infinito e retângulos invertidos, para que uma bounding box má falhe na chamada em vez de num relatório de preflight uma semana depois
Porque é que um placeholder precisa do seu próprio caminho de serialização MCID?
Porque o PDF/VCR-1 quer que /K seja um inteiro simples, e não é assim que um escritor geral de PDF marcado serializa a ligação entre um elemento de estrutura e o seu conteúdo. Sob a ISO 32000-1 §14.6, um elemento de estrutura referencia normalmente conteúdo marcado ou através de um inteiro MCID direto, quando o elemento já identifica a sua página através de /Pg, ou através de um dicionário de referência de conteúdo marcado que nomeia a página explicitamente. Uma biblioteca que emite saída marcada para muitos fins tende a padronizar a forma de dicionário porque compõe melhor entre páginas
A PDFlibPas escolheu aqui a opção estreita em vez da geral. SetCurrentStructElemDirectMCIDKid marca o elemento de estrutura atual, e apenas os elementos com essa marca serializam /K como um MCID direto com a página transportada em /Pg; tudo o resto mantém o caminho de dicionário MCR existente intocado. Isto é uma troca deliberada e é do tipo que vale a pena nomear em voz alta: adicionar um perfil não deve mudar os bytes de saída que todos os outros documentos marcados já produzem, pelo que o perfil recebe um caminho próprio em vez de um switch global. Todos os ficheiros marcados existentes mantêm a serialização anterior, e a superfície de regressão fica do tamanho da funcionalidade nova
Um percurso, dois consumidores
A PDFlibPas deriva o manifest de substituição e o veredito de conformidade do mesmo percurso de duas passadas, que é a razão pela qual o manifest pode ser confiado como plano de trabalho. O percurso preenche um record TPDFVCRManifestData enquanto valida: Fields da raiz do template, PageSelectionField quando um é declarado, e um array Placeholders de entradas TPDFVCRPlaceholderInfo que transportam FieldName, PageNumber, MCID, HasBBox e BBox. O ponto de entrada da conformidade é CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), e corre as suas verificações por ordem de dependência em vez de tudo de uma vez: primeiro a verificação PDF/X completa, depois GTS_PDFXVersion contra PDF/X-4, depois GTS_PDFVCRVersion lido do namespace http://www.npes.org/pdfvcr/ns/id/, e só então o percurso da estrutura. Passar Options como 1 faz parar no primeiro problema, que é o que se quer num gate de lote; passar 0 recolhe tudo, que é o que se quer num relatório
// Verificar um template que recebeu em vez de ter produzido
var
Issues: TStringList;
Stream: TFileStream;
I: Integer;
begin
Issues := TStringList.Create;
Stream := TFileStream.Create('supplier-template.pdf',
fmOpenRead or fmShareDenyWrite);
try
if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
for I := 0 to Issues.Count - 1 do
Writeln('PDF/VCR: ', Issues[I]);
finally
Stream.Free;
Issues.Free;
end;
end;
O manifest é um contrato, por isso só existe para ficheiros conformes
BuildPDFVCRManifest devolve JSON apenas quando a lista de problemas volta vazia, e devolve uma string vazia caso contrário. Isso não é código defensivo por si mesmo. Um manifest é consumido por um motor de composição que vai conduzir saída real a partir dele, e devolver uma lista parcial de campos extraída de um ficheiro com duas raízes de template convida o motor a tratar uma estrutura não conforme como um plano de substituição fiável. Através da API de documento, GetPDFVCRManifest(InputFileName, Password) envolve a mesma chamada e define o último código de erro como PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) quando o ficheiro não qualifica, para que uma extração falhada e um template vazio sejam distinguíveis. O JSON reporta fields, pageSelectionField como string ou null, e um array placeholders cujas entradas dão field, page, mcid e ou um bbox de quatro números ou null
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 significa que o ficheiro não é conforme a PDF/VCR-1,
// 401 significa que o ficheiro não pôde ser aberto
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Um detalhe para levar para o seu consumidor: o valor page segue a convenção de páginas baseada em zero da biblioteca, não a numeração baseada em um que uma pessoa vê num visualizador. Trocar estes é um off-by-one silencioso que põe o conteúdo na folha errada, e nenhum validador o vai apanhar porque ambos os números são estruturalmente válidos
Onde estão as fronteiras
O suporte PDF/VCR-1 na PDFlibPas cobre a criação de templates, o gate de gravação, o preflight autónomo e a extração de manifest. Não executa a substituição. Substituir dados nos placeholders e produzir as instâncias personalizadas é o passo de composição, e vive fora da biblioteca de propósito, porque o manifest é precisamente a interface que permite escrever um motor de composição de forma independente. O gate de gravação também recusa em vez de reparar. Ao contrário de perfis onde um flag de anotação errado pode ser normalizado silenciosamente, cada restrição PDF/VCR-1 que PDFVCRReadyForSave impõe é uma onde adivinhar mudaria o que o documento significa: inventar uma raiz de template, ou escolher a que campo um placeholder não ligado pertence, produz um ficheiro que valida e mente. A mesma linha de reparar-versus-recusar é discutida para um perfil diferente em o modo de autor PDF/E-1 e o preflight limitado, e o workflow para correr várias verificações de conformidade sobre um documento está coberto em o workbench de conformidade e assinatura
A criação, o percurso de estrutura limitado, o gate de gravação e o extrator de manifest vêm todos na biblioteca PDF PDFlibPas para Delphi, o que significa que um template pode ser produzido sob o perfil e depois verificado através de um caminho de código separado que nunca viu o estado do escritor, e essa separação é a única razão pela qual uma alegação de conformidade vale alguma coisa