-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.pyw
1372 lines (1198 loc) · 59.7 KB
/
main.pyw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# for GUI
import _tkinter
import tkinter as tk
from tkinter import ttk
from ttkthemes import ThemedTk
# for click through screen grab window
from win32gui import SetWindowLong, SetLayeredWindowAttributes
from win32con import WS_EX_LAYERED, WS_EX_TRANSPARENT, GWL_EXSTYLE, LWA_ALPHA
# for image taking and text conversion
from PIL import Image, ImageGrab, ImageTk, ImageChops, ImageEnhance
import pytesseract as pt # Using version 5.1.0.20220510
# for running image taking while keeping program running
from time import sleep
from threading import Thread
# for translation
from googletrans import Translator, LANGUAGES
import sys, os
title_string = "InstantTranslate"
# Flag for screen grab thread to stop after program closes.
stop_threads = False
# List of languages (capitalized) for use in main app window Combobox
old_language_list = list(LANGUAGES.values())
exclusion_list = ['Albanian', 'Armenian', 'Basque', 'Chichewa', 'Croatian', 'Hausa', 'Hawaiian', 'Hmong', 'Igbo',
'Malagasy', 'Odia', 'Samoan', 'Sesotho', 'Shona', 'Somali', 'Xhosa']
language_list = []
for i in range(len(old_language_list)):
old_language_list[i] = old_language_list[i].capitalize()
if old_language_list[i] not in exclusion_list:
if " " in old_language_list[i]:
broken_string = old_language_list[i].split()
if "(" in broken_string[1]:
broken_string[1] = broken_string[1][1::].capitalize()
broken_string[1] = "(" + broken_string[1]
else:
broken_string[1] = broken_string[1].capitalize()
fixed_string = broken_string[0] + " " + broken_string[1]
language_list.append(fixed_string)
else:
if old_language_list[i] == 'Hebrew':
if old_language_list[i - 1] != 'Hebrew':
language_list.append(old_language_list[i])
else:
language_list.append(old_language_list[i])
# Dict of googletrans language codes as keys, pytesseract 3-letter codes as values
language_map_pt_to_googletrans = {
'af': 'afr', # Afrikaans
'am': 'amh', # Amharic
'ar': 'ara', # Arabic
'az': 'aze', # Azerbaijani
'be': 'bel', # Belarusian
'bn': 'ben', # Bengali
'bs': 'bos', # Bosnian
'bg': 'bul', # Bulgarian
'ca': 'cat', # Catalan, Valencian
'ceb': 'ceb', # Cebuano
'cs': 'ces', # Czech
'co': 'cos', # Corsican
'cy': 'cym', # Welsh
'zh-cn': 'chi_sim', # Chinese Simplified
'zh-tw': 'chi_tra', # Chinese Traditional
'da': 'dan', # Danish
'de': 'deu', # German
'eo': 'epo', # Esperanto
'el': 'ell', # Greek
'en': 'eng', # English
'et': 'est', # Estonian
'tl': 'fil', # Filipino
'fa': 'fas', # Persian
'fi': 'fin', # Finnish
'fr': 'fra', # French
'fy': 'fry', # Western Frisian
'ga': 'gle', # Irish
'gd': 'gla', # Scots Gaelic
'gl': 'glg', # Galician
'gu': 'guj', # Gujarati
'ka': 'kat', # Georgian
'ht': 'hat', # Haitian Creole
'iw': 'heb', # Hebrew
'hi': 'hin', # Hindi
'hu': 'hun', # Hungarian
'is': 'isl', # Icelandic
'id': 'ind', # Indonesian
'it': 'ita', # Italian
'ja': 'jpn', # Japanese
'jw': 'jav', # Javanese
'kn': 'kan', # Kannada
'kk': 'kaz', # Kazakh
'km': 'khm', # Central Khmer
'ko': 'kor', # Korean
'ku': 'kmr', # Kurdish (Kurmanji, latin script)
'ky': 'kir', # Kyrgyz
'lb': 'ltz', # Luxembourgish
'lo': 'lao', # Lao
'la': 'lat', # Latin
'lv': 'lav', # Latvian
'lt': 'lit', # Lithuanian
'mk': 'mkd', # Macedonian
'ms': 'msa', # Malay
'ml': 'mal', # Malayalam
'mt': 'mlt', # Maltese
'mi': 'mri', # Maori
'mr': 'mar', # Marathi
'mn': 'mon', # Mongolian
'my': 'mya', # Myanmar (Burmese)
'ne': 'nep', # Nepali
'nl': 'nld', # Dutch, Flemish
'no': 'nor', # Norwegian
'pa': 'pan', # Punjabi, Panjabi
'ps': 'pus', # Pashto, Pushto
'pl': 'pol', # Polish
'pt': 'por', # Portuguese
'ro': 'ron', # Romanian, Moldovan
'ru': 'rus', # Russian
'es': 'spa', # Spanish
'sd': 'snd', # Sindhi
'si': 'sin', # Sinhala
'sk': 'slk', # Slovak
'sl': 'slv', # Slovenian
'sr': 'srp', # Serbian
'su': 'sun', # Sundanese
'sw': 'swa', # Swahili
'sv': 'swe', # Swedish
'ta': 'tam', # Tamil
'te': 'tel', # Telugu
'tg': 'tgk', # Tajik
'th': 'tha', # Thai
'tr': 'tur', # Turkish
'uk': 'ukr', # Ukrainian
'ur': 'urd', # Urdu
'ug': 'uig', # Uyghur
'uz': 'uzb', # Uzbek (check if Cyrilic version, which is uzb_cyrl)
'vi': 'vie', # Vietnamese
'yi': 'yid', # Yiddish
'yo': 'yor' # Yoruba
}
def get_path(filename):
if hasattr(sys, "_MEIPASS"):
return os.path.join(sys._MEIPASS, filename)
else:
return filename
os.environ["TESSDATA_PREFIX"] = str(get_path(r'Tesseract-OCR\tessdata'))
# pytesseract requires tesseract exe, location provided for bundling with pyinstaller package
pt.pytesseract.tesseract_cmd = get_path(r'Tesseract-OCR\tesseract.exe')
def update_lang_dict():
j = 0
for key in LANGUAGES:
LANGUAGES[key] = old_language_list[j]
j += 1
def stop_threads_true():
"""
Exit all threads on program exit.
"""
global stop_threads
stop_threads = True
return True
def center_window(self, boolean=True):
"""
Spawn new window in center of screen. Default to True for main window, False for other windows
will cause them to center on the main window and not the screen.
"""
if boolean is True:
self.tk.eval(f'tk::PlaceWindow {self._w} center')
else:
self.tk.eval(f'tk::PlaceWindow {self._w} widget {self.master}')
self.attributes('-topmost', True)
def set_click_through(hwnd):
"""
Make screen grab window not interactable.
"""
try:
styles = WS_EX_LAYERED | WS_EX_TRANSPARENT
SetWindowLong(hwnd, GWL_EXSTYLE, styles)
SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA)
except Exception:
pass
def make_title_bar(self):
"""
Use to make custom title bar that matches visual theme of the program.
"""
# Create custom window title bar to match theme.
self.overrideredirect(True)
title_bar = ttk.Frame(self, borderwidth=3)
# close root to close program if on main window, else close window for toplevel windows
if isinstance(self, App):
close_button = ttk.Button(title_bar, text='X', width=1,
command=lambda: [self.close_other_windows(), self.close_threads()])
else:
close_button = ttk.Button(title_bar, text='X', width=1,
command=self.reset_master_box)
icon = ImageTk.PhotoImage(Image.open(get_path("20.png")))
icon_label = ttk.Label(title_bar, image=icon)
icon_label.image = icon
window_title = ttk.Label(title_bar, text="" + title_string)
title_bar.pack(expand=False, fill=tk.X, side=tk.TOP)
close_button.pack(side=tk.RIGHT)
# minimize self through root
if isinstance(self, App):
mini_button = ttk.Button(title_bar, text='__', width=1, command=self.master.iconify)
mini_button.pack(side=tk.RIGHT)
else:
mini_button = ttk.Button(title_bar, text='__', width=1, command=self.hidden_window.iconify)
mini_button.pack(side=tk.RIGHT)
icon_label.pack(side=tk.LEFT)
window_title.pack(side=tk.LEFT)
# Return drag functionality to custom title bar.
window_title.bind('<B1-Motion>', lambda event: App.move_window(self, event))
window_title.bind('<ButtonPress-1>', lambda event: App.click_window(self, event))
title_bar.bind('<B1-Motion>', lambda event: App.move_window(self, event))
title_bar.bind('<ButtonPress-1>', lambda event: App.click_window(self, event))
# Divide the title bar space off from the main window space.
separator = tk.Frame(self, height=1, borderwidth=0, bg='#373737')
separator.pack(fill=tk.X, padx=5)
separator_underline = tk.Frame(self, height=1, borderwidth=0, bg='#414141')
separator_underline.pack(fill=tk.X, padx=5)
def toggle_slider(boolean, slide):
"""
Use to disable or enable horizontal sliders depending on user specification through checkboxes.
"""
if boolean is True:
slide.config(state='enabled')
else:
slide.config(state='disabled')
def update_display(label, boolean, tag="", int_flag=True):
"""
Updates number displayed alongside horizontal slide bars.
Use int_flag to specify int or float conversions (floats will display with 1 decimal point).
"""
if int_flag is True:
label.config(text=f"{tag}{int(boolean)}")
else:
label.config(text=f"{tag}{round(float(boolean), 1)}")
class IntegerEntry(ttk.Entry):
"""
Entry box that checks for float input and overwrites invalid input.
Used to determine sample time of grab window.
"""
def __init__(self, master, string):
self.var = tk.StringVar(value=string)
ttk.Entry.__init__(self, master, textvariable=self.var)
self.reset_value = string
# Thread for listening while input box is empty.
self.th = Thread(target=self.check_thread_helper, daemon=True)
if not self.th.is_alive():
self.th.start()
def check_input(self):
"""
Actual check input logic for thread to run.
"""
try:
if float(self.get()) > 0:
# Input is numbers, change value to reset to to be current input.
self.reset_value = self.get()
else:
# Input not numbers, replace value with smallest value.
self.delete(0, tk.END)
self.insert(0, '0.1')
except ValueError:
# Input not numbers, replace value with last good input.
self.delete(0, tk.END)
self.insert(0, self.reset_value)
def check_thread_helper(self):
"""
For thread to run to keep program responsive.
"""
# If empty, wait until box not in focus to refill so user has time to delete all text and re-enter.
try:
while stop_threads is False:
while self.master.master.focus_get() is self:
if self.get() == '':
pass
else:
self.check_input()
self.check_input()
except KeyError: # Python bug, tkinter doesn't understand popdown arrow on Combobox
pass
def close_thread(self):
if self.th.is_alive():
self.th.join()
class Root(ThemedTk):
"""
Make root window default to transparent. This root window will allow the program
to show on the task bar despite using overrideredirect for the actual main window.
When the main window is minimized, it will not show on the taskbar as a result of using
that method for more control over UI appearance.
"""
def __init__(self):
ThemedTk.__init__(self, theme='equilux', background=True, toplevel=True)
self.attributes('-alpha', 0.0)
self.title(title_string)
self.tk.call('wm', 'iconphoto', self._w, tk.PhotoImage(
file=get_path("24.png")))
self.bind("<Map>", lambda event: Root.on_root_deiconify(self, event))
self.bind("<Unmap>", lambda event: Root.on_root_iconify(self, event))
self.app = App(self)
self.app.mainloop()
def on_root_deiconify(self, event):
"""
Show main window if invisible root window is clicked from task bar.
"""
self.app.deiconify()
def on_root_iconify(self, event):
"""
Minimize main window if invisible root window is minimized.
"""
self.app.withdraw()
class App(tk.Toplevel):
"""
Main program window.
* User can press a button to initiate area select mode.
* User can adjust opacity of select area.
* User can select target language.
* User can determine if they would like the translation to appear overlaid
on the screen grab area or if they would like a separate window to read
the translation in.
* User can set time intervals for sampling grab area.
* User can open options adjustment window to help pytesseract read text from image.
* User can close all windows except main window.
"""
def __init__(self, master):
# Create window as a top level. Tk windows are reserved for roots.
tk.Toplevel.__init__(self, master)
# Make window spawn in center of screen.
center_window(self)
# Screen overlay present during user draw
self.overlay_window = None
# Translate area that will remain on screen
self.grab_window = None
self.grab_opacity = '0.1'
self.text_size = '9'
# Options window for user to adjust image settings for text reading
self.options_window = None
# Thread for image grab window
self.t = None
# Separate window for translation text
self.text_window = None
# Boolean for use thresholding & other options on image
self.thresholding_boolean = False
self.threshold = '100'
self.inversion_boolean = False
self.resize_boolean = False
self.contrast_boolean = False
self.resize = '1'
self.contrast = '1'
self.text_window_boolean = tk.BooleanVar()
self.text_window_boolean.set(True)
self.src_lang = tk.StringVar(self)
self.src_lang.set('Auto')
self.src_lang_boolean = tk.BooleanVar()
self.src_lang_boolean.set(False)
self.invert_grab_window = tk.BooleanVar()
self.invert_grab_window.set(True)
# For hiding on overlap of grab window
self.hidden = False
# Click position on title bar to be used for dragging.
self.x_pos = 0
self.y_pos = 0
make_title_bar(self)
master_frame = ttk.Frame(self)
# Add language choice dropdown.
# Color combobox dropdowns on this window to match Equilux theme.
self.option_add('*TCombobox*Listbox.background', self['background'])
self.option_add('*TCombobox*Listbox.foreground', '#a6a6a6')
# User-determined language to translate into
self.target_lang = tk.StringVar(self)
self.target_lang.set('English') # default value for dropdown is English
language_frame = ttk.Frame(master_frame)
language_label = ttk.Label(language_frame, text="Translate to:")
language_dropdown = ttk.Combobox(language_frame, state='readonly', textvariable=self.target_lang,
values=language_list)
language_label.pack()
language_dropdown.pack()
# Checkbox if user wants to specify src language
self.src_lang_checkbox = ttk.Checkbutton(language_frame, text="Translate from:",
variable=self.src_lang_boolean, onvalue=True, offvalue=False,
command=self.toggle_src_lang_dropdown)
self.src_lang_checkbox.pack()
self.src_lang_dropdown = ttk.Combobox(language_frame, state='disabled', textvariable=self.src_lang,
values=language_list)
self.src_lang_dropdown.pack()
language_frame.pack(padx=5, pady=5)
# Divider
separator_frame = tk.Frame(master_frame)
separator = tk.Frame(separator_frame, height=1, borderwidth=0, bg='#373737')
separator.pack(fill=tk.X)
separator_underline = tk.Frame(separator_frame, height=1, borderwidth=0, bg='#414141')
separator_underline.pack(fill=tk.X)
separator_frame.pack(fill=tk.X, pady=10, padx=5)
# Add time interval selection for grab window.
time_selection_frame = ttk.Frame(master_frame)
time_selection_label = ttk.Label(time_selection_frame, text="Image Sample Interval (sec):")
self.time_selection_entry = IntegerEntry(time_selection_frame, '1') # Default sample time is once a second.
time_selection_label.pack(pady=(0, 2))
self.time_selection_entry.pack()
time_selection_frame.pack()
# Change opacity of grab window option
self.grab_opacity_slide = ttk.Scale(master_frame, from_=0, to=1, orient='horizontal')
self.grab_opacity_slide.set(self.grab_opacity)
self.grab_opacity_label = ttk.Label(master_frame, text=f"Selection Opacity: "
f"{float(self.grab_opacity_slide.get())}")
self.grab_opacity_slide.config(command=lambda x: [update_display(self.grab_opacity_label,
self.grab_opacity_slide.get(),
tag="Selection Opacity: ", int_flag=False),
self.update_grab_window_opacity()], state='disabled')
self.grab_opacity_label.pack(pady=(5, 0))
self.grab_opacity_slide.pack()
checkboxes_frame = ttk.Frame(master_frame)
# Change text size for overlay text window
self.text_size_slide = ttk.Scale(master_frame, from_=1, to=40,
orient='horizontal')
self.text_size_slide.set(self.text_size)
self.text_size_label = ttk.Label(master_frame, text=f"Font Size: {int(self.text_size_slide.get())}")
self.text_size_slide.config(command=lambda x: [update_display(self.text_size_label,
self.text_size_slide.get(),
tag="Font Size: ", int_flag=True),
self.update_text_size()], state='disabled')
self.text_size_label.pack(pady=(5, 0))
self.text_size_slide.pack()
checkboxes_frame = ttk.Frame(master_frame)
# Text box option instead of grab window option for text
self.window_checkbox = ttk.Checkbutton(checkboxes_frame, text="Text Window",
variable=self.text_window_boolean, onvalue=True, offvalue=False,
command=self.text_window_generate, state='disabled')
self.window_checkbox.pack(side=tk.TOP, anchor=tk.NW)
# Invert grab window option
self.invert_window_checkbox = ttk.Checkbutton(checkboxes_frame, text="Invert Selection",
variable=self.invert_grab_window, onvalue=True, offvalue=False,
command=self.invert_grab_window_func, state='disabled')
self.invert_window_checkbox.pack(side=tk.BOTTOM, anchor=tk.NW)
checkboxes_frame.pack()
# Divider
separator_frame = tk.Frame(master_frame)
separator = tk.Frame(separator_frame, height=1, borderwidth=0, bg='#373737')
separator.pack(fill=tk.X)
separator_underline = tk.Frame(separator_frame, height=1, borderwidth=0, bg='#414141')
separator_underline.pack(fill=tk.X)
separator_frame.pack(fill=tk.X, pady=10, padx=5)
# Add screen grab button and bind click + drag motion to it.
button_frame = ttk.Frame(master_frame)
area_select_button = ttk.Button(button_frame, text="Select Area", command=self.screen_grab)
area_select_button.pack()
# Add button to open settings adjustment window.
self.options_button = ttk.Button(button_frame, text="Image Options", command=self.options_window_open,
state='disabled')
self.options_button.pack()
# Close other windows button
close_windows_button = ttk.Button(button_frame, text="Close Windows", command=self.close_other_windows)
close_windows_button.pack()
button_frame.pack(padx=5, pady=3)
master_frame.pack(padx=15, pady=15)
def hide_all_windows(self, x_right, y_bot):
"""
Call from grab_window when taking an image. Checks if any windows are obscuring the grab area
and hides them while image grabbing.
"""
# Probably can put all the subwindows into an array if reformatting in future.
if self.grab_window is not None:
if self.grab_opacity_slide.get() > 0:
self.grab_window.cv.pack_forget()
o_array = self.geometry().replace('x', '+').split('+')
array = [eval(item) for item in o_array]
if (self.grab_window.x_min <= array[2] <= x_right) or (
self.grab_window.x_min <= (array[0] + array[2]) <= x_right):
if (self.grab_window.y_min <= array[3] <= y_bot) or (
self.grab_window.y_min <= (array[1] + array[3]) <=
y_bot):
if self.state() == 'withdrawn':
self.hidden = True
else:
self.hidden = False
self.withdraw()
if self.text_window is not None and self.text_window.winfo_exists():
# Check if left side or right side of window is between left/right bounds of grab window
# format widthxheight+xcoord+ycoord
o_array = self.text_window.geometry().replace('x', '+').split('+')
array = [eval(item) for item in o_array]
if (self.grab_window.x_min <= array[2] <= x_right) or (
self.grab_window.x_min <= (array[2] + array[0]) <=
x_right):
if (self.grab_window.y_min <= array[3] <= y_bot) or (
self.grab_window.y_min <= (array[3] + array[1]) <=
y_bot):
if self.text_window.state() == 'withdrawn':
self.text_window.hidden = True
else:
self.text_window.hidden = False
self.text_window.withdraw()
if self.options_window is not None and self.options_window.winfo_exists():
o_array = self.options_window.geometry().replace('x', '+').split('+')
array = [eval(item) for item in o_array]
if self.grab_window.x_min <= array[0] <= x_right or (
self.grab_window.x_min <= (array[0] + array[2]) <= x_right):
if (self.grab_window.y_min <= array[1] <= y_bot) or (
self.grab_window.y_min <= array[1] + array[3] <= y_bot):
self.options_window.attributes('-alpha', 0)
def show_all_windows(self, x_right, y_bot):
"""
Call from grab_window after done taking image. Makes all hidden windows visible again.
"""
if self.grab_window is not None:
if self.grab_opacity_slide.get() > 0:
self.grab_window.cv.pack()
if not self.hidden:
o_array = self.geometry().replace('x', '+').split('+')
array = [eval(item) for item in o_array]
if self.grab_window.x_min <= array[0] <= x_right or (
self.grab_window.x_min <= (array[0] + array[2]) <= x_right):
if (self.grab_window.y_min <= array[1] <= y_bot) or (
self.grab_window.y_min <= array[1] + array[3] <= y_bot):
self.deiconify()
if self.text_window is not None and self.text_window.winfo_exists():
if not self.text_window.hidden:
o_array = self.text_window.geometry().replace('x', '+').split('+')
array = [eval(item) for item in o_array]
if self.grab_window.x_min <= array[0] <= x_right or (
self.grab_window.x_min <= (array[0] + array[2]) <= x_right):
if (self.grab_window.y_min <= array[1] <= y_bot) or (
self.grab_window.y_min <= array[1] + array[3] <= y_bot):
self.text_window.deiconify()
if self.options_window is not None and self.options_window.winfo_exists():
self.options_window.attributes('-alpha', 1)
def toggle_src_lang_dropdown(self):
if self.src_lang_boolean.get() is True:
self.src_lang_dropdown.config(state='readonly')
else:
self.src_lang_dropdown.config(state='disabled')
self.src_lang.set('Auto')
def update_grab_window_opacity(self):
"""
Change grab window opacity in real time with slider.
"""
self.grab_opacity = self.grab_opacity_slide.get()
self.grab_window.attributes("-alpha", float(self.grab_opacity))
def update_text_size(self):
"""
Change text size for grab window in real time with slider.
"""
self.text_size = self.text_size_slide.get()
self.grab_window.update_text_size(self.text_size)
def text_window_generate(self):
"""
Open or close separate window to display translated text.
"""
# if box unchecked, delete the text window
if self.text_window_boolean.get() is False:
self.text_window.destroy()
else:
# if box checked, create a text window...but if there is already a text window,
# create a text window with previous position coordinates so user can move the text window and
# spawn a new one, but have the new one stay in the same area.
if self.text_window is None:
self.text_window = TextWindow(self.grab_window.return_size().replace('x', '+'), self)
elif self.text_window.winfo_exists():
array = self.grab_window.return_size().replace('x', '+').split('+')
position = array[0] + '+' + array[1] + self.text_window.return_pos()
self.text_window.destroy()
self.text_window = TextWindow(position, self)
else:
self.text_window = TextWindow(self.grab_window.return_size().replace('x', '+'), self)
self.text_window_boolean.set(True)
def invert_grab_window_func(self):
"""
Invert color of text window overlay (default is white background with black text).
"""
self.grab_window.invert.set(self.invert_grab_window.get())
self.grab_window.refresh_color()
def close_other_windows(self):
"""
From main app window, close other extra windows and close threads.
"""
if self.overlay_window is not None and self.overlay_window.winfo_exists():
self.overlay_window.destroy()
if self.grab_window is not None and self.grab_window.winfo_exists():
self.grab_window.destroy()
self.options_button_disable(True)
if self.options_window is not None and self.options_window.winfo_exists():
self.options_window.destroy()
if self.text_window is not None and self.text_window.winfo_exists():
self.text_window.destroy()
def options_button_disable(self, boolean):
"""
Disable options menu, text window checkbox, opacity slider,
text size slider unless grab window exists.
"""
if boolean is False:
self.options_button.config(state='normal')
self.window_checkbox.config(state='normal')
self.grab_opacity_slide.config(state='normal')
self.text_size_slide.config(state='normal')
self.invert_window_checkbox.config(state='normal')
else:
self.options_button.config(state='disabled')
self.window_checkbox.config(state='disabled')
self.grab_opacity_slide.config(state='disabled')
self.text_size_slide.config(state='disabled')
self.invert_window_checkbox.config(state='disabled')
def click_window(self, event):
"""
Helper method for move_window to allow title bar to be dragged by click location and
not by top left corner.
"""
self.x_pos = event.x
self.y_pos = event.y
def move_window(self, event):
"""
Allow the main window to be dragged by the title bar despite using overrideredirect.
"""
self.geometry(f'+{event.x_root - self.x_pos}+{event.y_root - self.y_pos}')
def options_window_open(self):
"""
Open window to allow user to adjust image options. Options affect whether pytesseract can process
text from screen grab.
"""
self.options_window = OptionsWindow(self)
# Stop user from interacting with main window until options are closed.
self.options_window.grab_set()
self.options_window.wait_window()
def screen_grab(self):
"""
When user presses button to set up translate area, create overlay that covers screen
and allows user to draw a rectangle.
"""
self.overlay_window = OverlayWindow(self)
def create_grab_window(self, stored_values):
"""
Create screen grab window after overlay window used to draw rectangle.
"""
self.overlay_window.destroy()
self.grab_window = GrabWindow(stored_values, self)
# Create thread for the image grabbing window loop.
self.t = Thread(target=GrabWindow.screen_grab_loop, args=(
self.grab_window,), daemon=True)
if not self.t.is_alive():
self.t.start()
self.text_window_generate()
def close_threads(self):
stop_threads_true()
self.master.destroy()
class TextWindowHidden(tk.Toplevel):
"""
Hidden window to make text window minimizable and show up on task bar.
"""
def __init__(self, master):
tk.Toplevel.__init__(self, master)
self.attributes('-alpha', 0.0)
self.tk.call('wm', 'iconphoto', self._w, tk.PhotoImage(
file=get_path("24.png")))
self.bind("<Unmap>", lambda event: self.on_iconify(event))
self.bind("<Destroy>", lambda event: self.on_destroy(event))
self.bind("<FocusIn>", lambda event: self.on_deiconify(event))
def on_destroy(self, event):
"""
When hidden window is destroyed from taskbar, also destroy text window and reset master check box.
"""
self.master.reset_master_box()
def on_deiconify(self, event):
"""
Show main window if invisible root window is clicked from task bar.
"""
self.master.deiconify()
def on_iconify(self, event):
"""
Minimize main window if invisible root window is minimized.
"""
self.master.withdraw()
class TextWindow(tk.Toplevel):
"""
Window to display translated text if user selects option to use.
"""
def __init__(self, position, master):
tk.Toplevel.__init__(self, master)
self.height = 1
self.hidden_window = TextWindowHidden(self)
self.size(position)
self.hidden = False
# Click position on title bar to be used for dragging.
self.x_pos = 0
self.y_pos = 0
make_title_bar(self)
# Create text attributes
self.translation = self.master.grab_window.get_translation()
self.text = self.master.grab_window.get_text()
self.translated_input = ''
self.target_lang = ''
self.src_lang = ''
# Create tabs
main_frame = ttk.Notebook(self)
tab1 = ttk.Frame(main_frame)
tab2 = ttk.Frame(main_frame)
tab3 = ttk.Frame(main_frame)
main_frame.add(tab3, text='Input') # input first because user can use grabwindow for reading?
main_frame.add(tab2, text='Translation') # adding tab2 first because it's more relevant
main_frame.add(tab1, text='Source Text')
# Tab 3 (for user to translate inputs)
top_frame = ttk.Frame(tab3, height=self.height // 3)
bottom_frame = ttk.Frame(tab3, height=self.height // 2)
# user source translated
self.top_text_label_self = ttk.Label(top_frame, text=self.target_lang)
self.top_text_label_self.pack(side=tk.TOP, pady=5)
self_copy_button = ttk.Button(top_frame, text='Copy Text', command=lambda: self.copy_to_clip(self.translated_input))
self_copy_button.pack(side=tk.BOTTOM, pady=5)
scroll_frame3 = ttk.Frame(top_frame)
scroll3 = ttk.Scrollbar(scroll_frame3, orient=tk.VERTICAL)
scroll3.pack(side=tk.RIGHT, fill=tk.Y)
self.text_label_self = tk.Text(scroll_frame3, bg='#464646', bd=0, cursor='arrow', font='TkDefaultFont',
fg='#a6a6a6', insertbackground='#a6a6a6',
padx=25, yscrollcommand=scroll3.set)
self.text_label_self.pack(expand=True, fill=tk.BOTH, pady=5)
self.text_label_self.insert(tk.END, self.translated_input)
scroll_frame3.pack(expand=True, fill=tk.BOTH)
top_frame.pack(side=tk.TOP, fill=tk.X)
top_frame.pack_propagate(0)
# user source
self.top_lang_label_self = ttk.Label(bottom_frame, text=self.master.src_lang.get())
self.top_lang_label_self.pack(side=tk.TOP, pady=5)
scroll_frame4 = ttk.Frame(bottom_frame)
scroll4 = ttk.Scrollbar(scroll_frame4, orient=tk.VERTICAL)
scroll4.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
self.lang_label_self = tk.Text(scroll_frame4, bg='#545454', bd=0, font='TkDefaultFont',
fg='#a6a6a6', insertbackground='#a6a6a6',
padx=5, pady=5, yscrollcommand=scroll4.set)
self.lang_label_self.pack(expand=True, fill=tk.BOTH, pady=5)
scroll_frame4.pack(expand=True, fill=tk.BOTH)
bottom_frame.pack(padx=20, pady=10, side=tk.BOTTOM, fill=tk.X)
bottom_frame.pack_propagate(0)
# Tab 1 (Source)
self.lang_label = ttk.Label(tab1, text=self.src_lang)
self.lang_label.pack(side=tk.TOP, pady=5)
original_copy_button = ttk.Button(tab1, text='Copy Text', command=lambda: self.copy_to_clip(self.text))
original_copy_button.pack(side=tk.BOTTOM, pady=5)
scroll_frame1 = ttk.Frame(tab1)
scroll1 = ttk.Scrollbar(scroll_frame1, orient=tk.VERTICAL)
scroll1.pack(side=tk.RIGHT, fill=tk.Y)
self.text_label = tk.Text(scroll_frame1, bg='#464646', bd=0, cursor='arrow', font='TkDefaultFont',
fg='#a6a6a6', insertbackground='#a6a6a6',
padx=10, yscrollcommand=scroll1.set)
self.text_label.pack(expand=True, fill=tk.BOTH, pady=5)
self.text_label.insert(tk.END, self.text)
scroll_frame1.pack(expand=True, fill=tk.BOTH)
# Tab 2 (Translated)
self.target_label = ttk.Label(tab2, text=self.target_lang)
self.target_label.pack(side=tk.TOP, pady=5)
scroll_frame2 = ttk.Frame(tab2)
scroll2 = ttk.Scrollbar(scroll_frame2, orient=tk.VERTICAL)
scroll2.pack(side=tk.RIGHT, fill=tk.Y)
translated_copy_button = ttk.Button(tab2, text='Copy Text', command=lambda: self.copy_to_clip(self.translation))
translated_copy_button.pack(side=tk.BOTTOM, pady=5)
self.translation_label = tk.Text(scroll_frame2, bg='#464646', bd=0, cursor='arrow', font='TkDefaultFont',
fg='#a6a6a6', yscrollcommand=scroll2.set, insertbackground='#a6a6a6',
padx=10)
self.translation_label.pack(expand=True, fill=tk.BOTH, pady=5)
self.translation_label.insert(tk.END, self.translation)
scroll_frame2.pack(expand=True, fill=tk.BOTH)
main_frame.pack(fill=tk.BOTH, expand=True, pady=2)
def translate_input(self, translated_input):
"""
Set translation of user input text.
"""
self.translated_input = translated_input
self.text_label_self.delete(1.0, tk.END)
self.text_label_self.insert(tk.END, self.translated_input)
def get_input(self):
"""
Get user input text for translation.
"""
return self.lang_label_self.get("1.0", 'end-1c')
def copy_to_clip(self, text):
"""
Allow user to press button and copy text to clipboard.
"""
self.clipboard_clear()
self.clipboard_append(text)
def return_pos(self):
"""
Use for remaking translation text box if user selects new grab area while text window open.
"""
return str(f'+{self.winfo_x()}+{self.winfo_y()}')
def size(self, position):
"""
Set window dimensions. Minimum height and width set based on widgets in window and selection size.
"""
dimensions = position
dimensions_array = dimensions.split('+')
# Add headspace for title bar, other things, tabs in window size
dimensions_array[0] = int(dimensions_array[0]) # width
dimensions_array[1] = int(dimensions_array[1]) # height
if dimensions_array[0] < 155:
dimensions_array[0] = 155
if dimensions_array[1] < 138:
dimensions_array[1] = 138 + dimensions_array[1]
self.geometry(
f'{dimensions_array[0]}x{dimensions_array[1]}+'
f'{dimensions_array[2]}+{dimensions_array[3]}')
self.height = dimensions_array[1]
def reset_master_box(self):
"""
On close, reset master's checkbox to match state of window (closed).
"""
self.master.text_window_boolean.set(False)
self.destroy()
def update_translation(self):
self.text = self.master.grab_window.get_text()
self.text_label.delete(1.0, tk.END)
self.text_label.insert(tk.END, self.text)
self.translation = self.master.grab_window.get_translation()
self.translation_label.delete(1.0, tk.END)
self.translation_label.insert(tk.END, self.translation)
self.target_lang = self.master.grab_window.get_target_lang()
self.target_label.config(text=self.target_lang)
self.src_lang = self.master.grab_window.get_src_lang()
self.lang_label.config(text=self.src_lang)
self.top_text_label_self.config(text=self.target_lang) # bottom
self.top_lang_label_self.config(text=self.src_lang) # top
class OverlayWindow(tk.Toplevel):
"""
Fullscreen overlay that indicates the user is in area select mode. Is transparent and covered by
a canvas that the user can draw a rectangle on. Window will close itself once rectangle is complete
and a new screen grab window will spawn in the location of the rectangle selection.
"""
def __init__(self, master):
tk.Toplevel.__init__(self, master)
# Clear old screen grab windows if any exist.
if self.master.grab_window is not None:
self.master.grab_window.destroy()
self.master.grab_window = None
# Map containing first click location and release location.
self.stored_values = {'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0}
# Rectangle that user will draw
self.rect = None
# Create screen overlay to alert user that they are in drag mode.
self.attributes('-fullscreen', True, '-alpha', 0.3, '-topmost', True)
# prevent window from being closed by regular means, only guaranteed to work in Windows
self.overrideredirect(True)
# Create canvas, bind left click down, drag, and release.
self.cv = tk.Canvas(self, cursor="cross", width=self.winfo_screenwidth(),
height=self.winfo_screenheight())
self.cv.bind("<ButtonPress-1>", lambda event: OverlayWindow.mouse_down(self, event))
self.cv.bind("<B1-Motion>", lambda event: OverlayWindow.mouse_down_move(self, event))
self.cv.bind("<ButtonRelease-1>", lambda event: OverlayWindow.mouse_up(self, event))
self.cv.pack()
def mouse_down(self, event):
"""
When mouse clicked on screen overlay window, save location.
If rectangle not made yet, make rectangle so user can see where they are selecting.
"""
self.stored_values['x1'] = event.x
self.stored_values['y1'] = event.y
self.rect = self.cv.create_rectangle(self.stored_values['x1'],
self.stored_values['y1'], 1, 1, fill="")
def mouse_down_move(self, event):
"""
As mouse moves on screen overlay window while left click held down, save and update location.
Update rectangle so user can see where they are selecting. Will not update if user clicks without
dragging. The user will have to open another overlay window and drag a new selection window if they
click without dragging.
"""
self.stored_values['x2'] = event.x
self.stored_values['y2'] = event.y
self.cv.coords(self.rect, self.stored_values['x1'], self.stored_values['y1'],
self.stored_values['x2'], self.stored_values['y2'])
def mouse_up(self, event):
"""
Create window for screenshot location. Close overlay window. Resize text window if option is up.
"""
App.create_grab_window(self.master, self.stored_values)
# if self.master.text_window is not None:
# self.master.text_window_generate()
class GrabWindow(tk.Toplevel):
"""
Create a transparent rectangle that displays selected area to be translated.
Window remains on top of other windows and is not interactable.
"""
def __init__(self, stored_values, master):
tk.Toplevel.__init__(self, master)
self.overrideredirect(True)
# Enable options button in main app window
App.options_button_disable(self.master, False)
# Determine window location (top left corner) and dimensions.
self.x_min = min(stored_values['x1'], stored_values['x2'])