├── 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 | Logo 9 | 10 | 11 |

Word Search Puzzle Generator

12 | 13 |
14 | 15 | 16 | 17 |
18 | Table of Contents 19 |
    20 |
  1. 21 | About The Project 22 | 25 |
  2. 26 |
  3. 27 | Getting Started 28 | 32 |
  4. 33 |
  5. Usage
  6. 34 |
  7. Roadmap
  8. 35 |
  9. Contributing
  10. 36 |
  11. License
  12. 37 |
  13. Contact
  14. 38 |
  15. Acknowledgments
  16. 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 | ![image](https://user-images.githubusercontent.com/66069146/144716879-2d90047d-a42b-40ba-abc7-72e1ef375844.png) 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 | ![Untitled](https://user-images.githubusercontent.com/66069146/144716963-7e6589fe-39d7-43d9-b822-f7fe24c957b8.png) 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 | --------------------------------------------------------------------------------