Artigo Técnico

Placeholders de Template PDF/VCR-1 em Delphi com PDFlibPas

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

Onde a PDFlibPas coloca o modelo de substituição PDF/VCR-1: a raiz do template pendura diretamente do StructTreeRoot com um attribute owner de GTS_Template que declara GTS_Fields e o GTS_Pages opcional, enquanto cada placeholder aninhado dentro transporta GTS_Replacement, um GTS_Generator de PassThrough, um campo GTS_Data, um GTS_BBox opcional e aponta para a sua página através de /Pg e para o seu conteúdo através de /K
A definição de substituição vive na hierarquia de estrutura lógica em vez de num dicionário de página, o que faz de cada consumidor um percorredor de árvore e transforma o elemento num nó de grafo que um atacante pode moldar

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

Validação em duas passadas do ValidatePDFVCRStructure na PDFlibPas: a passada um conta os filhos diretos do StructTreeRoot com GTS_Template e recusa qualquer coisa que não seja exatamente um, a passada dois desce por todos os ramos de topo para rejeitar uma raiz de template aninhada e qualquer GTS_Replacement fora do ramo do template, limitada a 64 níveis e um milhão de nós
Um validador que para depois de localizar a primeira raiz de template nunca vê uma segunda, pelo que o percurso cobre todos os ramos de topo e regista os números de objeto para que um ciclo seja reportado em vez de seguido

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

Um percurso, dois consumidores na PDFlibPas: o mesmo percurso limitado preenche o TPDFVCRManifestData com Fields, PageSelectionField e um array Placeholders de entradas TPDFVCRPlaceholderInfo, enquanto o CheckCompliancePDFVCR corre a verificação PDF/X completa, GTS_PDFXVersion, GTS_PDFVCRVersion do namespace npes.org e só depois o percurso da estrutura, devolvendo 100 apenas quando o ficheiro qualifica
O manifest é um contrato, pelo que o BuildPDFVCRManifest devolve JSON apenas quando a lista de problemas volta vazia e reporta o erro 608 caso contrário, impedindo um ficheiro não conforme de se fazer passar por um plano de trabalho fiável
// 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