Notxor tiene un blog

Defenestrando la vida

Convertir un pdf en imágenes a texto en org-mode

Notxor
2019-07-25

Llega el verano y estoy preparando mi lector de libros electrónicos y mi tablet para la lectura estival. Normalmente leo mucho, pero en verano aún más. Tengo toda una lista de libros para cargar en los dispositivos, la mayoría en formato epub, otros en pdf. Aquí es donde empiezan los problemas con los formatos: pdf es un formato que se inventó para imprimir en papel; si tienes una pantalla del tamaño mínimo de una hoja de papel lo verás bien; si no, leer un libro en pdf se convertirá en un suplicio. De hecho, en la tablet según qué ficheros los puedo leer más o menos decentemente, pero otros no. El contenido del archivo no se ajusta a la pantalla y se hace incómodo leer, pasar página, etc. En el libro electrónico, ese cacharrito con pantalla de «tinta electrónica» del tamaño de un móvil grande o de una tablet pequeña, los pdf se convierten en una tortura.

Se pueden convertir los pdf a texto de forma fácil, si el pdf contiene texto. Que puede parecer una tontería, pero hay muchos pdf que en realidad son una serie de imágenes de páginas escaneadas y cuando los abres para mirar sólo contienen una ristra de imágenes.

Me gustaría que existiera algún modo de hacer que el pdf se convirtiera en epub de forma «automágica», pero no conozco ninguna herramienta que lo haga. Por otro lado, después de pegarme con el asunto un poco, la complejidad del problema es grande, porque envuelve tener una conversión de imagen a texto utilizando OCR y aunque los OCR han avanzado mucho, la salida del texto está plagada de errores: manchas en el escaneo, saltos de línea en medio de palabras, errores de codificación como confundir la «m» con «ni» o la «l» con el «1», o al revés.

La conversión por tanto la debemos hacer por pasos y supervisar los resultados:

  1. Extraer las imágenes.
  2. Convertir en texto
  3. Supervisar y corregir los errores.

Así pues, vamos por partes.

Proceso manual

Vamos a suponer que lo hacemos todo a mano, página a página. Los pasos serían los siguientes.

Extraer las imágenes con poppler-tools

Estas son unas herramientas similares a pdftk, concretamente un fork de xpdf y entre otras cosas nos permitirá extraer las imágenes que componen el pdf utilizando la herramienta pdfimages.

Para extraer páginas con pdfimages la línea de comandos es sencilla de explicar:

pdfimages -png -f 41 -l 50 archivo-origen.pdf nombre-base

Suponiendo que el archivo-origen.pdf es el que deseamos convertir, la opción -png indica el tipo de fichero gráfico que se generará. La opción -f 41 indica que comience a extraer (first) en la página 41 y la opción l 50 indica que deje de hacerlo (last) en la 50. El parámetro nombre-base hará que las páginas convertidas en gráficos tengan los nombres de fichero nombre-base-000.png hasta nombre-base-009.png. Como se puede apreciar, la herramienta comienza la cuenta siempre desde cero, por lo que tenemos que tener cuidado si no queremos sobrescribir los ficheros con otros.

Convertir la imagen en texto con tesseract

En el momento en que hemos extraído una imagen con el texto es el momento de que el OCR haga su magia y lo convierta en algo legible.

tesseract -l lang pagina.png fichero-salida

También es una línea sencilla de entender, está el comando tesseract y las opciones casi se explican solas: la opción -l lang especifica el idioma en el que se encuentra el texto que se debe extraer. La opción pagina.png es la imagen escaneada y fichero-salida es el nombre de fichero que se convertirá en fichero-salida.txt cuando el OCR haya terminado.

Con esos dos comandos, pdfimages y tesseract, consigo el texto plano. Pero la pregunta fundamental es: ¿Página a página?.

Automatizar el proceso

Ir página a página está bien si son ficheros cortos de tres o cuatro páginas. Sin embargo, un libro es harina de otro costal, cientos, o quizá miles, de páginas una a una es la muerte a pellizcos. Así que habrá que hacer un procesamiento por lotes.

Voy a explicar todo el proceso según lo voy haciendo desde cero, comenzando por análisis del problema hasta la codificación de un sencillo módulo .el que nos haga el trabajo más o menos «automágicamente».

Análisis del problema

Un libro tiene una estructura de árbol en su índice (también puede tener forma de árbol muerto en su construcción, pero de momento nos quedaremos con los electrónicos). Es decir, se estructura en partes, en capítulos y secciones, subsecciones, párrafos. Algo que explora muy bien org-mode y que no hace falta que nos detengamos demasiado en ello. En mi caso he decidido que cada capítulo de un libro vaya en un documento org aparte.

Estoy tomando uno de los libros que tengo para convertir y haremos un capítulo para las pruebas:

Captura-pantalla-inicio-conversor.png

