viernes, 2 de octubre de 2009

Los abundantes rizos de HAL

De las cuatro partes en que se dividía nuestro problema inicial queda la última, la de concatenar los pdfs generados por cada página del libro en un único pdf allí donde la obra escaneada consta de varias páginas. Tampoco esta tarea reviste dificultad especial. La herramienta adecuada a esta fin es, como comentamos entonces, pdftk.

Un rápido vistazo por la página de manual de pdftk nos da la solución. Pongamos, por ejemplo, que queremos obtener un único pdf, con el nombre Dowland_The-Frog-Galliard.pdf a partir de estos tres:

Dowland_The-Frog-Galliard_1.pdf
Dowland_The-Frog-Galliard_2.pdf
Dowland_The-Frog-Galliard_3.pdf

La operación se realiza con la siguiente instrucción, que asume que en nuestro directorio de hojas escaneadas no hay otra cosa que pdfs ---se recordará que el guión escanear-a-pdf, diseñado en el artículo anterior, suprimía debidamente los ficheros del mismo nombre que no fueran pdfs:

pdftk Dowland_The-Frog-Galliard_* cat output Dowland_The-Frog-Galliard.pdf

Con esta expresión pedimos a pdftk que tome como entrada todos los ficheros que empiezan por Dowland_The-Frog-Galliard_ (por tanto, todas las páginas correspondientes a esta obra de Dowland), los concatene (cat) y produzca como resultado (output) el fichero Dowland_The-Frog-Galliard.pdf. (Es importante destacar que pdftk acepta varios pdfs como entrada y que podemos referirnos a un conjunto de ellos mediante el comodín '*'. Este tipo de abreviatura, que hemos visto de pasada en alguna otra ocasión, resulta de enorme utilidad cuando el objetivo es ejecutar operaciones para un conjunto de ficheros cuyos elementos no pueden especificarse completamente a priori. De hecho, la solución que veremos a continuación aprovecha plenamente esta característica de la lengua de HAL.)

El artículo y la conversación podrían terminar aquí, si no fuera porque el lector ya no es un usuario principiante de la consola y porque no debería conformarse sino con una solución general y automática para todos los casos posibles.

Una solución automática no podemos obtenerla para la primera tarea, más allá, de donde lo hemos logrado. Es decir, no es practicable producir un pdf para toda hoja del libro o, lo que es lo mismo, dejar el libro en el escáner y regresar a las tres horas para apagar el aparato con la certeza de que éste habrá digitalizado todas sus páginas. Aquí, naturalmente, hay un límite físico, pues no disponemos del hardware que sea capaz de tal hazaña. Aunque existen robots que escanean libros ---que pasan las hojas por sí solos antes de escanearlas--- su precio está muy por encima de nuestro presupuesto, y plantearse construir uno supera nuestra destreza ingenieril. No queda más remedio que realizar a mano la tarea de selección de la hoja, de su ubicación en el escáner y de su procesamiento mediante nuestro guión escanear-a-pdf. Pero el límite físico desaparece en el mismo momento en que HAL está en posesión de todos los ficheros digitales necesarios. Ahí ya no tenemos disculpa.

Imaginemos, por ejemplo, que tras una sesión de escaneo, hemos obtenido los siguientes ficheros correspondientes a las páginas de dos obras de Dowland:

Dowland_The-Frog-Galliard_1.pdf
Dowland_The-Frog-Galliard_2.pdf
Dowland_The-Frog-Galliard_3.pdf
Dowland_A-Fancy_1.pdf
Dowland_A-Fancy_2.pdf
Dowland_A-Fancy_3.pdf
Dowland_A-Fancy_4.pdf

Lo que, naturalmente, desearíamos es que HAL ejecutase, sin nuestra intervención, estas dos instrucciones:

pdftk Dowland_The-Frog-Galliard_* cat output Dowland_The-Frog-Galliard.pdf
pdftk Dowland_A-Fancy_* cat output Dowland_Fantasia.pdf

La cosa no parece difícil. Tenemos en general un enunciado con esta forma:

pdftk obra_* cat output obra.pdf

donde el texto en cursiva corresponde a una variable ---un pronombre, si se quiere aplicar la metáfora lingüística--- que estará en cada caso por el nombre de la obra correspondiente.

Podemos pergeñar, sin rebanarnos los sesos, un procedimiento para manejar esta clase de situación, lo hemos hecho en otros momentos. Podríamos seguir, por ejemplo, la siguiente estrategia:

  1. Diseñar una instrucción que lea todos los ficheros del directorio que contiene las hojas escaneadas y extraiga el nombre de cada obra distinta.

  2. Crear una variable, digamos, la variable OBRAS, y asignarle como valor los nombres de obras producidos en la instrucción anterior.

  3. Recorrer cada nombre almacenado en la variable OBRAS y ejecutar para cada uno de ellos la instrucción pdftk propuesta antes.


