Artigo Técnico

Saída PDF/X-1a, X-3 e X-4 em Delphi com PDFlibPas

O PDFlibPas produz arquivos de impressão PDF/X a partir do Delphi com uma chamada, o TPDFlib.SetPDFXMode, que seleciona PDF/X-1a:2003, PDF/X-3:2003 ou PDF/X-4. A chamada escreve a identificação XMP pdfxid, adiciona um output intent GTS_PDFX, declara /Trapped /False, fixa a versão do PDF e, a partir daí, recusa chamadas de desenho que a parte escolhida proíbe. O CheckFileCompliance com teste 3 audita o arquivo salvo

Essa é a resposta curta. A longa importa porque gráficas não rejeitam arquivos por serem feios, elas rejeitam no preflight, geralmente numa sexta-feira, geralmente por algo que o código gerador poderia ter pegado: um logo RGB num job X-1a, uma fonte referenciada em vez de embutida, uma página sem tamanho de corte. O PDF/X existe para tornar essas falhas estruturais em vez de visuais, e a parte do trabalho que cabe à biblioteca é impedir que você as produza em primeiro lugar

Qual parte do PDF/X uma aplicação Delphi deve escolher?

Escolha PDF/X-4 a menos que o workflow de destino diga o contrário, e escolha PDF/X-1a quando disser. As três partes diferem em exatamente três eixos, cor, transparência e versão base do PDF, e cada eixo vira um gate dentro do PDFlibPas:

  • PDF/X-1a:2003 (ISO 15930-4): sintaxe PDF 1.4, apenas CMYK, cinza e cores spot, sem cor baseada em ICC, sem transparência. O arquivo já vem separado nas cores de impressão, então a impressora não faz conversão de cor nenhuma
  • PDF/X-3:2003 (ISO 15930-6): ainda PDF 1.4 e ainda sem transparência, mas espaços gerenciados por cor são permitidos, e RGB de dispositivo é aceito quando o próprio output intent descreve uma condição RGB
  • PDF/X-4 (ISO 15930-7): sintaxe PDF 1.6, com transparência viva e optional content (camadas). Essa é a parte para a qual os RIPs modernos são construídos, porque achatar transparência na saída é de onde vinha boa parte da dor antiga do X-1a

O X-1a ainda está por toda parte na entrega de anúncios e em workflows de impressão mais antigos que querem um arquivo que dê para expor sem pensar. O X-4 é a melhor escolha de engenharia sempre que a gráfica aceita: a transparência sobrevive intacta, e as decisões de cor acontecem no RIP com o perfil real da prensa em vez de no seu código. Os números de modo são 1, 2 e 3 respectivamente, e o GetInformation(203) reporta o modo ativo de volta. O PDFlibPas também define números de modo mais altos para a família PDF/X-6 no PDF 2.0, que seguem regras de versão e output intent diferentes e estão fora do escopo aqui

O que o SetPDFXMode escreve no arquivo

O TPDFlib.SetPDFXMode cuida da identificação e do boilerplate que um leitor PDF/X verifica primeiro, de modo que o código da aplicação só precisa fornecer o perfil real da prensa e o conteúdo. Concretamente, uma chamada bem-sucedida faz quatro coisas:

  • Escreve a string de edição no XMP como pdfxid:GTS_PDFXVersion (PDF/X-1a:2003, PDF/X-3:2003 ou PDF/X-4). As edições de 2003 identificam a parte completamente por essa string, então nenhuma propriedade de conformidade separada é escrita
  • Adiciona um output intent se o documento não tiver nenhum e define o seu subtipo como /S /GTS_PDFX, o subtipo que leitores PDF/X procuram (a ISO 32000-1 §14.11.5 define o dicionário de output intent)
  • Define /Trapped /False no dicionário de informações do documento, porque o gerador nunca faz trapping e o PDF/X exige que o estado de trapping seja declarado em vez de ficar desconhecido
  • Fixa a versão base: 1.4 para X-1a e X-3, 1.6 para X-4

Mais dois reparos acontecem no momento de salvar. Toda página sem TrimBox recebe um igual ao seu MediaBox, já que o PDF/X exige um tamanho final declarado em cada página (a ISO 32000-1 §14.11.2 cobre os limites de página), e as anotações recebem a flag de impressão que a norma espera. O fallback de TrimBox é uma rede de segurança, não uma decisão de layout. Um job com sangria total precisa de um trim box real dentro de um media box maior, e o guia de padrões de TrimBox, BleedBox e CropBox no PDFlibPas cobre como definir essas caixas e como a herança pode te surpreender

uses
  PDFlibrary;

