Artículo técnico

Convertir un PDF a un único espacio de color en Delphi

Un encabezado relleno con 1 0 0 rg, rojo puro, vuelve de una conversión a grises como 0.299 g. El azul puro aterriza en 0.114 g, lo bastante cerca del negro como para que un logo azul marino y el texto del cuerpo cuesten distinguirse. Esos números no son un accidente de redondeo: son los pesos de luminancia de BT.601, los mismos que aplica un visor cuando muestra una página RGB en un dispositivo de grises, y deciden cuánto contraste sobrevive cuando un folleto a color tiene que volverse uno en grises

Qué calcula RecolorDocument para cada color

PDF Library for Delphi expone la conversión como RecolorDocument(TargetSpace) sobre TPDFlib, con 1 para grises, 2 para RGB y 3 para CMYK. Devuelve la cantidad de cosas que convirtió, y la geometría, las posiciones de texto y la ubicación de imágenes se quedan donde estaban. Solo cambian los valores de color y los nombres de espacio de color alrededor

Todo color pasa por RGB en su camino al destino. El gris es la suma de luminancia 0.299 R + 0.587 G + 0.114 B. CMYK llega a RGB por la regla de device simple de ISO 32000-1 §10.3, donde cada canal es uno menos la suma de su tinta y el negro, clampeado a cero. RGB llega a CMYK metiendo el componente más grande en el negro: K = 1 - max(R, G, B), y las tintas restantes se escalan de lo que queda. Unos resultados que vale la pena conocer antes de mandar cualquier cosa a imprenta:

  • Un gris de 0.4 g convertido a CMYK se vuelve 0 0 0 0.6 k, solo tinta negra, que es lo que una imprenta quiere para texto y filetes
  • RGB 1 0.5 0 (naranja) se vuelve 0 0.5 1 0 k, sin nada de negro
  • Un rich black como 0.6 0.4 0.4 1 k se vuelve 0 g camino a grises, así que las tintas extra simplemente desaparecen
  • El cian puro, 1 0 0 0 k, se convierte en 0.701 g, más claro de lo que la mayoría espera de una tinta saturada

Los operandos convertidos se escriben con a lo sumo tres decimales y siempre con punto como separador decimal, sea cual sea el locale de la máquina que corre el job. ISO 32000-1 §7.3.3 no permite otra cosa en un content stream, y un convertidor que formatee números con el locale del sistema escribirá 0,5 g en una estación de trabajo alemana, que un lector parsea como el número 0 seguido de un operador desconocido

var
  Lib: TPDFlib;
  Converted: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('brochure.pdf', '') <> 1 then
      raise Exception.Create('Could not open brochure.pdf');
    Converted := Lib.RecolorDocument(1);   // 1 = grises, 2 = RGB, 3 = CMYK
    WriteLn(Format('%d colors, images and gradients converted', [Converted]));
    Lib.SaveToFile('brochure-gray.pdf');
  finally
    Lib.Free;
  end;
end;

Una página cuyos colores ya están en el espacio destino conserva sus bytes de contenido originales. RecolorDocument cuenta solo conversiones que de verdad cruzan de un espacio a otro, así que un documento que ya está en grises reporta 0 y nada suyo se reescribe. En un archivo grande eso importa: un diff de entrada y salida muestra exactamente las páginas que cambiaron

¿Por qué RecolorPage deja la foto de la portada a color?

RecolorPage convierte los operadores de color de la página seleccionada y hasta ahí llega. Las imágenes y los gradientes son objetos a nivel de documento, a menudo compartidos por varias páginas, y solo RecolorDocument los convierte. Así que la manera obvia de convertir solo una portada, seleccionar la página 1 y llamar a RecolorPage(3), le da encabezados y filetes en CMYK sobre una fotografía RGB

Esa separación es deliberada. Un image XObject dibujado en la portada puede estar también dibujado en la página 12, y convertirlo por una página lo convierte para ambas. Una llamada con alcance de página que metiera mano silenciosamente en recursos compartidos cambiaría páginas que el llamador nunca seleccionó. Esa es la peor sorpresa de las dos, así que la llamada por página toca solo lo que pertenece a la página

// Operadores de la portada únicamente; imágenes y gradientes no se convierten aquí
Lib.SelectPage(1);
Changed := Lib.RecolorPage(3);
if Changed = 0 then
  WriteLn('Cover already uses CMYK operators (or the target was out of range)');
