Skip to content

Commit 9aa96f1

Browse files
committed
v0.1.1
1 parent f04f924 commit 9aa96f1

File tree

3 files changed

+120
-61
lines changed

3 files changed

+120
-61
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
33
File : README.md
44
Maintainer : Felix C. Stegerman <[email protected]>
5-
Date : 2021-05-09
5+
Date : 2021-05-10
66
77
Copyright : Copyright (C) 2021 Felix C. Stegerman
8-
Version : v0.1.0
8+
Version : v0.1.1
99
License : AGPLv3+
1010
1111
}}}1 -->

kanjidraw/gui.py

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
# File : kanjidraw/gui.py
77
# Maintainer : Felix C. Stegerman <[email protected]>
8-
# Date : 2021-05-09
8+
# Date : 2021-05-10
99
#
1010
# Copyright : Copyright (C) 2021 Felix C. Stegerman
1111
# Version : v0.0.1
@@ -16,7 +16,7 @@
1616
# {{{1
1717
r"""
1818
19-
Tkinter GUI.
19+
Handwritten kanji recognition: tkinter GUI.
2020
2121
""" # }}}1
2222

@@ -26,7 +26,7 @@
2626
import tkinter.font as tk_font
2727
from tkinter import Tk, Button, Canvas, Frame, Label
2828

29-
from . import load_json, matches
29+
from .lib import kanji_data, matches
3030

3131
NAME, TITLE = "kanjidraw", "Kanji Draw"
3232
HEIGHT, WIDTH, BACKGROUND = 400, 400, "#ccc"
@@ -39,10 +39,9 @@ def gui(): # {{{1
3939
win.title(TITLE)
4040
win.columnconfigure(0, weight = 1)
4141
win.rowconfigure(0, weight = 1)
42-
font = tk_font.Font(size = FONTSIZE)
4342

44-
data = load_json()
45-
max_strokes = max(data.keys())
43+
font = tk_font.Font(size = FONTSIZE)
44+
max_strokes = max(kanji_data().keys())
4645
drawing, x, y, strokes, lines = False, 0, 0, [], []
4746

4847
def on_mousedown(event):
@@ -54,27 +53,22 @@ def on_mousedown(event):
5453
enable_buttons()
5554

5655
def on_mousemove(event):
57-
nonlocal x, y
58-
if drawing:
59-
draw_line(x, y, event.x, event.y)
60-
x, y = event.x, event.y
56+
if drawing: draw_line(event.x, event.y)
6157

6258
def on_mouseup(event):
63-
nonlocal drawing, x, y
59+
nonlocal drawing
6460
if drawing:
65-
draw_line(x, y, event.x, event.y)
66-
drawing, x, y = False, event.x, event.y
61+
draw_line(event.x, event.y)
62+
drawing = False
6763
strokes[-1] += [x * 255.0 / HEIGHT, y * 255.0 / WIDTH]
6864
update_strokes()
6965

7066
def on_undo():
7167
if strokes:
7268
strokes.pop()
73-
for l in lines.pop():
74-
canvas.delete(l)
69+
for l in lines.pop(): canvas.delete(l)
7570
update_strokes()
76-
if not strokes:
77-
disable_buttons()
71+
if not strokes: disable_buttons()
7872

7973
def on_clear():
8074
strokes.clear()
@@ -83,35 +77,49 @@ def on_clear():
8377
disable_buttons()
8478

