├── VarelaRound-Regular.ttf
├── README.md
├── WordSearchPuzzleGen.py
└── WordSearchPuzzleGUI.py
/VarelaRound-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/K9Developer/WordSearchPuzzleGenerator/HEAD/VarelaRound-Regular.ttf
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Word Search Puzzle Generator
12 |
13 |
14 |
15 |
16 |
17 |
18 | Table of Contents
19 |
20 | -
21 | About The Project
22 |
25 |
26 | -
27 | Getting Started
28 |
32 |
33 | - Usage
34 | - Roadmap
35 | - Contributing
36 | - License
37 | - Contact
38 | - Acknowledgments
39 |
40 |
41 |
42 |
43 |
44 |
45 | ## About The Project
46 |
47 | [About WSPG](https://github.com/KingOfTNT10/WordSearchPuzzleGenerator)
48 |
49 | I started this project because believe it or not in school we had an assignment to do a search word puzzle, but
50 | I didn't know if there's diagonals in the puzzle, so I was a bit frustrated, So I asked myself: "I could do that better"
51 | Well first I hope I was right and second it wasn't easy, not at all... But I learnt a lot from that project
52 | for example working with images etc.
53 | and I'm very grateful for that
54 |
55 | (back to top)
56 |
57 |
58 |
59 | ### Built With
60 |
61 | * [Python](https://www.python.org/)
62 |
63 |
64 | (back to top)
65 |
66 |
67 |
68 |
69 | ## Getting Started
70 |
71 | This is an example of how you may give instructions on setting up your project locally.
72 | To get a local copy up and running follow these simple example steps.
73 |
74 | ### Prerequisites
75 |
76 | This is an example of how to list things you need to use the software and how to install them.
77 | * pywin32
78 | ```sh
79 | pip install pywin32
80 | ```
81 | * webbrowser
82 | ```sh
83 | pip install webbrowser
84 | ```
85 | * Pillow / PIL
86 | ```sh
87 | pip install Pillow
88 | ```
89 | * PySimpleGui
90 | ```sh
91 | pip install PySimpleGui
92 | ```
93 | * PyQt5
94 | ```sh
95 | pip install PyQt5
96 | ```
97 | * textwrap
98 | ```sh
99 | pip install textwrap3
100 | ```
101 |
102 | ### Installation
103 |
104 | 1. Clone the repo
105 | ```sh
106 | git clone https://github.com/KingOfTNT10/WordSearchPuzzleGenerator
107 | ```
108 |
109 | (back to top)
110 |
111 |
112 |
113 |
114 | ## Usage
115 |
116 | To run the software you need to run the GUI file (WordSearchPuzzleGUI.py)
117 | then you can edit all the options that relate to the puzzle
118 | 
119 | And then you have the next options
120 | * Save
121 | * Copy
122 | * Print
123 | * Save config
124 |
125 | The image looks like this:
126 | 
127 |
128 |
129 |
130 |
131 | (back to top)
132 |
133 |
134 |
135 |
136 | ## Roadmap
137 |
138 | - [ ] Playable With Computer
139 | - [ ] Mark first word
140 | - [ ] Changeable font
141 | - [ ] More options for GUI
142 | - [ ] Changeable theme
143 | - [ ] General options
144 |
145 | See the [open issues](https://github.com/KingOfTNT10/WordSearchPuzzleGenerator/issues) for a full list of proposed features (and known issues).
146 |
147 | (back to top)
148 |
149 |
150 |
151 |
152 | ## Contributing
153 |
154 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
155 |
156 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
157 | Don't forget to give the project a star! Thanks again!
158 |
159 | 1. Fork the Project
160 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
161 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
162 | 4. Push to the Branch (`git push origin feature/AmazingFeature`)
163 | 5. Open a Pull Request
164 |
165 | (back to top)
166 |
167 |
168 | (back to top)
169 |
170 |
171 |
172 |
173 | ## Contact
174 |
175 | Gmail - KingOfTNT10@gmail.com
176 |
177 | Project Link: [https://github.com/KingOfTNT10/WordSearchPuzzleGenerator](https://github.com/KingOfTNT10/WordSearchPuzzleGenerator)
178 |
179 | (back to top)
180 |
--------------------------------------------------------------------------------
/WordSearchPuzzleGen.py:
--------------------------------------------------------------------------------
1 | import random
2 | import secrets
3 | import textwrap
4 |
5 | from PIL import Image, ImageDraw, ImageFont
6 | from fontTools.ttLib import TTFont
7 |
8 | multiplier = 2
9 | global_font = 'VarelaRound-Regular.ttf'
10 |
11 |
12 | def create_grid(cells_in_row=10, cell_size=10, line_color=(0, 0, 0), background_color=(255, 255, 255), line_width=10,
13 | high_res=False):
14 | """
15 | Creates a grid as an image according to some parameters.
16 |
17 | :param cells_in_row: The number of cells/squares in a row/column in the grid
18 | :type cells_in_row: int
19 | :param cell_size: The size of each cell in the grid
20 | :type cell_size: int
21 | :param line_color: The line color of the separator lines of the grid
22 | :type line_color: Tuple(int, int, int) | str
23 | :param background_color: The background color of the grid
24 | :type background_color: Tuple(int, int, int) | str
25 | :param line_width: The line width of the separators of the grid
26 | :type line_width: int
27 | :param high_res: True if the resolution multiplier is bigger than 2
28 | :type high_res: bool
29 | :returns: (The grid as a PIL image, A coord set of all the coordinates of the cells in the grid)
30 | :rtype: Tuple(PIL.Image, Dict{List[Tuple(int, int)]})
31 | """
32 |
33 | # Lowers the resolutions of the grid because it can be resized and it wont change the look
34 | ml = multiplier // 2
35 |
36 | # Setup variables
37 | cell_size = cell_size * ml
38 | line_width = int(line_width)
39 | coords = {}
40 | coord_set = {}
41 | counter = 0
42 |
43 | # Creating a new image to draw on
44 | dim = cell_size * cells_in_row * ml
45 | img = Image.new('RGB', (dim, dim), color=background_color)
46 |
47 | width, height = img.size
48 |
49 | # Initializes the ImageDraw.Draw for the img so I can draw on it
50 | img_draw = ImageDraw.Draw(img)
51 |
52 | # Looping and drawing lines horizontally for the grid
53 | for i in range(cells_in_row):
54 | shape = [(0, i * cell_size * ml), (width, i * cell_size * ml)]
55 | img_draw.line(shape, fill=line_color, width=line_width * int(ml / 2))
56 |
57 | # Looping and drawing lines vertically for the grid
58 | for i in range(cells_in_row):
59 | shape = [(i * cell_size * ml, 0), (i * cell_size * ml, height)]
60 | img_draw.line(shape, fill=line_color, width=line_width * int(ml / 2))
61 |
62 | # Drawing the left lines on the outline of the grid
63 | img_draw.line([(0, height - 1), (width, height - 1)], fill=line_color, width=line_width * int(ml / 2))
64 | img_draw.line([(width - 1, 0), (width - 1, height)], fill=line_color, width=line_width * int(ml / 2))
65 |
66 | # Getting positions of each cell and adding to a 3d array && dict
67 | if high_res:
68 | ml = multiplier + 2
69 | else:
70 | ml = multiplier + 1
71 | for y in range(0, cells_in_row):
72 | y = (y * cell_size * ml + int((cell_size / 2 * ml))) + ml ** 2 + int(cell_size // 2)
73 | counter += 1
74 | for x in range(0, cells_in_row):
75 | x = (x * cell_size * ml + int((cell_size / 2 * ml)))
76 | coord_set[(x, y)] = None
77 | coords[counter] = coord_set
78 | coord_set = {}
79 |
80 | return img, coords
81 |
82 |
83 | def check(coords, start_coord: tuple, word: str, words, allow_diagonal):
84 | """
85 | Checks if a word can be placed on the grid in a certain spot.
86 |
87 | :param coords: A 3d array && dict of all the coords on the grid
88 | :type coords: Dict{List[Tuple(int, int)]}
89 | :param start_coord: A random coordinate on the grid that the check will start from
90 | :type start_coord: Tuple(int, int)
91 | :param word: The word to check if possible and where possible to place the chars of the word
92 | :type word: str
93 | :param words: The list of all words that was given to the program
94 | :type words: List[str, ...]
95 | :param allow_diagonal:
96 | :type allow_diagonal: bool
97 | :returns: If the function found valid spots for the word to be placed in it will return the coords
98 | if not it will return None
99 | :rtype: List[Tuple(int, int), ...] | None
100 | """
101 |
102 | def check_right():
103 |
104 | """
105 | Checks if the word can be placed to the right of the start_coord.
106 |
107 | :returns: The valid coords List[Tuple(int, int)] if it has a valid place to be placed at and return None if it
108 | cannot be placed
109 | :rtype: List[Tuple(int, int)] | None
110 | """
111 |
112 | # Setup variables
113 | coord_set = []
114 | cc = 0
115 | good_coords = []
116 |
117 | # Gets the coord_set that contains the start coord
118 | for i in coords:
119 | if start_coord in list(coords[i].keys()):
120 | coord_set = coords[i]
121 |
122 | for c, i in enumerate(coord_set):
123 |
124 | # Checks if the loop has passed the start coord (so it wont give coords to the left of the start coord)
125 | # so it can start checking
126 | if c >= list(coord_set.keys()).index(start_coord):
127 |
128 | # Checks if the char in the currently processed cell is None (empty)
129 | # or is the same as the char in that cell
130 | if coord_set[list(coord_set.keys())[c]] is None or \
131 | coord_set[list(coord_set.keys())[c]] == list(word)[cc]:
132 |
133 | cc += 1
134 | # Appends the position if it has passed all the checks
135 | good_coords.append(list(coord_set.keys())[c])
136 |
137 | # Checks if we got the right number of positions (number of chars in the word) if there's the right
138 | # number it will break out of the loop so I wont get a coord list that is longer than the word
139 | if len(good_coords) == len(word):
140 | break
141 | else:
142 | break
143 |
144 | # If we have enough positions it will return them if not it will return None which means the word cannot be
145 | # placed there (from the start_coord to the right)
146 | if len(good_coords) == len(word):
147 | return good_coords
148 | else:
149 | return None
150 |
151 | def check_down():
152 |
153 | """
154 | Checks if the word can be placed down of the start_coord.
155 |
156 | :returns: The valid coords List[Tuple(int, int)] if it has a valid place to be placed at and return None if it
157 | cannot be placed
158 | :rtype: List[Tuple(int, int)] | None
159 | """
160 |
161 | # Setup variables
162 | index = 0
163 | coord_set = 0
164 | cc = 0
165 | good_coords = []
166 |
167 | # Gets the index of the start_coord in the coord_set and sets
168 | # a variable that contains the coord_set of the start_coord
169 | for i in coords:
170 | if start_coord in coords[i]:
171 | index = list(coords[i]).index(start_coord)
172 | coord_set = i
173 | break
174 |
175 | for counter, ind in enumerate(coords):
176 |
177 | # Checks if the index of the currently processed cell is bigger than
178 | # the index of the coord_set of the start_coord
179 | if ind >= coord_set:
180 |
181 | # Checks if the currently processed cell's char is or None (empty) or the same char
182 | # of the currently processed char of the word (so the words will combine)
183 | if coords[ind][list(coords[ind].keys())[index]] is None or \
184 | coords[ind][list(coords[ind].keys())[index]] == list(word)[cc]:
185 |
186 | cc += 1
187 |
188 | # Appends the position if it has passed all the checks
189 | good_coords.append(list(coords[ind].keys())[index])
190 |
191 | # Checks if we got the right number of positions (number of chars in the word) if there's the right
192 | # number it will break out of the loop so I wont get a coord list that is longer than the word
193 | if len(good_coords) == len(word):
194 | break
195 | else:
196 | break
197 |
198 | # If we have enough positions it will return them if not it will return None which means the word cannot be
199 | # placed there (from the start_coord down)
200 | if len(good_coords) == len(word):
201 | return good_coords
202 | else:
203 | return None
204 |
205 | def check_diagonal_ltr():
206 |
207 | """
208 | Checks if the word can be placed diagonally from the left to the right and down.
209 |
210 | :returns: The valid coords List[Tuple(int, int)] if it has a valid place to be placed at and return None if it
211 | cannot be placed
212 | :rtype: List[Tuple(int, int)] | None
213 | """
214 |
215 | # Setup variables
216 | index = 0
217 | start_coord_set = 0
218 | good_coords = []
219 | cc = 0
220 |
221 | # Gets the index of the start_coord in the coord_set
222 | for c in coords:
223 | if start_coord in coords[c]:
224 | start_coord_set = c
225 | index = list(coords[c].keys()).index(start_coord) - 1
226 |
227 | for counter, coord_set in enumerate(coords):
228 |
229 | # Checks if the index of the set is lower than the currently processed set which means the next
230 | # char is below the char before it
231 | if counter >= start_coord_set:
232 |
233 | # Checks if the currently processed cell's char is or None (empty) or the same char
234 | # of the currently processed char of the word (so the words will combine)
235 | if coords[counter][list(coords[counter].keys())[index]] is None or \
236 | coords[counter][list(coords[counter].keys())[index]] == list(word)[cc]:
237 |
238 | index += 1
239 |
240 | # Checks if the word can continue going forwards by checking if the index is bigger than the
241 | # length of a coord set
242 | if index > len(coords[coord_set]) - 1:
243 | return None
244 |
245 | # Checks if the currently processed cell's char is or None (empty) or the same char
246 | # of the currently processed char of the word (so the words will combine)
247 | if coords[counter][list(coords[counter].keys())[index]] is None or \
248 | coords[counter][list(coords[counter].keys())[index]] == list(word)[cc]:
249 | pass
250 | else:
251 | break
252 |
253 | cc += 1
254 |
255 | # Appends the position if it has passed all the checks
256 | good_coords.append(list(coords[counter].keys())[index])
257 |
258 | # Checks if we got the right number of positions (number of chars in the word) if there's the right
259 | # number it will break out of the loop so I wont get a coord list that is longer than the word
260 | if len(good_coords) == len(word):
261 | return good_coords
262 | continue
263 | else:
264 | break
265 |
266 | # If we have enough positions it will return them if not it will return None which means the word cannot be
267 | # placed there (from the start_coord down diagonally)
268 | if len(good_coords) == len(word):
269 | return good_coords
270 | else:
271 | return None
272 |
273 | # A list of all of the functions
274 | options = [check_down, check_right]
275 |
276 | # If diagonals are allowed it will append the diagonal function to the list
277 | if allow_diagonal:
278 | options.append(check_diagonal_ltr)
279 |
280 | # Gets a random function from the list and stores the value in a variable
281 | res = random.choice(options)
282 |
283 | # If diagonals are allowed and the current processed word is the first in the word list it will make it diagonal
284 | # so theres a determined diagonal
285 | if allow_diagonal and word == words[0]:
286 | res = check_diagonal_ltr
287 |
288 | return res()
289 |
290 |
291 | def get_font_characters(font_path):
292 |
293 | """
294 | Gets all chars in a font.
295 | source: https://stackoverflow.com/a/19438403
296 |
297 | :param font_path: A path for a specific font
298 | :type font_path: str
299 | :returns: A list of all characters in a specific font
300 | :rtype: List[str, ...]
301 | """
302 |
303 | # Gets all chars in a font - source: https://stackoverflow.com/a/19438403
304 | with TTFont(font_path) as font_file:
305 | characters = list(chr(y[0]) for x in font_file["cmap"].tables for y in x.cmap.items())
306 |
307 | return characters
308 |
309 |
310 | def populate_list(lst, target_num, randomize=False, hard_randomizer=False):
311 |
312 | """
313 | Adds elements to the list from the list until the length of the list reaches the target_number.
314 |
315 | :param lst: The list that we want to make bigger (have a specific amount of elements)
316 | :type: lst: list
317 | :param target_num: The number of elements we want the list to reach
318 | :type: target_num: int
319 | :param randomize: If True shuffle the list if not dont
320 | :type: randomize: bool
321 | :param hard_randomizer: If True choose even random elements from the list to append to the list
322 | :type: hard_randomizer: bool
323 | :raises IndexError: "Number of elements in the list are bigger than the target number"
324 | :returns: A list that has this specific amount of elements
325 | :rtype: list
326 | """
327 |
328 | # If randomize is true it will shuffle the list
329 | if randomize:
330 | random.shuffle(lst)
331 |
332 | # Gets the number of elements left (the number of elements we want - the number of elements in the list)
333 | elements_left = target_num - len(lst)
334 |
335 | # If theres more elements to the list than the target_num it will return an error
336 | if elements_left < 0:
337 | raise IndexError("Number of elements in the list are bigger than the target number")
338 |
339 | # If the target num and the number of elements in the list are the same it will check if randomize is True if it
340 | # is it will shuffle the list and return the list as it is
341 | if elements_left == 0:
342 | if randomize:
343 | random.shuffle(lst)
344 | return lst
345 |
346 | # Appends a random elements from the list to the list if hard_randomizer is True if not it would just go by order
347 | for i in range(elements_left):
348 | lst.append(lst[i if not hard_randomizer else secrets.randbelow(len(lst) - 1)])
349 |
350 | # Shuffles the list a "couple" times
351 | for i in range(50):
352 | if randomize:
353 | random.shuffle(lst)
354 | return lst
355 |
356 |
357 | def draw_chars(words, available_chars='abcdefghijklmnopqrstuvwxyz', allow_diagonal=True, cells_in_row=15,
358 | square_size=10, grid_separator_color=(0, 0, 0), grid_background_color=(255, 255, 255),
359 | grid_separator_width=10, grid_text_color=(0, 0, 255), grid_text_size=50, random_char_color=None,
360 | add_randomized_chars=True, random_seed=None, high_res=False):
361 | """
362 | Draws all the chars (random and the words the user has decided) to an image.
363 |
364 | :param words: A list of words that need to be drawn to the grid
365 | :type words: List[str, ...]
366 | :param available_chars: A string of all available characters that'll be written in
367 | the free cells (without chars in there)
368 | :type available_chars: str
369 | :param allow_diagonal: A boolean that decides if there will be diagonals when the word list is being drawn
370 | :type allow_diagonal: bool
371 | :param cells_in_row: The number of cells in a row/column in the grid
372 | :type cells_in_row: int
373 | :param square_size: The square/cell size in the grid
374 | :type square_size: int
375 | :param grid_separator_color: The color of the separator lines in the grid
376 | :type grid_separator_color: Tuple(int, int, int) | str
377 | :param grid_background_color: The color of the background of the grid
378 | :type grid_background_color: Tuple(int, int, int) | str
379 | :param grid_separator_width: The grid separator line width
380 | :type grid_separator_width: int
381 | :param grid_text_color: The grid's text color
382 | :type grid_text_color: Tuple(int, int, int) | str
383 | :param grid_text_size: The grid's text size
384 | :type grid_text_size: int
385 | :param random_char_color: The color of the random characters
386 | :type random_char_color: Tuple(int, int, int) | str
387 | :param add_randomized_chars: Decides if randomized chars will be drawn
388 | :type add_randomized_chars: bool
389 | :param random_seed: A random seed that can be inputted
390 | :type random_seed: str | int
391 | :param high_res: If high_res is bigger than 2 it will be True if not it will be False
392 | :type high_res: bool
393 | :returns: A grid with the words and randomized chars (if add_randomized_chars is True) as an image
394 | :rtype: PIL.Image
395 | """
396 |
397 | global multiplier
398 | random.seed(random_seed)
399 | if random_char_color is None:
400 | random_char_color = grid_text_color
401 |
402 | # Calls the create_grid function and stores the output in vars
403 | img, coords = create_grid(cells_in_row=cells_in_row, cell_size=square_size, line_color=grid_separator_color,
404 | background_color=grid_background_color, line_width=grid_separator_width,
405 | high_res=high_res)
406 |
407 | # Resizes the image to be multiplied by 3 and it would look the same because the grid is made out of straight lines
408 | img = img.resize((img.size[0] * 3, img.size[0] * 3), Image.NEAREST)
409 |
410 | if type(words) == str:
411 | words = words.split(',')
412 |
413 | # Initializes the ImageDraw.Draw for the img so I can draw on it
414 | img = img.convert("RGBA")
415 | img_draw = ImageDraw.Draw(img, "RGBA")
416 |
417 | for word_counter, word in enumerate(words):
418 |
419 | # If the word's first char is in hebrew it would reverse the word
420 | if list(word)[0] in 'אבגדהוזחטיכלמנסעפצקרשת':
421 | word = list(word)
422 | word.reverse()
423 |
424 | # Gets a random coord out of a random set
425 | c = list(coords.values())
426 | t = c[secrets.randbelow(len(c))]
427 | start_coord = list(t)[secrets.randbelow(len(c))]
428 |
429 | # Checks if the word can be placed with the start coord as the start coord set above
430 | word_coords = check(coords, start_coord, word, words, allow_diagonal)
431 |
432 | # If the first try didn't work it will loop until it will find valid coords for the word
433 | while word_coords is None:
434 | c = list(coords.values())
435 | t = c[secrets.randbelow(len(c))]
436 | start_coord = list(t)[secrets.randbelow(len(c))]
437 | word_coords = check(coords, start_coord, word, words, allow_diagonal)
438 |
439 | # Writes the chars with the chars the check function returned
440 | for counter, c in enumerate(word_coords):
441 | font = ImageFont.truetype(global_font, int(grid_text_size * 2))
442 | img_draw.text((c[0], c[1] - multiplier * (2 if high_res else 4) + (97 if high_res else 2)),
443 | list(word)[counter].upper(), fill=grid_text_color, font=font,
444 | anchor='mb')
445 |
446 | # Sets all the coords the check function
447 | # returned as occupied (sets it as the char that was drawn in that coord)
448 | for i in coords:
449 | if c in list(coords[i].keys()):
450 | coords[i][c] = list(word)[counter]
451 |
452 | # Draw a circle around the first word on the grid
453 | if word_counter == 0:
454 | shape = [
455 | (word_coords[0][0]-img_draw.textsize(word[0], font)[0], word_coords[0][1] - multiplier * (2 if high_res else 4) + (97 if high_res else 2)-img_draw.textsize(word[0], font)[1]),
456 | (word_coords[-1][0], word_coords[-1][1])
457 | ]
458 |
459 | # If the user has inputted a random seed it will set the seed to that if not it will set it to a random one
460 | if random_seed is not None:
461 | random.seed(random_seed)
462 |
463 | if add_randomized_chars:
464 | free_coords = []
465 | available_chars = list(available_chars)
466 |
467 | # Appends to a list all the coords that have None in them (that are empty)
468 | for i in coords:
469 | for c, x in enumerate(coords[i]):
470 | if coords[i][x] is None:
471 | free_coords.append(list(coords[i].keys())[c])
472 |
473 | font = ImageFont.truetype(global_font, int(grid_text_size * 2))
474 |
475 | # Calls the populate_list function and stores the returned value in a var
476 | if len(available_chars) <= len(free_coords):
477 | available_chars = populate_list(available_chars, len(free_coords), True, False)
478 | else:
479 | random.shuffle(available_chars)
480 |
481 | # Draws random chars all over the free coords (that have no chars on them)
482 | for c, coord in enumerate(free_coords):
483 | img_draw.text((coord[0], coord[1] - multiplier * (2 if high_res else 4) + (97 if high_res else 2)),
484 | available_chars[c].upper(), fill=random_char_color,
485 | font=font, anchor='mb')
486 |
487 | return img
488 |
489 |
490 | def fit_text(text, text_size, text_color, max_horizontal_chars, box_outline_color, box_background_color,
491 | box_outline_width, font_file, word_bank_outline, high_res):
492 |
493 | """
494 | :param text: The text that needs to be fir inside the bounding box (words separated by ,)
495 | :type text: str
496 | :param text_size: The size of the text
497 | :type text_size: int
498 | :param text_color: The color of the text
499 | :type text_color: Tuple(int, int, int) | str
500 | :param max_horizontal_chars: The number of the max horizontal chars allowed in one line
501 | :type max_horizontal_chars: int
502 | :param box_outline_color: The color of the outline of the bounding box
503 | :type box_outline_color: Tuple(int, int, int) | str
504 | :param box_background_color: The color of the background of the bounding box
505 | :type box_background_color: Tuple(int, int, int) | str
506 | :param box_outline_width: The thickness of the outline of the bounding box
507 | :type box_outline_width: int
508 | :param font_file: The file of the font of the text
509 | :type font_file: str
510 | :param word_bank_outline: Whether there will be an outline or not
511 | :type word_bank_outline: bool
512 | :param high_res: If high_res is bigger than 2 it will be True if not it will be False
513 | :type high_res: bool
514 | :return: The image
515 | :rtype: PIL.Image
516 | """
517 |
518 | text_size *= multiplier // 2
519 | text_size -= int(text_size//3)
520 |
521 | # Replaces every ',' to '-' because the wrapper library will associate '-' as a separator and sorts by length and
522 | # removes all spaces
523 | text = text.replace(' ', '')
524 | text = text.split(',')
525 |
526 | # Checks if theres a hebrew letter in the word if there is it will reverse it
527 | for c, i in enumerate(text):
528 | if list(i)[0] in 'אבגדהוזחטיכלמנסעפצקרשת':
529 | i = list(i)
530 | i.reverse()
531 | text[c] = (''.join(i))
532 |
533 | text.sort(key=len, reverse=True)
534 | text = ','.join(text).upper()
535 | text = text.replace(',', '-')
536 |
537 | # Changes the text size to fit the box if the longest word cannot fit in one line
538 | font = ImageFont.truetype(font_file, text_size)
539 | longest_word = sorted(text.split('-'), key=len)[-1]
540 | if longest_word[0] == ' ':
541 | longest_word = longest_word[1:]
542 | longest_word_size = font.getsize(longest_word)[0]
543 | while longest_word_size >= 1100:
544 | longest_word = sorted(text.split('-'), key=len)[-1]
545 | if longest_word[0] == ' ':
546 | longest_word = longest_word[1:]
547 | longest_word_size = font.getsize(longest_word)[0]
548 | text_size -= 1
549 | font = ImageFont.truetype(font_file, text_size)
550 |
551 | # Initializes the text wrapper
552 | wrapper = textwrap.TextWrapper()
553 | wrapper.max_lines = 3
554 | wrapper.placeholder = '...'
555 | wrapper.break_long_words = False
556 | wrapper.width = max_horizontal_chars-10
557 |
558 | # Wrap the text
559 | text = wrapper.fill(text=text)
560 |
561 | # Create a new image according to the size of the text
562 | img = Image.new('RGBA', (font.getsize_multiline((max_horizontal_chars-10)*'A')[0]+20,
563 | (font.getsize_multiline(text)[1]+20+(60 if high_res else 20) +
564 | box_outline_width+(text.count('\n')*10))), (0, 0, 0, 0))
565 |
566 | # Initializes the ImageDraw.Draw for the img so I can draw on it
567 | draw = ImageDraw.Draw(im=img)
568 |
569 | # If word_bank_outline is true it will draw a bunch or rectangles according to box_outline_width
570 | if word_bank_outline:
571 | w, h = img.size
572 | for i in range(0, box_outline_width):
573 | shape = [(0 + i, 0 + i), (w - i, h - i)]
574 | draw.rectangle(shape, box_background_color, box_outline_color)
575 |
576 | # Replaces every '-' back to ','
577 | text = text.replace('-', ', ').upper()
578 |
579 | # Checks if the first char is ' ' if it is it will be cut out
580 | if text[0] == ' ':
581 | text = text[1:]
582 |
583 | # Draws the text onto the bounding box
584 | draw.multiline_text(xy=(10 + (20 if high_res else 0), 0), text=text, font=font, fill=text_color, spacing=20)
585 |
586 | return img
587 |
588 |
589 | def create_search_word_puzzle(words, random_chars='abcdefghijklmnopqrstuvwxyz', allow_diagonal=True,
590 | cells_in_row='auto', square_size=10, grid_separator_color='black',
591 | grid_background_color='white', grid_separator_width=10, grid_text_color='black',
592 | grid_text_size=50, random_char_color=None, add_randomized_chars=True, page_color='white',
593 | page_title='Search word puzzle Game', title_color='black', word_bank_outline=True,
594 | word_bank_outline_color='black', word_bank_outline_width=10, word_bank_fill_color='white',
595 | words_in_word_bank_color='black', random_seed=None, subtitle='Circle the words',
596 | subtitle_color='gray', title_size=120, subtitle_size=80, words_in_word_bank_size=100,
597 | res_multiplier=2):
598 |
599 | """
600 | The function that connects all the other functions and makes it to one image.
601 |
602 | :param words: A list of words that need to be drawn to the grid
603 | :type words: List[str, ...]
604 | :param random_chars: A string of all available characters that'll be written in
605 | the free cells (without chars in there)
606 | :type random_chars: str
607 | :param allow_diagonal: A boolean that decides if there will be diagonals when the word list is being drawn
608 | :type allow_diagonal: bool
609 | :param cells_in_row: The number of cells in a row/column in the grid
610 | :type cells_in_row: int
611 | :param square_size: The square/cell size in the grid
612 | :type square_size: int
613 | :param grid_separator_color: The color of the separator lines in the grid
614 | :type grid_separator_color: Tuple(int, int, int) | str
615 | :param grid_background_color: The color of the background of the grid
616 | :type grid_background_color: Tuple(int, int, int) | str
617 | :param grid_separator_width: The grid separator line width
618 | :type grid_separator_width: int
619 | :param grid_text_color: The grid's text color
620 | :type grid_text_color: Tuple(int, int, int) | str
621 | :param grid_text_size: The grid's text size
622 | :type grid_text_size: int
623 | :param random_char_color: The color of the random characters
624 | :type random_char_color: Tuple(int, int, int) | str
625 | :param add_randomized_chars: Decides if randomized chars will be drawn
626 | :type add_randomized_chars: bool
627 | :param random_seed: A random seed that can be inputted
628 | :type random_seed: str | int
629 | :param subtitle: A text that will be shown on the page
630 | :type subtitle: str
631 | :param subtitle_color: The subtitle's color
632 | :type subtitle_color: Tuple(int, int, int) | str
633 | :param title_size: The title's size
634 | :type title_size: int
635 | :param subtitle_size: The subtitle's size
636 | :type subtitle_size: int
637 | :param words_in_word_bank_size: The words' size in the word bank
638 | :type words_in_word_bank_size: int
639 | :param res_multiplier: The resolutions multiplier
640 | :type res_multiplier: int
641 | :param page_color: The page's color
642 | :type page_color: Tuple(int, int, int) | str
643 | :param page_title: The page's title
644 | :type page_title: str
645 | :param title_color: The page title's color
646 | :type title_color: Tuple(int, int, int) | str
647 | :param word_bank_outline: Decides if there will be an outline for the word bank
648 | :type word_bank_outline: bool
649 | :param word_bank_outline_color: The word bank outline's color
650 | :type word_bank_outline_color: Tuple(int, int, int) | str
651 | :param word_bank_outline_width: The world bank's outline thickness
652 | :type word_bank_outline_width: int
653 | :param word_bank_fill_color: The word bank's fill color
654 | :type word_bank_fill_color: Tuple(int, int, int) | str
655 | :param words_in_word_bank_color: The word bank's word color
656 | :type words_in_word_bank_color: Tuple(int, int, int) | str
657 | :raises AttributeError
658 | :returns: The page
659 | :rtype: PIL.Image
660 | """
661 |
662 | # Setup variables according to multiplier
663 | global multiplier
664 | multiplier = res_multiplier
665 |
666 | if res_multiplier == 2:
667 | grid_text_size = 13
668 | elif res_multiplier == 4:
669 | grid_text_size = 213
670 |
671 | if res_multiplier == 2:
672 | word_bank_outline_width = 100-1
673 | elif res_multiplier == 4:
674 | word_bank_outline_width = 100-1
675 |
676 | if res_multiplier == 2:
677 | grid_separator_width = 100-1
678 | elif res_multiplier == 4:
679 | grid_separator_width = 100-40
680 |
681 | square_size = square_size * multiplier // (20 // multiplier)
682 | grid_separator_width = grid_separator_width // multiplier
683 | word_bank_outline_width = 100//word_bank_outline_width * multiplier
684 | title_size = title_size * multiplier
685 | subtitle_size = int(subtitle_size * multiplier // 1.7)
686 | words_in_word_bank_size = words_in_word_bank_size
687 |
688 | for i, word in enumerate(words):
689 | words[i] = word.replace('\n', '')
690 |
691 | long = []
692 |
693 | # If the cells in row is 'auto' it will set it to the longest word's length + 2
694 | if cells_in_row == 'auto':
695 | cells_in_row = len(max(words, key=len)) + 2
696 |
697 | # Appends all the words that their length is bigger than the cells_in_row
698 | for word in words:
699 | if len(word) > cells_in_row:
700 | long.append(word)
701 |
702 | # Raises an error if there are words longer
703 | if len(long) > 0:
704 | raise AttributeError(f"The maximum characters of a word can be {cells_in_row} you have invalid words: {long}")
705 |
706 | random.seed(random_seed)
707 |
708 | font = ImageFont.truetype(global_font, title_size)
709 |
710 | # Calls the function draw_chars and stores it's returned value into a var
711 | img = draw_chars(words=words, available_chars=random_chars, allow_diagonal=allow_diagonal,
712 | cells_in_row=cells_in_row, square_size=square_size, grid_separator_color=grid_separator_color,
713 | grid_background_color=grid_background_color, grid_separator_width=grid_separator_width,
714 | grid_text_color=grid_text_color,
715 | grid_text_size=grid_text_size, random_char_color=random_char_color,
716 | add_randomized_chars=add_randomized_chars, random_seed=random_seed,
717 | high_res=res_multiplier > 2)
718 |
719 | # Creates the page image
720 | page = Image.new('RGB', (2480 * multiplier // 2, 3508 * multiplier // 2), color=page_color)
721 |
722 | # Initializes the ImageDraw.Draw for the page so I can draw on it
723 | page_draw = ImageDraw.Draw(page)
724 |
725 | # Checks if the page starts with a letter in the hebrew language if it does it will reverse it
726 | if len(page_title) != 0 and list(page_title)[0] in 'אבגדהוזחטיכלמנסעפצקרשת':
727 | page_title = list(page_title)
728 | page_title.reverse()
729 | page_title = ''.join(page_title)
730 |
731 | # Draws the title on the page
732 | page_draw.text((page.size[0] // 2, page.size[1] // 18+10), page_title, fill=title_color, font=font, anchor='mb',
733 | align='center')
734 | font = ImageFont.truetype(global_font, subtitle_size)
735 |
736 | # Draws the subtitle on the page
737 | page_draw.multiline_text((page.size[0] / 15, page.size[1] // 15), subtitle, fill=subtitle_color, font=font,
738 | align='left')
739 |
740 | # Calls the function fit_text and then stores it's returned values into a var
741 | word_bank = fit_text(text=', '.join(words), text_size=words_in_word_bank_size, text_color=words_in_word_bank_color,
742 | max_horizontal_chars=35, box_outline_color=word_bank_outline_color,
743 | box_background_color=word_bank_fill_color, box_outline_width=word_bank_outline_width,
744 | font_file=global_font, word_bank_outline=word_bank_outline, high_res=res_multiplier > 2)
745 | font = ImageFont.truetype(global_font, 75 * multiplier // 2)
746 |
747 | # If the mode is high res it will resize it with a better mode (high quality)
748 | if res_multiplier == 2:
749 | img = img.resize((2000 * multiplier // 2, 2000 * multiplier // 2), resample=Image.MEDIANCUT)
750 | else:
751 | img = img.resize((2000 * multiplier // 2, 2000 * multiplier // 2), resample=Image.LANCZOS)
752 |
753 | # Draws text on the page according to the allow_diagonal
754 | page_draw.text(((page.size[0] - img.size[0]) // 2, page.size[1] // 6),
755 | 'Including diagonals' if allow_diagonal else 'Not including diagonals', fill='gray',
756 | font=font)
757 |
758 | # If the mode is high res it will resize it with a better mode (high quality)
759 | if res_multiplier == 2:
760 | word_bank = word_bank.resize((int(word_bank.size[0]*2 - word_bank.size[0]//4), int(word_bank.size[1]*2 -
761 | word_bank.size[1]//4)), resample=Image.MEDIANCUT)
762 | else:
763 | word_bank = word_bank.resize((word_bank.size[0]*2 - word_bank.size[0]//4, word_bank.size[1]*2 -
764 | word_bank.size[1]//4), resample=Image.LANCZOS)
765 |
766 | # Pastes the word bank on the page
767 | page.paste(word_bank, ((page.size[0] - word_bank.size[0]) // 2, page.size[1] // 5 + img.size[1] +
768 | (multiplier * 10)))
769 |
770 | # Draws the credit on the page
771 | page_draw.text((multiplier * 10, page.size[1] - page.size[1] // 20), 'Made By KingOfTNT10', fill=(128, 128, 128, 50), font=font)
772 |
773 | # Pastes the img on the page
774 | page.paste(img, ((page.size[0] - img.size[0]) // 2, page.size[1] // 5))
775 |
776 | return page
777 |
--------------------------------------------------------------------------------
/WordSearchPuzzleGUI.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import io
3 | import json
4 | import sys
5 | import uuid
6 | import webbrowser
7 |
8 | import PySimpleGUI as sg
9 | import win32clipboard
10 | from PIL import Image
11 | from PyQt5 import QtGui
12 | from PyQt5.QtCore import Qt
13 | from PyQt5.QtGui import QPainter
14 | from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
15 | from PyQt5.QtWidgets import QApplication
16 |
17 | import WordSearchPuzzleGen as SWP
18 |
19 | # Setup variables
20 | global layout, image
21 | sg.theme('Dark')
22 | seed = str(uuid.uuid4()).replace('-', '')
23 | default_font = 'Dosis 12 bold'
24 | category_name_font = 'Dosis 11 bold'
25 | github_view_data = """iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAD
26 | sMAAA7DAcdvqGQAAAV4SURBVGhDzZnNbhxFEMfH65z8tR+OjUVix7n4YIOUPAHYES8RLnkLMEKIF/CVSOHC1S/hTeQLEgpEDv
27 | YFoQQQKGDi3bUdLsRr+l9dNVvT0zPTvd6Y/DY1XVXT3VXV0zM73iQjYvno6KhzHgH6Y5wdfnHGuB0KJMTqhRkzsDoUNW5jaNC
28 | SGtgeCTwl5mxYTxxRq8CBLoXYKxR0Rer1+q3LLAIgHuKyeXG2t7fbmPT/AvE5leGJfRK9KfgJV0rhPsTgZrM51I33Juh0Ot1W
29 | q9VkM4f3HsHlfJuKAMgnaptNTEzQjf22UvQAyG0tdGY1x/zCNauYUYvXF5PH331r7RHw+RdfJg8efM1Wkvz14nfW8vgezRlHW
30 | RFgTgpxOCwJWgUWR4JSMuaALKrmdIvRBr6xS58OcwvvsgYwNPSrxddXQmv/wHf44g/W/Zg6cON3raVu9qoiCMSUuFg2sVM/J+
31 | L65UK7vtwcOLBegZtv1LvWIN75QCdh+7xPjozPCOrL1jI4ZzXxmQ/PHQsVYgYHjpXwwLQ0TNkp2meTo5Zty0DPpm70wHR03lF
32 | XJI0tAhwfze34cgJ8fpEhQCFBf9zMzi+YGLKqiFf8kfO+XnTOFOt+yK8+iBcI5V8zryI/kFmFzYsEgUgHyi8iV5waR9KxjuSu
33 | ZCCS/5iZoHLY7Nw7rBWhH69aD6G8/8vDP1krBt8pQfcIwogAbVuhdSbRxzAZjBUBWg9gOfxmp0tvgnKbijoHwflcH4jTz2aq7
34 | YEvMz6AO3c+uhe0tVpX51m7fJ79/BP+QmXLT7vdfhhUSHPWFOJu5dhbQcA4TckceJt68v3jZGlpkT3FBG4tE43qVeLaGbFJaD
35 | vVMU5L2sdIZozdYiFFgLgvxGB4n6dovYTMmDjCnlomgAiCpUmyLWT6Obbrlzanc6vPVYF7JKgQ/eovU1MgtKI7QYuSEL8+n9G
36 | 5jWFra+tR0M3eaF01R3RDQbrVFPkEOVc0h79v9+hv1osxC30zqBBQb85Sm6ZhlKKRbooafY5Sp4N/rl7nJWvl0Dc7fmZhOwiJ
37 | V1Z+yanMOeiYJ2wp/Uj+WI9lc1Gewaii3myxZkBwXs0c3nNwFGWsz1m91zmyZgXYVqZ5TuFCtxeoNwbFFNWiUwJ68nzK2Rb0u
38 | mFFAGwranGIKQSgGPvMKsOmN2Y+tmdYiONu9U8HGimEHr9iaL66fz+Zqft/ocSKUUA3N9ipmAP+yU1APhbg2Jhv2CJAqviuyn
39 | S9wSt6npz0qp8Jtr/NTbeCtn/79ZfKl8EqdCHpF6Lx5ZbfJm/rQ5IhyGroVgSIPoIiMvnqb3bvkh+jGI7+yaeb1lmEbxsZoWV
40 | zfCMgk6++8oRvi03N2NVD5x+fPk1u3FgiWzPNfWQw+kKnq2+mJJ19APrJcc8akegtJeTetVZWVm6zmnKKgCYDlLj23vvJ1HQ9
41 | 2d3dpXN7e3tkI0FaAhbRz/t4D8v6SDfNMPjyA7nKwM7OTnt9ff1DNolr15eSXsANH8PpyTFrYeAtd2NjY53NDN5CgO9/rKamZ
42 | 1grBitdOKlDTCFD/Y8VwCD3PQyBIZ9tbpptYl/dIR/fvZvq5pDq1tT2QGKoKiIIbDMTuJKJyaloCQHxOZVSgnbB6urqrf39/d
43 | JfJCcnp+xsstg0sznI6tODBpfImjj16vSUDT9ra2u3Dw4OnrBZCoULBSvEag6zwqyF88+r4kJ8j9gyCu8RH5jcULBXpUa3dYG
44 | /cD0Qo4kgbAYTVQjTRSDAtkXnhgtH+aJlyfhZV/CUmHOoZ/wwhaRwbHCz3+93aefpXFnsrUJa6kN/jLPD46+Ay4Un8LA8Pj5+
45 | r1arfTB+5Ur6pXr2+vXDfj95dHb27zfGfG69oyJJ/gNAqt4egK2qkgAAAABJRU5ErkJggg=="""
46 |
47 | def get_scaling():
48 | """
49 | Gets scaling data.
50 | Source: https://github.com/PySimpleGUI/PySimpleGUI/issues/4998#issuecomment-985360403
51 | :return: The scaling data
52 | :rtype: float
53 | """
54 |
55 | root = sg.tk.Tk()
56 | scaling = root.winfo_fpixels('1i') / 72
57 | root.destroy()
58 | return scaling
59 |
60 |
61 | def to_clipboard(target_img):
62 | """
63 | Puts a PIL image into the clipboard.
64 | Source: https://stackoverflow.com/a/7052068/16469230
65 | :param target_img: A PIL image to send to the clipboard
66 | :type target_img: PIL.Image
67 | :returns: None
68 | :rtype: None
69 | """
70 |
71 | def send_to_clipboard(clip_type, image_data):
72 | win32clipboard.OpenClipboard()
73 | win32clipboard.EmptyClipboard()
74 | win32clipboard.SetClipboardData(clip_type, image_data)
75 | win32clipboard.CloseClipboard()
76 |
77 | output = io.BytesIO()
78 | target_img.convert("RGB").save(output, "BMP")
79 | data = output.getvalue()[14:]
80 | output.close()
81 | send_to_clipboard(win32clipboard.CF_DIB, data)
82 |
83 |
84 | def pil2pixmap(im):
85 | """
86 | Converts a PIL image to a PyQt5 Pixmap.
87 | Source: https://stackoverflow.com/a/48705903/16592435
88 | :param im: A pil image to convert to a pixmap
89 | :type im: PIL.Image
90 | :returns: A pixmap
91 | :rtype: PyQt5.QtGui.QPixmap
92 | """
93 |
94 | if im.mode == "RGB":
95 | r, g, b = im.split()
96 | im = Image.merge("RGB", (b, g, r))
97 | im2 = im.convert("RGBA")
98 | data = im2.tobytes("raw", "RGBA")
99 | qim = QtGui.QImage(
100 | data, im.size[0], im.size[1], QtGui.QImage.Format_ARGB32)
101 | qim = qim.scaled(int(2480 * 2), int(3508 * 2),
102 | Qt.KeepAspectRatio, Qt.SmoothTransformation)
103 | pixmap = QtGui.QPixmap.fromImage(qim)
104 | return pixmap
105 |
106 |
107 | def save_configuration(variables_info):
108 | """
109 | Saves all the settings for the generator as a file that can be loaded later.
110 | :param variables_info: All variable values and names
111 | :type variables_info: List[Tuple(*, str)]
112 | :returns: None
113 | :rtype: None
114 | """
115 |
116 | # Pops up a message that requests the user to save as a file, then store the file into a var
117 | save_filename = sg.popup_get_file("Save as", file_types=(("CONFIG", '.swpconfig'), ("CONFIG", '.swpconfig')),
118 | save_as=True, no_window=True)
119 | if save_filename:
120 |
121 | # Stores all variable names and values inside a dict
122 | conf = {}
123 | for i in variables_info:
124 | variable_name = i[1]
125 | value = i[0]
126 | conf[variable_name] = value
127 |
128 | conf = str(conf)
129 |
130 | # Corrects all python vars to string
131 | conf = conf.replace("'", '"')
132 | conf = conf.replace("True", '"True"')
133 | conf = conf.replace("False", '"False"')
134 | conf = conf.replace("None", '"None"')
135 |
136 | # Encodes the dictionary with base64
137 | message_bytes = conf.encode('ascii')
138 | base64_bytes = base64.b64encode(message_bytes)
139 | base64_message = base64_bytes.decode('ascii')
140 |
141 | # Creates a file in the selected directory
142 | with open(save_filename, 'w') as f:
143 | f.write(base64_message)
144 |
145 | sg.popup(f'Settings Configuration file saved as {save_filename}')
146 |
147 |
148 | def load_configuration(window):
149 | """
150 | Loads a .swpconfig file and load the settings.
151 | :param window: The window so it can update the loaded values
152 | :type window: PySimpleGui.Window
153 | :return: None
154 | :rtype: None
155 | """
156 |
157 | # Popups a message that requests the user to select a file to load the settings off of
158 | save_filename = sg.popup_get_file(
159 | "Load configuration file", file_types=(("CONFIG", '.swpconfig'), ("CONFIG", '.swpconfig')), no_window=True
160 | )
161 |
162 | # A dict that associates all the var keys with the keys of the window
163 | key_dict = {
164 | "allow_diagonals": '-1TOGGLE-',
165 | "add_random_chars": '-2TOGGLE-',
166 | "add_word_bank_outline": '-3TOGGLE-',
167 | "grid_line_color": '-1COLOR_COMBO-',
168 | "grid_background_color": '-2COLOR_COMBO-',
169 | "grid_text_color": '-3COLOR_COMBO-',
170 | "grid_random_chars_color": '-4COLOR_COMBO-',
171 | "page_background_color": '-5COLOR_COMBO-',
172 | "page_title_color": '-6COLOR_COMBO-',
173 | "page_subtitle_color": '-7COLOR_COMBO-',
174 | "word_bank_outline_color": '-8COLOR_COMBO-',
175 | "word_bank_background_color": '-9COLOR_COMBO-',
176 | "word_bank_word_color": '-10COLOR_COMBO-',
177 | "words": '-INPUT1STR-',
178 | "available_random_chars": '-INPUT2STR-',
179 | "page_title": '-INPUT3STR-',
180 | "page_subtitle": '-INPUT4STR-',
181 | "grid_cell_size": '-INPUT1NUM-',
182 | "grid_cells_in_row": '-INPUT2NUM-',
183 | "grid_line_width": '-INPUT3NUM-',
184 | "grid_text_size": '-INPUT4NUM-',
185 | "word_bank_outline_width": '-INPUT5NUM-',
186 | "word_bank_word_size": '-INPUT6NUM-',
187 | "page_title_size": '-INPUT7NUM-',
188 | "page_subtitle_size": '-INPUT8NUM-',
189 | "random_seed": "-OTHER1-"
190 | }
191 |
192 | if save_filename:
193 |
194 | # Opens the selected file and stores it's content into a var
195 | data = open(save_filename).read().replace('\n', '')
196 |
197 | # Decodes the data with base64
198 | decoded_data = base64.b64decode(data + '==').decode()
199 |
200 | try:
201 |
202 | # Converts the decoded string to a python dict
203 | data = json.loads(decoded_data)
204 |
205 | # Updates all the window elements according to the loaded data
206 | for counter, key in enumerate(data):
207 | w_key = key_dict[key]
208 | window[w_key].update(data[key].replace(
209 | 'None', '') if type(data[key]) == str else data[key])
210 |
211 | except json.JSONDecodeError as ex:
212 | sg.popup(
213 | f'Error while opening the config ({save_filename}) \nError:\n {ex}')
214 |
215 |
216 | def reset_to_default(window):
217 | """
218 | Reset all element values to default.
219 | :param window: The window so it can update the loaded values
220 | :type window: PySimpleGui.Window
221 | :return: None
222 | :rtype: None
223 | """
224 |
225 | # Toggles
226 | window['-1TOGGLE-'].update(True)
227 |
228 | window['-2TOGGLE-'].update(True)
229 | window['-3TOGGLE-'].update(True)
230 |
231 | # Colors
232 | window['-1COLOR_COMBO-'].update('Black')
233 | window['-2COLOR_COMBO-'].update('White')
234 | window['-3COLOR_COMBO-'].update('Black')
235 | window['-4COLOR_COMBO-'].update('Black')
236 | window['-5COLOR_COMBO-'].update('White')
237 | window['-6COLOR_COMBO-'].update('Black')
238 | window['-7COLOR_COMBO-'].update('Gray')
239 | window['-8COLOR_COMBO-'].update('Black')
240 | window['-9COLOR_COMBO-'].update('White')
241 | window['-10COLOR_COMBO-'].update('Black')
242 |
243 | # Strings
244 | window['-INPUT1STR-'].update('Bagel, Three, House')
245 | window['-INPUT2STR-'].update('abcdefghijklmnopqrstuvwxyz')
246 | window['-INPUT3STR-'].update('Search word puzzle')
247 | window['-INPUT4STR-'].update('Circle the correct words')
248 |
249 | # Numbers
250 | window['-INPUT1NUM-'].update('100')
251 | window['-INPUT2NUM-'].update('auto')
252 | window['-INPUT3NUM-'].update('100')
253 | window['-INPUT4NUM-'].update('100')
254 | window['-INPUT5NUM-'].update('100')
255 | window['-INPUT6NUM-'].update('100')
256 | window['-INPUT7NUM-'].update('100')
257 | window['-INPUT8NUM-'].update('100')
258 |
259 | # Other
260 | window['-OTHER1-'].update('')
261 |
262 |
263 | def print_image(target_image):
264 | """
265 | This function handles the print preview and the print itself.
266 | Source: https://github.com/PyQt5/PyQt/issues/145#issuecomment-975009897
267 | :param target_image: A pil image to print preview and print
268 | :type target_image: PIL.Image
269 | :return: None
270 | :rtype: None
271 | """
272 |
273 | def draw_image(printer_obj):
274 | painter = QPainter()
275 | painter.begin(printer_obj)
276 | painter.setPen(Qt.red)
277 | painter.drawPixmap(0, 0, pil2pixmap(target_image.resize(
278 | (int(2480 * 2), int(3508 * 2)), Image.LANCZOS)))
279 | painter.end()
280 | dlg.close()
281 | app.exit()
282 |
283 | app = QApplication(sys.argv)
284 | printer = QPrinter(QPrinter.HighResolution)
285 | printer.setResolution(600)
286 | printer.setCreator('KingOfTNT10 : IlaiK')
287 | dlg = QPrintPreviewDialog(printer)
288 | dlg.paintRequested.connect(draw_image)
289 | dlg.exec_()
290 | app.exec_()
291 | dlg.close()
292 | app.exit()
293 |
294 |
295 | def save_file(img):
296 | """
297 | Saves a PIL image to a file.
298 | :param img: A PIL image to save to a file
299 | :type img: PIL.Image
300 | :return: None
301 | :rtype: None
302 | """
303 |
304 | # Pops up a message that requests the user to save the image as a file
305 | save_filename = sg.popup_get_file(
306 | "Save as", file_types=(("PNG", '.png'), ("JPG", ".jpg")), save_as=True, no_window=True
307 | )
308 | if save_filename:
309 | # Save the image
310 | img.save(save_filename)
311 |
312 | sg.popup(f"Saved: {save_filename}")
313 |
314 |
315 | def image_to_bios(target_image, size: tuple):
316 | """
317 | Saves a PIL image to bios after it gets resized and keeps it's ratio
318 | and returns it's value as a base64 encoded data.
319 | :param target_image: A PIL image to get as a base64 encoded data
320 | :type target_image: PIL.Image
321 | :param size: The wanted size of the image
322 | :type size: Tuple(int, int)
323 | :return: The image as a base64 encoded data
324 | :rtype: str
325 | """
326 |
327 | target_image.thumbnail(size)
328 | bio = io.BytesIO()
329 | target_image.save(bio, format="PNG")
330 | value = bio.getvalue()
331 | bio.close()
332 | return value
333 |
334 |
335 | def setup_layout():
336 | """
337 | Set's up the layout of the window
338 | :return: None
339 | :rtype: None
340 | """
341 |
342 | global layout
343 |
344 | toggle_tab = [
345 | [sg.Checkbox("Diagonals", default=True, key='-1TOGGLE-', pad=(20, 0), font=default_font, enable_events=True,
346 | background_color=sg.theme_background_color())],
347 | [sg.Checkbox("Add randomized characters", default=True, key='-2TOGGLE-', pad=(20, 0), font=default_font,
348 | enable_events=True, background_color=sg.theme_background_color())],
349 | [sg.Checkbox("Add word bank outline", default=True, key='-3TOGGLE-', pad=(20, 0), font=default_font,
350 | enable_events=True, background_color=sg.theme_background_color())],
351 |
352 | ]
353 |
354 | color_tab = [
355 | [sg.Text("Grid - line color:", pad=(20, 0), font=default_font, background_color=sg.theme_background_color()),
356 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
357 | key='-1COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
358 | background_color=sg.theme_background_color())],
359 | [sg.Text("Grid - background color:", pad=(20, 0), font=default_font,
360 | background_color=sg.theme_background_color()),
361 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
362 | key='-2COLOR_COMBO-', default_value="White", font=default_font, enable_events=True,
363 | background_color=sg.theme_background_color())],
364 | [sg.Text("Grid - text color:", pad=(20, 0), font=default_font, background_color=sg.theme_background_color()),
365 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
366 | key='-3COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
367 | background_color=sg.theme_background_color())],
368 | [sg.Text("Grid - randomized characters color:", pad=(20, 0), font=default_font,
369 | background_color=sg.theme_background_color()),
370 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
371 | key='-4COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
372 | background_color=sg.theme_background_color())],
373 | [sg.Text("Page - background color:", pad=(20, 0), font=default_font,
374 | background_color=sg.theme_background_color()),
375 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
376 | key='-5COLOR_COMBO-', default_value="White", font=default_font, enable_events=True,
377 | background_color=sg.theme_background_color())],
378 | [sg.Text("Page - title color:", pad=(20, 0), font=default_font,
379 | background_color=sg.theme_background_color()),
380 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
381 | key='-6COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
382 | background_color=sg.theme_background_color())],
383 | [sg.Text("Page - subtitle color:", pad=(20, 0), font=default_font,
384 | background_color=sg.theme_background_color()),
385 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
386 | key='-7COLOR_COMBO-', default_value="Gray", font=default_font, enable_events=True,
387 | background_color=sg.theme_background_color())],
388 | [sg.Text("Word bank - outline color:", pad=(20, 0), font=default_font,
389 | background_color=sg.theme_background_color()),
390 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
391 | key='-8COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
392 | background_color=sg.theme_background_color())],
393 | [sg.Text("Word bank - background color:", pad=(20, 0), font=default_font,
394 | background_color=sg.theme_background_color()),
395 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
396 | key='-9COLOR_COMBO-', default_value="White", font=default_font, enable_events=True,
397 | background_color=sg.theme_background_color())],
398 | [sg.Text("Word bank - word color:", pad=(20, 0), font=default_font,
399 | background_color=sg.theme_background_color()),
400 | sg.Combo(['Red', 'Orange', 'Yellow', 'White', 'Black', 'Blue', 'Green', 'Purple', 'Pink', 'Gray'],
401 | key='-10COLOR_COMBO-', default_value="Black", font=default_font, enable_events=True,
402 | background_color=sg.theme_background_color())],
403 |
404 | ]
405 |
406 | string_tab = [
407 | [sg.Text("Words (seperated by ','):", pad=(20, 0), font=default_font,
408 | background_color=sg.theme_background_color()),
409 | sg.Column([[sg.Input(default_text="Bagel, Three, House", font=default_font,
410 | size=(25, 1), key='-INPUT1STR-', enable_events=True,
411 | background_color=sg.theme_background_color())]])],
412 | [sg.Text("Available random characters:", pad=(20, 0), font=default_font,
413 | background_color=sg.theme_background_color()),
414 | sg.Column([[sg.Input(default_text="abcdefghijklmnopqrstuvwxyz", font=default_font,
415 | size=(25, 1), key='-INPUT2STR-', enable_events=True,
416 | background_color=sg.theme_background_color())]])],
417 | [sg.Text("Page title:", pad=(20, 0), font=default_font,
418 | background_color=sg.theme_background_color()),
419 | sg.Input(default_text="Search Word Puzzle", font=default_font,
420 | size=(25, 1), key='-INPUT3STR-', enable_events=True, background_color=sg.theme_background_color())],
421 | [sg.Text("Page subtitle:", pad=(20, 0), font=default_font,
422 | background_color=sg.theme_background_color()),
423 | sg.Column([[sg.Multiline(default_text="Circle the correct words", key='-INPUT4STR-', font=default_font,
424 | autoscroll=True, size=(30, 1), background_color=sg.theme_background_color(),
425 | enable_events=True)]])],
426 |
427 | ]
428 |
429 | number_tab = [
430 | [sg.Text("Grid - cell size:", font=default_font, pad=(20, 0), background_color=sg.theme_background_color()),
431 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT1NUM-',
432 | enable_events=True, background_color=sg.theme_background_color())]])],
433 | [sg.Text("Grid - cells in a row ('auto' to automatically set size):", font=default_font, pad=(20, 0),
434 | background_color=sg.theme_background_color()),
435 | sg.Column([[sg.Input(default_text="auto", font=default_font, size=(4, 1), key='-INPUT2NUM-',
436 | enable_events=True, background_color=sg.theme_background_color())]])],
437 | [sg.Text("Grid - line thickness:", font=default_font, pad=(20, 0),
438 | background_color=sg.theme_background_color()),
439 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT3NUM-',
440 | enable_events=True, background_color=sg.theme_background_color())]])],
441 | [sg.Text("Grid - text size:", font=default_font, pad=(20, 0), background_color=sg.theme_background_color()),
442 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT4NUM-',
443 | background_color=sg.theme_background_color(), enable_events=True)]])],
444 | [sg.Text("Word bank - outline thickness:", font=default_font, pad=(20, 0),
445 | background_color=sg.theme_background_color()),
446 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT5NUM-',
447 | enable_events=True, background_color=sg.theme_background_color())]])],
448 | [sg.Text("Word bank - word size:", font=default_font, pad=(20, 0),
449 | background_color=sg.theme_background_color()),
450 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT6NUM-',
451 | enable_events=True, background_color=sg.theme_background_color())]])],
452 | [sg.Text("Page - title size:", font=default_font, pad=(20, 0),
453 | background_color=sg.theme_background_color()),
454 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT7NUM-',
455 | enable_events=True, background_color=sg.theme_background_color())]])],
456 | [sg.Text("Page - subtitle size:", font=default_font, pad=(20, 0),
457 | background_color=sg.theme_background_color()),
458 | sg.Column([[sg.Input(default_text="100", font=default_font, size=(3, 1), key='-INPUT8NUM-',
459 | enable_events=True, background_color=sg.theme_background_color())]])],
460 |
461 | ]
462 |
463 | other_tab = [
464 | [sg.Text("Random seed (leave empty for randomness):", font=default_font, pad=(20, 0),#7af820
465 | background_color=sg.theme_background_color()), sg.Input(default_text="", font=default_font,
466 | size=(3, 1), key='-OTHER1-',
467 | enable_events=True,
468 | background_color=sg.theme_background_color())],
469 | [sg.Checkbox('Enhanced resolution (worse performance)', key='switch', enable_events=True,
470 | tooltip='This option is recommended only for viewing the end result', pad=(20, 0))],
471 | [sg.Button('Randomize', font=default_font,
472 | key='RANDOMIZE', pad=(20, 10))],
473 |
474 | ]
475 |
476 | tabgroup_layout = [[
477 | sg.Tab('Toggles', toggle_tab),
478 | sg.Tab('Colors', color_tab),
479 | sg.Tab('Strings', string_tab),
480 | sg.Tab('Numbers', number_tab),
481 | sg.Tab('Other', other_tab),
482 | ]]
483 |
484 | options_tab_group = [[
485 | sg.TabGroup(tabgroup_layout, enable_events=True,
486 | key='-TAB_GROUP-', expand_x=False, expand_y=True),
487 | sg.Image(key='-CWIMAGE-', right_click_menu=['&Right', ['Copy', 'Save as...', 'Print']])],
488 | [sg.Button("Reset to default", key='-RESET-', button_color=sg.theme_background_color(),
489 | mouseover_colors=sg.theme_background_color()),
490 | sg.Button(image_data=github_view_data, size=50, mouseover_colors=sg.theme_background_color(),
491 | button_color=sg.theme_background_color(), border_width=0, key='-VIEW_GITHUB-')
492 | ]
493 | ]
494 |
495 | start_window = [
496 | [sg.Button('Create Page', key='-CREATE_PAGE-')]
497 | ]
498 |
499 | menu_def = [['&!File', ['&!Save', '&!Open...']]]
500 |
501 | layout = [
502 | [sg.Menu(menu_def, visible=True, key='MENU')],
503 | [sg.Column(options_tab_group, visible=False, key='OPTIONS'),
504 | sg.Column(start_window, key='START')]
505 | ]
506 |
507 |
508 | def move_center(window):
509 | """
510 | Moves the window to the center of the screen.
511 | :param window: The window that needs to be moved to the center
512 | :type window: PySimpleGui.Window
513 | :return: None
514 | :rtype: None
515 | """
516 |
517 | # Gets the center of the screen
518 | screen_width, screen_height = window.get_screen_dimensions()
519 | win_width, win_height = window.current_size_accurate()
520 | x, y = (screen_width - win_width) // 2, (screen_height - win_height) // 2
521 |
522 | # Moves the window
523 | window.move(x, y)
524 |
525 |
526 | def window_loop():
527 | """
528 | The main window loop with all the error handling and generator calls.
529 | :return: None
530 | """
531 |
532 | global seed, layout, image
533 |
534 | # Set's up the window
535 |
536 | window = sg.Window("Search word puzzle generator",
537 | layout, finalize=True, resizable=True)
538 | window.grab_any_where_on()
539 |
540 | # window.Maximize()
541 | toggle = False
542 |
543 | # Window loop
544 | while True:
545 | event, values = window.read()
546 |
547 | if event == '-VIEW_GITHUB-':
548 | webbrowser.open('https://github.com/KingOfTNT10/WordSearchPuzzleGenerator') # Go to example.com
549 |
550 | if event == 'Copy':
551 | event = '-COPY_TO_CLIPBOARD-'
552 |
553 | if event == 'Save as...':
554 | event = '-SAVE_FILE-'
555 |
556 | if event == 'Print':
557 | event = '-PRINT-'
558 |
559 | if event == 'Save':
560 | event = '-SAVE_CONFIG-'
561 |
562 | if event == 'Open...':
563 | event = '-LOAD_CONFIG-'
564 |
565 | if event == '-CREATE_PAGE-':
566 | window['OPTIONS'].update(visible=True)
567 | window['START'].update(visible=False)
568 | window['MENU'].update([['&File', ['&Save', '&Open...']]])
569 | window.move(0, 0)
570 |
571 | # Detects if the window closed if it does it will exit the program
572 | if event == sg.WINDOW_CLOSED or event == 'EXIT':
573 | window.close()
574 | exit(0)
575 |
576 | # If the randomize button was clicked it will randomize the seed
577 | if event == 'RANDOMIZE':
578 | seed = str(uuid.uuid4()).replace('-', '')
579 |
580 | # If the enhanced res toggle button was clicked it will switch the image and the variable and change the tooltip
581 | if event == 'switch':
582 | toggle = window['switch'].get()
583 |
584 | # If the user has pressed the load config button it will call the load_configuration function,
585 | # This function is before the image gen because it needs to update after the changes have been made
586 | if event == '-LOAD_CONFIG-':
587 | load_configuration(window)
588 |
589 | # -------- Store all window element values into variables --------#
590 |
591 | # Toggles
592 | allow_diagonals = window['-1TOGGLE-'].get()
593 | add_random_chars = window['-2TOGGLE-'].get()
594 | add_word_bank_outline = window['-3TOGGLE-'].get()
595 |
596 | # Colors
597 | grid_line_color = window['-1COLOR_COMBO-'].get()
598 | grid_background_color = window['-2COLOR_COMBO-'].get()
599 | grid_text_color = window['-3COLOR_COMBO-'].get()
600 | grid_random_chars_color = window['-4COLOR_COMBO-'].get()
601 | page_background_color = window['-5COLOR_COMBO-'].get()
602 | page_title_color = window['-6COLOR_COMBO-'].get()
603 | page_subtitle_color = window['-7COLOR_COMBO-'].get()
604 | word_bank_outline_color = window['-8COLOR_COMBO-'].get()
605 | word_bank_background_color = window['-9COLOR_COMBO-'].get()
606 | word_bank_word_color = window['-10COLOR_COMBO-'].get()
607 |
608 | # Strings
609 | words = window['-INPUT1STR-'].get()
610 | available_random_chars = window['-INPUT2STR-'].get()
611 | page_title = window['-INPUT3STR-'].get()
612 | page_subtitle = window['-INPUT4STR-'].get()
613 |
614 | # Numbers
615 | grid_cell_size = window['-INPUT1NUM-'].get()
616 | grid_cells_in_row = window['-INPUT2NUM-'].get()
617 | grid_line_width = window['-INPUT3NUM-'].get()
618 | grid_text_size = window['-INPUT4NUM-'].get()
619 | word_bank_outline_width = window['-INPUT5NUM-'].get()
620 | word_bank_word_size = window['-INPUT6NUM-'].get()
621 | page_title_size = window['-INPUT7NUM-'].get()
622 | page_subtitle_size = window['-INPUT8NUM-'].get()
623 |
624 | # Other
625 | random_seed = None if window['-OTHER1-'].get(
626 | ) == "" else window['-OTHER1-'].get()
627 |
628 | # Set's up the the error list variable
629 | checks = [False, False, False, False, False,
630 | False, False, False, False, False, False]
631 |
632 | # Convert the words variable from a string to a list
633 | t_words = []
634 | for i in words.replace(' ', '').replace(',,', ',').split(','):
635 | if i != '':
636 | t_words.append(i)
637 | words = t_words
638 |
639 | # If the words list is empty it will show an error
640 | if not words:
641 | window['-INPUT1STR-'].set_tooltip("This field can't be empty")
642 | window['-INPUT1STR-'].ParentRowFrame.config(background='red')
643 | checks[0] = False
644 |
645 | # If the words list is bigger than 15 it will show an error
646 | elif len(words) > 15:
647 | window['-INPUT1STR-'].ParentRowFrame.config(background='red')
648 | window['-INPUT1STR-'].set_tooltip(
649 | "You have reached the max word limit (15)")
650 | checks[0] = False
651 |
652 | # If the chars in the word list is bigger than 60 it will show an error
653 | elif len(''.join(words)) > 65:
654 | window['-INPUT1STR-'].ParentRowFrame.config(background='red')
655 | window['-INPUT1STR-'].set_tooltip(
656 | "You have reached the max character limit (65)")
657 | checks[0] = False
658 |
659 | # If the number of words is lower than 3 it will show an error
660 | elif len(words) < 2:
661 | window['-INPUT1STR-'].set_tooltip("You need at least 3 words")
662 | window['-INPUT1STR-'].ParentRowFrame.config(background='red')
663 | checks[0] = False
664 |
665 | # Checks if the variable grid_cells_in_row is NOT auto
666 | # (which means the user has set the number of cells in a row for the grid)
667 | elif grid_cells_in_row != 'auto':
668 |
669 | # If the length of the longest word in the words is bigger than grid_cells_in_row it will show an error
670 | if len(max(words, key=len)) >= int(grid_cells_in_row):
671 | window['-INPUT1STR-'].set_tooltip(f"The length of {max(words, key=len)} is larger then "
672 | f"or equal to the cells in a row, either change the "
673 | f"word or\nchange the cells in a row to 'auto' or to "
674 | f"more than {len(max(words, key=len))}")
675 | window['-INPUT1STR-'].ParentRowFrame.config(background='red')
676 | checks[0] = False
677 |
678 | else:
679 | window['-INPUT1STR-'].set_tooltip('All good :)')
680 | window['-INPUT1STR-'].ParentRowFrame.config(
681 | background=sg.theme_background_color())
682 | checks[0] = True
683 |
684 | else:
685 | window['-INPUT1STR-'].set_tooltip('All good :)')
686 | window['-INPUT1STR-'].ParentRowFrame.config(
687 | background=sg.theme_background_color())
688 | checks[0] = True
689 |
690 | # If the number of new lines in the subtitle is bigger than 2 it will show an error
691 | if page_subtitle.count('\n') > 2:
692 | window['-INPUT4STR-'].set_tooltip(
693 | 'You have reached the maximum lines (3)')
694 | window['-INPUT4STR-'].ParentRowFrame.config(background='red')
695 | checks[-1] = False
696 |
697 | else:
698 | window['-INPUT4STR-'].set_tooltip('All good :)')
699 | window['-INPUT4STR-'].ParentRowFrame.config(
700 | background=sg.theme_background_color())
701 | checks[-1] = True
702 |
703 | # If available_random_chars (the random chars it will put on the grid) is empty it will show an error
704 | if available_random_chars == "":
705 | window['-INPUT2STR-'].set_tooltip("This field cannot be empty!")
706 | window['-INPUT2STR-'].ParentRowFrame.config(background='red')
707 | checks[1] = False
708 |
709 | else:
710 | window['-INPUT2STR-'].set_tooltip('All good :)')
711 | checks[1] = True
712 | window['-INPUT2STR-'].ParentRowFrame.config(
713 | background=sg.theme_background_color())
714 |
715 | # If the cell size is empty it will show an error
716 | if grid_cell_size == "":
717 | window['-INPUT1NUM-'].set_tooltip("This field cannot be empty!")
718 | window['-INPUT1NUM-'].ParentRowFrame.config(background='red')
719 | checks[2] = False
720 |
721 | # If the cell size is not a number it will show an error
722 | elif not grid_cell_size.isdigit():
723 | window['-INPUT1NUM-'].set_tooltip("This field has to be a number!")
724 | window['-INPUT1NUM-'].ParentRowFrame.config(background='red')
725 | checks[2] = False
726 |
727 | else:
728 | grid_cell_size = int(grid_cell_size)
729 |
730 | # If the cell size is bigger than 100 it will show an error
731 | if grid_cell_size > 100:
732 | window['-INPUT1NUM-'].set_tooltip(
733 | "This field has to be lower than or equal to 100")
734 | window['-INPUT1NUM-'].ParentRowFrame.config(background='red')
735 | checks[2] = False
736 |
737 | else:
738 | window['-INPUT1NUM-'].set_tooltip('All good :)')
739 | window['-INPUT1NUM-'].ParentRowFrame.config(
740 | background=sg.theme_background_color())
741 | checks[2] = True
742 |
743 | # If the cells in a row field is empty it will show an error
744 | if grid_cells_in_row == "":
745 | window['-INPUT2NUM-'].set_tooltip("This field cannot be empty!")
746 | window['-INPUT2NUM-'].ParentRowFrame.config(background='red')
747 | checks[3] = False
748 |
749 | # If the cells in a row field is NOT auto and is not a digit it will show an error
750 | elif grid_cells_in_row != "auto" and not grid_cells_in_row.isdigit():
751 | window['-INPUT2NUM-'].set_tooltip(
752 | "This field has to be a number or 'auto'")
753 | window['-INPUT2NUM-'].ParentRowFrame.config(background='red')
754 | checks[3] = False
755 |
756 | else:
757 |
758 | # If the cells in a row field is NOT auto it will convert it to a number
759 | if grid_cells_in_row != "auto":
760 | grid_cells_in_row = int(grid_cells_in_row)
761 |
762 | # If the cells in a row are bigger than 100 it will show an error
763 | if grid_cells_in_row > 100:
764 | window['-INPUT2NUM-'].set_tooltip(
765 | "This field has to be lower than or equal to 100")
766 | window['-INPUT2NUM-'].ParentRowFrame.config(
767 | background='red')
768 | checks[3] = False
769 | else:
770 |
771 | # If the length of the longest word in the words is bigger
772 | # than grid_cells_in_row it will show an error
773 | if words and len(max(words, key=len)) >= grid_cells_in_row:
774 | window['-INPUT2NUM-'].set_tooltip(
775 | f"The length of {max(words, key=len)} is larger then or equal to the cells in a row, "
776 | f"either change the word or\nchange the cells in a row to "
777 | f"'auto' or to more than {len(max(words, key=len))}")
778 | window['-INPUT2NUM-'].ParentRowFrame.config(
779 | background='red')
780 | checks[0] = False
781 |
782 | else:
783 | window['-INPUT2NUM-'].set_tooltip('All good :)')
784 | window['-INPUT2NUM-'].ParentRowFrame.config(
785 | background=sg.theme_background_color())
786 | checks[3] = True
787 |
788 | else:
789 | window['-INPUT2NUM-'].set_tooltip('All good :)')
790 | window['-INPUT2NUM-'].ParentRowFrame.config(
791 | background=sg.theme_background_color())
792 | checks[3] = True
793 |
794 | # If the line width of the grid is empty it will show an error
795 | if grid_line_width == "":
796 | window['-INPUT3NUM-'].set_tooltip("This field cannot be empty!")
797 | window['-INPUT3NUM-'].ParentRowFrame.config(background='red')
798 | checks[4] = False
799 |
800 | # If the line width of the grid is NOT a digit it will show an error
801 | elif not grid_line_width.isdigit():
802 | window['-INPUT3NUM-'].set_tooltip("This field has to be a number!")
803 | window['-INPUT3NUM-'].ParentRowFrame.config(background='red')
804 | checks[4] = False
805 |
806 | # If line width of the grid is bigger than 100 is will show an error
807 | elif int(grid_line_width) > 100:
808 | window['-INPUT3NUM-'].set_tooltip(
809 | "This field has lower than or equal to 100")
810 | window['-INPUT3NUM-'].ParentRowFrame.config(background='red')
811 | checks[4] = False
812 |
813 | else:
814 | grid_line_width = int(grid_line_width)
815 | window['-INPUT3NUM-'].set_tooltip("All good :)")
816 | window['-INPUT3NUM-'].ParentRowFrame.config(
817 | background=sg.theme_background_color())
818 | checks[4] = True
819 |
820 | # If the text size of the grid is empty it will show an error
821 | if grid_text_size == "":
822 | window['-INPUT4NUM-'].set_tooltip("This field cannot be empty!")
823 | window['-INPUT4NUM-'].ParentRowFrame.config(background='red')
824 | checks[5] = False
825 |
826 | # If the text size of the grid is NOT a number it will show an error
827 | elif not grid_text_size.isdigit():
828 | window['-INPUT4NUM-'].set_tooltip("This field has to be a number!")
829 | window['-INPUT4NUM-'].ParentRowFrame.config(background='red')
830 | checks[5] = False
831 |
832 | # If the text size of the grid is bigger than 100 it will show an error
833 | elif int(grid_text_size) > 100:
834 | window['-INPUT4NUM-'].set_tooltip(
835 | "This field has lower than or equal to 100")
836 | window['-INPUT4NUM-'].ParentRowFrame.config(background='red')
837 | checks[5] = False
838 |
839 | else:
840 | grid_text_size = int(grid_text_size)
841 | window['-INPUT4NUM-'].set_tooltip("All good :)")
842 | window['-INPUT4NUM-'].ParentRowFrame.config(
843 | background=sg.theme_background_color())
844 | checks[5] = True
845 |
846 | # If the word bank outline width is empty it will show an error
847 | if word_bank_outline_width == "":
848 | window['-INPUT5NUM-'].set_tooltip("This field cannot be empty!")
849 | window['-INPUT5NUM-'].ParentRowFrame.config(background='red')
850 | checks[6] = False
851 |
852 | # If the word bank outline width is NOT a number it will show an error
853 | elif not word_bank_outline_width.isdigit():
854 | window['-INPUT5NUM-'].set_tooltip("This field has to be a number!")
855 | window['-INPUT5NUM-'].ParentRowFrame.config(background='red')
856 | checks[6] = False
857 |
858 | # If the word bank outline width is bigger than 100 it will show an error
859 | elif int(word_bank_outline_width) > 100:
860 | window['-INPUT5NUM-'].set_tooltip(
861 | "This field has lower than or equal to 100")
862 | window['-INPUT5NUM-'].ParentRowFrame.config(background='red')
863 | checks[6] = False
864 |
865 | else:
866 | word_bank_outline_width = int(word_bank_outline_width)
867 | window['-INPUT5NUM-'].set_tooltip("All good :)")
868 | window['-INPUT5NUM-'].ParentRowFrame.config(
869 | background=sg.theme_background_color())
870 | checks[6] = True
871 |
872 | # If the word bank's word size is empty it will show an error
873 | if word_bank_word_size == "":
874 | window['-INPUT6NUM-'].set_tooltip("This field cannot be empty!")
875 | window['-INPUT6NUM-'].ParentRowFrame.config(background='red')
876 | checks[7] = False
877 |
878 | # If the word bank's word size is NOT a number it will show an error
879 | elif not word_bank_word_size.isdigit():
880 | window['-INPUT6NUM-'].set_tooltip("This field has to be a number!")
881 | window['-INPUT6NUM-'].ParentRowFrame.config(background='red')
882 | checks[7] = False
883 |
884 | # If the word bank's word size is bigger than 100 it will show an error
885 | elif int(word_bank_word_size) > 100:
886 | window['-INPUT6NUM-'].set_tooltip(
887 | "This field has lower than or equal to 100")
888 | window['-INPUT6NUM-'].ParentRowFrame.config(background='red')
889 | checks[7] = False
890 |
891 | else:
892 | word_bank_word_size = int(word_bank_word_size)
893 | window['-INPUT6NUM-'].set_tooltip("All good :)")
894 | window['-INPUT6NUM-'].ParentRowFrame.config(
895 | background=sg.theme_background_color())
896 | checks[7] = True
897 |
898 | # If the page title size is empty it will show an error
899 | if page_title_size == "":
900 | window['-INPUT7NUM-'].set_tooltip("This field cannot be empty!")
901 | window['-INPUT7NUM-'].ParentRowFrame.config(background='red')
902 | checks[8] = False
903 |
904 | # If the page title size is NOT a number it will show an error
905 | elif not page_title_size.isdigit():
906 | window['-INPUT7NUM-'].set_tooltip("This field has to be a number!")
907 | window['-INPUT7NUM-'].ParentRowFrame.config(background='red')
908 | checks[8] = False
909 |
910 | # If the page title size is bigger than it will show an error
911 | elif int(page_title_size) > 100:
912 | window['-INPUT7NUM-'].set_tooltip(
913 | "This field has lower than or equal to 100")
914 | window['-INPUT7NUM-'].ParentRowFrame.config(background='red')
915 | checks[8] = False
916 |
917 | else:
918 | page_title_size = int(page_title_size)
919 | window['-INPUT7NUM-'].set_tooltip("All good :)")
920 | window['-INPUT7NUM-'].ParentRowFrame.config(
921 | background=sg.theme_background_color())
922 | checks[8] = True
923 |
924 | # If the page subtitle size is empty it will show an error
925 | if page_subtitle_size == "":
926 | window['-INPUT8NUM-'].set_tooltip("This field cannot be empty!")
927 | window['-INPUT8NUM-'].ParentRowFrame.config(background='red')
928 | checks[9] = False
929 |
930 | # If the page subtitle size is NOT a number it will show an error
931 | elif not page_subtitle_size.isdigit():
932 | window['-INPUT8NUM-'].set_tooltip("This field has to be a number!")
933 | window['-INPUT8NUM-'].ParentRowFrame.config(background='red')
934 | checks[9] = False
935 |
936 | # If the page subtitle size is bigger than 100 it will show an error
937 | elif int(page_subtitle_size) > 100:
938 | window['-INPUT8NUM-'].set_tooltip(
939 | "This field has lower than or equal to 100")
940 | window['-INPUT8NUM-'].ParentRowFrame.config(background='red')
941 | checks[9] = False
942 |
943 | else:
944 | page_subtitle_size = int(page_subtitle_size)
945 | window['-INPUT8NUM-'].set_tooltip("All good :)")
946 | window['-INPUT8NUM-'].ParentRowFrame.config(
947 | background=sg.theme_background_color())
948 | checks[9] = True
949 |
950 | # If all the checks have passed it will continue
951 | if not all(checks) and event in ['-SAVE_FILE-', '-PRINT-', '-COPY_TO_CLIPBOARD-']:
952 | sg.popup_error(
953 | 'There are some errors in the input! look for red boxes')
954 |
955 | if all(checks):
956 | # Converts the string of words to a list
957 | tmp_words = []
958 | for i in words:
959 | if i != "":
960 | tmp_words.append(i.replace(' ', ''))
961 |
962 | # Calls the search word puzzle generator with the settings the user has inputted
963 | if event != '-TAB_GROUP-':
964 | image = SWP.create_search_word_puzzle(
965 | words=tmp_words,
966 | random_chars=available_random_chars,
967 | allow_diagonal=allow_diagonals,
968 | cells_in_row=grid_cells_in_row,
969 | square_size=grid_cell_size,
970 | grid_separator_color=grid_line_color,
971 | grid_background_color=grid_background_color,
972 | grid_separator_width=grid_line_width,
973 | grid_text_color=grid_text_color,
974 | grid_text_size=grid_text_size,
975 | random_char_color=grid_random_chars_color,
976 | add_randomized_chars=add_random_chars,
977 | page_color=page_background_color,
978 | page_title=page_title,
979 | title_color=page_title_color,
980 | word_bank_outline=add_word_bank_outline,
981 | word_bank_outline_color=word_bank_outline_color,
982 | word_bank_outline_width=word_bank_outline_width,
983 | word_bank_fill_color=word_bank_background_color,
984 | words_in_word_bank_color=word_bank_word_color,
985 | random_seed=random_seed if random_seed is not None else seed,
986 | subtitle=page_subtitle,
987 | subtitle_color=page_subtitle_color,
988 | title_size=page_title_size,
989 | subtitle_size=page_subtitle_size,
990 | words_in_word_bank_size=word_bank_word_size,
991 |
992 | # This is the resolution multiplier and it will rise automatically if
993 | # the user has printed/copied/saved the image
994 | res_multiplier=4 if (
995 | event in ['-SAVE_FILE-', '-PRINT-', '-COPY_TO_CLIPBOARD-'] or toggle) else 2
996 | )
997 | # If the user has not pressed on the save/print/copy image buttons it will show the image on the page
998 | if event not in ['-SAVE_FILE-', '-PRINT-', '-COPY_TO_CLIPBOARD-']:
999 | window["-CWIMAGE-"].update(
1000 | data=image_to_bios(image, (750, 750)))
1001 |
1002 | # If the user has pressed the save file button it will call the save_file function
1003 | if event == '-SAVE_FILE-':
1004 | save_file(image)
1005 |
1006 | # If the user has pressed the print button it will call the print_image function
1007 | elif event == '-PRINT-':
1008 | print_image(image)
1009 |
1010 | # If the user has pressed the copy button it will call the to_clipboard function
1011 | elif event == '-COPY_TO_CLIPBOARD-':
1012 | to_clipboard(image)
1013 |
1014 | # If the user has pressed the save config button it will call the save_configuration function
1015 | elif event == '-SAVE_CONFIG-':
1016 | save_configuration([(allow_diagonals, f'{allow_diagonals=}'.split('=')[0]),
1017 | (add_random_chars,
1018 | f'{add_random_chars=}'.split('=')[0]),
1019 | (add_word_bank_outline,
1020 | f'{add_word_bank_outline=}'.split('=')[0]),
1021 | (grid_line_color,
1022 | f'{grid_line_color=}'.split('=')[0]),
1023 | (grid_background_color,
1024 | f'{grid_background_color=}'.split('=')[0]),
1025 | (grid_text_color,
1026 | f'{grid_text_color=}'.split('=')[0]),
1027 | (grid_random_chars_color,
1028 | f'{grid_random_chars_color=}'.split('=')[0]),
1029 | (page_background_color,
1030 | f'{page_background_color=}'.split('=')[0]),
1031 | (page_title_color,
1032 | f'{page_title_color=}'.split('=')[0]),
1033 | (page_subtitle_color,
1034 | f'{page_subtitle_color=}'.split('=')[0]),
1035 | (word_bank_outline_color,
1036 | f'{word_bank_outline_color=}'.split('=')[0]),
1037 | (word_bank_background_color,
1038 | f'{word_bank_background_color=}'.split('=')[0]),
1039 | (word_bank_word_color,
1040 | f'{word_bank_word_color=}'.split('=')[0]),
1041 | (', '.join(words), f'{words=}'.split('=')[0]),
1042 | (available_random_chars,
1043 | f'{available_random_chars=}'.split('=')[0]),
1044 | (page_title,
1045 | f'{page_title=}'.split('=')[0]),
1046 | (page_subtitle,
1047 | f'{page_subtitle=}'.split('=')[0]),
1048 | (grid_cell_size,
1049 | f'{grid_cell_size=}'.split('=')[0]),
1050 | (grid_cells_in_row,
1051 | f'{grid_cells_in_row=}'.split('=')[0]),
1052 | (grid_line_width,
1053 | f'{grid_line_width=}'.split('=')[0]),
1054 | (grid_text_size,
1055 | f'{grid_text_size=}'.split('=')[0]),
1056 | (word_bank_outline_width,
1057 | f'{word_bank_outline_width=}'.split('=')[0]),
1058 | (word_bank_word_size,
1059 | f'{word_bank_word_size=}'.split('=')[0]),
1060 | (page_title_size,
1061 | f'{page_title_size=}'.split('=')[0]),
1062 | (page_subtitle_size,
1063 | f'{page_subtitle_size=}'.split('=')[0]),
1064 | (random_seed, f'{random_seed=}'.split('=')[0])])
1065 |
1066 | # If the user has pressed the reset to default button it will call the reset_to_default function
1067 | elif event == '-RESET-':
1068 | reset_to_default(window)
1069 |
1070 | # If there was even one error it will disable the buttons and show a tooltip on the buttons
1071 | else:
1072 | pass
1073 |
1074 |
1075 | # If the file was ran directly it will run the program
1076 | if __name__ == '__main__':
1077 | setup_layout()
1078 | window_loop()
1079 |
--------------------------------------------------------------------------------