Lib.SaveToFile('cover-cmyk-operators.pdf');

// Para un documento que tiene que ser un solo espacio de punta a punta, conviértalo entero
Lib.RecolorDocument(3);
Lib.SaveToFile('all-cmyk.pdf');

Mire el valor de retorno en ese snippet. Ambas llamadas devuelven 0 para un destino fuera de 1 a 3, y 0 también es la respuesta para una página que no necesitó cambios, así que un typo en el destino se ve exactamente igual que un documento que ya estaba correcto. Valide usted el argumento antes de la llamada. RecolorDocument restaura la página que estuviera seleccionada antes de correr, lo que significa que puede llamarlo en el medio de un job página por página sin volver a seleccionar después

Un detalle más de alcance. La pasada de operadores lee el content stream propio de cada página, las instrucciones de dibujo listadas en la página misma, y convierte g, rg, k, sus formas de trazo, y sc / scn cuando el espacio actual es un device space. Cuando /DeviceRGB cs precede a un sc convertido, el nombre del espacio también se reescribe al destino. Salte ese renombrado en un convertidor escrito a mano y el lector queda buscando tres componentes RGB donde ahora hay un valor gris

Imágenes y paletas: cuáles cambian, cuáles conservan sus bytes

Las imágenes importan tanto como los operadores. Un folleto convertido a grises con solo sus operadores manejados sigue mostrando fotografías a color en páginas grises, lo que se lee peor que el original o que una conversión completa. RecolorDocument convierte una imagen cuando tiene ocho bits por componente, un device color space, y o bien ninguna compresión o Flate plain sin parámetros de decode

Las imágenes indexadas toman otro camino. Sus samples son índices a una tabla de colores, así que la tabla se convierte y cada sample conserva el índice que ya tenía. Un screenshot o un chart guardado como imagen de paleta de 256 colores se convierte así sin importar cómo estén empacados o comprimidos sus samples, porque los samples jamás se decodifican. La base de la paleta tiene que ser un device space; una paleta sobre un perfil ICC conserva su tabla

Diagrama que muestra qué partes de un PDF reescribe RecolorDocument en Delphi y qué partes, como imágenes JPEG y soft masks, pasan byte a byte idénticas
Una pasada de RecolorDocument reescribe operadores, imágenes elegibles, paletas indexadas y pares de gradientes mientras los bytes JPEG, las máscaras y los colores atados a perfiles pasan sin tocar

Una imagen JPEG o codificada con fax conserva sus bytes. Cambiarle los colores significaría decodificarla, convertirla y recomprimirla, lo que agrega una segunda generación de artefactos JPEG y cambia el tamaño del archivo de maneras que nadie pidió. Si un job de verdad necesita cada foto en grises, la re-codificación es una decisión aparte con su propia calidad, y preferimos que el llamador la tome explícitamente a que una conversión de color la tome por él

Los soft masks y los stencil masks se quedan como están por una razón distinta. Un soft mask suele ser una imagen DeviceGray, pero sus valores significan cobertura. Convertir uno a RGB de tres componentes hace que un visor deje de tratarlo como transparencia; termina pintado como artwork gris encima del contenido que se supone moldea. RecolorDocument colecta cada objeto referenciado vía /SMask o /Mask antes de convertir cualquier imagen, y una imagen con /ImageMask true no tiene color que convertir para empezar

Los colores fijados vía espacios ICCBased, Separation, Indexed o Pattern a nivel de operador también quedan tal como están escritos. Convertir un spot color o un valor atado a perfil a gris de device sin leer su perfil cambiaría cómo se ve la página, y una pasada cuyo trabajo es conservar la apariencia no puede ponerse a adivinar qué quería decir un perfil

Gradientes: el espacio de color y los stops se mueven juntos

Un shading declara su espacio de color una vez y le entrega cada stop a una función. Renombre /ColorSpace de DeviceRGB a DeviceGray mientras la función sigue devolviendo tres números, y un lector toma el primero de ellos, el canal rojo, como nivel de gris. Un gradiente de azul a blanco renderiza entonces como una banda que arranca en negro. RecolorDocument recorre primero el árbol completo de funciones en modo lectura y no convierte nada salvo que cada nodo suyo sea convertible; solo entonces reescribe los stops y renombra el espacio