procedure BuildX1aAd(const OutFile, PressProfile: string);
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.SetPDFXMode(1) <> 1 then          // 1 = PDF/X-1a:2003
      raise Exception.CreateFmt('PDF/X mode refused (error %d)',
        [Pdf.LastErrorCode]);

    // Substitua o perfil padrão pela condição real da prensa
    if Pdf.LoadOutputIntentProfile(PressProfile, 'DeviceCMYK') <> 1 then
      raise Exception.Create('Could not load the CMYK output profile');

    // Embed = 1 embute o programa de fonte completo; PDF/X não aceita 0
    if Pdf.AddTrueTypeFont('Arial {1252}', 1) = 0 then
      raise Exception.Create('Font could not be loaded for embedding');

    Pdf.SetTextSize(24);
    Pdf.SetFillColorCMYK(0, 1, 1, 0);           // vermelho process, sem RGB
    Pdf.DrawText(72, 720, 'Spring catalog, page 1');

    Pdf.SaveToFile(OutFile);
  finally
    Pdf.Free;
  end;
end;

A ordem nesse esboço é deliberada. O LoadOutputIntentProfile substitui o perfil de um output intent que já existe, então precisa rodar depois que o SetPDFXMode tiver criado um. A chamada também não consegue conjurar um intent do nada: em um documento sem output intent ela simplesmente retorna 0

Por que o output intent padrão precisa ser substituído?

O output intent que o SetPDFXMode adiciona para os modos 1 a 3 declara DeviceCMYK, mas os bytes ICC por trás dele são o perfil sRGB que vem com a biblioteca com a contagem de componentes reescrita para 4. Esse padrão existe por compatibilidade com versões anteriores e basta para tornar o arquivo estruturalmente PDF/X, mas o perfil embutido não descreve prensa nenhuma. Uma ferramenta de preflight que inspeciona o cabeçalho do perfil vai ver um perfil RGB vestindo um rótulo CMYK, e um impressor que confia no intent para prova recebe um absurdo

Então trate LoadOutputIntentProfile(FileName, 'DeviceCMYK') como obrigatório para trabalho de impressão de verdade e passe o perfil ICC que o seu provedor de impressão indicar para o papel e a prensa. O TPDFlib.LoadOutputIntentProfile troca o stream DestOutputProfile e atualiza /N, e deixa o resto do dicionário de intent como está escrito. Em particular, o identificador de condição de saída e as strings descritivas do intent padrão ainda carregam valores no estilo sRGB, então se a sua gráfica casa pelo identificador em vez do perfil embutido, confirme com ela o que ela verifica antes de enviar. Essa é uma lacuna genuína dos padrões da era 2003, não algo que o seu código fez de errado

Armadilha um: a versão que você não escolheu

Um documento novo do PDFlibPas começa no padrão do gerador, PDF 1.5, que fica acima da base 1.4 exigida por PDF/X-1a e PDF/X-3. O TPDFlib.SetPDFXMode resolve isso silenciosamente no caso comum: se a versão de salvamento não foi travada, ele baixa a versão do documento para a base da parte e segue em frente. A primeira implementação recusava, e todo SetPDFXMode(1) em um documento novo retornava 0, um bom exemplo de regra correta no papel e inútil na prática

A recusa ainda existe para o único caso em que ela faz sentido. Se algo antes chamou LockSaveVersion com uma versão acima da base, o SetPDFXMode retorna 0 com LastErrorCode 601, porque quem chamou fixou a versão de propósito e a biblioteca não vai sobrepor isso. Em código de configuração de documento compartilhado, em que um helper trava a versão por motivos alheios, é exatamente aí que um job X-1a morre:

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // definido por um helper genérico de "padrões da casa"
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: 1.7 está travado, PDF/X-1a precisa de 1.4.
    // Pule o travamento para jobs de impressão. PDF/X-4 não escapa aqui:
    // a base 1.6 dele também fica abaixo do travamento 1.7 e falha igual.
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

O mesmo gate explica um limite relacionado. O modo PDF/X é um interruptor de criação de documento: o SetPDFXMode funciona em um documento novo e retorna 0 em um que foi carregado de um arquivo, porque a biblioteca não pode dar garantia sobre conteúdo que não escreveu. Converter um PDF existente para PDF/X é outro trabalho, que exige inspeção e reparo, não uma flag

Armadilha dois: gates que recusam em silêncio

Uma vez que um modo PDF/X está ativo, o PDFlibPas recusa cada chamada de conteúdo que a parte escolhida proíbe, no momento em que você a faz, em vez de deixar o arquivo falhar no preflight depois. As recusas cobrem fills e strokes em RGB de dispositivo (todo o X-1a; X-3 e X-4 a menos que o output intent seja RGB), cores baseadas em ICC no X-1a, transparência e blend modes no X-1a e X-3, optional content no X-1a e X-3, actions JavaScript, anexos de arquivos embutidos, o filtro de imagem LZW, fontes Standard 14 que ficariam sem embutir e criptografia de chave pública. Criptografia por senha via Encrypt não é barrada na criação, mas o validador sinaliza qualquer arquivo criptografado, então mantenha jobs de impressão sem criptografia

