Creative Commons License
Excepto donde se indique otra cosa, todo el contenido de este lugar está bajo una licencia de Creative Commons.
Taquiones > sysadmin > Impresora virtual PDF en CUPS

Impresora virtual PDF en CUPS

Introducción

Dada la forma en la que trabaja CUPS añadirle una impresora virtual es bastante factible y más si se trata de una que produzca documentos en formato PDF.

El paquete cups-pdf es el indicado para ello, y ya que su configuración puede ser un poco delicada en el escenario que persigo voy a anotar aquí algunas cosas.

Configuración

Los pasos a seguir son:

  1. Instalar el paquete (lo que ya reinicia el propio servidor CUPS).
  2. Crear una impresora virtual en el servidor:
    1. Asignarle el puerto de impresión (ó backend) cupspdf:/
    2. Seleccionar como modelo de impresora Generic/PostScript Color.
  3. Revisar la configuración en /etc/cups/cups-pdf.conf para adaptarla a nuestro entorno, especialmente las siguientes variables:
    1. Grp: grupo de trabajo al que cambiar nada más comenzar la ejecución.
    2. Spool: ruta de trabajo, que ya está creada por el paquete.
    3. Out: ruta donde situar el documento PDF final.
    4. AnonDirName: ruta donde guardar los PDF sin usuario reconocible ó asignados a nobody.
    5. GhostScript: en Debian Etch el paquete cupsys depende de gs-esp, pero los documentos PDF no se construyen correctamente, en concreto desaparece la capa de texto, por lo que no se puede buscar texto dentro de ellos. Para solucionarlo basta con instalar el paquete gs-gpl, que puede coexistir perfectamente con gs-esp, y cambiar esta variable a /usr/bin/gs-gpl.
    6. GSTmp: este directorio debe existir y tener permisos de escritura al menos para el grupo indicado en Grp.
    7. GSCall: si empleamos papel en tamaño A4 es muy conveniente añadir la opción -sPAPERSIZE=a4 a esta variable, puesto que tanto gs-esp como gs-gpl parecen usar Letter como valor predeterminado. Al menos eso es lo que he podido sacar en claro tras echarle un vistazo al archivo de definiciones gs_statd.ps donde se indica que la familia de tamaños por defecto corresponde a US y no a Europa.

Permisos de acceso

PENDIENTE DE COMPLETAR

Añadidos

Juegos de caracteres

Este aspecto es necesario tenerlo en cuenta únicamente para los nombres de los documentos PDF generados, sobre todo si proceden de entornos Windows, porque al final nos podemos encontrar con un buen número de documentos untitled ó con frases extrañas dada la sustitución masiva de caracteres internacionales.

La variable DecodeHexStrings (0) de la configuración permite que se empleen caracteres UTF-8 en los documentos y se reconozcan como tales.

Si está desactivado (con valor cero ó comentada) una página web con un título como RHD299H LG : LG España podría resultar en un nombre de archivo como:

job_57281-RHD299H_LG___LG_Espa__a.pdf

en lugar de

job_57276-RHD299H_LG___LG_España.pdf

y es sólo un ejemplo fácil, otros títulos quedan irreconocibles.

Envío por correo electrónico

El paquete cups-pdf incluye entre sus ejemplos un programa capaz de realizar esta tarea, muy flexible y bien pensando. Su autor es Nickolay Kondrashov, está escrito en Perl y bajo licencia GPL.

La instalación y puesta en marcha puede realizarse de esta forma:

  1. Descomprimir y copiar el archivo ejecutable cups-pdf-dispatch.gz a un directorio de ejecutables como /usr/local/bin.
  2. Instalar la configuración de cups-pdf-dispatch.conf a /etc/cups y asegurarse de que tiene permisos de lectura para todo el mundo, puesto que el programa es invocado como el usuario final que recibe el documento.
  3. Ajustar la configuración como describo en la siguiente sección.
  4. Incluir este programa en el funcionamiento de cups-pdf, añadiéndolo en la opción PostProcessing de su configuración.

Configuración de cups-pdf-dispatch

El archivo de configuración está escrito también en lenguaje Perl por lo que debe ser sintácticamente correcto.