Las funciones que reescribe son las exponenciales, cuyos dos colores extremos /C0 y /C1 son arrays corrientes, y las stitching functions construidas a partir de ellas. Un color /Background se convierte con el resto. Un shading definido por una tabla sampleada o un programa PostScript conserva su definición, igual que los tipos de mesh 4 a 7, que guardan los colores dentro de un stream en lugar de una función. Reescribir cualquiera de esos es regeneración, un job distinto de convertir un color

Diagrama de PDF Library for Delphi de la conversión de gradientes emparejando un espacio de color de shading con su función de stops a través de objetos independientes, patterns y recursos de shading de página, mientras los shadings sampleados, PostScript y mesh conservan su propia definición
Convertir un gradiente significa mover juntos su espacio de color y los stops detrás de él dondequiera que viva el shading, y los shades de mesh o de función sampleada conservan su propia definición en cambio

Encontrar los gradientes es una trampa en sí misma. Las herramientas de diseño rara vez le dan a un shading un objeto propio; suele sentarse inline dentro del pattern dictionary que lo pinta, o dentro de los recursos /Shading de una página. Cuando construimos la pasada de gradientes, un escaneo de objetos de nivel superior casi no encontró ninguno. La búsqueda ahora sigue el grafo de objetos, acotada a doce niveles de profundidad y visitando cada número de objeto una vez, así que un recurso compartido por todas las páginas se convierte una vez y un archivo que apunta un objeto a sí mismo no puede ciclar

Dónde encaja el recoloreo antes de una entrega a imprenta

Trate RecolorDocument como normalización. Convierte valores entre device spaces; no aplica un perfil ICC, no honra un rendering intent ni simula una condición de imprenta. Para un job que tiene que coincidir con una prueba de contrato, un flujo guiado por perfiles es la herramienta correcta, y una pasada de recoloreo es a lo sumo el paso que mete todo en un solo espacio antes de que ese flujo corra

Donde se gana la vida es en el caso de producción cotidiano: una entrega que tiene que ser en grises y llega con un logo RGB, un reporte interno que no debe imprimirse a color, un lote que un paso de preflight rechazará hasta que cada archivo use un solo espacio. Cualquier cosa que sale del edificio debería pasar por RecolorDocument, nunca por RecolorPage solo, porque un archivo medio convertido pasa un chequeo visual rápido y luego falla en la imprenta. Corra el veredicto en el mismo job: las notas sobre preflight PDF/A y PDF/UA y sobre el workbench de compliance y firma muestran cómo

// IsValidRecolorTarget es un helper que usted agrega; la librería no tiene tal llamada
function IsValidRecolorTarget(Space: Integer): Boolean;
begin
  Result := (Space >= 1) and (Space <= 3);
end;

procedure NormalizeFile(const Src, Dst: string; Space: Integer);
var
  Lib: TPDFlib;
begin
  if not IsValidRecolorTarget(Space) then
    raise EArgumentException.CreateFmt('Bad recolor target %d', [Space]);
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      raise Exception.CreateFmt('Could not open %s', [Src]);
    if Lib.RecolorDocument(Space) = 0 then
      WriteLn(Src, ': already in the target space, copied unchanged');
    Lib.SaveToFile(Dst);
  finally
    Lib.Free;
  end;
end;

Un chequeo rápido que puede correr sobre la salida: abra el contenido de una página convertida en cualquier inspector de PDF después de una conversión a grises. El contenido propio de la página no debería contener ya ningún operador rg, RG, k o K. El color que sobrevive a la pasada viene de los casos de arriba: un JPEG, un color atado a perfil o spot, un gradiente sampleado o de mesh, o artwork dibujado dentro de un form XObject, cuyas instrucciones viven en un stream propio en vez del contenido de la página

Si usted además construye el artwork vectorial en vez de solo convertirlo, el recorrido de gráficos vectoriales, shaders y gradientes muestra cómo se crean los shadings y patterns en primer lugar, lo que hace las reglas de gradientes de arriba más fáciles de predecir

La pasada de color corre en el propio proceso: PDF Library for Delphi es Pascal nativo para Delphi, C++Builder y Lazarus, así que normalizar un lote de archivos entrantes no necesita un convertidor externo ni herramienta de línea de comando en el servidor. La página del producto PDF Library for Delphi lista el resto de su API de procesamiento de documentos