Artículo técnico

Importación de EPS en PDFlibPas: rutas, imágenes y texto

Un logo EPS colocado en una página de 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 lugar donde la semántica de gráficos de PostScript y PDF se separan, así que la importación EPS de PDFlibPas para Delphi interpreta el programa PostScript y escribe cada diferencia como operadores PDF nativos

Interpretar el programa o pasarlo tal cual

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 cualquier otro lugar no tiene efecto, y la descripción general de §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 corren el programa por un intérprete y emiten caminos ordinarios, imágenes inline y texto. Mande el artwork por el intérprete, y reserve un PS XObject para un fragmento dirigido al RIP de una sala de impresión concreta, el raster image processor que va delante de una prensa o de una impresora de producción. Interpretar EPS es también un trabajo distinto de la importación vectorial de EMF, donde la entrada es una lista de records GDI para reproducir; un archivo EPS es un programa

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Se guarda verbatim como un stream /Subtype /PS y se referencia 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 PDF, y qué produce una traducción de operadores línea por línea
Comportamiento de PostScriptComportamiento de PDFResultado de una traducción literal
gsave / grestore guardan y restauran el camino actual y el punto actualq / Q dejan el camino en paz, y pintar lo termina (§8.5.1)Un contorno o un segmento que continúa después de grestore desaparece
clip conserva el camino actual para pintarlo despuésA W lo sigue un operador de pintura, por lo general n, que termina el caminoUn trazo después del clip no tiene nada que dibujar
ImageMatrix mapea el espacio de usuario al espacio de imagenToda imagen llena el cuadrado unitario, primera fila arriba (§8.9.4)Las imágenes bottom-up o rotadas aterrizan volteadas o fuera de sitio
true imagemask pinta los bits 1Un 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 artworkLa colocación es la matriz que el writer emitaEl artwork se corre o se escala fuera de su caja prevista

¿Adónde fue a parar el contorno después de grestore?

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

Línea de tiempo que compara gsave fill grestore stroke de PostScript con la traducción literal q f Q S en la importación EPS de PDFlibPas para Delphi
El mapeo literal q f Q termina el camino en el relleno, así que PDFlibPas conserva un camino PostScript privado y el trazo después de grestore todavía encuentra su geometría
uses
  SysUtils, PDFlibrary;

const
  // Triángulo relleno con contorno: el relleno corre dentro de gsave/grestore,
  // y el trazo después de grestore reutiliza el camino 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 en 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 conserva su propio camino PostScript, aparte de lo que el stream de contenido del PDF tenga en el momento, y escribe el camino PDF solo cuando algo pinta o recorta. Cada punto queda fijado bajo la matriz de transformación vigente cuando corrieron moveto o lineto, como en PostScript, donde un scale o un concat posterior jamás mueve puntos ya puestos en el camino. 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 caminos más el punto actual, la matriz, la fuente y el espacio de color, así que los guardados anidados jamás copian caminos enteros

El recorte es donde los dos modelos de camino se vuelven a separar, y la importación sigue ahí también a PostScript: clip y eoclip emiten W n para el lado PDF pero dejan el camino lógico en su lugar para el trazo que suele venir después, mientras que rectclip, que arranca un camino vacío nuevo en PostScript, emite re W n y lo limpia. El almacenamiento de caminos tiene un tope de 100,000 nodos, y las repeticiones dibujan contra el presupuesto de operaciones compartido, así que un archivo hostil no puede ciclar 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 borde de la imagen al cuadrado unitario del espacio de usuario con la fila de muestras superior arriba, una matriz implícita imagen-a-usuario de [1/w 0 0 -1/h 0 1]. El ImageMatrix de PostScript mapea el espacio de usuario al espacio de imagen. La matriz de colocación de PDF es por lo tanto [w 0 0 -h 0 h] multiplicada por la inversa del ImageMatrix. Para la matriz común de arriba hacia abajo [w 0 0 -h 0 h] el producto es la identidad. Por eso los convertidores ingenuos se ven bien en el uso casual. Un escaneo bottom-up 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 el 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 sobrevivientes sobre la altura completa produciría una imagen distorsionada que se ve intacta

