Braille Translator Tool

import tkinter as tk

from tkinter import filedialog, messagebox

from PIL import Image, ImageDraw, ImageTk

import math

import os


# -------------------------

# Braille mapping utilities

# -------------------------


# Dot-number definitions for letters a..z (Grade-1 braille)

LETTER_DOTS = {

    'a': [1],

    'b': [1,2],

    'c': [1,4],

    'd': [1,4,5],

    'e': [1,5],

    'f': [1,2,4],

    'g': [1,2,4,5],

    'h': [1,2,5],

    'i': [2,4],

    'j': [2,4,5],

    'k': [1,3],

    'l': [1,2,3],

    'm': [1,3,4],

    'n': [1,3,4,5],

    'o': [1,3,5],

    'p': [1,2,3,4],

    'q': [1,2,3,4,5],

    'r': [1,2,3,5],

    's': [2,3,4],

    't': [2,3,4,5],

    'u': [1,3,6],

    'v': [1,2,3,6],

    'w': [2,4,5,6],

    'x': [1,3,4,6],

    'y': [1,3,4,5,6],

    'z': [1,3,5,6],

}


# Common punctuation (Grade-1)

PUNCT_DOTS = {

    ',': [2],

    ';': [2,3],

    ':': [2,4],

    '.': [2,5,6],

    '?': [2,6],

    '!': [2,3,5],

    '(': [2,3,6,5],  # open parenthesis commonly encoded as ⠶ (but implementations vary)

    ')': [3,5,6,2],  # mirrored / alternative — we'll use same as '(' for simplicity

    "'": [3],

    '-': [3,6],

    '/': [3,4],

    '"': [5,6,2,3],  # approximate

    '@': [4,1],      # uncommon; approximate

    '#': [3,4,5,6],  # number sign (we will use official number sign below)

}


# Braille special signs

NUMBER_SIGN = [3,4,5,6]   # ⠼

CAPITAL_SIGN = [6]        # prefix for capital (single capital) — optional use

SPACE = []                # no dots for space -> unicode U+2800


# Build dot -> Unicode mapping utility

def dots_to_braille_unicode(dots):

    """

    dots: list of integers 1..8 (we use 1..6)

    returns: single unicode braille character

    """

    code = 0x2800

    for d in dots:

        if 1 <= d <= 8:

            code += 1 << (d - 1)

    return chr(code)


# Precompute maps

LETTER_TO_BRAILLE = {ch: dots_to_braille_unicode(dots) for ch, dots in LETTER_DOTS.items()}

PUNCT_TO_BRAILLE = {p: dots_to_braille_unicode(dots) for p, dots in PUNCT_DOTS.items()}

NUMBER_SIGN_CHAR = dots_to_braille_unicode(NUMBER_SIGN)

CAPITAL_SIGN_CHAR = dots_to_braille_unicode(CAPITAL_SIGN)

SPACE_CHAR = chr(0x2800)


# Digits mapping in Grade-1: number sign + letters a-j represents 1-0

DIGIT_TO_LETTER = {

    '1': 'a', '2': 'b', '3': 'c', '4': 'd', '5': 'e',

    '6': 'f', '7': 'g', '8': 'h', '9': 'i', '0': 'j'

}


# -------------------------

# Translation function

# -------------------------

