Misceláneas de Chipaca

in English

U+1F929 GRINNING FACE WITH STAR EYES del tipo de letra Noto Emoji de Google, vía ImageMagick y xbmbraille, como lo renderiza la terminal kitty.

XBM❤braille

No siempre hay una buena razón para jugar con algo, pero a veces algo pide a gritos que te mandes a jugar. En este caso en particular, el bloque de Unicode para braille te da una pantalla de pixeles gorditos que podés prender de a uno, en la terminal. Al mismo tiempo, XBM te da una representación de una imagen monocromática muy sencilla, con lo cual jugar con las dos cosas al mismo tiempo es muy natural.

Braille en Unicode

Hay patrones Braille en Unicode, desde (U+2800 BRAILLE PATTERN BLANK) hasta (U+28FF BRAILLE PATTERN DOTS-12345678). Con excepción de -BLANK, cada caracter lleva el nombre de los puntos que están presentes, y los puntos van numerados del 1 al 8 de la forma obvia:

14
25
36
78

el nombre siempre va ordenado, osea que es DOTS-167 y no DOTS-176 por más que el 7 está en la primer columna.

Estos caracteres piden a gritos ser usados como el punto de partida de un lienzo Un canvas, bah. , o por lo menos de una pantalla en la que controlás los pixeles individuales. Lo único que necesitás es conseguir un mapa de bits monocromático, y traducir los pixeles a los caracteres correctos.

XBM

Probablemente tengas algunos archivos XBM en tu escritorio Linux.
Son archivos de texto.
Son sospechosamente parecidos a código C.
Si estás escribiendo C probablemente los incluyas directamente, empotrándolos como un capo.

: locate .xbm | tail -n1
/usr/share/themes/Syscrash/openbox-3/max_toggled.xbm
: !! | xargs cat
locate .xbm | tail -n1 | xargs cat
#define max_toggled_width 6
#define max_toggled_height 6
static unsigned char max_toggled_bits[] = {
   0x3c, 0x27, 0x25, 0x3d, 0x11, 0x1f };

Hay algunas pequeñas variantes (por ejemplo, algunas versiones no dicen unsigned), y algunos son diseñados para usar como punteros y tienen alguna información adicional, pero la idea es esa. Si alguna vez miraste archivos XPM, estos son aún mejores para meterles mano porque con esos podés ver la imagen directamente (con achicar los ojos un poquito), pero sería un desperdicio para nuestro caso (ya que son paleteados, y de 8 bits por pixel), así que nos quedamos con los XBM.

En esa entrada en Wikipedia dice que

Los datos de la imagen XBM consisten de una línea de valores almacenados en un array estático. Como cada bit representa un pixel (0 para blanco y 1 para negro), cada byte en el array contiene la información para ocho pixeles, con el pixel superior izquierdo del mapa de bits representado por el bit más bajo del primer byte del array. Si el ancho de la imagen no es un múltiplo de 8, los bits adicionales en el último byte de cada fila son ignorados.

Ese “0 para blanco y 1 para negro” es al revés de lo que uno esperaría, pero en la práctica va a depender de si tu terminal está configurada con las letras más claras o más oscuras que el fondo.

Chorros de pixeles

Entonces asumamos que tenemos un chorro de bytes que corresponden a los pixeles en el mapa de bits, horizontalmente. Vamos a necesitar mirar cuatro de estos chorros a la vez, y para cada cuarteto de bytes entrelazados que consumimos deberíamos estar produciendo cuatro caracteres Unicode (suponiendo que el ancho de la imagen es múltiplo de 8). Los primeros dos bits de cada cuarteto de bytes determinan el primer caracter Unicode, los segundos dos bits determinan el segundo caracter, y así. Obviamente vamos a necesitar una tablita, pero si los cuatro bytes entrantes son a, b, c, y d, entonces buscar estos datos en la tabla va a ser algo así como Aprecio mucho poder escribir literales en binario para facilitar seguir lo que está pasando.