En una primera aproximación, para ponerlo en marcha rápidamente, debemos retocar las siguientes variables:

  • $CHARSET contiene el juego de caracteres que se emplea para el mensaje de correo.
  • $FROM_MAILADDR es la dirección de remite del mensaje.
  • $MAX_ATTACHMENT_SIZE está establecido en 5 Mb, pero puede ser necesario retocarlo porque si se sobrepasa el mensaje no se envía.
  • $TRY_STRIP_JOB_IDS activa el intento de supresión del prefijo que tanto Samba como cups-pdf realizan.
  • $REMOVE_SENT determina si los archivos se borran tras enviarlos por correo.
  • $GET_USER_MAILADDR_SUB es una función Perl que recibe como parámetro el nombre del usuario y debe retornar la dirección de correo electrónico a la que enviar el mensaje.

La última variable que menciono es la más compleja de todas puesto que se trata de código Perl y son necesarias nociones de programación en este lenguaje.

Mi idea es que los archivos, una vez creados en la carpeta del usuario, sean remitidos por correo electrónico al usuario siempre que éste así lo disponga. Y la manera de hacerlo es crear un archivo en su carpeta llamado mailto conteniendo en una línea su dirección de correo; en caso de que este archivo no exista, ó el contenido esté comentado, no se enviará ningún mensaje. El programa de Nickolay ya ha pensando en esto, y termina sin errores en caso de no tener una dirección de envío.

Las modificaciones al archivo pueden verse aquí:

    1 # cups-pdf-dispatch.conf
    2 # Configuration file for cups-pdf-dispatch.
    3 # This file is interpreted by perl.
    4 
    5 
    6 # $GET_USER_MAILADDR_SUB
    7 # Reference to a function which converts username to e-mail address.
    8 # Arguments: username
    9 # Returns: e-mail address
   10 #
   11 
   12 my $_user_realname = undef;
   13 
   14 use locale;
   15 
   16 $GET_USER_MAILADDR_SUB =
   17 sub {
   18     my $username = shift;
   19     my $filename = "/var/spool/pdf/${username}/mailto";
   20     my $mailto = undef;
   21 
   22     if (-r $filename) {
   23         if (open(MAILTO,"< $filename")) {
   24             while (<MAILTO>) {
   25                 chomp;
   26                 next if /^#/;
   27                 if (not $mailto) {
   28                     $mailto = $_;
   29                     last;
   30                 }
   31             }
   32             close(MAILTO);
   33         }
   34         else {
   35             warn "could not open ${filename}: $!";
   36         }
   37     }
   38     else {
   39         $mailto = sprintf("%s@%s", $username, hostname());
   40     }
   41 
   42     if ($mailto =~ m{^([^<]+)\s<([^>]+)>}) {
   43         $_user_realname = $1;
   44         $mailto = $2;
   45     }
   46 
   47     return $mailto;
   48 };
   49 
   50 # $GET_USER_REALNAME_SUB
   51 # Reference to a function which converts username to user's realname (used
   52 # when constructing To: header).
   53 # Arguments: username
   54 # Returns: user's real name
   55 #
   56 
   57 $GET_USER_REALNAME_SUB =
   58 sub {
   59     my $username = shift;
   60 
   61     if (defined $_user_realname) {
   62         return $_user_realname;
   63     }
   64     else {
   65         # (i.e. user's real name from gecos)
   66         return (split( /,/, (getpwnam($username))[6], 2 ))[0];
   67     }
   68 };
   69 

Para comprobar que lo escrito sea sintácticamente correcto siempre podemos usar el intérprete Perl para ello:

# perl -cw /etc/cups/cups-pdf-dispatch.conf
Name "main::GET_USER_MAILADDR_SUB" used only once: possible typo at cups-pdf-dispatch.conf line 106.    
Name "main::REMOVE_SENT" used only once: possible typo at cups-pdf-dispatch.conf line 83.
...
cups-pdf-dispatch.conf syntax OK
#

los avisos que vemos nos indican, precisamente, que esas variables han tomado un valor, así que a menos que estemos usándolo con el programa no debemos preocuparnos.

Y así, el archivo mailto puede contener algo como lo siguiente:

#
#   Dirección de correo a la 
#   que enviar los documentos PDF
#
Víctor Moral <victor@taquiones.net>

Interioridades

Información que he ido agrupando durante la búsqueda de problemas.

Esquema de funcionamiento

cups-pdf, como cualquier backend de CUPS recibe los siguientes parámetros cuando es invocado

job user title num-copies options [ filename ]

numerados desde el uno al seis, puesto que el primero es siempre el nombre del programa.

Lo siguiente es un detalle de su funcionamiento en el que intento resaltar los puntos clave para depurar errores, puesto que casi siempre son achacables a permisos de acceso.