def translate_to_braille(text, use_capital_prefix=True, use_number_prefix=True):

    """

    Translate plain text into Grade-1 Braille Unicode string.

    Options:

      - use_capital_prefix: if True, prefix capitals with the capital sign (⠠)

      - use_number_prefix: if True, prefix digit sequences with number sign (⠼)

    Returns braille_unicode_string

    """

    out = []

    i = 0

    n = len(text)

    while i < n:

        ch = text[i]

        if ch.isspace():

            out.append(SPACE_CHAR)

            i += 1

            continue


        # Digit sequence handling

        if ch.isdigit():

            if use_number_prefix:

                out.append(NUMBER_SIGN_CHAR)

            # consume contiguous digits

            while i < n and text[i].isdigit():

                d = text[i]

                letter_equiv = DIGIT_TO_LETTER.get(d, None)

                if letter_equiv:

                    out.append(LETTER_TO_BRAILLE[letter_equiv])

                else:

                    # fallback: space for unknown

                    out.append(SPACE_CHAR)

                i += 1

            continue


        # Letter

        if ch.isalpha():

            if ch.isupper():

                if use_capital_prefix:

                    out.append(CAPITAL_SIGN_CHAR)

                ch_low = ch.lower()

            else:

                ch_low = ch

            code = LETTER_TO_BRAILLE.get(ch_low)

            if code:

                out.append(code)

            else:

                out.append(SPACE_CHAR)

            i += 1

            continue


        # Punctuation

        if ch in PUNCT_TO_BRAILLE:

            out.append(PUNCT_TO_BRAILLE[ch])

            i += 1

            continue


        # Fallback: try common mapping for punctuation by replacement

        if ch == '"':

            out.append(PUNCT_TO_BRAILLE.get('"', SPACE_CHAR))

            i += 1

            continue


        # Unknown character: attempt to include as space placeholder

        out.append(SPACE_CHAR)

        i += 1


    return "".join(out)


# -------------------------

# Braille image rendering

# -------------------------

def render_braille_image(braille_text, dot_radius=8, dot_gap=10, cell_gap=16, bg_color=(255,255,255)):

    """

    Render braille_text (unicode braille characters) into a PIL Image.

    Each braille cell is 2 (columns) x 3 (rows) of dots.

    We read the Unicode braille codepoints and draw filled circles for active dots.

    Returns PIL.Image (RGB).

    """

    # Compute rows & columns: we'll wrap to a max columns per line for reasonable width

    max_cols = 40  # characters per row, adjust if needed


    # Split into lines by breaking long strings

    chars = list(braille_text)

    lines = [chars[i:i+max_cols] for i in range(0, len(chars), max_cols)]


    # cell size

    cell_w = dot_radius*2 + dot_gap

    cell_h = dot_radius*3 + dot_gap*2  # 3 rows

    img_w = len(lines[0]) * (cell_w + cell_gap) + 2*cell_gap if lines else 200

    img_h = len(lines) * (cell_h + cell_gap) + 2*cell_gap if lines else 100


    img = Image.new("RGB", (img_w, img_h), color=bg_color)

    draw = ImageDraw.Draw(img)


    for row_idx, line in enumerate(lines):

        for col_idx, ch in enumerate(line):

            x0 = cell_gap + col_idx * (cell_w + cell_gap)

            y0 = cell_gap + row_idx * (cell_h + cell_gap)

            # Determine dot pattern from unicode char

            codepoint = ord(ch)

            base = 0x2800

            mask = codepoint - base

            # dot positions for 1..6 are arranged:

            # (col0,row0)=dot1  (col1,row0)=dot4

            # (col0,row1)=dot2  (col1,row1)=dot5

            # (col0,row2)=dot3  (col1,row2)=dot6

            dot_positions = [

                (0,0,1),  # dot1

                (0,1,2),  # dot2

                (0,2,3),  # dot3

                (1,0,4),  # dot4

                (1,1,5),  # dot5

                (1,2,6),  # dot6

            ]

            for col, r, dotn in dot_positions:

                bit = (mask >> (dotn-1)) & 1

                cx = x0 + col * (dot_radius + dot_gap/2) + dot_radius + 4

                cy = y0 + r * (dot_radius + dot_gap/2) + dot_radius + 4

                bbox = [cx - dot_radius, cy - dot_radius, cx + dot_radius, cy + dot_radius]

                if bit:

                    draw.ellipse(bbox, fill=(0,0,0))

                else:

                    # draw faint circle to indicate empty dot (optional)

                    draw.ellipse(bbox, outline=(200,200,200))

    return img


# -------------------------

# GUI

# -------------------------