Diagrama que deriva la matriz de colocación de PDF desde la convención ImageMatrix de PostScript para la importación EPS de PDFlibPas en Delphi, con resultados derechos y volteados y el manejo de filas truncadas
La matriz de colocación es el mapeo del cuadrado unitario de PDF por la inversa del ImageMatrix, así que el artwork de arriba hacia abajo aterriza derecho y solo los escaneos bottom-up se voltean

¿Qué bit pinta 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 image mask, o sea que una muestra 0 marca la página con el color actual y una muestra 1 deja el fondo en paz, y [1 0] lo revierte. El booleano de PostScript dice qué valor de muestra pinta. Léalo 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 el stencil como una imagen inline con /IM true para que pinte en el color que el programa haya fijado, y en la forma de diccionario acepta solo esos dos arreglos Decode

Polaridad de imagemask de PostScript mapeada a arreglos Decode de PDF en la importación 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 más pequeño. 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. Correr cada muestra cuatro bits a la izquierda la mete en 16 bits sin perder nada, pero el valor más grande se vuelve 65520, no 65535, así que con el Decode por defecto cada muestra brillante sale unos 0.023 por ciento demasiado oscura. La importación conserva el corrimiento 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, la importación 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 le 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 un quotesingle recto en WinAnsi, lo que convierte apóstrofos tipográficos en 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 arreglo /Differences que nombra los 256 códigos en orden StandardEncoding, así que ningún código depende de cómo llene su tabla base el 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 su código seleccionó antes de la importación queda intacta, porque recodificar una fuente en caché para acomodar al 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 después de SetEmbedAllFonts(1), el texto EPS falla con el error 538 y la página queda como estaba. Para salida de archivo la ruta más limpia es un EPS exportado con el texto convertido a contornos, que se importa como caminos planos en cualquier modo; las fuentes que ya quedaron sin incrustar en un archivo existente necesitan incrustar las fuentes faltantes para PDF/A en cambio. El texto EPS mapea a las 14 fuentes estándar por sus nombres exactos; el artwork que incrusta su propio programa de fuentes se importa limpio una vez que su texto está en contornos, lo que además evita que una fuente sustituta local corra cada línea

Bounding boxes, comentarios DSC y archivos EPS binarios

La colocación escala el bounding box del artwork al rectángulo que usted pasa, así que la caja tiene que venir del lugar 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 de alta resolución válida le gana a la entera. Una búsqueda de substring también encuentra esas keywords dentro de strings literales, cuerpos de procedimientos y datos binarios de imagen. Los archivos EPS cargan las tres cosas. Un valor de header (atend) difiere los números a un comentario del trailer, leído cuando el escáner lo alcanza; un archivo sin ninguna caja recurre a 612 por 792, y una caja vacía o invertida falla con el error 124. Width y Height escalan la caja de manera independiente: pase 0 para ambos para conservar el tamaño natural, o pase ambos en la relación de aspecto de la caja, porque un solo valor distinto de cero estira el artwork por un solo eje

Los datos binarios deciden el punto de entrada. DrawPostScript toma un WideString, fino para PostScript construido en código, pero las muestras crudas de imagen, los datos Flate leídos a través de currentfile y el envoltorio DOS EPS contienen bytes que no sobreviven un viaje por texto Unicode. ImportEPSFromFile lee el archivo como bytes crudos, reconoce el header de vista previa C5 D0 D3 C6, verifica que el offset y la longitud del segmento PostScript caigan dentro del archivo, y ejecuta solo ese segmento mientras ignora la vista previa TIFF o WMF. Ambas llamadas toman a lo sumo 4 MiB de fuente, exigen Options = 0, devuelven 1 o 0, y anexan o el dibujo completo o nada. La razón del fallo vive en LastErrorCode, que se reinicia al leerse, así que captúrelo una sola 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;   // leerlo lo limpia, así que léalo una sola 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 nombre un operador, reexporte el artwork desde su editor como EPS plano con el texto convertido a contornos: eso suele ser más rápido que perseguir al operador, y el artwork 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