Si aplicamos las herramientas de manipulación de ficheros y flujos de texto conocidas de anteriores conversaciones, podemos llegar a diseñar una instrucción de extracción como la que se indica en el primer paso de nuestro proceso. Podríamos, por ejemplo, construir por medio de tuberías, una instrucción, cada una de cuyas secciones realizase estas operaciones:

  1. Listar todos los ficheros del directorio que contiene las hojas escaneadas en pdf.

  2. Seleccionar de la lista las hojas que constituyen páginas de una misma obra, o sea, aquellas que terminen con un número (el número de página). Nótese que puede haber nombres de fichero sin número al final, que corresponderían a las obras formadas por una sola hoja o página.

  3. Extraer de esos nombres de fichero la parte relativa al nombre de la obra, esto es, el nombre del fichero sin el número de página.

  4. Eliminar las repeticiones en la lista obtenida en los pasos anteriores.


Una instrucción como la siguiente refleja esta serie de manipulaciones:

ls | grep -E '.*_[[:digit:]]+.pdf' | cut -d'_' -f1,2 | sort -u

Va de suyo que la instrucción funciona ---en concreto, el grep y el cut---, porque asumimos un determinado formato para el nombre de nuestros ficheros, a saber:

autor_nombre-de-obra_página.pdf

Gracias a él, el patrón de expresión regular para grep puede contar con que el número de página, de haberlo, está justo antes de la especificación de extensión y después del resto del nombre del fichero. Asimismo cut puede utilizar el carácter '_' como delimitador de campos y devolver los campos primero y segundo del nombre del fichero proporcionado como entrada ---o sea, los que contienen el nombre del autor y de la obra, sin número de página (campo tercero)---, y que identifican exhaustivamente el nombre de la obra del caso.

En general, y cuando se trata de manipular nombres de ficheros, conviene siempre establecer un formato uniforme fácilmente manipulable por las herramientas habituales con las que contamos. Esto nos librará de un montón de quebraderos de cabeza.

Asignar el resultado de esta instrucción a la variable OBRAS es trivial. De hecho, sabemos ya que tanto la instrucción anterior como la asignación se pueden realizar en un único paso, por medio de la sustitución de órdenes:

OBRAS=$(ls | grep -E '.*_[[:digit:]]+.pdf' | cut -d'_' -f1,2 | sort -u)

Lo que resta es recorrer el contenido de la variable OBRAS. Tras la operación anterior dicha variable será una lista de los nombres de obras que constan de varias páginas. Nada mejor, pues, que nuestro conocido for para aplicar sobre cada miembro de esa lista la operación de concatenación de pdfs sugerida al principio:

for obra in $OBRAS
do
pdftk ${obra}_* cat output ${obra}.pdf
done

Es fácil tratar de sacar más partido del for y, por ejemplo, eliminar las páginas independientes, una vez producida la partitura completa, o, incluso, guardar en un fichero de registro referencia de las obras procesadas y del número de páginas de las que consta cada una de ellas. El guión completo, con estos añadidos y comentarios al código, sería el siguiente:

# Lista de las obras que contienen varias páginas. El formato de
# fichero de cada página es autor_titulo-de-la-obra_<n>, donde <n>
# es el número de página
OBRAS=$(ls | grep -E '.*_[[:digit:]].pdf' | cut -d'_' -f1,2 | sort -u)

for obra in $OBRAS
do
# número de páginas de la obra procesada
npags=$(ls ${obra}_* | wc -l)
# concatena las páginas de la obra
pdftk ${obra}_* cat output ${obra}.pdf
# elimina las páginas sueltas
rm ${score}_*
# añade una línea de registro
echo "$obra: $npags páginas" >> pdftk_log
done

Como se ve, tareas cuya automatización podría resultar intratable con herramientas gráficas superpotentes, se resuelven en pocos pasos a través de la humilde consola.

2 comentarios:

  1. Una pequeña corrección: en el primer for, OBRAS es una variable que tiene que ser expandida:

    for obra in $OBRAS

    En el script final está bien.

    Aprovecho este pequeño comentario para darte las gracias por retomar esta serie. Las entradas siguen teniendo una calidad estupenda.

    ResponderEliminar
  2. Corregido lo de $OBRAS. Gracias.

    Gracias también por los ánimos. No dejo el serial, pero más que retomarlo, me temo que, por el momento, me tendré que limitar a añadir entradas según vayan surgiendo, es decir, sin regularidad alguna y con poca estructuración. Mi tiempo libre no da para más.

    Por cierto, aún queda un colofón "cron-ométrico" de esta última conversación. A ver si para después el puente está listo ;-)

    ResponderEliminar