Skip to content

Instantly share code, notes, and snippets.

@simonLeary42
Last active May 2, 2025 16:22
Show Gist options
  • Save simonLeary42/1ba1093b5565a712c8a1f819971f9cd1 to your computer and use it in GitHub Desktop.
Save simonLeary42/1ba1093b5565a712c8a1f819971f9cd1 to your computer and use it in GitHub Desktop.
import sys
import shutil
import argparse
from PIL import Image
parser = argparse.ArgumentParser()
parser.add_argument("path")
parser.add_argument("--width", type=int, default=None)
parser.add_argument("--height", type=int, default=None)
args = parser.parse_args()
# for debugging
# def display_dots(dots: list[None | int]) -> str:
# output = [["" for _ in range(2)] for _ in range(3)]
# braille_dot_index_to_x_y = [None, [0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2]]
# for i, (x, y) in enumerate(braille_dot_index_to_x_y[1:], start=1):
# if dots[i]:
# output[y][x] = "#"
# return output
img = Image.open(args.path)
input_height, input_width = img.size
if args.height is None and args.width is not None:
w2h = input_width / input_height
args.height = args.width * w2h
print(
f"defaulting height={args.height} from width and aspect ratio...",
file=sys.stderr,
)
if args.width is None and args.height is not None:
h2w = input_height / input_width
args.width = args.height * h2w
print(
f"defaulting width={args.height} from height and aspect ratio...",
file=sys.stderr,
)
if args.width is None and args.height is None:
# if neither is specified, use half the width of tty
# and use original aspect ratio to calculate height
tty_cols, tty_rows = shutil.get_terminal_size()
width_cols = tty_cols / 2
width_pixels = width_cols * 2 # braille character is 2 dots wide
args.width = width_pixels
print(
f"defaulting width={width_pixels} from tty...",
file=sys.stderr,
)
w2h = input_width / input_height
args.height = args.width * w2h
print(
f"defaulting height={args.height} from width and aspect ratio...",
file=sys.stderr,
)
if remainder := args.height % 4:
args.height -= remainder
print(f"decreasing height by {remainder} since it must be a multiple of 4...", file=sys.stderr)
if remainder := args.width % 2:
args.width -= remainder
print(f"decreasing width by {remainder} since it must be a multiple of 2...", file=sys.stderr)
args.width = int(args.width)
args.height = int(args.height)
if args.height > input_height:
raise RuntimeError(
f"image too small! height={input_height}, requested output height={args.height}"
)
if args.width > input_width:
raise RuntimeError(f"image too small! width={input_width}, requested output width={args.width}")
# print("squishing image vertically to compensate for gaps in braille...", file=sys.stderr)
# img = img.resize((img.size[0], int(img.size[1] * 0.9)))
img = img.resize((args.width, args.height))
img = img.convert("1") # black and white, not grayscale
# img.save("/tmp/img2braille_intermediate.png")
output_lines = []
# starting at top left
for y0 in range(0, args.height, 4):
output_lines.append("")
for x0 in range(0, args.width, 2):
# 1 4
# 2 5
# 3 6
# 7 8
# 1 2 3 4 5 6 7 8
# 2^0 2^1 2^2 2^3 2^4 2^5 2^6 2^7
braille_dot_index_and_pow2_to_x_and_y = {
(1, 0): (0, 0),
(2, 1): (0, 1),
(3, 2): (0, 2),
(4, 3): (1, 0),
(5, 4): (1, 1),
(6, 5): (1, 2),
(7, 6): (0, 3),
(8, 7): (1, 3),
}
# dots = [None] * 9 # there is no index 0, dots[0] is a placeholder
unicode_num = int("2800", 16)
for (dot_index, pow2), (x, y) in braille_dot_index_and_pow2_to_x_and_y.items():
pixel = img.getpixel((x0 + x, y0 + y))
# dots[dot_index] = pixel
if pixel:
unicode_num += 2**pow2
if unicode_num == int("2800", 16):
output_lines[-1] += " "
else:
output_lines[-1] += chr(unicode_num)
for line in output_lines:
print(line)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment