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.

jueves, 1 de octubre de 2009

HAL también escanea (II)

Provistos de las herramientas adecuadas y consultadas someramente sus respectivas páginas de manual, construir el esquema de nuestro guión para escanear cada página y obtener un pdf ajustado a nuestros intereses es trivial [En cursiva se presentan el nombre del fichero que corresponde a la página escaneada; con rojo y verde indicamos, respectivamente, los ficheros de entrada y de salida implicados en cada parte del proceso]:

scanimage > fichero
unpaper fichero fichero.pbm
convert fichero.pbm fichero.pdf

Se observará que en la primera instrucción hemos omitido la extensión del tipo de fichero (pbm) por mera cuestión de conveniencia. Como sabemos que el fichero es un pbm y, puesto que unpaper producirá también un pbm, evitamos algo más prolijo como:

scanimage > fichero-version-inicial.pbm
unpaper fichero-version-inicial.pmb fichero-version-retocada.pbm
...

Este esquema se puede enriquecer con opciones adecuadas para cada una de las instrucciones que lo componen:

scanimage --resolution 600dpi --mode Lineart --progress > fichero
unpaper --size a4 fichero fichero.pbm
convert -compress Group4 fichero.pbm fichero.pdf

Las opciones de scanimage tienen el siguiente significado:

--resolution 600dpi

Define la resolución que tendrá la imagen resultante. En nuestro caso 600 puntos por pulgada, una resolución bastante alta. Resoluciones inferiores como 300dpi suelen ser suficientes y tienen la ventaja de acelerar la duración del proceso físico del escaneo.

--mode Lineart

Determina la sensibilidad del escáner a las diferencias de matiz o color. En este caso interesa considerar sólo las diferencias entre blancos y negros (modo Lineart), la opción común en páginas de libros convencionales.

--progress

Presenta en la consola o terminal una barra visual del progreso del proceso físico del escaneo.


Por su parte, la opción de unpaper es comprensible de suyo:

--size a4

Retoca el fichero dado como primer argumento para que se adapte a las dimensiones de un tamaño a4. unpaper es un programa sofisticado con un buen número de opciones, aquí nos limitamos a un uso muy básico.


Finalmente, en cuanto a convert tenemos la siguiente opción:

-compress Group4

Determina el algoritmo de compresión que se aplicará en la conversión. Con este algoritmo conseguimos, por ejemplo, que el pdf de una página de nuestro libro ronde los 100k sin apenas pérdida de calidad. Dicho sea de paso, Imagemagick, que es la herramienta general a la que pertenece convert, es una utilidad tremendamente especializada y eficaz en el tratamiento de imágenes. [Más información en http://www.imagemagick.org].


Definida la sucesión de instrucciones que constituye el proceso completo de transformación de la página del libro en un fichero pdf, queda construir un guión que pueda aplicarse a cualquier página en general. No es algo distinto de lo que ya hemos hecho en otras ocasiones (ver, en particular, este artículo).

Por tanto, se trata de añadir lo que sea necesario para poder pedirle a HAL algo como esto:

./escanear-a-pdf Dowland_The-Frog-Galliard_1

donde Dowland_The-Forg-Galliard-1 es el nombre del fichero (sin la extensión .pdf) que alojará el pdf de la hoja del libro correspondiente a la primera página de la pieza de Dowland titulada The Frog Galliard, y donde, por descontado, escanear-a-pdf es el nombre de la instrucción o guión que vamos a diseñar.

El artículo citado antes nos da la pista necesaria para que sea cosa de niños la generalización que deseamos. Basta, como se ve, con sustituir la variable escrita anteriormente con el nombre fichero por un parámetro posicional:

scanimage --resolution 600dpi --mode Lineart --progress > "$1"
unpaper --size a4 "$1" "$1".pbm
convert -compress Group4 "$1".pbm "$1".pdf

(Las comillas que rodean al parámetro posicional ---se recordará--- sirven para evitar problemas cuando el nombre de fichero elegido contiene espacios.)

Podemos, ya que estamos, incluir al final una instrucción de limpieza que elimine los ficheros intermedios:

scanimage --resolution 600dpi --mode Lineart --progress > "$1"
unpaper --size a4 "$1" "$1".pbm
convert -compress Group4 "$1".pbm "$1".pdf
rm "$1" "$1".pbm

Con todo ello estamos en condiciones de producir los pdfs deseados de cuantas páginas del libro vayamos a escanear, por ejemplo:

./escanear-a-pdf Dowland_The-Frog-Galliard-1
./escanear-a-pdf Dowland_The-Frog-Galliard-2
./escanear-a-pdf Dowland_The-Rrog-Galliard-3
./escanear-a-pdf Dowland_Fantasía-1
...


Como decíamos, cosa de niños.