Problem
For a small project of mine, I will be needing to work a lot with braille translation. To do that, I have already finished a small program that I can use to translate braille into English letters. To make the whole thing a little more modular, I exported all my braille logic into a small module:
from typing import NoReturn, List
from bitmap import BitMap
from enum import Enum
from bidict import bidict
class Dot(Enum):
TOP_LEFT = 0
MIDDLE_LEFT = 1
BOTTOM_LEFT = 2
TOP_RIGHT = 3
MIDDLE_RIGHT = 4
BOTTOM_RIGHT = 5
class BrailleGlyph(BitMap):
UNICODE_BLOCK = 10240
MAPPING = bidict(
{"00000000": " ",
"00000001": "A",
"00000011": "B",
"00001001": "C",
"00011001": "D",
"00010001": "E",
"00001011": "F",
"00011011": "G",
"00010011": "H",
"00001010": "I",
"00011010": "J",
"00000101": "K",
"00000111": "L",
"00001101": "M",
"00011101": "N",
"00010101": "O",
"00001111": "P",
"00011111": "Q",
"00010111": "R",
"00001110": "S",
"00011110": "T",
"00100101": "U",
"00100111": "V",
"00111010": "W",
"00101101": "X",
"00111101": "Y",
"00110101": "Z", }
)
def __init__(self, maxnum: int = 8) -> NoReturn:
super().__init__(maxnum)
self.history: List[Dot] = []
def __str__(self) -> str:
ones = int(self.tostring()[4:], 2)
tens = int(self.tostring()[:4], 2) * 16
return chr(BrailleGlyph.UNICODE_BLOCK + tens + ones)
@classmethod
def from_char(cls, char: str) -> "BrailleGlyph":
return BrailleGlyph.fromstring(cls.MAPPING.inverse[char.upper()])
def is_empty(self) -> bool:
return self.history == []
def click(self, index: Dot) -> NoReturn:
if not self.test(index.value):
self.set(index.value)
self.history.append(index)
def delete(self) -> NoReturn:
self.flip(self.history.pop().value)
def get_dots(self) -> List[Dot]:
return self.history
def to_ascii(self) -> str:
try:
return BrailleGlyph.MAPPING[self.tostring()]
except KeyError:
return "?"
class BrailleTranslator:
def __init__(self) -> NoReturn:
self.glyphs = [BrailleGlyph()]
def __str__(self) -> str:
return "".join(map(str, self.glyphs))
def new_glyph(self) -> NoReturn:
self.glyphs.append(BrailleGlyph())
def delete(self) -> NoReturn:
if self.glyphs[-1].is_empty():
if len(self.glyphs) != 1:
self.glyphs.pop()
else:
self.glyphs[-1].delete()
def click(self, index: Dot) -> NoReturn:
self.glyphs[-1].click(index)
def get_current_glyph(self) -> BrailleGlyph:
return self.glyphs[-1]
def translate(self) -> str:
return "".join(map(lambda x: x.to_ascii(), self.glyphs))
The primary use-case is going to be – me clicking dots (or some buttons on my keyboard that will set dots on the glyph) and the translation module will just remember what has been clicked and will display results in real-time.
To avoid reinventing too much I used the external bitmap
and bidict
libraries which both seem very fitting for a task like this.
Is there some way to make this more pythonic? Or maybe some obvious flaws?
Here is also a sample usage:
def braille_translator(self) -> NoReturn:
graph = self.window["-GRAPH-"]
output = self.window["-BRAILLE_OUTPUT-"]
graphed_dots = []
translator = BrailleTranslator()
circle_mapping = {Dot.BOTTOM_LEFT: (50, 250),
Dot.BOTTOM_RIGHT: (150, 250),
Dot.MIDDLE_LEFT: (50, 150),
Dot.MIDDLE_RIGHT: (150, 150),
Dot.TOP_LEFT: (50, 50),
Dot.TOP_RIGHT: (150, 50), }
dot_mapping = {"1": Dot.BOTTOM_LEFT,
"2": Dot.BOTTOM_RIGHT,
"4": Dot.MIDDLE_LEFT,
"5": Dot.MIDDLE_RIGHT,
"7": Dot.TOP_LEFT,
"8": Dot.TOP_RIGHT, }
while True:
event, values = self.window.read()
if event in (None, 'Exit') or "EXIT" in event:
exit()
elif event in "124578": # Left side of numpad!
translator.click(dot_mapping[event])
elif event == " ":
translator.new_glyph()
elif "BackSpace" in event:
translator.delete()
current_dots = translator.get_current_glyph().get_dots()
for circle in [d for d in graphed_dots if d not in current_dots]:
graph.delete_figure(circle)
graphed_dots.remove(circle)
for dot in [d for d in current_dots if d not in graphed_dots]:
circle = graph.draw_circle(circle_mapping[dot],
20, fill_color=GUI.THEME_COLOR)
graphed_dots.append(circle)
output.update(translator.translate())
And here’s a GIF of the sample usage in action:
It’s a thin line between over-fitting the translation module too much for my current task and keeping it modular for future uses!
Solution
First a few small things:
If you expect this to be used in other programs, it should be documented: comments to explain implementation choices and details and docstrings to explain how to use the classes and functions in the module.
A small suggestion: When modeling something, it is often helpful if the interface uses the same numbering or nomenclature used in the real world. For example, the upper left dot is often referred to as dot 1, not dot 0. So the class Dot(enum)
might use values 1-6 rather than 0-5. But perhaps the names are the interface and the values are internal.
Multiplying by 16 is the same as shifting by 4 bits. That is, this:
def __str__(self) -> str:
offset = int(self.tostring(), 2)
return chr(BrailleGlyph.UNICODE_BLOCK + offset)
Is the same as
def __str__(self) -> str:
ones = int(self.tostring()[4:], 2)
tens = int(self.tostring()[:4], 2) * 16
return chr(BrailleGlyph.UNICODE_BLOCK + tens + ones)
Use dict.get(key, default)
with a default value instead of the try... except
block:
def to_ascii(self) -> str:
return BrailleGlyph.MAPPING.get(self.tostring(), "?")
The BitMap
library is overkill and seems to get in the way. For example, the code converts the bits to a string, then converts the string to an int in base 2. But the bits were already stored as a byte in the bit map. I suspect it would be cleaner to implement the bit operations directly.
It seems odd that a BrailleGlyph
object should keep a list of the dots in the order they were added (history
). That’s like the letter ‘A’ knowing what order the lines were added. .history
is there so the editor can implement an ‘undo’ feature, but that seems to be a job for the editor, not the BrailleGlyph
class.
Being blind, I just want to point out that Braille is written in another way, when we do dot-mapping by hand.
The top left is 1, middle left is 2, bottom left is 3
Top right is 4, middle right 5, bottom right is 6.
For computer Braille (as used for contractions and marks on a refreshable Braille display), there are also two bottom dots – left and right, 7 and 8 respectively.
So in literary Braille (on a book page), the word “Hi” would be written 6, 125, 24. Whereas on a Refreshable Braille Display (Computer Braille) it would be written 1257, 24.
If you get the numbers correct, it is easy to make conversion tables for other alphabets such as Swedish with ÅÄÖ, Greek, Hebrew, etc. Making it possible to define any character in a standardised way.
Just for what it’s worth…