← proyectoscaligrama
// proyecto — caligrama

caligrama

Un poema que, además de leerse, dibuja aquello de lo que habla.

caligrama — captura
01// historia

Un detalle que se leyera

Un día quise hacerle un detalle a una persona cercana en mi vida. Se me ocurrió usar una imagen y un caligrama, eso que aprendí en el colegio: un poema cuyas letras dibujan aquello de lo que habla.

No quería una imagen bonita y ya. Quería algo que se leyera, pero que dijera algo más allá de las palabras.

02// historia

Nada me terminó de convencer

Revisé algunas herramientas. Casi todo lo que encontré era arte ASCII por densidad: convierten la imagen en una rampa de símbolos como . : * % @ para simular sombras. Se ve bien, pero los caracteres no dicen nada. Yo quería que cada letra fuera parte del mensaje, en orden, y que fuera de la figura solo quedaran espacios.

Hice un par de prototipos en Python con Pillow y pywhatkit. Funcionaban, pero arrastraban dependencias pesadas para algo que al final devuelve un simple texto.

Logo de Python convertido en un caligrama desde la terminal

↳ El logo de Python y el mismo logo escrito con una frase, generado con un solo comando.

03// decision

Mi primera librería, y en Rust. ¿Por qué no?

Siempre había querido publicar una librería de Python y nunca había tenido la excusa. Esta vez la tenía, y decidí complicármela un poco: escribirla toda en Rust, con PyO3 y maturin, con ayuda de la IA para moverme en un terreno que no conocía.

La idea era hacer algo nuevo que le pudiera servir a otros programas de Python, de forma local. Por eso el paquete no tiene dependencias de Python: ni Pillow ni numpy. Leer la imagen, encontrar la figura y componer el texto pasa en Rust, dentro de una sola wheel abi3 que sirve de Python 3.9 en adelante. Funciona sin conexión y no manda nada a ningún lado.

100% Rust
núcleo
0
dependencias Python
Linux · macOS · Windows
plataformas
04// arquitectura

Encontrar la silueta sin que nadie la marque

Lo difícil fue saber qué parte de la imagen es la figura. Si la imagen trae transparencia uso el canal alfa. Si no, estimo el color del fondo con la mediana de los píxeles del borde y corto por distancia de color con Otsu. Con la luminancia sola no alcanzaba: en el logo de Python el amarillo es casi tan claro como el fondo blanco.

Después relleno los huecos, pero solo cuenta como fondo lo que toca el borde. Así la barriga blanca de un gato sobre fondo blanco sigue siendo gato. La figura se reduce a una rejilla de caracteres (corrigiendo que una letra en la terminal es el doble de alta que de ancha) y el texto se escribe por grafemas, para que las tildes y los emojis no se partan.

Antes de escribir el poema puedes preguntarle a la librería cuántas letras caben y desde qué ancho entra completo. A mí me sirvió para ajustar el texto a la figura y no al revés.

05// resultado

Que además se mueva

En la versión 0.2.0 agregué animaciones. El texto puede avanzar dentro de la figura fotograma a fotograma, y la figura puede latir. Se reproduce en la terminal o se exporta como un SVG animado. El título del repositorio y el corazón de esta página los generó la propia librería.

Se usa desde Python con import caligrama o desde la terminal con el comando caligrama. El CLI también está en Rust. Cada tag publica en PyPI las wheels para Linux, macOS y Windows con Trusted Publishing, sin tokens guardados en ningún lado.

La palabra CALIGRAMA escrita con la palabra caligrama, animada

↳ La palabra CALIGRAMA escrita con la palabra caligrama. Nueve fotogramas, uno por letra, en bucle.

06// resultado

Con los colores de la imagen

Cuando lo puse al lado de un script hecho a mano con rich, noté lo que le faltaba: todo salía de un solo color, o de dos que yo elegía. En la versión 0.3.0 cada letra toma el color que tiene la imagen en ese punto. La copa del árbol sale verde y el tronco café, y nadie le dijo dónde estaba cada cosa.

El color de cada letra es el promedio de los píxeles de la figura que caen en su celda. Sigue siendo texto: el color viaja como códigos ANSI dentro del mismo str, y el SVG lo respeta.

Un árbol escrito con un poema; la copa en verdes y el tronco en cafés

↳ Un árbol escrito con un poema. Cada letra lleva el color medio de la imagen en su celda.

07// resultado

Lo puse a competir

Quería saber si de verdad era mejor que lo que ya existía, así que armé un benchmark: el mismo árbol y el mismo poema en pywhatkit, ascii_magic, un script con Pillow y rich hecho a mano, y caligrama. Todos a 70 columnas.

Las herramientas que cambian el brillo por símbolos reconocen la imagen, pero no escriben nada: 0 % de palabras legibles. El script a mano sí escribe el poema, aunque sin espacios y con dos colores fijos, y ahí se lee el 17 %. caligrama deja enteras el 80 % de las palabras, dibuja en unos 2 ms, se importa en 1 ms y no instala nada más.

80 % vs 0–17 %
palabras legibles
~2 ms
tiempo por dibujo
1 vs 2–27
paquetes que instala
El mismo árbol dibujado por pywhatkit, ascii_magic, un script con Pillow y rich, y caligrama, con una tabla de métricas

↳ Mismo árbol, mismo poema, 70 columnas. Las cifras salen de bench/benchmark.py, que está en el repositorio.

08// aprendizaje

Un visor no es un mensaje

También lo comparé con viu y catimg, que muestran fotos en la terminal pintando dos píxeles de color por celda. Ahí perdí, y está bien: son más fieles a la imagen que cualquier dibujo hecho con letras.

La diferencia aparece cuando copias el resultado y lo pegas donde no hay color, como un chat o un commit. De viu queda un rectángulo de bloques. De caligrama queda la forma, y el mensaje sigue ahí. Para eso lo hice.

viu y caligrama con color y el mismo resultado pegado como texto plano, con una tabla de métricas

↳ viu y caligrama con color, y lo que queda de cada uno al pegarlo como texto plano.

09// aprendizaje

Sencilla, por ahora

Hoy caligrama hace una cosa y la hace bien: imagen más texto, sale un texto con forma. Empezó como un regalo y terminó siendo mi primer paquete en PyPI. Mi plan es seguir haciéndola más poderosa que las otras opciones que existen, sin perder lo que me gusta de ella: se instala con un pip install y no necesita nada más.

Construido con:Rust, PyO3 (abi3), maturin, el crate image y GitHub Actions con Trusted Publishing a PyPI.
siguiente proyecto
Code Arena→

Porque una competencia de programación universitaria debe sentirse como una arena en vivo, no como un formulario estático.