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 -
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣴⣶⠶⠶⠶⠶⢶⣶⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢀⣴⡾⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠿⣶⣄⡀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⠟⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⡟⢿⣦⡀⠀⠀⠀⠀
⠀⠀⢀⣼⠟⠁⢠⣿⣿⣦⢀⣀⣀⣀⠀⠀⠀⠀⢀⣀⣀⣀⢀⣾⣿⣿⠀⠙⢿⣆⠀⠀⠀
⠀⢀⣾⣯⣤⣴⣾⣿⣿⣿⣿⣿⣿⠟⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣿⣧⠀⠀
⠀⣾⠏⠙⠻⣿⣿⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⡿⠟⠉⢻⣇⠀
⢸⡟⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⡏⠀⠀⠀⠈⣿⡀
⣿⡇⠀⠀⠀⠀⣿⠟⠋⠀⠈⠙⠛⠀⠀⠀⠀⠀⠀⠘⠛⠉⠁⠀⠙⢿⡇⠀⠀⠀⠀⢿⡇
⣿⡇⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡇
⢻⡇⠀⠀⠀⢀⣤⣤⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣠⣤⣄⠀⠀⠀⠀⣿⡇
⠸⣿⠀⠀⠀⠘⣧⣄⣈⣉⡉⠉⠛⠛⠛⠛⠛⠛⠛⠛⠋⠉⢉⣉⣀⣠⡿⠀⠀⠀⢰⡿⠀
⠀⢹⣧⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⢠⣿⠃⠀
⠀⠀⠹⣷⡀⠀⠀⠀⠙⠿⣿⣿⡿⠟⠛⠋⠉⠛⠛⠿⣿⣿⡿⠟⠉⠀⠀⠀⣠⡿⠃⠀⠀
⠀⠀⠀⠘⢿⣦⡀⠀⠀⠀⠀⠉⠛⠓⠶⠶⠶⠶⠖⠛⠋⠁⠀⠀⠀⠀⢀⣼⠟⠁⠀⠀⠀
⠀⠀⠀⠀⠀⠙⠿⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⠟⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⢶⣦⣤⣄⣀⣀⣀⣀⣀⣠⣤⣶⠾⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠛⠛⠛⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