Artigo Técnico

Importar EPS em PDF exige traduzir gsave e imagemask

Um logotipo EPS colocado numa página de PDF pode sair com o contorno faltando, uma marca de uma cor só impressa como o próprio negativo dela, ou Æ transformado em á. Cada sintoma marca um lugar onde as semânticas gráficas de PostScript e PDF discordam, então o import de EPS do PDFlibPas para Delphi interpreta o programa PostScript e grava cada diferença como operadores PDF nativos

Interpretar o programa ou repassá-lo

O PostScript pode morar dentro de um PDF de dois jeitos, e só um deles mostra algo na tela. A ISO 32000-1 §8.8.2 define um PostScript XObject, um stream /Subtype /PS cujos bytes são copiados, sem interpretação, para o trabalho de impressão só quando o dispositivo de saída é uma impressora PostScript; em todo o resto ele não tem efeito, e a visão geral da §8.8 diz que PostScript XObjects não devem ser usados. O PDF/A os exclui de vez. O TPDFlib.NewPostScriptXObject e o DrawPostScriptXObject gravam essa forma de repasse, enquanto o ImportEPSFromFile e o DrawPostScript rodam o programa por um interpretador e emitem caminhos comuns, imagens inline e texto. Mande a arte pelo interpretador, e reserve um PS XObject para um fragmento destinado ao RIP de uma gráfica, o raster image processor à frente de uma máquina de impressão ou de uma impressora de produção. Interpretar EPS também é um trabalho diferente do import vetorial de EMF, em que a entrada é uma lista de records GDI para reproduzir; um arquivo EPS é um programa

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Armazenado verbatim como um stream /Subtype /PS e referenciado com Do.
  // Executa só numa impressora PostScript; viewers não desenham nada
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Onde o modelo de execução PostScript e o PDF se separam, e o que uma tradução operador a operador produz
Comportamento PostScriptComportamento PDFResultado de uma tradução literal
gsave / grestore salvam e restauram o caminho atual e o ponto atualq / Q deixam o caminho em paz, e pintar o encerra (§8.5.1)Um contorno ou um segmento de continuação após o grestore desaparece
clip mantém o caminho atual para pintura posteriorO W é seguido de um operador de pintura, normalmente o n, que encerra o caminhoUm stroke depois do clip não tem nada para desenhar
O ImageMatrix mapeia o espaço de usuário para o espaço da imagemToda imagem preenche o quadrado unitário, primeira linha no topo (§8.9.4)Imagens bottom-up ou rotacionadas chegam invertidas ou fora de lugar
O true imagemask pinta os bits 1Um stencil com o Decode [0 1] padrão pinta os bits 0 (§8.9.6.2)Um logotipo de uma cor só sai impresso como o negativo dele
Fontes Roman Standard-14 usam StandardEncoding por padrãoO nome StandardEncoding não tem significado especial (§9.6.6.1)Acentos, aspas e ligaduras trocam letras
Os comentários %%BoundingBox e %%HiResBoundingBox definem a área da arteO posicionamento é a matriz que o writer emitirA arte se desloca ou escala para fora da caixa pretendida

Para onde foi o contorno depois do grestore?

O contorno sumiu porque o PDF jamais salvou o caminho. A ISO 32000-1 §8.5.1 resume numa frase: “No PDF (ao contrário do PostScript), o caminho atual não faz parte do estado gráfico”, e depois que um caminho é pintado não há caminho atual até um m ou re começar um novo. Os produtores PostScript contam com a regra oposta. O jeito usual de desenhar uma forma preenchida com um contorno é gsave fill grestore stroke: o fill consome uma cópia salva do caminho, o grestore traz o caminho de volta, e o stroke pinta a mesma geometria. Mapeie gsave para q, fill para f e grestore para Q, e o stroke chega com um caminho vazio, então a forma fica preenchida e o contorno se foi. Um lineto após o grestore que continua o caminho restaurado falha do mesmo jeito, como um segmento faltando

Linha do tempo comparando o gsave fill grestore stroke do PostScript com a tradução literal q f Q S no import de EPS do PDFlibPas para Delphi
o mapa literal q f Q encerra o caminho no fill, então o PDFlibPas mantém um caminho PostScript privado e o stroke depois do grestore ainda encontra a geometria dele
uses
  SysUtils, PDFlibrary;

const
  // Triângulo preenchido com um contorno: o fill roda dentro de gsave/grestore,
  // e o stroke depois do grestore reutiliza o caminho restaurado
  TriangleEPS =
    '%!PS-Adobe-3.0 EPSF-3.0'#10 +
    '%%BoundingBox: 0 0 200 120'#10 +
    'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
    'gsave 0.85 setgray fill grestore'#10 +
    '3 setlinewidth 0 setgray stroke'#10;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);   // Left e Top medidos a partir do canto superior esquerdo
    // Width e Height de 0 mantêm o tamanho da caixa delimitadora de 200 x 120
    if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
      PDF.SaveToFile('triangle.pdf')
    else
      Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
    // Esperado: um triângulo cinza claro com um contorno preto de 3 unidades.
    // Uma tradução literal q/f/Q/S preenche o triângulo e descarta o contorno
  finally
    PDF.Free;
  end;
end.

O PDFlibPas mantém o próprio caminho PostScript dele, à parte do que o content stream do PDF segura no momento, e grava o caminho PDF só quando algo pinta ou recorta. Cada ponto é fixado sob a matriz de transformação vigente quando o moveto ou o lineto rodou, como no PostScript, onde um scale ou concat posterior jamais move pontos já no caminho. O currentpoint mapeia o ponto armazenado de volta através da matriz atual, então ele responde no espaço de usuário presente. Um gsave registra uma referência ao fim da cadeia de caminhos mais o ponto atual, matriz, fonte e espaço de cores, então salvamentos aninhados nunca copiam caminhos inteiros

O recorte é onde os dois modelos de caminho se separam de novo, e o import segue o PostScript aí também: clip e eoclip emitem W n para o lado PDF mas deixam o caminho lógico no lugar para o stroke que costuma vir depois, enquanto o rectclip, que começa um caminho vazio novo no PostScript, emite re W n e o limpa. O armazenamento de caminhos tem teto de 100.000 nós, e as repetições consomem o orçamento de operações compartilhado, então um arquivo hostil não pode fazer loop de gsave e stroke para inflar a saída

Posicionando imagens: o ImageMatrix aponta para o outro lado

Uma imagem EPS chega invertida quando o ImageMatrix dela é tratado como posicionamento, porque ele descreve o mapeamento oposto. No PDF, a §8.9.4 amarra a borda da imagem ao quadrado unitário do espaço de usuário com a primeira linha de amostras no topo, uma matriz implícita de imagem para usuário de [1/w 0 0 -1/h 0 1]. O ImageMatrix do PostScript mapeia o espaço de usuário para dentro do espaço da imagem. A matriz de posicionamento PDF é portanto [w 0 0 -h 0 h] multiplicada pelo inverso do ImageMatrix. Para a matriz top-down comum [w 0 0 -h 0 h] o produto é a identidade. É por isso que conversores ingênuos parecem certos no uso cotidiano. Uma varredura bottom-up escrita com [w 0 0 h 0 0] dá [1 0 0 -1 0 1], uma inversão vertical dentro do quadrado unitário, e matrizes rotacionadas ou enviesadas não precisam de caso especial. Quando uma fonte de dados finita termina cedo, o PDFlibPas pinta só as linhas completas, mantém o ImageMatrix original e escala pelas linhas que de fato chegaram, então uma imagem truncada preenche a faixa de cima da área dela na escala correta; esticar as linhas sobreviventes pela altura inteira produziria uma figura distorcida que parece intacta

Diagrama derivando a matriz de posicionamento PDF da convenção de ImageMatrix do PostScript para o import de EPS do PDFlibPas no Delphi, com resultados em pé e invertidos e tratamento de linhas truncadas
a matriz de posicionamento é o mapeamento do quadrado unitário do PDF vezes o ImageMatrix inverso, então arte top-down chega em pé e só varreduras bottom-up invertem

Qual bit o imagemask pinta?

Com true como operando de polaridade, o imagemask pinta os bits 1, e no PDF isso é Decode [1 0]. A ISO 32000-1 §8.9.6.2 define [0 1] como o default para uma image mask, o que significa que uma amostra 0 marca a página com a cor atual e uma 1 deixa o fundo em paz, e [1 0] inverte isso. O booleano PostScript diz qual valor de amostra pinta. Leia-o como “inverta o default do PDF” e o logotipo sai impresso como um negativo, um bloco sólido com a marca recortada dele. O PDFlibPas mapeia true para [1 0] e false para [0 1], emite o stencil como uma imagem inline com /IM true para que ele pinte na cor que o programa definiu, e na forma de dicionário aceita só esses dois arrays Decode

Polaridade de imagemask do PostScript mapeada para arrays Decode do PDF no import de EPS do PDFlibPas para Delphi: true pinta os bits 1, false imprime o negativo
o booleano PostScript nomeia o valor de amostra que pinta, e mapear true para Decode [1 0] evita que um logotipo de uma cor só saia impresso como o próprio negativo

Amostras de doze bits escondem uma divergência menor. O PDF não tem imagens de 12 bits; a §8.9.5.1 permite 1, 2, 4, 8 e, a partir do PDF 1.5, 16 bits por componente. Deslocar cada amostra quatro bits para a esquerda a encaixa em 16 bits sem perder nada, mas o maior valor vira 65520, não 65535, então com o Decode padrão toda amostra clara sai cerca de 0,023 por cento mais escura. O import mantém o deslocamento e estica o endpoint superior do Decode por 65535/65520, o que restaura todo valor normalizado. Uma imagem de 12 bits eleva a versão de saída para PDF 1.5 automaticamente; em modo PDF/A ou PDF/X, ou quando a versão de saída está travada abaixo de 1.5, o import para com o erro 538 antes de a página mudar

Por que Æ sai como á?

