Artigo Técnico

PDF/X-1a, X-3 e X-4 para impressão em Delphi com PDFlibPas

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

Essa é a resposta curta. A longa importa porque as gráficas não rejeitam ficheiros por serem feios, rejeitam-nos no preflight, normalmente numa sexta-feira, normalmente por algo que o código gerador podia ter apanhado: um logótipo RGB num trabalho X-1a, uma fonte que foi referenciada em vez de incorporada, uma página sem tamanho de corte. O PDF/X existe para tornar essas falhas estruturais e não visuais, e o papel da biblioteca no trabalho é impedir que as produza desde o início

Que parte PDF/X deve uma aplicação Delphi visar?

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

  • PDF/X-1a:2003 (ISO 15930-4): sintaxe PDF 1.4, apenas cores CMYK, cinzentas e diretas (spot), sem cor baseada em ICC, sem transparência. O ficheiro já está separado nas cores de impressão, por isso a gráfica 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 geridos por cor são permitidos, e o RGB de dispositivo é aceite 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 conteúdo opcional (camadas). Esta é a parte para a qual os RIP modernos são construídos, porque achatar transparência na saída era a origem de boa parte da dor antiga do X-1a

O X-1a ainda está por todo o lado na entrega de publicidade e em workflows de impressão mais antigos que querem um ficheiro que possam expor sem pensar. O X-4 é a melhor escolha de engenharia sempre que a gráfica o aceita: a transparência sobrevive intacta, e as decisões de cor acontecem no RIP com o perfil real de impressão em vez de no seu código. Os números de modo são 1, 2 e 3 respetivamente, e o GetInformation(203) reporta o modo ativo. 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 de output intent diferentes e ficam fora do âmbito aqui

O que o SetPDFXMode escreve no ficheiro

O TPDFlib.SetPDFXMode trata a identificação e o boilerplate que um leitor PDF/X verifica primeiro, por isso o código da aplicação só tem de fornecer o perfil real de impressão 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 através dessa string, por isso não é escrita nenhuma propriedade de conformidade separada
  • Acrescenta um output intent se o documento não tiver nenhum e põe o seu subtipo em /S /GTS_PDFX, o subtipo que os leitores PDF/X procuram (a ISO 32000-1 §14.11.5 define o dicionário de output intent)
  • Põe /Trapped /False no dicionário de informação do documento, porque o escritor nunca aplica 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 duas reparações acontecem na gravação. Cada 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 da página), e as anotações recebem a flag de impressão que a norma espera. A substituição do TrimBox é uma rede de segurança, não uma decisão de layout. Um trabalho com sangria total precisa de um trim box real dentro de um media box maior, e o guia das predefinições de TrimBox, BleedBox e CropBox no PDFlibPas cobre como definir essas caixas e como a herança o pode 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]);

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

    // Embed = 1 incorpora o programa de fonte completo; o 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 de quadricromia, 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á exista, por isso tem de correr depois de o SetPDFXMode ter criado um. A chamada também não consegue conjurar um intent do nada: num documento sem output intent simplesmente devolve 0

Porque é que o output intent predefinido precisa de ser substituído?

O output intent que o SetPDFXMode acrescenta 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. Essa predefinição existe por compatibilidade retroativa e chega para tornar o ficheiro estruturalmente PDF/X, mas o perfil incorporado não descreve imprensa nenhuma. Uma ferramenta de preflight que inspecione o cabeçalho do perfil verá um perfil RGB com etiqueta CMYK, e um impressor que confie no intent para prova recebe disparates

Por isso trate o LoadOutputIntentProfile(FileName, 'DeviceCMYK') como obrigatório para trabalho de impressão a sério e passe o perfil ICC que o seu fornecedor de impressão indicar para o papel e a máquina. O TPDFlib.LoadOutputIntentProfile troca o stream DestOutputProfile e atualiza o /N, e deixa o resto do dicionário do intent como está. Em particular, o identificador da condição de saída e as strings descritivas do intent predefinido continuam a trazer valores de estilo sRGB, por isso se a sua gráfica compara pelo identificador em vez de pelo perfil incorporado, confirme com ela o que verifica antes de enviar. Isso é uma lacuna genuína das predefinições da era 2003, não algo que o seu código tenha feito mal

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

Um documento PDFlibPas novo começa na predefinição do escritor, PDF 1.5, que fica acima da base 1.4 que o PDF/X-1a e o PDF/X-3 exigem. O TPDFlib.SetPDFXMode resolve isso em silêncio no caso comum: se a versão de gravação não tiver sido bloqueada, baixa a versão do documento para a base da parte e continua. A primeira implementação recusava em vez disso, e todo o SetPDFXMode(1) num documento novo devolvia 0, o que é um bom exemplo de uma regra correta no papel e inútil na prática

A recusa continua a existir para o único caso em que faz sentido. Se algo anterior chamou LockSaveVersion com uma versão acima da base, o SetPDFXMode devolve 0 com LastErrorCode 601, porque quem chamou fixou a versão de propósito e a biblioteca não passa por cima disso. Em código de configuração de documento partilhado, onde um helper bloqueia a versão por razões alheias, é exatamente aqui que um trabalho X-1a morre:

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // definido por um helper genérico de "predefinições da casa"
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: 1.7 bloqueado, o PDF/X-1a precisa de 1.4.
    // Salte o bloqueio em trabalhos de impressão. O PDF/X-4 não escapa:
    // a base 1.6 também fica abaixo do bloqueio de 1.7 e falha igual.
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