8579
def on_done():
86-
results_frame = Frame(win)
87-
for i, (_, kanji) in enumerate(matches(strokes, data)):
80+
res_frame = Frame(win)
81+
res_btns = Frame(res_frame)
82+
btn_back = Button(res_btns, text = "Go Back", command = on_back(res_frame))
83+
lbl_info = Label(res_btns, text = "Click to copy to clipboard")
84+
res_grid = Frame(res_frame)
85+
for i, (_, kanji) in enumerate(matches(strokes)):
8886
col, row = i % ROWS, i // ROWS
89-
results_frame.columnconfigure(col, weight = 1)
90-
results_frame.rowconfigure(row, weight = 1)
91-
btn = Button(results_frame, text = kanji, font = font,
92-
command = on_select_kanji(results_frame, kanji))
87+
res_grid.columnconfigure(col, weight = 1)
88+
res_grid.rowconfigure(row, weight = 1)
89+
btn = Button(res_grid, text = kanji, font = font,
90+
command = on_select_kanji(res_frame, kanji))
9391
btn.grid(column = col, row = row, sticky = "nsew")
94-
results_frame.grid(row = 0, column = 0, sticky = "nsew")
95-
96-
def on_select_kanji(results_frame, kanji):
97-
def handler():
92+
btn_back.pack(side = tk.LEFT, padx = 5, pady = 5)
93+
lbl_info.pack(side = tk.LEFT, padx = 5, pady = 5)
94+
res_btns.pack()
95+
res_grid.pack()
96+
res_frame.grid(row = 0, column = 0, sticky = "nsew")
97+
win.bind("<Escape>", on_back(res_frame))
98+
99+
def on_back(res_frame):
100+
def f(event = None):
101+
win.unbind("<Escape>")
102+
res_frame.destroy()
103+
return f
104+
105+
def on_select_kanji(res_frame, kanji):
106+
def f():
98107
copy_to_clipboard(kanji)
99-
results_frame.destroy()
108+
res_frame.destroy()
100109
on_clear()
101-
return handler
110+
return f
102111

103-
def draw_line(x1, y1, x2, y2):
104-
l = canvas.create_line(x1, y1, x2, y2, width = LINEWIDTH,
105-
capstyle = tk.ROUND)
112+
def draw_line(x2, y2):
113+
nonlocal x, y
114+
l = canvas.create_line(x, y, x2, y2, width = LINEWIDTH, capstyle = tk.ROUND)
106115
lines[-1].append(l)
116+
x, y = x2, y2
107117

108118
def disable_buttons():
109-
for w in [btn_undo, btn_clear, btn_done]:
110-
w.config(state = tk.DISABLED)
119+
for w in [btn_undo, btn_clear, btn_done]: w.config(state = tk.DISABLED)
111120

112121
def enable_buttons():
113-
for w in [btn_undo, btn_clear, btn_done]:
114-
w.config(state = tk.NORMAL)
122+
for w in [btn_undo, btn_clear, btn_done]: w.config(state = tk.NORMAL)
115123

116124
def update_strokes():
117125
lbl_strokes.config(text = "Strokes: {}".format(len(strokes)))

kanjidraw/lib.py

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,75 @@
55
#
66
# File : kanjidraw/lib.py
77
# Maintainer : Felix C. Stegerman <[email protected]>
8-
# Date : 2021-05-09
8+
# Date : 2021-05-10
99
#
1010
# Copyright : Copyright (C) 2021 Felix C. Stegerman
11-
# Version : v0.1.0
11+
# Version : v0.1.1
1212
# License : AGPLv3+
1313
#
1414
# -- ; }}}1
1515

1616
# {{{1
1717
r"""
1818
19-
Handwritten kanji recognition library.
19+
Handwritten kanji recognition: library.
20+
21+
>>> strokes = [[125.5875, 28.6875, 48.45, 196.35], [104.55, 93.7125, 195.7125, 223.125]]
22+
>>> for s, k in matches(strokes): print(int(s), k)
23+
99 人
24+
96 九
25+
96 乂
26+
93 八
27+
93 入
28+
90 儿
29+
89 勹
30+
88 乃
31+
87 又
32+
87 几
33+
87 冂
34+
85 匕
35+
82 亻
36+
81 卜
37+
78 亠
38+
75 冖
39+
40+
>>> strokes = [[17.85, 102.0, 83.5125, 51.0], [45.9, 26.775, 49.0875, 210.375], [33.7875, 152.3625, 61.8375, 133.875], [103.9125, 54.825, 211.0125, 58.65], [139.6125, 31.2375, 142.8, 64.3875], [178.5, 42.075, 179.1375, 66.3], [108.375, 121.125, 106.4625, 182.325], [113.475, 114.1125, 205.275, 189.975], [163.2, 116.025, 164.475, 181.05], [126.225, 148.5375, 198.2625, 158.7375], [109.0125, 200.8125, 205.9125, 191.25]]
41+
>>> for s, k in matches(strokes): print(int(s), k)
42+
91 描
43+
89 猫
44+
86 桷
45+
85 淌
46+
84 猟
47+
83 掩
48+
83 培
49+
82 猛
50+
82 控
51+
82 清
52+
82 捨
53+
81 陪
54+
81 措
55+
81 袷
56+
81 掠
57+
81 淹
58+
80 桶
59+
80 掘
60+
80 舳
61+
80 猪
62+
80 掃
63+
79 情
64+
79 捺
65+
79 陷
66+
79 探
2067
2168
""" # }}}1
2269