Los valores por defecto que se incluyen en el paquete Debian los incluyo entre paréntesis.

  1. Prepara el entorno de trabajo:
    1. Cambia el group del proceso indicado por la variable Grp de la configuración (lp).
    2. Intenta identificar el usuario que envía el trabajo en el archivo /etc/passwd.
      1. Si lo consigue y es la primera vez, crea un directorio con su nombre en la localización donde almacenar los trabajos, determinado por la variable Out (/var/spool/cups-pdf/${USER}), y le asigna los permisos a ese mismo usuario. Esta es la principal razón de que sea necesario que funcione como root.
      2. Si no lo consigue emplea el directorio para usuarios anónimos definido en la variable AnonDirName (/var/spool/cups-pdf/ANONYMOUS) y establece como usuario activo al indicado en la variable AnonUser (nobody).
  2. Crea el archivo PDF:
    1. Añade un archivo al directorio de trabajo, definido en la variable Spool (/var/spool/cups-pdf/SPOOL), y le asigna el identificador del usuario propietario del trabajo.
    2. Copia en ese archivo la fuente de entrada, bien leyendo de la entrada estándar, bien leyendo de un archivo recibido como último parámetro (el sexto).
    3. Obtiene y prepara un nombre para el documento final.
    4. Prepara una orden para invocar al programa --gs-- con los parámetros necesarios para crear el documento PDF directamente en la carpeta final del usuario, para lo que se escinde en un proceso hijo en el cual:
      1. Cambia en el entorno la variable TMPDIR con el valor de la configuración de GSTmp (/var/tmp).
      2. Cambia el GID y el UID del proceso a los que correspondan con el usuario propietario del trabajo.
      3. Ejecuta el programa --gs-- con el contenido de la variable GSCall y los parámetros reunidos anteriormente.
      4. Cambia los permisos del archivo PDF empleando el valor de UserUMask (0077) como máscara.
      5. Si la variable PostProcessing contiene algún valor se toma como la ruta de un programa al que llamar en este momento con los siguientes parámetros:
        • Ruta al archivo PDF.
        • Nombre del usuario tal y como lo ha determinado anteriormente.
        • Nombre del usuario tal y como lo ha recibido él como parámetro (el segundo).
  3. El proceso padre espera a que te termine la creación del proceso hijo y:
    1. Borra el archivo de la cola de trabajo (/var/spool/cups-pdf/SPOOL).
    2. Libera memoria

Nombre del documento PDF

El nombre final del documento PDF es muy importante, y cups-pdf tiene varias formas de componerlo.

  • Durante la creación del archivo de trabajo en la cola, el programa efectúa una copia línea a línea del texto y, saltándose los bloques de PostScript encapsulado que pueda contener, intenta localizar una meta variable llamada %%Title y leer su contenido.
  • El tercer parámetro que recibe, si contiene algo, puede ser también el título.

¿ Cómo seleccionar uno ú otro en caso de que existan los dos ? Si la variable TitlePref (0) contiene el valor cero tiene preferencia el título encontrado en el documento, mientras que si el valor es uno la preferencia la tienen los parámetros.

Una vez elegido el texto del título el programa lo completa de esta forma:

  1. Elimina algunos caracteres especiales: salto de línea y retorno de carro.
  2. Si la variable DecodeHexStrings tiene el valor 1 se intenta decodificar los textos hexadecimales para permitir títulos internacionales.
  3. Elimina, si los encuentra, cualquier carácter de apertura y cierre de paréntesis.
  4. También borra cualquier carácter separador de directorios (/).
  5. Quita la extensión del archivo.
  6. Por último limpia otros caracteres especiales, dependiendo del valor de DecodeHexStrings:
    1. Si tiene el valor cero, reemplaza caracteres especiales, no mostrables según el código ASCII con guiones bajos (_).
    2. Si tiene el valor uno, hace lo mismo pero llama a las funciones isalnum() y isascii() en lugar de comparar numéricamente cada carácter.

Al terminar el proceso anterior podemos tener el título limpio de caracteres extraños ó totalmente vacío. En el primer caso se le añade un prefijo al nombre del archivo con el texto job_ y el número de trabajo de impresión (recibido como primer parámetro), sólo si la variable Label tiene el valor uno. En el segundo caso, cuando no hay título como tal, se forma uno con el prefijo antes citado, el número de trabajo y el literal untitled_document.

Referencias y enlaces

Existen multitud de referencias y artículos sobre este tema:

y algunos más técnicos sobre las interioridades de CUPS:

  • Tutorial de Till Kamppeter sobre la comunicación con los servidores de impresión, los clientes y las impresoras; más útil para desarrollo que para administración, pero muy interesante.