O que as recusas não compartilham é um código de erro, e essa é a armadilha. Os gates de espaço de cor em SetFillColor, SetLineColor, as chamadas de cor baseada em ICC e o EncryptPublicKey definem LastErrorCode como 601, o código de conformidade da biblioteca que os gates do PDF/A também usam. Os gates de transparência, JavaScript, anexo, LZW e AddStandardFont simplesmente retornam 0 e deixam o LastErrorCode intocado, o que significa que ele pode ainda guardar um valor de uma chamada anterior. Código que verifica apenas o LastErrorCode depois de um lote de chamadas de desenho vai perder metade das recusas, então verifique cada valor de retorno:

procedure Require(Ok: Integer; const What: string);
begin
  if Ok = 0 then
    raise EPrintConformance.CreateFmt('%s refused under PDF/X', [What]);
end;

Require(Pdf.SetFillColorCMYK(0, 0, 0, 1), 'CMYK fill');
Require(Pdf.SetTransparency(40), 'transparency');   // 0 em X-1a / X-3
Require(Pdf.AddLinkToJavaScript(72, 72, 120, 20,
  'app.alert(1)', 0), 'JavaScript link');           // 0 em todas as partes

Fontes merecem um aviso próprio. O AddStandardFont retorna 0 sob PDF/X porque a biblioteca não vem com os contornos das Standard 14 e portanto não pode embuti-los. O AddTrueTypeFont é diferente: no modo PDF/A ele promove Embed = 0 para uma embutida completa por você, mas no modo PDF/X não, então uma fonte de sistema referenciada entra no arquivo e só o validador pega. Passe 1 (completa) ou 2 (subconjunto) você mesmo, ou chame SetEmbedAllFonts(1) uma vez na configuração para que um 0 distraído não passe batido

Como verificar um arquivo PDF/X antes de ir para a impressora?

Rode CheckFileCompliance com ComplianceTest = 3 no arquivo salvo. O TPDFlib.CheckFileCompliance retorna 0 quando não registrou nenhuma constatação e, caso contrário, um handle de string list cujos itens são linhas prefixadas com PDFX-. A auditoria PDF/X lê o arquivo pelo mesmo leitor de streaming do preflight PDF/A e PDF/UA no PDFlibPas, e verifica as regras que um preflight de impressão verifica primeiro:

  • O arquivo não está criptografado, o XMP carrega um GTS_PDFXVersion conhecido, e a versão do PDF não excede a base da parte
  • Existe exatamente um output intent, o subtipo dele é GTS_PDFX, e ele carrega um perfil de destino e um identificador de condição de saída
  • O dicionário de informações declara /Trapped
  • Toda página tem um TrimBox que fica dentro do MediaBox, e um BleedBox, quando presente, contém o TrimBox
  • Toda fonte usada em uma página tem um programa embutido, com fontes Type0 verificadas através da fonte descendente
  • Nenhum tipo de anotação proibido (RichMedia, Screen, 3D, Movie, Sound, FileAttachment), nenhuma open action JavaScript ou JavaScript em nível de documento, nenhum arquivo embutido e nenhum filtro LZWDecode em lugar algum do grafo de objetos
function PreflightPDFX(Pdf: TPDFlib; const FileName: string;
  Report: TStrings): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 3, 0);  // 3 = PDF/X
  Result := ListId = 0;
  if Result then
    Exit;
  try
    for I := 1 to Pdf.GetStringListCount(ListId) do       // itens são 1-based
      Report.Add(Pdf.GetStringListItem(ListId, I));
  finally
    Pdf.ReleaseStringList(ListId);
  end;
end;

Seja claro sobre o que essa aprovação significa. O validador do PDFlibPas audita as regras estruturais listadas acima, e são elas que pegam erros de gerador. Ele não é um preflight com precisão de cor: não verifica cobertura total de tinta, resolução de imagem nem se o perfil embutido corresponde à condição em que a sua impressora roda. Use-o como gate no seu pipeline de build ou exportação, e deixe as verificações específicas de prensa para o perfil de preflight da gráfica, que é o que eles vão rodar de qualquer jeito

FAQ

O PDFlibPas suporta transparência PDF/X-4?

Sim. No SetPDFXMode(3), o PDFlibPas permite transparência, blend modes e grupos de optional content e escreve um arquivo PDF 1.6. Essas mesmas chamadas retornam 0 sob PDF/X-1a e PDF/X-3

O PDFlibPas converte um PDF existente para PDF/X?

Não. O SetPDFXMode se aplica apenas a documentos criados na sessão atual do TPDFlib e retorna 0 para um arquivo carregado. Você ainda pode auditar qualquer PDF existente com CheckFileCompliance e teste 3

Que código de erro uma chamada PDF/X recusada retorna?

Recusas de espaço de cor e de criptografia de chave pública definem LastErrorCode como 601. Outros gates, como transparência, JavaScript e fontes Standard 14, apenas retornam 0, então sempre teste o resultado da função

Saída PDF/X, o validador PDF/X e o resto do conjunto de ferramentas de impressão e arquivamento fazem parte do PDFlibPas, a biblioteca PDF nativa em Object Pascal da losLab para Delphi. Veja a página do produto PDFlibPas Delphi PDF Library para edições, plataformas e a referência completa da API