Cuaderno de GNUtas

Normalización Unicode en Emacs / Org Mode / LuaLaTeX

Una buena parte de los ficheros de word originales de donde extraigo el material (vía pandoc) para componer el Diccionario Hispánico de la Tradición Clásica venía con una curiosa anomalía técnica. Resulta que, por alguna extraña razón que se me escapa, muchos caracteres con diacrítico aparecen en la forma combinada (NFD) y no en la esperable forma precompuesta (NFC). Los casos son muchos, variados y aleatorios, de tal forma que a veces aparece el carácter de la «a» con acento (por ejemplo) en una forma y otras veces en la otra (?). Así que corregirlo a mano era un poco latoso. La fuente con que estoy componiendo tampoco tiene ninguna propiedad otf que haga la normalización de oficio, de modo que, ¿cómo normalizar ese desaguisado?

Antes de continuar, no está de más refrescar un poco la memoria acerca de cómo Unicode gestiona los caracteres complejos, donde una letra base debe ir acompañada por uno o más signos diacríticos. El estándar, por supuesto (y sin ahondar demasiado en la cuestión), representa esos grupos indisolubles de signos de una forma canónica. Y cuenta con dos vías para hacerlo. La normal, es decir, la que vemos en documentos electrónicos, navegadores o simplemente cuando escribimos español en nuestro teclado y pulsamos letras como la «a» con acento, es la denominada «Forma Normalizada de Composición» (NFC, en sus siglas en inglés). Aquí las letras con sus diacríticos forman un único carácter «precompuesto». Si en mi teclado, pues, escribo la letra «ñ», lo que estoy añadiendo es el carácter que Unicode llama LATIN SMALL LETTER N WITH TILDE y que recibe el código U+00f1. Naturalmente, los caracteres precompuestos son los deseables, porque nos ofrecen un diseño siempre consistente. Pero Unicode también se asegura una segunda vía «de escape», que puede ser útil en ciertas emergencias de carencias de caracteres necesarios o para algunos análisis internos de los programas. Se trataría de la «Forma Normalizada de Descomposición» (NFD), donde el carácter complejo se forma mediante la adición de la letra base y unos signos combinatorios que en las fuentes son siempre de «espacio cero» (zero width). Es decir, se inserta el signo y éste no añade un espacio, para que quede ubicado con más o menos fortuna sobre o bajo la letra a la que el diacrítico debe afectar1. Entre las dos formas de representar los caracteres complejos Unicode establece una equivalencia canónica. De suerte que el signo precompuesto de la «ñ» que describimos más arriba debe descomponerse, según inapelable mandato de Unicode, en dos caracteres (cuando estamos usando NFD, se entiende): el carácter U+006e (LATIN SMALL LETTER N) y el U+0303 (COMBINING TILDE).

Bien, ya hemos refrescado la memoria, pero estábamos en lo de arreglar el estado catastrófico de nuestros documentos. Podemos echar mano de varios remedios, que paso sin demora a comentar.

El incombustible Python

Normalizar el Unicode en Python es una tarea fácil gracias al módulo unicodedata. Si tenemos la palabra:

cuñado

donde los caracteres son:

c ... u ... n ... ̃  ... a  ... d  ... o

bastaría algo así como:

import unicodedata
a="cuñado"
print(unicodedata.normalize("NFC",a))
cuñado

donde los caracteres serían:

c ... u ... ñ  ... a  ... d  ... o

Con esta premisa, no sería muy traumático crearse algún script. Lo que pasa es que a mí Python me resulta espeso y aburrido. Y me muevo más como pez en el agua dentro de Elisp.

Elisp, Emacs y Org Mode

En Emacs Lisp la normalización es muchísimo más sencilla si cabe, mediante la función ucs-normalize-NFC-string, del paquete ucs-normalize.el2. Con la palabra del ejemplo anterior, podemos evaluar la expresión:

(ucs-normalize-NFC-string "cuñado")
cuñado

(es decir, con la «ñ» otra vez).

Como el texto lo compilo con LuaLATEX desde Org Mode, me bastó con escribir e incluir esta función en el documento Org, que se evalúa durante la exportación:

#+BIND: org-export-filter-final-output-functions (mi-filtro-latex-normaliza-NFC)
#+begin_src emacs-lisp :exports results :results none
  (defun mi-filtro-latex-normaliza-NFC (texto backend info)
    (when (org-export-derived-backend-p backend 'latex)
      (setq texto (ucs-normalize-NFC-string texto))))
#+end_src

Y mano de santo.

El paquete uninormalize

Luego descubrí una tercera solución, dentro de LuaLATEX, que es la del paquete uninormalize. No está en CTAN de momento, pero se puede descargar de su repo de github aquí: https://github.com/michal-h21/uninormalize No lo he probado mucho, pero parece que funciona bien.

Y por último…

Javier Bezos nos comentaba en un pasado mensaje a la lista de correo de ES-TeX que la próxima versión de LuaTEX incorporará el motor de renderizado de fuentes HarfBuzz. Lo podremos disfrutar en la entrega de 2020 de TeXLive, pero para los impacientes ya está disponible en la versión de desarrollo de LuaTEX. Sólo habrá que cargar con fontspec la opción para las fuentes Renderer=Harfbuzz. Y ya no serían necesarias las artimañas anteriores.

Estrambote

Para ver si una palabra lleva caracteres NFC o NFD, por cierto, escribí esta funcioncilla en Elisp, que me hace el apaño y me es muy útil. Descompone los caracteres de la palabra donde está el cursor y los muestra en una ventana temporal, cada uno junto a su código:

(defun analiza-caracteres-palabra ()
  (interactive)
  (if
      (not (current-word t t))
      (error "Ninguna palabra que analizar...")
    (let
        ((palabra (current-word t t)))
      (save-excursion
        (with-temp-buffer
          (insert palabra)
          (goto-char (point-min))
          (while (re-search-forward "\\(.\\)" nil t)
            (replace-match (concat (match-string 1) "\s" "(" (format "#%x" (string-to-char (match-string 1))) ")\s...\s") t nil))
          (setq palabra (buffer-string)))
        (when (get-buffer "*palabra analizada*")
          (kill-buffer "*palabra analizada*"))
        (get-buffer-create "*palabra analizada*")
        (set-buffer "*palabra analizada*")
        (insert palabra)
        (temp-buffer-window-show "*palabra analizada*"
                                 '((display-buffer-below-selected display-buffer-at-bottom)
                                   (inhibit-same-window . t)
                                   (window-height . fit-window-to-buffer)))))))

palabra-analizada.png

Publicado: 29/02/20

Última actualización: 21/01/22


Índice general

Acerca de...

Esta obra está bajo una licencia de Creative Commons Reconocimiento-NoComercial 4.0 Internacional.

Notas al pie de página:

1

Lo normal es que el diacrítico no se ubique del todo bien, ya que las letras tienen un ancho variable. Para ajustarlo mejor las fuentes OpenType pueden aplicar ciertas propiedades de posicionamiento para diacríticos combinados, como mark.

2

Tenemos también su contrapartida inversa ucs-normalize-NFD-string. Y también para una región: ucs-normalize-NFC-region y ucs-normalize-NFD-region

© Juan Manuel Macías
Creado con esmero en
GNU Emacs