En el visor de la derecha, zathura, nos indica que el capítulo que queremos convertir comienza en la página 20 así que podemos darle a nuestra entrada del árbol una propiedad con C-c C-x P y luego teclearemos en la línea desde: 20 y repetir el proceso con todas las propiedades que necesitamos. En un principio, el nombre del fichero original, dónde empezar la conversión, dónde parar y el idioma en el que está el texto. Esto último es importante porque el OCR se adaptará al alfabeto y ortografía del texto. Nos deberá quedar algo así:

Captura-pantalla-fin-conversor.png

Como se puede apreciar el texto para convertir los documentos es bastante sensible:

:PROPERTIES:
:desde:    20
:hasta:    34
:fichero:  ./la-mastro-de-la-ringoj.pdf
:idioma:   epo
:END:

Vamos al código que hará la conversión por lotes y lo analizaré con todo detalle:

;;; procesar-ocr.el

;; Variables globales para el proceso

(defvar fichero ""
  "Variable que contiene el nombre de fichero a convertir.")
(defvar inicio 0
  "Número de página donde comenzar la conversión.")
(defvar fin 0
  "Número de página donde finalizar la conversión.")
(defvar idioma ""
  "Lenguaje de la lista de `tesseract --listlangs`")

; TODO - Habría que comprobar que existen esas propiedades
(defun iniciar ()
  "Iniciar las variables globales para el proceso."
  (setq fichero (org-entry-get (point) "fichero"))
  (setq inicio  (string-to-number (org-entry-get (point) "desde")))
  (setq fin     (string-to-number (org-entry-get (point) "hasta")))
  (setq idioma  (org-entry-get (point) "idioma")))

(defun procesar-pagina (num)
  "Inserta el texto obtenido al procesar una imagen en el /point/."
  (message "Procesando la página %s" num)
  ;; Obtener la imagen con el texto
  (call-process-shell-command (format "pdfimages -png -f %d -l %d %s pagina" num num fichero))
  ;; Obtener el texto de la imagen
  (call-process-shell-command (format "tesseract -l %s pagina-000.png temporal" idioma))
  ;; Meter el texto en el punto
  (insert-file-contents "temporal.txt")
  ;; Borrar los ficheros temporales
  (shell-command "rm pagina-000.png temporal.txt"))

(defun procesar-ocr ()
  "Procesa las páginas especificadas del documento dado en el buffer actual."
  (interactive)
  (iniciar)       ; Establece las variables globales
  (mapc 'procesar-pagina (number-sequence fin inicio -1)))

Iniciar el proceso

La función interactiva, la que se convierte en comando de emacs es la función procesar-ocr. Es muy sencilla, sólo hace tres cosas:

  1. Se declara como interactiva.
  2. Inicia los datos con los valores de las propiedades expresadas en el buffer desde el que se llama.
  3. Genera una lista con los números de página que tiene que procesar.

Este último punto lo explicaré más despacio: esto es Lisp, esconde un bucle. Si vienes de otros lenguajes de programación es posible que viendo procesar-pagina estés esperando algún tipo de bucle. Sin embargo, en este caso lo que hacemos es utilizar mapc para que aplique procesar-pagina a una lista con el número de páginas. Los números se los pasamos en una lista generada por la forma (number-sequence fin inicio -1) que devuelve una lista desde fin a inicio con un paso -1. ¿Por qué lo hago al revés? Pues porque luego, al insertar el texto en el lugar del cursor, emacs irá empujando hacia abajo lo insertado. Pero no adelantemos acontecimientos.

El proceso de la página

La función procesar-pagina sólo necesita el parámetro con el número de página.

He utilizado la forma call-process-shell-command porque la llamada a shell-command que suele ser más habitual se hace de forma asíncrona. Eso hace que, dado que el sistema OCR tarda unos segundos en hacer la conversión y como utilizamos siempre los mismos ficheros intermedios, una llamada los sobrescribiría cuando aún no ha terminado de procesarlos la anterior.

Una vez generado el texto se inserta en la posición del punto (point) con la forma insert-file-contents.

Et voilà

Conseguida la conversión

Captura-pantalla-texto-convertido.png

Ahora toca arreglar todos los errores de conversión, eliminar los espacios en blanco sobrantes, los guiones que marcan la continuidad de las palabras en la línea siguiente, los signos de puntuación mal colocados o equivocados, arreglar los párrafos, introducir los formatos de texto: cursivas, negritas, etc. Un trabajo largo y aburrido, pero más sencillo que ir haciéndolo página a página. Y bastante más corto que pasar todo el texto a mano, por supuesto.

Categoría: emacs

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en esta cuenta de Mastodon, también en esta otra cuenta de Mastodon y en Diaspora con el nick de Notxor.

Si usas habitualmente XMPP (si no, te recomiendo que lo hagas), puedes encontrar también un pequeño grupo en el siguiente enlace: notxor-tiene-un-blog@salas.suchat.org

Disculpen las molestias.