class BrailleGUI:

    def __init__(self, root):

        self.root = root

        root.title("Braille Translator Tool — Grade-1 (Uncontracted)")

        root.geometry("820x520")


        # Input frame

        frame_in = tk.LabelFrame(root, text="Input Text", padx=8, pady=8)

        frame_in.pack(fill="both", padx=12, pady=8)


        self.text_input = tk.Text(frame_in, height=6, wrap="word", font=("Arial", 12))

        self.text_input.pack(fill="both", expand=True)

        self.text_input.insert("1.0", "Hello, World! 123")


        # Controls

        ctrl = tk.Frame(root)

        ctrl.pack(fill="x", padx=12)

        tk.Button(ctrl, text="Translate", command=self.on_translate).pack(side="left", padx=6, pady=6)

        tk.Button(ctrl, text="Render Braille Image (Preview)", command=self.on_render_preview).pack(side="left", padx=6, pady=6)

        tk.Button(ctrl, text="Save Braille Image...", command=self.on_save_image).pack(side="left", padx=6, pady=6)

        tk.Button(ctrl, text="Copy Braille Unicode to Clipboard", command=self.on_copy_clipboard).pack(side="left", padx=6, pady=6)


        # Output frame (braille unicode text)

        frame_out = tk.LabelFrame(root, text="Braille (Unicode)", padx=8, pady=8)

        frame_out.pack(fill="both", padx=12, pady=8, expand=True)


        self.braille_text_widget = tk.Text(frame_out, height=6, wrap="word", font=("Segoe UI Symbol", 20))

        self.braille_text_widget.pack(fill="both", expand=True)

        self.braille_text_widget.config(state="disabled")


        # Image preview area

        preview_frame = tk.LabelFrame(root, text="Image Preview", padx=8, pady=8)

        preview_frame.pack(fill="both", padx=12, pady=8)

        self.preview_label = tk.Label(preview_frame)

        self.preview_label.pack()

        self.last_preview_image = None  # keep reference to avoid GC


    def on_translate(self):

        txt = self.text_input.get("1.0", "end").rstrip("\n")

        if not txt.strip():

            messagebox.showwarning("Input required", "Please enter some text to translate.")

            return

        braille = translate_to_braille(txt)

        self.braille_text_widget.config(state="normal")

        self.braille_text_widget.delete("1.0", "end")

        self.braille_text_widget.insert("1.0", braille)

        self.braille_text_widget.config(state="disabled")


    def on_render_preview(self):

        braille = self.braille_text_widget.get("1.0", "end").rstrip("\n")

        if not braille:

            messagebox.showinfo("No Braille", "Translate text first (click Translate).")

            return

        img = render_braille_image(braille, dot_radius=8, dot_gap=10, cell_gap=14)

        self.show_preview(img)


    def on_save_image(self):

        braille = self.braille_text_widget.get("1.0", "end").rstrip("\n")

        if not braille:

            messagebox.showinfo("No Braille", "Translate text first (click Translate).")

            return

        img = render_braille_image(braille, dot_radius=10, dot_gap=12, cell_gap=16)

        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG image","*.png")], title="Save Braille image")

        if path:

            img.save(path)

            messagebox.showinfo("Saved", f"Braille image saved to:\n{path}")


    def on_copy_clipboard(self):

        braille = self.braille_text_widget.get("1.0", "end").rstrip("\n")

        if not braille:

            messagebox.showinfo("No Braille", "Translate text first (click Translate).")

            return

        # Use Tk clipboard

        self.root.clipboard_clear()

        self.root.clipboard_append(braille)

        messagebox.showinfo("Copied", "Braille Unicode copied to clipboard.")


    def show_preview(self, pil_img):

        # Resize preview if too big

        max_w, max_h = 760, 240

        w, h = pil_img.size

        scale = min(max_w / w, max_h / h, 1.0)

        if scale < 1.0:

            pil_img = pil_img.resize((int(w*scale), int(h*scale)), Image.LANCZOS)

        tk_img = ImageTk.PhotoImage(pil_img)

        self.preview_label.config(image=tk_img)

        self.preview_label.image = tk_img  # keep ref


# -------------------------

# Run the app

# -------------------------

def main():

    root = tk.Tk()

    app = BrailleGUI(root)

    root.mainloop()


if __name__ == "__main__":

    main()


No comments: