A PDFlibPas implementa 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 carrega /O /GTS_Template para declarar o conjunto de campos e cada placeholder carrega /O /GTS_Replacement para vincular um campo de dados. Nada disso é um dicionário de nível de página
Essa frase única é a parte que a maioria das implementações erra, e errar custa mais do que uma validação reprovada. Um job de dados variáveis cujo plano de substituição é mal interpretado não crasha. Ele imprime, em volume, com o campo errado na caixa errada, e a primeira pessoa a notar é o cliente segurando cinquenta mil malotes
Onde a definição de substituição de fato vive
O PDF/VCR-1 coloca seu modelo de substituição na hierarquia de estrutura lógica descrita na ISO 32000-1 §14.7, não em um 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 em um único lugar via GTS_Fields mais um GTS_Pages opcional nomeando o campo que dirige a seleção de página
Placeholders são elementos de estrutura aninhados dentro dessa raiz, com attribute owner GTS_Replacement. Cada um carrega GTS_Generator (que a PDFlibPas exige ser PassThrough), um valor GTS_Data nomeando exatamente um campo declarado e um GTS_BBox opcional. O vínculo com a página também não é uma anotação de página: o elemento aponta para sua página via /Pg e para a sequência de marked content via /K. Escolher estrutura em vez de dicionário de página carrega então uma consequência fácil de enunciar e fácil de subestimar: a estrutura tree é um grafo geral alcançado a partir do catálogo, então qualquer coisa que consuma o perfil precisa ser um tree walker com todos os perigos que isso implica, em vez de um leitor de dicionário. Se você nunca trabalhou com a estrutura tree antes, a mecânica está detalhada em estrutura de PDF marcado e acessibilidade
Por que checar apenas o primeiro ramo de topo deixa arquivos ruins passarem?
Porque StructTreeRoot pode ter muitos filhos diretos, e um validador que para depois de localizar a raiz do template nunca vê o resto deles. Essa é a falha específica que a PDFlibPas foi construída para evitar, e ela produz exatamente o tipo de falso positivo que é pior do que nenhuma checagem: um arquivo que reporta conformidade e contém uma segunda raiz de template, ou um placeholder fora do ramo do template onde nenhum consumidor vai honrá-lo
Então ValidatePDFVCRStructure na PDFlibPas roda duas passadas sobre a mesma tree. A primeira passada enumera todo filho direto de StructTreeRoot e conta os que carregam GTS_Template; qualquer coisa diferente de exatamente um é recusada antes de um único placeholder ser lido. A segunda passada desce em todos os ramos de topo, não apenas o do template, e rejeita duas coisas ao mesmo tempo: um elemento aninhado carregando GTS_Template que não é a raiz, e qualquer elemento GTS_Replacement encontrado fora do ramo do template
A travessia é limitada em 64 níveis de profundidade e 1.000.000 de nós, e registra o object number e o generation number de todo 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 tuning de performance. Uma estrutura tree é um grafo de objetos que um atacante controla, e um validador que recursa sem teto é um stack overflow com extensão de arquivo
Criando um template, e as restrições que a API se recusa a flexibilizar
A PDFlibPas expõe a autoria através de uma API de bracketing que impõe as regras estruturais no momento da escrita em vez de descobri-las no momento do save. SetPDFVCRMode coloca o documento no perfil, o que internamente exige o modo base PDF/X-4 da ISO 15930-7 e liga a flag de mark info, porque um arquivo PDF/VCR-1 sem marked content 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ágina, e é estrito de maneiras que valem conhecer antes de você desenhar seu schema de dados. Nomes de campo passam por PDFVCRValidFieldName, que aceita apenas letras ASCII, dígitos, underscore, hífen e ponto, porque esses são os caracteres que serializam em um nome de PDF sem ambiguidade de escaping. PDFVCRNormalizeFieldList mantém a lista case-sensitive e rejeita duplicatas na hora, e o campo de seleção de página precisa ele mesmo aparecer 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 vincula 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 te compra. BeginPDFVCRPlaceholder se recusa a abrir a menos que um template esteja ativo e a tag stack tenha exatamente um nível de profundidade, que é como a PDFlibPas garante que placeholders são filhos diretos da raiz do template e nunca se aninham uns dentro dos outros. Coordenadas também são validadas, rejeitando NaN, infinito e retângulos invertidos, então um bounding box ruim falha na chamada em vez de em um relatório de preflight uma semana depois
Por que um placeholder precisa do seu próprio caminho de serialização de MCID?
Porque o PDF/VCR-1 quer /K como um inteiro puro, e não é assim que um writer de PDF marcado genérico serializa o vínculo entre um elemento de estrutura e seu conteúdo. Sob a ISO 32000-1 §14.6, um elemento de estrutura normalmente referencia marked content ou através de um inteiro MCID direto, quando o elemento já identifica sua página via /Pg, ou através de um dicionário de referência de marked content que nomeia a página explicitamente. Uma biblioteca que emite saída marcada para muitos fins tende a padronizar na forma de dicionário porque ela compõe melhor entre páginas
A PDFlibPas tomou a opção estreita aqui em vez da geral. SetCurrentStructElemDirectMCIDKid marca o elemento de estrutura atual, e apenas elementos que carregam essa flag serializam /K como um MCID direto com a página carregada em /Pg; todo o resto mantém o caminho de dicionário MCR existente intocado. Isso é uma troca deliberada e é do tipo que vale nomear em voz alta: adicionar um perfil não deveria mudar os bytes de saída que todo outro documento marcado já produz, então o perfil ganha um caminho próprio em vez de um switch global. Todo arquivo marcado existente mantém sua serialização anterior, e a superfície de regressão fica do tamanho da nova feature
Uma travessia, dois consumidores
A PDFlibPas deriva o manifest de substituição e o veredito de conformidade da mesma caminhada de duas passadas, e é por isso que o manifest pode ser confiado como um plano de job. A travessia preenche um record TPDFVCRManifestData enquanto valida: Fields da raiz do template, PageSelectionField quando um é declarado, e um array Placeholders de entradas TPDFVCRPlaceholderInfo carregando FieldName, PageNumber, MCID, HasBBox e BBox. O ponto de entrada de conformidade é CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), e ele roda suas checagens em ordem de dependência em vez de todas de uma vez: a checagem PDF/X completa primeiro, depois GTS_PDFXVersion contra PDF/X-4, depois GTS_PDFVCRVersion lido do namespace http://www.npes.org/pdfvcr/ns/id/, e só então a caminhada de estrutura. Passar Options como 1 faz ele parar no primeiro problema, que é o que você quer em um gate de batch; passar 0 coleta tudo, que é o que você quer em um relatório
// Verifique um template que você recebeu, não um que você produziu
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, então só existe para arquivos conformes
BuildPDFVCRManifest retorna JSON apenas quando a lista de problemas volta vazia, e retorna uma string vazia caso contrário. Isso não é código defensivo por si só. Um manifest é consumido por um engine de composição que vai dirigir saída real a partir dele, e devolver uma lista parcial de campos extraída de um arquivo com duas raízes de template convida o engine a tratar uma estrutura não conforme como um plano de substituição confiável. Pela document API, GetPDFVCRManifest(InputFileName, Password) embrulha a mesma chamada e define o último código de erro como PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) quando o arquivo não se qualifica, então uma extração falha e um template vazio são distinguíveis. O JSON reporta fields, pageSelectionField como string ou null, e um array placeholders cujas entradas dão field, page, mcid e 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 arquivo não está em conformidade com PDF/VCR-1,
// 401 significa que o arquivo não pôde ser aberto
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Um detalhe para levar ao seu consumidor: o valor page segue a convenção de página zero-based da biblioteca inteira, não a numeração one-based que um humano vê em um viewer. Misturar os dois é um off-by-one silencioso que coloca conteúdo na folha errada, e nenhum validador vai pegar isso porque ambos os números são estruturalmente válidos
Onde ficam as fronteiras
O suporte a PDF/VCR-1 na PDFlibPas cobre autoria de template, o save gate, preflight standalone e extração de manifest. Ele 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 que um engine de composição seja escrito independentemente. O save gate também recusa em vez de reparar. Diferente de perfis onde uma flag de anotação errada pode ser normalizada silenciosamente, toda restrição PDF/VCR-1 que PDFVCRReadyForSave impõe é uma em que adivinhar mudaria o que o documento significa: inventar uma raiz de template, ou escolher a qual campo um placeholder não vinculado pertence, produz um arquivo que valida e mente. A mesma linha reparar-versus-recusar é discutida para um perfil diferente em o author mode PDF/E-1 e o preflight limitado, e o workflow para rodar várias checagens de conformidade sobre um documento é coberto em o workbench de conformidade e assinatura
Autoria, a caminhada de estrutura limitada, o save gate e o extrator de manifest vêm todos na biblioteca PDF Delphi PDFlibPas, o que significa que um template pode ser produzido sob o perfil e então verificado por um caminho de código separado que nunca viu o estado do writer, e essa separação é o único motivo de uma alegação de conformidade valer alguma coisa