table[(a&0b00000011)<<0+(b&0b00000011)<<2+(c&0b00000011)<<4+(d&0b00000011)<<6],
table[(a&0b00001100)>>2+(b&0b00001100)<<0+(c&0b00001100)<<2+(d&0b00001100)<<4],
table[(a&0b00110000)>>4+(b&0b00110000)>>2+(c&0b00110000)<<0+(d&0b00110000)<<2],
table[(a&0b11000000)>>6+(b&0b11000000)>>4+(c&0b11000000)>>2+(d&0b11000000)<<0]

Armar la tabla no tiene mayor complicación si podemos buscar caracteres por nombre. En Python esto es muy sencillo Ver go#32937 acerca de hacerlo en Go. :

from unicodedata import lookup

table = []
for i in range(255):
    # REMEMBER 0 is 'on'
    dots = []
    if (i & 0b00000001) == 0:
        dots.append('1')
    if (i & 0b00000010) == 0:
        dots.append('4')
    if (i & 0b00000100) == 0:
        dots.append('2')
    if (i & 0b00001000) == 0:
        dots.append('5')
    if (i & 0b00010000) == 0:
        dots.append('3')
    if (i & 0b00100000) == 0:
        dots.append('6')
    if (i & 0b01000000) == 0:
        dots.append('7')
    if (i & 0b10000000) == 0:
        dots.append('8')
    dots.sort()
    table.append(lookup("BRAILLE PATTERN DOTS-" + "".join(dots)))
table.append(lookup("BRAILLE PATTERN BLANK"))

podés imprimir esta tabla, alinearla bien y confirmar visualmente que está bien,

'⣿', '⣾', '⣷', '⣶', '⣽', '⣼', '⣵', '⣴', '⣯', '⣮', '⣧', '⣦', '⣭', '⣬', '⣥', '⣤',
'⣻', '⣺', '⣳', '⣲', '⣹', '⣸', '⣱', '⣰', '⣫', '⣪', '⣣', '⣢', '⣩', '⣨', '⣡', '⣠',
'⣟', '⣞', '⣗', '⣖', '⣝', '⣜', '⣕', '⣔', '⣏', '⣎', '⣇', '⣆', '⣍', '⣌', '⣅', '⣄',
'⣛', '⣚', '⣓', '⣒', '⣙', '⣘', '⣑', '⣐', '⣋', '⣊', '⣃', '⣂', '⣉', '⣈', '⣁', '⣀',
'⢿', '⢾', '⢷', '⢶', '⢽', '⢼', '⢵', '⢴', '⢯', '⢮', '⢧', '⢦', '⢭', '⢬', '⢥', '⢤',
'⢻', '⢺', '⢳', '⢲', '⢹', '⢸', '⢱', '⢰', '⢫', '⢪', '⢣', '⢢', '⢩', '⢨', '⢡', '⢠',
'⢟', '⢞', '⢗', '⢖', '⢝', '⢜', '⢕', '⢔', '⢏', '⢎', '⢇', '⢆', '⢍', '⢌', '⢅', '⢄',
'⢛', '⢚', '⢓', '⢒', '⢙', '⢘', '⢑', '⢐', '⢋', '⢊', '⢃', '⢂', '⢉', '⢈', '⢁', '⢀',
'⡿', '⡾', '⡷', '⡶', '⡽', '⡼', '⡵', '⡴', '⡯', '⡮', '⡧', '⡦', '⡭', '⡬', '⡥', '⡤',
'⡻', '⡺', '⡳', '⡲', '⡹', '⡸', '⡱', '⡰', '⡫', '⡪', '⡣', '⡢', '⡩', '⡨', '⡡', '⡠',
'⡟', '⡞', '⡗', '⡖', '⡝', '⡜', '⡕', '⡔', '⡏', '⡎', '⡇', '⡆', '⡍', '⡌', '⡅', '⡄',
'⡛', '⡚', '⡓', '⡒', '⡙', '⡘', '⡑', '⡐', '⡋', '⡊', '⡃', '⡂', '⡉', '⡈', '⡁', '⡀',
'⠿', '⠾', '⠷', '⠶', '⠽', '⠼', '⠵', '⠴', '⠯', '⠮', '⠧', '⠦', '⠭', '⠬', '⠥', '⠤',
'⠻', '⠺', '⠳', '⠲', '⠹', '⠸', '⠱', '⠰', '⠫', '⠪', '⠣', '⠢', '⠩', '⠨', '⠡', '⠠',
'⠟', '⠞', '⠗', '⠖', '⠝', '⠜', '⠕', '⠔', '⠏', '⠎', '⠇', '⠆', '⠍', '⠌', '⠅', '⠄',
'⠛', '⠚', '⠓', '⠒', '⠙', '⠘', '⠑', '⠐', '⠋', '⠊', '⠃', '⠂', '⠉', '⠈', '⠁', '⠀'