A mesma barreira explica um limite relacionado. O modo PDF/X é um interruptor de criação de documento: o SetPDFXMode funciona num documento novo e devolve 0 num que foi carregado de um ficheiro, porque a biblioteca não pode responder por conteúdo que não escreveu. Converter um PDF existente em PDF/X é um trabalho diferente que precisa de inspeção e reparação, não de uma flag

Armadilha dois: barreiras que recusam em silêncio

Mal um modo PDF/X fica ativo, o PDFlibPas recusa cada chamada de conteúdo que a parte escolhida proíbe, no momento em que a faz, em vez de deixar o ficheiro falhar no preflight mais tarde. As recusas cobrem preenchimentos e traços 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, conteúdo opcional no X-1a e X-3, ações JavaScript, anexos de ficheiros incorporados, o filtro de imagem LZW, fontes Standard 14 que ficariam sem incorporar e encriptação de chave pública. A encriptação por palavra-passe através do Encrypt não é barrada na criação, mas o validador assinala qualquer ficheiro encriptado, por isso mantenha os trabalhos de impressão sem encriptação

O que as recusas não partilham é um código de erro, e aqui está a armadilha. As barreiras de espaço de cor em SetFillColor, SetLineColor, as chamadas de cor baseada em ICC e o EncryptPublicKey põem o LastErrorCode em 601, o código de conformidade da biblioteca que as barreiras PDF/A também usam. As barreiras de transparência, JavaScript, anexo, LZW e AddStandardFont simplesmente devolvem 0 e deixam o LastErrorCode intacto, o que significa que ele pode ainda guardar um valor de uma chamada anterior. Código que só verifica o LastErrorCode depois de um lote de chamadas de desenho perde metade das recusas, por isso 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

As fontes merecem aviso próprio. O AddStandardFont devolve 0 sob PDF/X porque a biblioteca não traz os contornos das Standard 14 e por isso não os consegue incorporar. O AddTrueTypeFont é diferente: em modo PDF/A promove Embed = 0 a uma incorporação completa por si, mas em modo PDF/X não o faz, por isso uma fonte de sistema referenciada entra no ficheiro e só o validador a apanha. Passe 1 (completa) ou 2 (subconjunto) você mesmo, ou chame SetEmbedAllFonts(1) uma vez na configuração para que um 0 despistado não escape

Como verificar um ficheiro PDF/X antes de o enviar para a gráfica?

Corra o CheckFileCompliance com ComplianceTest = 3 sobre o ficheiro gravado. O TPDFlib.CheckFileCompliance devolve 0 quando não registou achados, e caso contrário um handle de lista de strings cujos itens são linhas prefixadas com PDFX-. A auditoria PDF/X lê o ficheiro pelo mesmo leitor de streaming que o preflight PDF/A e PDF/UA no PDFlibPas, e verifica as regras que um preflight de impressão verifica primeiro:

  • O ficheiro não está encriptado, o XMP traz um GTS_PDFXVersion conhecido, e a versão PDF não excede a base da parte
  • Existe exatamente um output intent, o seu subtipo é GTS_PDFX, e traz um perfil de destino e um identificador de condição de saída
  • O dicionário de informação declara /Trapped
  • Cada página tem um TrimBox dentro do MediaBox, e um BleedBox, quando presente, contém o TrimBox
  • Cada fonte usada numa página tem um programa incorporado, com as fontes Type0 verificadas através da sua fonte descendente
  • Nenhum tipo de anotação proibido (RichMedia, Screen, 3D, Movie, Sound, FileAttachment), nenhuma ação JavaScript de abertura nem JavaScript ao nível do documento, nenhum ficheiro incorporado e nenhum filtro LZWDecode em ponto 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 quanto ao que esse passe significa. O validador do PDFlibPas audita as regras estruturais listadas acima, e são essas que apanham erros do gerador. Não é um preflight com fidelidade de cor: não verifica cobertura total de tinta, resolução de imagem nem se o perfil incorporado corresponde à condição que a sua gráfica roda. Use-o como a barreira no seu pipeline de build ou exportação, e deixe as verificações específicas de impressão para o perfil de preflight da gráfica, que é o que ela vai correr de qualquer forma

FAQ

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

Sim. Em SetPDFXMode(3), o PDFlibPas permite transparência, blend modes e grupos de conteúdo opcional e escreve um ficheiro PDF 1.6. Essas mesmas chamadas devolvem 0 sob PDF/X-1a e PDF/X-3

O PDFlibPas converte um PDF existente em PDF/X?

Não. O SetPDFXMode aplica-se apenas a documentos criados na sessão TPDFlib atual e devolve 0 para um ficheiro carregado. Ainda pode auditar qualquer PDF existente com o CheckFileCompliance e o teste 3

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

As recusas de espaço de cor e de encriptação de chave pública põem o LastErrorCode em 601. Outras barreiras, como transparência, JavaScript e fontes Standard 14, apenas devolvem 0, por isso teste sempre o resultado da função

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