Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added img2braille/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions img2braille/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

from .main import main


if __name__ == '__main__':
sys.exit(main())
41 changes: 41 additions & 0 deletions img2braille/algo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from enum import Enum

from PIL.Image import Image


class Algo(Enum):
RGB_SUM = 'RGBsum'
R = 'R'
G = 'G'
B = 'B'
BW = 'BW'


def apply_algo(img: Image, algo: Algo) -> Image:
"""Applies chosen color mode to the image"""

if algo is Algo.RGB_SUM:
return img.convert("RGB")
elif algo is Algo.R:
return adjust_to_color(img, 0)
elif algo is Algo.G:
return adjust_to_color(img, 1)
elif algo is Algo.B:
return adjust_to_color(img, 2)
elif algo is Algo.BW:
# TODO: check if this actually works with black/white images
return img.convert("RGB")
raise AssertionError # Should never occur


def adjust_to_color(img: Image, pos: int) -> Image:
"""Takes an image and returns a new image with the same size
The new image only uses either the R, G or B values of the original image"""
# TODO: it seems like this does not create a new image? In that case the docstring and usages should be changed

for y in range(img.size[1]):
for x in range(img.size[0]):
val = img.getpixel((x, y))[pos]
img.putpixel((x, y), (val, val, val))
return img

114 changes: 114 additions & 0 deletions img2braille/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from argparse import ArgumentParser, Namespace

from PIL.Image import open as img_open

from .algo import apply_algo, Algo


def main() -> int:
args = parse_args()

# TODO: simplify and split

img = img_open(args.input)
img = img.resize((args.width, round((args.width * img.size[1]) / img.size[0])))
off_x = img.size[0] % 2
off_y = img.size[1] % 4
if off_x + off_y > 0:
img = img.resize((img.size[0] + off_x, img.size[1] + off_y))
original_img = img.copy()

img = apply_algo(img, Algo[args.calc])
img = img.convert("RGB")
average = calc_average(img, args.calc, args.autocontrast)
if args.dither:
img = img.convert("1")
img = img.convert("RGB")

y_size = img.size[1]
x_size = img.size[0]
y_pos = 0
x_pos = 0
line = ''
while y_pos < y_size - 3:
x_pos = 0
while x_pos < x_size:
line += color_average_at_cursor(original_img, (x_pos, y_pos), args.color)
line += block_from_cursor(img, (x_pos, y_pos), average, args.noempty, args.blank)
if args.color in {"html", "htmlbg"}:
line += "</font>"

x_pos += 2
if args.color in {"ansi", "ansifg", "ansiall"}:
line += "\x1b[0m"
print(line)
if args.color in {"html", "htmlbg", "htmlall"}:
print("</br>")
line = ''
y_pos += 4

return 0


def parse_args() -> Namespace:
parser = ArgumentParser('img2braille')

parser.add_argument(
"input",
type=str,
help="image file"
)
parser.add_argument(
"-w", "--width",
type=int,
default=200,
help="determines output width in number of chars",
)
parser.add_argument(
"-i", "--noinvert",
dest='invert',
action='store_false',
help="don't invert colors (for bright backrounds with dark text)",
)
parser.add_argument(
"-d", "--dither",
action='store_true',
help="use dithering (recommended)",
)
# TODO: rename "--calc" to "--filter"; choices: R, G, B, <none> ; description: "uses the specified channel. combines R, G and B channel if not specified. doesn't apply to images with 1 channel"
# note: unsure how images should be handled that have 2 or more than 3 channels, unsure what to do with alpha channel
# adjust related functions
parser.add_argument(
"--calc",
type=str,
choices=["RGBsum", "R", "G", "B", "BW"],
default='RGBsum',
help="determines color values used for calculating dot values (on/off) are calculated",
)
parser.add_argument(
"-n", "--noempty",
action='store_true',
help='don\'t use U+2800 "Braille pattern dots-0" (can fix spacing problems))',
)
parser.add_argument(
"-c", "--color",
type=str,
choices=["none", "ansi", "ansifg", "ansiall", "html", "htmlbg", "htmlall"],
default='none',
help="adds color for html or ansi ascaped output",
)
parser.add_argument(
"-a", "--autocontrast",
action='store_true',
help="automatically adjusts contrast for the image",
)
parser.add_argument(
"-b", "--blank",
action='store_true',
help="U+28FF everywhere. If all you want is the color output",
)

# TODO: add "--algorithm" flag; support dithering algorithms: bayer matrix, floyd-steinberg, threshold, etc.
# note: default should be threshold? maybe something nicer looking instead.

return parser.parse_args()
Binary file added lain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[build-system]
requires = ["setuptools>=66.0.0", "setuptools-scm>=7.1.0"]
build-backend = "setuptools.build_meta"

[project]
name = "img2braille"
authors = [
{name = "", email = ""}, # TODO: Please fill this in
]
description = "Turns images into Unicode Braille art."
readme = "README.md"
requires-python = ">=3.7"
keywords = ["img2braille", "image", "braille", "img", "ascii", "art", "ascii-art"]
license = {file = "LICENSE"}
classifiers = [
# TODO: Please choose one of:
# "Development Status :: 1 - Planning",
# "Development Status :: 2 - Pre-Alpha",
# "Development Status :: 3 - Alpha",
# "Development Status :: 4 - Beta",
# "Development Status :: 5 - Production/Stable",
# "Development Status :: 6 - Mature",
# "Development Status :: 7 - Inactive",
"Programming Language :: Python :: 3",
"Environment :: Console",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"Pillow>=9.4.0",
]
dynamic = ["version"]

