Artículo técnico

Importar EPS en Delphi: gsave, ImageMatrix e imagemask

Un logo EPS colocado en una página PDF puede salir con su contorno desaparecido, una marca de un solo color impresa como su propio negativo, o Æ convertido en á. Cada síntoma señala un punto donde la semántica de gráficos de PostScript y PDF discrepan, así que el importador EPS de PDFlibPas para Delphi interpreta el programa PostScript y escribe cada diferencia como operadores PDF nativos

Interpretar el programa o pasarlo sin tocar

PostScript puede vivir dentro de un PDF de dos maneras, y solo una de ellas muestra algo en pantalla. ISO 32000-1 §8.8.2 define un PostScript XObject, un stream /Subtype /PS cuyos bytes se copian, sin interpretar, al trabajo de impresión solo cuando el dispositivo de salida es una impresora PostScript; en el resto de sitios no tiene efecto, y la visión general §8.8 dice que los PostScript XObjects no deben usarse. PDF/A los excluye sin más. TPDFlib.NewPostScriptXObject y DrawPostScriptXObject escriben esa forma de paso directo, mientras que ImportEPSFromFile y DrawPostScript hacen correr el programa por un intérprete y emiten trazados ordinarios, imágenes inline y texto. Manda las ilustraciones por el intérprete, y reserva un PS XObject para un fragmento destinado al RIP de una sala de impresión, el raster image processor delante de una prensa o impresora de producción. Interpretar EPS es también un trabajo distinto de la importación vectorial EMF, donde la entrada es una lista de registros GDI que se reproducen; un archivo EPS es un programa

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Guardado verbatim como un stream /Subtype /PS y referenciado con Do.
  // Solo se ejecuta en una impresora PostScript; los visores no dibujan nada
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Dónde se separan el modelo de ejecución de PostScript y el de PDF, y qué produce una traducción de operadores línea a línea
Comportamiento de PostScriptComportamiento de PDFResultado de una traducción literal
gsave / grestore guardan y restauran el trazado actual y el punto actualq / Q dejan el trazado en paz, y pintar lo termina (§8.5.1)Un contorno o un segmento que continúa tras grestore desaparece
clip conserva el trazado actual para pintarlo despuésW va seguido de un operador de pintura, normalmente n, que termina el trazadoUn trazo tras el recorte no tiene nada que dibujar
ImageMatrix mapea el espacio de usuario al espacio de imagenCada imagen llena el cuadrado unitario, primera fila arriba (§8.9.4)Las imágenes de abajo a arriba o rotadas aterrizan volteadas o mal colocadas
true imagemask pinta los bits 1Una stencil con el Decode [0 1] por defecto pinta los bits 0 (§8.9.6.2)Un logo de un solo color se imprime como su negativo
Las fuentes Roman Standard-14 usan StandardEncoding por defectoEl nombre StandardEncoding no tiene significado especial (§9.6.6.1)Acentos, comillas y ligaduras cambian letras
Los comentarios %%BoundingBox y %%HiResBoundingBox definen el área del arteLa colocación es la matriz que el escritor emitaEl arte se desplaza o se escala fuera de su caja prevista

¿Dónde fue a parar el contorno tras grestore?

El contorno desapareció porque PDF nunca guardó el trazado. ISO 32000-1 §8.5.1 lo dice en una frase: «En PDF (a diferencia de PostScript), el trazado actual no forma parte del estado gráfico», y después de que un trazado se pinte no hay trazado actual hasta que m o re empiece uno nuevo. Los productores PostScript se apoyan en la regla contraria. La manera habitual de dibujar una forma rellena con contorno es gsave fill grestore stroke: el relleno consume una copia guardada del trazado, grestore devuelve el trazado, y el trazo pinta la misma geometría. Mapea gsave a q, fill a f y grestore a Q, y el trazo llega con un trazado vacío, así que la forma queda rellena y el contorno se fue. Un lineto tras grestore que continúa el trazado restaurado falla igual, como un segmento desaparecido

Línea de tiempo que compara el gsave fill grestore stroke de PostScript con la traducción literal q f Q S en el importador EPS de PDFlibPas para Delphi
el mapeo literal q f Q termina el trazado en el relleno, así que PDFlibPas guarda un trazado PostScript privado y el trazo tras grestore aún encuentra su geometría
uses
  SysUtils, PDFlibrary;

const
  // Triángulo relleno con contorno: el relleno corre dentro de gsave/grestore,
  // y el trazo tras grestore reutiliza el trazado 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 y Top medidos desde la esquina superior izquierda
    // Width y Height de 0 conservan el tamaño del bounding box 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: un triángulo gris claro con un contorno negro de 3 unidades.
    // Una traducción literal q/f/Q/S rellena el triángulo y pierde el contorno
  finally
    PDF.Free;
  end;
end.

PDFlibPas guarda su propio trazado PostScript, aparte de lo que el flujo de contenido PDF contenga en cada momento, y escribe el trazado PDF solo cuando algo pinta o recorta. Cada punto queda fijo bajo la matriz de transformación vigente cuando corrió moveto o lineto, como en PostScript, donde un scale o concat posterior nunca mueve puntos ya presentes en el trazado. currentpoint mapea el punto guardado de vuelta a través de la matriz actual, así que responde en el espacio de usuario presente. Un gsave registra una referencia al final de la cadena de trazados más el punto actual, matriz, fuente y espacio de color, así que los guardados anidados nunca copian trazados enteros

El recorte es donde los dos modelos de trazado se vuelven a separar, y el importador sigue ahí también a PostScript: clip y eoclip emiten W n para el lado PDF pero dejan el trazado lógico en su sitio para el trazo que suele venir después, mientras que rectclip, que empieza un trazado vacío nuevo en PostScript, emite re W n y lo limpia. El almacenamiento de trazados tiene un tope de 100.000 nodos, y las reproducciones consumen del presupuesto de operaciones compartido, así que un archivo hostil no puede hacer bucles de gsave y stroke para inflar la salida

Colocar imágenes: ImageMatrix apunta al revés

Una imagen EPS aterriza volteada cuando su ImageMatrix se trata como colocación, porque describe el mapeo contrario. En PDF, §8.9.4 ata el contorno de la imagen al cuadrado unitario del espacio de usuario con la primera fila de muestras arriba, una matriz implícita de imagen a usuario de [1/w 0 0 -1/h 0 1]. La ImageMatrix de PostScript mapea el espacio de usuario al espacio de imagen. La matriz de colocación PDF es por tanto [w 0 0 -h 0 h] multiplicada por la inversa de ImageMatrix. Para la matriz común de arriba a abajo [w 0 0 -h 0 h] el producto es la identidad. Por eso los convertidores ingenuos parecen correctos en uso casual. Un escaneo de abajo a arriba escrito con [w 0 0 h 0 0] da [1 0 0 -1 0 1], un volteo vertical dentro del cuadrado unitario, y las matrices rotadas o sesgadas no necesitan caso especial. Cuando una fuente de datos finita termina antes de tiempo, PDFlibPas pinta solo las filas completas, conserva la ImageMatrix original y escala por las filas que realmente llegaron, así que una imagen truncada llena la banda superior de su área a la escala correcta; estirar las filas supervivientes sobre la altura completa produciría una imagen distorsionada que parece intacta

Diagrama que deriva la matriz de colocación PDF de la convención ImageMatrix de PostScript para el importador EPS de PDFlibPas en Delphi, con resultados erguidos y volteados y manejo de filas truncadas
la matriz de colocación es el mapeo al cuadrado unitario de PDF por la inversa de la ImageMatrix, así que el arte de arriba a abajo aterriza erguido y solo los escaneos de abajo a arriba se voltean

¿Qué bit pinta un imagemask?

Con true como operando de polaridad, imagemask pinta los bits 1, y en PDF eso es Decode [1 0]. ISO 32000-1 §8.9.6.2 define [0 1] como el default de una máscara de imagen: una muestra 0 marca la página con el color actual y un 1 deja el fondo en paz, y [1 0] lo invierte. El booleano de PostScript dice qué valor de muestra pinta. Léelo como «invierte el default de PDF» y el logo se imprime como un negativo, un bloque sólido con la marca recortada de él. PDFlibPas mapea true a [1 0] y false a [0 1], emite la stencil como imagen inline con /IM true para que pinte en el color que haya fijado el programa, y en la forma de diccionario solo acepta esos dos arrays Decode

Polaridad de imagemask de PostScript mapeada a arrays Decode de PDF en el importador EPS de PDFlibPas para Delphi: true pinta los bits 1, false imprime el negativo
el booleano de PostScript nombra el valor de muestra que pinta, y mapear true a Decode [1 0] evita que un logo de un solo color se imprima como su negativo

Las muestras de doce bits esconden un desajuste menor. PDF no tiene imágenes de 12 bits; §8.9.5.1 permite 1, 2, 4, 8 y, desde PDF 1.5, 16 bits por componente. Desplazar cada muestra cuatro bits a la izquierda la encaja en 16 bits sin perder nada, pero el valor mayor pasa a ser 65520, no 65535, así que con el Decode por defecto cada muestra brillante sale un 0,023 por ciento demasiado oscura. El importador conserva el desplazamiento y estira el extremo superior del Decode por 65535/65520, lo que restaura cada valor normalizado. Una imagen de 12 bits eleva la versión de salida a PDF 1.5 automáticamente; en modo PDF/A o PDF/X, o cuando la versión de salida está bloqueada por debajo de 1.5, el importador se detiene con el error 538 antes de que la página cambie