23-
import gzip, json, os, re, sys
70+
import gzip, itertools, json, os, re, sys
2471
import xml.etree.ElementTree as ET
2572

2673
from collections import namedtuple
2774
from enum import Enum
2875

29-
__version__ = "0.1.0"
76+
__version__ = "0.1.1"
3077

3178
DATAFILE = os.path.join(os.path.dirname(__file__), "data.json")
3279

@@ -41,7 +88,7 @@
4188
STROKE_LOCATION_WEIGHT = 0.6
4289
CLOSE_WEIGHT = 0.7
4390

44-
DirAndLoc = namedtuple("DirAndLoc", "starts ends dirs moves".split())
91+
SEDM = namedtuple("SEDM", "starts ends dirs moves".split())
4592

4693
class Direction(Enum): # {{{1
4794
X, N, NE, E, SE, S, SW, W, NW = range(-1, 8)
@@ -131,15 +178,32 @@ def strict_match(a, b): # {{{1
131178
# }}}1
132179

133180
def _directions_and_locations(lines):
134-
return DirAndLoc(
181+
return SEDM(
135182
tuple( Location.of_point(*l[:2]) for l in lines ),
136183
tuple( Location.of_point(*l[2:]) for l in lines ),
137184
tuple(map(Direction.of_line, lines)),
138185
tuple(map(Direction.of_move, lines[1:], lines[:-1]))
139186
)
140187

188+
def matches(lines, data = None, match = strict_match,
189+
max_results = 25, cutoff = 0.75):
190+
"""
191+
Find best matches; yields a (score, kanji) pair for the first
192+
max_results matches that have a score >= max_score * cutoff.
193+
"""
194+
if data is None: data = kanji_data()
195+
it = data[len(lines)].items()
196+
ms = sorted(( (match(lines, l), k) for k, l in it ), reverse = True)
197+
mm = ms[0][0] * cutoff
198+
return itertools.takewhile(lambda m: m[0] >= mm, ms[:max_results])
199+
200+
def kanji_data():
201+
if kanji_data._data is None: kanji_data._data = _load_json()
202+
return kanji_data._data
203+
kanji_data._data = None
204+
141205
# FIXME: better kanji unicode ranges
142-
def parse_kanjivg(file): # {{{1
206+
def _parse_kanjivg(file): # {{{1
143207
data = {}
144208
with gzip.open(file) as f:
145209
for e in ET.parse(f).getroot():
@@ -179,29 +243,16 @@ def _path_to_line(path): # {{{1
179243
return tuple( int(v * 255 / 109) for v in [x1, y1, x2, y2] )
180244
# }}}1
181245

182-
def load_json(file = DATAFILE):
246+
def _load_json(file = DATAFILE):
183247
"""Load data from JSON file."""
184248
with open(file) as fh:
185249
return { int(k): v for k, v in json.load(fh).items() }
186250

187-
def save_json(file, data):
251+
def _save_json(file, data):
188252
"""Save data to JSON file."""
189253
with open(file, "w") as fh:
190254
json.dump(data, fh, sort_keys = True)
191255

192-
def matches(lines, data, match = strict_match, max_results = 25,
193-
cutoff = 0.75):
194-
"""
195-
Find best matches; yields a (score, kanji) pair for the first
196-
max_results matches that have a score >= max_score * cutoff.
197-
"""
198-
it = data[len(lines)].items()
199-
ms = sorted(( (match(lines, l), k) for k, l in it ), reverse = True)
200-
max_score = ms[0][0]
201-
for m in ms[:max_results]:
202-
if m[0] < max_score * cutoff: break
203-
yield m
204-
205256
if __name__ == "__main__":
206257
if "--doctest" in sys.argv:
207258
verbose = "--verbose" in sys.argv

0 commit comments

Comments
 (0)