por ahora vamos bien.

En Go esto podría ser un []rune; si lo guardamos en una constante de tipo string no lo podemos indexar directamente porque cada runa va a estar guardada como tres bytes—pero podríamos hacer un poco de matemática y ahorrar un poco de espacio. Vamos a tener que pesar estas opciones en algún momento. Por qué no ahora? Si comparamos

const brailidx = "<... un string hecho restándole 0x2800 a cada una de las runas ...>"

func bit2brail(b uint8) rune {
	return 0x2800 + rune(conv_be[b])
}

con

var brailidx = []rune{ /* ... esas runas ... */ }

func bit2brail(b uint8) rune {
	return brailidx[b]
}

vemos que la primera versión es ~40% más rápida:

BenchmarkStringAndMath-12    	4848230	      246.2 ns/op
BenchmarkRuneSlice-12        	3539121	      347.3 ns/op

Mirando la salida en assembler go build -gcflags -S . podés ver que desperdiciamos algunos ciclos comprobando los límites del slice, cosa que podemos ahorrar cambiándolo a un array

var brailidx = [...]rune{ /* ... esas runas ... */ }

y con esto llegamos a que es solamente un ~20% más lento:

BenchmarkStringAndMath-12    	4914315	      239.4 ns/op
BenchmarkRuneSlice-12        	4165580	      289.2 ns/op

Me gusta que en la versión con el string los datos son inmutables, pero no me gusta que es bastante más difícil de leer. Podremos tener las dos cosas?

const brailidx = `
⣿ ⣾ ⣷ ⣶ ⣽ ⣼ ⣵ ⣴ ⣯ ⣮ ⣧ ⣦ ⣭ ⣬ ⣥ ⣤ ⣻ ⣺ ⣳ ⣲ ⣹ ⣸ ⣱ ⣰ ⣫ ⣪ ⣣ ⣢ ⣩ ⣨ ⣡ ⣠
... etc ...`

func bit2brail(b uint8) rune {
	return []rune(brailidx[4*int(b)+1 : 4*(int(b)+1)])[0]
}

lamentablemente esto paga el precio de esa conversión a []rune, y volvemos a comprobar los límites,

BenchmarkRuneConst-12        	 546738	     2148 ns/op

y aunque es bastante más rápido hacerlo uno explícitamente

func bit2brail(b uint8) rune {
	n := 4*int(b) + 1
	if n < 0 || n > len(brailidxr) {
		return '⠀'
	}
	r, _ := utf8.DecodeRuneInString(brailidxr[n:])
	return r
}

sigue siendo la peor opción:

BenchmarkStringAndMath-12    	4997421	      245.6 ns/op
BenchmarkRuneSlice-12        	4116244	      290.7 ns/op
BenchmarkRuneConst-12        	1530085	      791.0 ns/op

Expresiones regulares

Ahora queremos una forma de leer un XBM a algo útil. La forma más rápida para mí es escribir lo que sé en una expresión regular.

Vamos a querer volver a visitar esto una vez que todo funcione para hacer un parser de verdad que la gente normal pueda entender, pero por ahora

(?sm)^#define\s+\S+_width\s+(\d+)\s*$
#define\s+\S+_height\s+(\d+)\s*$
(?:#define\s+\S+_x_hot\s+\d+$
#define\s+\S+_y_hot\s+\d+$
)?static\s+(?:unsigned\s+)?char\s+(\S+)_bits\s*\[\]\s*=\s*{\s*$
((?:\s*0x[[:xdigit:]]{2}\s*,)*\s*0x[[:xdigit:]]{1,2})\s*,?\s*\}

debería alcanzar para leer todo el archivo, y después 0x[[:xdigit:]]{1,2} para obtener los dígitos hexadecimales. Esto no va a funcionar para archivos más viejos (en formato “X10”) donde los datos vienen en números de 16 bits en vez de 8, pero en la práctica nunca me he topado con esta versión fuera de pedirle al Gimp que me haga uno, así que no me preocupa por ahora.

Trabajo a futuro

Usando ImageMagick es fácil crear archivos XBM con un pixel prendido:

: convert -size 4x4 xc:black -fill white -draw "point 1,2" XBM:
#define _width 4
#define _height 4
static char _bits[] = {
  0x0F, 0x0F, 0x0D, 0x0F, };

y entonces podés crear un pequeño diccionario para comprobar que cada bit en un XBM es leído correctamente, y otro para comprobar que sale como el caracter braille correspondiente. Esto te va a dejar meterte en los rincones más complicados, como manejar archivos XBM de tamaño 3×3 o 7×7.

También desde acá podés mirar hacer fuzz testing del lector de XBM, antes de deshacerte de esa expresión regular.

O, en una de esas, divertirte.

: go install chipaca.com/xbmbraille@latest
: convert +dither -font Noto-Emoji -pointsize 64 label:🤩 -trim XBM:- | xbmbraille -n -
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣴⣶⠶⠶⠶⠶⢶⣶⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⡾⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣶⣄⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⠟⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⢿⣦⡀⠀⠀⠀⠀
⠀⠀⢀⣼⠟⠁⢠⣿⣿⣦⢀⣀⣀⣀⠀⠀⠀⠀⢀⣀⣀⣀⢀⣾⣿⣿⠀⠙⢿⣆⠀⠀⠀
⠀⢀⣾⣯⣤⣴⣾⣿⣿⣿⣿⣿⣿⠟⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣿⣧⠀⠀
⠀⣾⠏⠙⠻⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⡿⠟⠉⢻⣇⠀
⢸⡟⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡏⠀⠀⠀⠈⣿⡀
⣿⡇⠀⠀⠀⠀⣿⠟⠋⠀⠈⠙⠛⠀⠀⠀⠀⠀⠀⠘⠛⠉⠁⠀⠙⢿⡇⠀⠀⠀⠀⢿⡇
⣿⡇⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡇
⢻⡇⠀⠀⠀⢀⣤⣤⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣠⣤⣄⠀⠀⠀⠀⣿⡇
⠸⣿⠀⠀⠀⠘⣧⣄⣈⣉⡉⠉⠛⠛⠛⠛⠛⠛⠛⠛⠋⠉⢉⣉⣀⣠⡿⠀⠀⠀⢰⡿⠀
⠀⢹⣧⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⢠⣿⠃⠀
⠀⠀⠹⣷⡀⠀⠀⠀⠙⠿⣿⣿⡿⠟⠛⠋⠉⠛⠛⠿⣿⣿⡿⠟⠉⠀⠀⠀⣠⡿⠃⠀⠀
⠀⠀⠀⠘⢿⣦⡀⠀⠀⠀⠀⠉⠛⠓⠶⠶⠶⠶⠖⠛⠋⠁⠀⠀⠀⠀⢀⣼⠟⠁⠀⠀⠀
⠀⠀⠀⠀⠀⠙⠿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⠟⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⢶⣦⣤⣄⣀⣀⣀⣀⣀⣠⣤⣶⠾⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠛⠛⠛⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