¿Por qué Æ sale como á?

Una fuente Roman en PostScript usa StandardEncoding salvo que el programa la recodifique, y PDF no puede nombrar esa tabla: ISO 32000-1 §9.6.6.1 no da significado especial al nombre, dejando WinAnsiEncoding, MacRomanEncoding y MacExpertEncoding como las opciones predefinidas. Las tablas del Anexo D.2 muestran qué sale mal cuando los códigos se le entregan a WinAnsi sin cambios: el octal 341 (0xE1) es AE en StandardEncoding y aacute en WinAnsi, así que Æ se vuelve á; 0xAE es la ligadura fi en uno y ® en el otro; 0x27 es quoteright en StandardEncoding y una quotesingle recta en WinAnsi, lo que convierte apóstrofos tipográficos en los de máquina de escribir. Para cada fuente Roman Standard-14 que use un programa, PDFlibPas escribe un recurso de fuente Type 1 privado cuyo diccionario /Encoding lleva /BaseEncoding /WinAnsiEncoding y un array /Differences que nombra los 256 códigos en orden StandardEncoding, así que ningún código depende de cómo rellene su tabla base un visor. Los anchos vienen de la misma codificación, un mapa ToUnicode deja que copiar y buscar devuelvan Æ, y Symbol y ZapfDingbats conservan sus códigos incorporados. La fuente que tu código seleccionó antes de la importación queda intacta, porque recodificar una fuente en caché para acomodar el EPS cambiaría texto ya colocado con ella

Esos recursos nombran las fuentes sin incrustarlas, lo que choca con los perfiles que exigen incrustación. En un documento configurado para PDF/A, PDF/X o PDF/UA, o tras SetEmbedAllFonts(1), el texto EPS falla con el error 538 y la página se queda como estaba. Para salida de archivo el camino más limpio es un EPS exportado con el texto convertido a contornos, que importa como trazados planos en cualquier modo; las fuentes ya dejadas sin incrustar en un archivo existente necesitan incrustar las fuentes que faltan para PDF/A en cambio. El texto EPS mapea a las 14 fuentes estándar por sus nombres exactos; una ilustración que incrusta su propio programa de fuentes importa limpiamente una vez que su texto está en contornos, lo que también evita que una fuente sustituta local mueva cada línea

Bounding boxes, comentarios DSC y archivos EPS binarios

La colocación escala el bounding box del arte al rectángulo que pases, así que la caja tiene que venir del sitio correcto. PDFlibPas lee %%BoundingBox y %%HiResBoundingBox solo de líneas de comentario DSC reales, o sea comentarios de nivel superior que empiezan una línea, y una caja válida de alta resolución gana a la entera. Una búsqueda de subcadenas simple también encuentra esas palabras clave dentro de cadenas literales, cuerpos de procedimientos y datos de imagen binarios. Los archivos EPS llevan las tres cosas. Un valor de cabecera (atend) aplaza los números a un comentario del trailer, leído cuando el escáner llega a él; un archivo sin caja alguna recurre a 612 por 792, y una caja vacía o invertida falla con el error 124. Width y Height escalan la caja por separado: pasa 0 para ambos para conservar el tamaño natural, o pasa ambos en la relación de aspecto de la caja, porque un único valor distinto de cero estira el arte solo a lo largo de un eje

Los datos binarios deciden el punto de entrada. DrawPostScript toma un WideString, fino para PostScript construido en código, pero las muestras de imagen crudas, los datos Flate leídos a través de currentfile y el envoltorio DOS EPS contienen bytes que no sobreviven a un viaje por texto Unicode. ImportEPSFromFile lee el archivo como bytes crudos, reconoce la cabecera de vista previa C5 D0 D3 C6, comprueba que el offset y la longitud del segmento PostScript caen dentro del archivo, y ejecuta solo ese segmento ignorando la vista previa TIFF o WMF. Ambas llamadas aceptan como máximo 4 MiB de origen, exigen Options = 0, devuelven 1 o 0, y añaden o el dibujo entero o nada. El motivo del fallo vive en LastErrorCode, que se restablece al leerse, así que captúralo una vez

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0: tamaño natural desde %%HiResBoundingBox o %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // leer limpia el código, así que léelo una 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.

Cuando el error 538 nombra un operador, reexporta la ilustración desde su editor como EPS plano con el texto convertido a contornos: suele ser más rápido que perseguir al operador, y el arte en contornos también se coloca en un documento PDF/A o PDF/UA construido con la PDFlibPas Delphi PDF Library sin el conflicto de fuentes Standard-14