Uma fonte Roman no PostScript usa StandardEncoding a menos que o programa a recodifique, e o PDF não pode nomear essa tabela: a ISO 32000-1 §9.6.6.1 não dá significado especial ao nome, deixando WinAnsiEncoding, MacRomanEncoding e MacExpertEncoding como as escolhas predefinidas. As tabelas do Anexo D.2 mostram o que dá errado quando os códigos são entregues ao WinAnsi sem mudança: o octal 341 (0xE1) é AE na StandardEncoding e aacute na WinAnsi, então Æ vira á; o 0xAE é a ligadura fi num e ® no outro; o 0x27 é quoteright na StandardEncoding e um quotesingle reto na WinAnsi, o que transforma apóstrofos tipográficos em apóstrofos de máquina de escrever. Para cada fonte Roman Standard-14 que um programa usa, o PDFlibPas grava um recurso de fonte Type 1 privado cujo dicionário /Encoding tem /BaseEncoding /WinAnsiEncoding e um array /Differences que nomeia todos os 256 códigos na ordem da StandardEncoding, então nenhum código depende de como um viewer preenche a tabela base dele. As larguras vêm da mesma codificação, um mapa ToUnicode permite que copiar e buscar devolvam Æ, e Symbol e ZapfDingbats mantêm os códigos embutidos delas. A fonte que o seu código selecionou antes do import fica intocada, porque recodificar uma fonte em cache para agradar o EPS mudaria texto já posicionado com ela

Esses recursos nomeiam as fontes sem embuti-las, o que colide com perfis que exigem embedding. Num documento configurado para PDF/A, PDF/X ou PDF/UA, ou após um SetEmbedAllFonts(1), texto de EPS falha com o erro 538 e a página fica como estava. Para saída de arquivamento, o caminho mais limpo é um EPS exportado com o texto convertido em contornos, que importa como caminhos simples em qualquer modo; fontes já deixadas sem embedding num arquivo existente precisam de embutir as fontes ausentes para PDF/A em vez disso. Texto de EPS mapeia nas 14 fontes padrão pelos nomes exatos delas; arte que embute o próprio programa de fonte dela importa limpo uma vez que o texto dela esteja em contornos, o que também impede uma fonte substituta local de deslocar cada linha

Caixas delimitadoras, comentários DSC e arquivos EPS binários

O posicionamento escala a caixa delimitadora da arte para o retângulo que você passa, então a caixa precisa vir do lugar certo. O PDFlibPas lê %%BoundingBox e %%HiResBoundingBox só de linhas de comentário DSC reais, ou seja, comentários de nível superior que começam uma linha, e uma caixa de alta resolução válida vence a de números inteiros. Uma busca de substring simples também encontra essas palavras-chave dentro de strings literais, corpos de procedimento e dados de imagem binários. Arquivos EPS carregam os três. Um valor de header (atend) adia os números para um comentário no trailer, lido quando o scanner o alcança; um arquivo sem caixa nenhuma cai para 612 por 792, e uma caixa vazia ou invertida falha com o erro 124. Width e Height escalam a caixa independentemente: passe 0 para os dois para manter o tamanho natural, ou passe os dois na proporção da caixa, porque um único valor diferente de zero estica a arte ao longo de um eixo só

Dados binários decidem o ponto de entrada. O DrawPostScript recebe um WideString, serve para PostScript construído em código, mas amostras de imagem cruas, dados Flate lidos via currentfile e o wrapper DOS de EPS contêm bytes que não sobrevivem a uma viagem por texto Unicode. O ImportEPSFromFile lê o arquivo como bytes crus, reconhece o header de preview C5 D0 D3 C6, confere que o offset e o comprimento do segmento PostScript caem dentro do arquivo, e executa só esse segmento ignorando o preview TIFF ou WMF. As duas chamadas aceitam no máximo 4 MiB de fonte, exigem Options = 0, devolvem 1 ou 0, e anexam ou o desenho inteiro ou nada. O motivo da falha fica no LastErrorCode, que zera quando lido, então capture-o uma vez só

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: tamanho natural a partir de %%HiResBoundingBox ou %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // ler limpa o código, então leia uma vez
  case Code of
    402:
      Writeln(FileName, ': file cannot be opened');
    PDFLIB_ERROR_POSTSCRIPT_INVALID:      // 124
      Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
    PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED:  // 538
      Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
    PDFLIB_ERROR_POSTSCRIPT_BUDGET:       // 539
      Writeln(FileName, ': source over 4 MiB or a work limit reached');
  else
    Writeln(FileName, ': import failed with code ', Code);
  end;
end;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    if PlaceEPS(PDF, 'logo.eps', 40, 40) then
      PDF.SaveToFile('letterhead.pdf');
  finally
    PDF.Free;
  end;
end.

Quando o erro 538 nomeia um operador, reexporte a arte do editor dela como EPS simples com o texto convertido em contornos: isso costuma ser mais rápido que caçar o operador, e arte em contornos também se posiciona num documento PDF/A ou PDF/UA construído com a PDFlibPas Delphi PDF Library sem o conflito de fontes Standard-14