[project.entry-points]
img2braille = "img2braille.main:main"
1 change: 0 additions & 1 deletion requirements.txt

This file was deleted.

142 changes: 6 additions & 136 deletions script.py
Original file line number Diff line number Diff line change
@@ -1,106 +1,19 @@
from PIL import Image
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("input", type=str, help="image file")
parser.add_argument(
"-w", "--width",
type=int,
default=200,
help="determines output width in number of chars",
)
parser.add_argument(
"-i", "--noinvert",
dest='invert',
action='store_false',
help="don't invert colors (for bright backrounds with dark text)",
)
parser.add_argument(
"-d", "--dither",
action='store_true',
help="use dithering (recommended)",
)
# TODO: rename "--calc" to "--filter"; choices: R, G, B, <none> ; description: "uses the specified channel. combines R, G and B channel if not specified. doesn't apply to images with 1 channel"
# note: unsure how images should be handled that have 2 or more than 3 channels, unsure what to do with alpha channel
# adjust related functions
parser.add_argument(
"--calc",
type=str,
choices=["RGBsum", "R", "G", "B", "BW"],
default='RGBsum',
help="determines color values used for calculating dot values (on/off) are calculated",
)
parser.add_argument(
"-n", "--noempty",
action='store_true',
help='don\'t use U+2800 "Braille pattern dots-0" (can fix spacing problems))',
)
parser.add_argument(
"-c", "--color",
type=str,
choices=["none", "ansi", "ansifg", "ansiall", "html", "htmlbg", "htmlall"],
default='none',
help="adds color for html or ansi ascaped output",
)
parser.add_argument(
"-a", "--autocontrast",
action='store_true',
help="automatically adjusts contrast for the image",
)
parser.add_argument(
"-b", "--blank",
action='store_true',
help="U+28FF everywhere. If all you want is the color output",
)

# TODO: add "--algorithm" flag; support dithering algorithms: bayer matrix, floyd-steinberg, threshold, etc.
# note: default should be threshold? maybe something nicer looking instead.


# Arg Parsing
args = parser.parse_args()

# Adjustment To Color Calculation
# Takes an image and returns a new image with the same size
# The new image only uses either the R, G or B values of the original image
def adjust_to_color(img, pos):
for y in range(img.size[1]):
for x in range(img.size[0]):
val = img.getpixel((x, y))[pos]
img.putpixel((x, y), (val, val, val))
return img

# Applies chosen color mode to the image
def apply_algo(img, algo):
if algo == "RGBsum":
img = img.convert("RGB")
elif algo == "R":
img = adjust_to_color(img, 0)
elif algo == "G":
img = adjust_to_color(img, 1)
elif algo == "B":
img = adjust_to_color(img, 2)
elif algo == "BW":
# TODO: check if this actually works with black/white images
img = img.convert("RGB")
return img

# Average Calculation
# Takes an image and returns the averade color value
def calc_average(img, algorythm, autocontrast):
def calc_average(img, algo, autocontrast):
if autocontrast:
average = 0
for y in range(img.size[1]):
for x in range(img.size[0]):
if algorythm == "RGBsum":
if algo == "RGBsum":
average += img.getpixel((x, y))[0] + img.getpixel((x, y))[1] + img.getpixel((x, y))[2]
elif algorythm == "R":
elif algo == "R":
average = img.getpixel((x, y))[0]
elif algorythm == "G":
elif algo == "G":
average = img.getpixel((x, y))[1]
elif algorythm == "B":
elif algo == "B":
average = img.getpixel((x, y))[2]
elif algorythm == "BW":
elif algo == "BW":
average = img.getpixel((x, y))
else:
average += img.getpixel((x, y))[0] + img.getpixel((x, y))[1] + img.getpixel((x, y))[2]
Expand Down Expand Up @@ -162,46 +75,3 @@ def color_average_at_cursor(original_img, pos, colorstyle):
return "<font style=\"color:#{:02x}{:02x}{:02x};background-color:#{:02x}{:02x}{:02x}\">".format(px[0], px[1], px[2], px[0], px[1], px[2])
else:
return ""

# Iterates over the image and does all the stuff
def iterate_image(img, original_img, dither, autocontrast, noempty, colorstyle, blank):
img = apply_algo(img, args.calc)
img = img.convert("RGB")
average = calc_average(img, args.calc, autocontrast)
if dither:
img = img.convert("1")
img = img.convert("RGB")

y_size = img.size[1]
x_size = img.size[0]
y_pos = 0
x_pos = 0
line = ''
while y_pos < y_size - 3:
x_pos = 0
while x_pos < x_size:
line += color_average_at_cursor(original_img, (x_pos, y_pos), colorstyle)
line += block_from_cursor(img, (x_pos, y_pos), average, noempty, blank)
if colorstyle in {"html", "htmlbg"}:
line += "</font>"

x_pos += 2
if colorstyle in {"ansi", "ansifg", "ansiall"}:
line += "\x1b[0m"
print(line)
if colorstyle in {"html", "htmlbg", "htmlall"}:
print("</br>")
line = ''
y_pos += 4

# Image Initialization
img = Image.open(args.input)
img = img.resize((args.width, round((args.width * img.size[1]) / img.size[0])))
off_x = img.size[0] % 2
off_y = img.size[1] % 4
if off_x + off_y > 0:
img = img.resize((img.size[0] + off_x, img.size[1] + off_y))
original_img = img.copy()

# Get your output!
iterate_image(img, original_img, args.dither, args.autocontrast, args.noempty, args.color, args.blank)