diff --git a/BeatPrints/poster.py b/BeatPrints/poster.py index 5c971f9..767fb5c 100644 --- a/BeatPrints/poster.py +++ b/BeatPrints/poster.py @@ -91,7 +91,8 @@ def track( accent: bool = False, theme: ThemesSelector.Options = "Light", pcover: Optional[str] = None, - ) -> None: + return_image: bool = False, + ) -> Optional[Image.Image]: """ Generates a poster for a track, which includes lyrics. @@ -101,6 +102,10 @@ def track( accent (bool, optional): Adds an accent at the bottom of the poster. Defaults to False. theme (ThemesSelector.Options, optional): Specifies the theme to use. Must be one of "Light", "Dark", "Catppuccin", "Gruvbox", "Nord", "RosePine", or "Everforest". Defaults to "Light". pcover (Optional[str]): Path to a custom cover image. Defaults to None. + return_image (bool, optional): If True, returns the PIL Image object instead of saving. Defaults to False. + + Returns: + Optional[Image.Image]: The PIL Image object of the poster if return_image is True, otherwise None. """ # Check if the theme is valid or not @@ -148,13 +153,16 @@ def track( anchor="lt", ) - # Save the generated poster with a unique filename - name = filename(metadata.name, metadata.artist) - poster.save(os.path.join(self.save_to, name)) - - print( - f"✨ Poster for {metadata.name} by {metadata.artist} saved to {self.save_to}" - ) + if return_image: + return poster + else: + # Save the generated poster with a unique filename + name = filename(metadata.name, metadata.artist) + poster.save(os.path.join(self.save_to, name)) + print( + f"✨ Poster for {metadata.name} by {metadata.artist} saved to {self.save_to}" + ) + return None def album( self, @@ -163,7 +171,8 @@ def album( accent: bool = False, theme: ThemesSelector.Options = "Light", pcover: Optional[str] = None, - ) -> None: + return_image: bool = False, + ) -> Optional[Image.Image]: """ Generates a poster for an album, which includes track listing. @@ -173,6 +182,10 @@ def album( accent (bool, optional): Add an accent at the bottom of the poster. Defaults to False. theme (ThemesSelector.Options, optional): Specifies the theme to use. Must be one of "Light", "Dark", "Catppuccin", "Gruvbox", "Nord", "RosePine", or "Everforest". Defaults to "Light". pcover (Optional[str]): Path to a custom cover image. Defaults to None. + return_image (bool, optional): If True, returns the PIL Image object instead of saving. Defaults to False. + + Returns: + Optional[Image.Image]: The PIL Image object of the poster if return_image is True, otherwise None. """ # Check if the theme mentioned is valid or not @@ -223,9 +236,13 @@ def album( ) x += column_width + s.SPACING # Adjust x for next column - # Save the generated album poster with a unique filename - name = filename(metadata.name, metadata.artist) - poster.save(os.path.join(self.save_to, name)) - print( - f"✨ Album poster for {metadata.name} by {metadata.artist} saved to {self.save_to}" - ) + if return_image: + return poster + else: + # Save the generated album poster with a unique filename + name = filename(metadata.name, metadata.artist) + poster.save(os.path.join(self.save_to, name)) + print( + f"✨ Album poster for {metadata.name} by {metadata.artist} saved to {self.save_to}" + ) + return None \ No newline at end of file diff --git a/BeatPrints/wallpaper.py b/BeatPrints/wallpaper.py new file mode 100644 index 0000000..da4584c --- /dev/null +++ b/BeatPrints/wallpaper.py @@ -0,0 +1,82 @@ +from PIL import Image, ImageDraw, ImageFilter + +def draw_drop_shadow(base_image, offset=(10, 10), shadow_color=(0, 0, 0, 128), blur_radius=10): + """Draws a drop shadow behind the given image.""" + shadow = Image.new('RGBA', base_image.size, (0, 0, 0, 0)) + shadow_draw = ImageDraw.Draw(shadow) + shadow_draw.rectangle((0, 0, base_image.width, base_image.height), fill=shadow_color) + + blurred_shadow = shadow.filter(ImageFilter.GaussianBlur(radius=blur_radius)) + + shadow_offset = Image.new('RGBA', (base_image.width + abs(offset[0]) * 2, base_image.height + abs(offset[1]) * 2), (0, 0, 0, 0)) + shadow_offset.paste(blurred_shadow, (abs(offset[0]), abs(offset[1]))) + + #Can probably be dropped + final_shadow = Image.new('RGBA', shadow_offset.size, (0, 0, 0, 0)) + final_shadow.paste(shadow_offset, (0, 0)) + + return final_shadow + +def generate_wallpaper(resolution, image_paths, bg_color): + width_res, height_res = resolution + num_images = len(image_paths) + + if not 1 <= num_images <= 10: + raise ValueError("Number of images must be between 1 and 10.") + # Math... Basically take the Wallpaper Width/Height, calculate what the appropriate size of the posters should be + # Horizontal gaps between images are set to be 1/16 the size of the images + # Gaps between the images and the edges of the wallpaper are set to be at least double the size of the inner gaps + # At least one side has to give. The vertical or the horizontal edge gaps + # The vertical_gap was chosen arbitrairly as the baseline for comparison + + if num_images > 0: + inner_gap_horizontal_ver = width_res / (3 + (17 * num_images)) + vertical_gap_horizontal_ver = 2 * inner_gap_horizontal_ver + vertical_gap_vertical_ver = height_res/(2 + (8*26/19)) + + if vertical_gap_horizontal_ver < vertical_gap_vertical_ver: + inner_gap = inner_gap_horizontal_ver + image_width = inner_gap * 16 + vertical_gap = vertical_gap_horizontal_ver + image_height = (29/19) * image_width + + else: + vertical_gap = vertical_gap_vertical_ver + inner_gap = vertical_gap / 2 + image_height = vertical_gap * 26 * 8 / 19 + image_width = image_height * 19 / 29 + + wallpaper = Image.new('RGB', resolution, bg_color) + shadow_offset = (int(inner_gap / 8), int(inner_gap / 8)) + shadow_color = (0, 0, 0, 128) + blur_radius = int(inner_gap / 8) + + if num_images > 0: + total_images_width = num_images * image_width + total_inner_gaps_width = (num_images - 1) * inner_gap + horizontal_available_space = width_res - total_images_width - total_inner_gaps_width + horizontal_outer_gap = horizontal_available_space / 2 + vertical_available_space = height_res - image_height + vertical_outer_gap = vertical_available_space / 2 + + y_position = vertical_outer_gap + x_position = horizontal_outer_gap + + for path in image_paths: + try: + img = Image.open(path).convert("RGBA") + resized_img = img.resize((int(image_width), int(image_height))) + + # Draw drop shadow + shadow = draw_drop_shadow(resized_img, offset=shadow_offset, shadow_color=shadow_color, blur_radius=blur_radius) + wallpaper.paste(shadow, (int(x_position) + shadow_offset[0], int(y_position) + shadow_offset[1]), shadow) + + # Paste the actual image + wallpaper.paste(resized_img, (int(x_position), int(y_position)), resized_img) + + x_position += image_width + inner_gap + except FileNotFoundError: + print(f"Error: Image not found at {path}") + # Handle error as needed + + return wallpaper \ No newline at end of file diff --git a/cli/prompt.py b/cli/prompt.py index 6607101..af34d04 100644 --- a/cli/prompt.py +++ b/cli/prompt.py @@ -3,7 +3,8 @@ from rich import print from cli import conf, exutils, validate -from BeatPrints import lyrics, spotify, poster, errors +from BeatPrints import lyrics, spotify, poster, errors, wallpaper +import os # Initialize components ly = lyrics.Lyrics() @@ -220,7 +221,7 @@ def poster_features(): return theme, accent, image_path -def create_poster(): +def create_poster(return_image=False): """ Create a poster based on user input. """ @@ -236,6 +237,8 @@ def create_poster(): # Clear the screen exutils.clear() + generated_image = None + # Generate posters if poster_type == "Track Poster": track = select_track(conf.SEARCH_LIMIT) @@ -244,19 +247,106 @@ def create_poster(): lyrics = handle_lyrics(track) exutils.clear() - ps.track(track, lyrics, accent, theme, image) + generated_image = ps.track(track, lyrics, accent, theme, image, return_image=return_image) else: album = select_album(conf.SEARCH_LIMIT) if album: - ps.album(*album, accent, theme, image) + generated_image = ps.album(*album, accent, theme, image, return_image=return_image) + + return generated_image def main(): exutils.clear() try: - create_poster() + creation_type = questionary.select( + "• What would you like to create?", + choices=["Poster", "Wallpaper"], + style=exutils.lavish, + qmark="✨", + ).unsafe_ask() + + if creation_type == "Poster": + create_poster() + elif creation_type == "Wallpaper": + num_posters = questionary.text( + "• How many posters (1-10) would you like in your wallpaper?", + validate=validate.NumericValidator(limit=10), + style=exutils.lavish, + qmark="🖼️", + ).unsafe_ask() + num_posters = int(num_posters) + + if 1 <= num_posters <= 10: + poster_images = [] + temp_poster_paths = [] + for i in range(num_posters): + print(f"\nCreating poster {i+1} for the wallpaper:") + poster_image = create_poster(return_image=True) + if poster_image: + poster_images.append(poster_image) + # Optionally save to temporary files if memory becomes an issue + temp_path = f"temp_poster_{i+1}.png" + poster_image.save(temp_path) + temp_poster_paths.append(temp_path) + else: + print("Error creating a poster. Wallpaper creation aborted.") + # Clean up any created temporary files + for path in temp_poster_paths: + try: + os.remove(path) + except FileNotFoundError: + pass + return + + wallpaper_width = questionary.text( + "• Enter the desired wallpaper width:", + validate=validate.NumericValidator(limit=9999), + style=exutils.lavish, + qmark="📏", + ).unsafe_ask() + wallpaper_height = questionary.text( + "• Enter the desired wallpaper height:", + validate=validate.NumericValidator(limit=9999), + style=exutils.lavish, + qmark="📐", + ).unsafe_ask() + wallpaper_resolution = (int(wallpaper_width), int(wallpaper_height)) + wallpaper_bg_color = questionary.text( + "• Enter the background color for the wallpaper (e.g., slategrey, #RRGGBB):", + style=exutils.lavish, + qmark="🎨", + ).unsafe_ask() + + try: + wallpaper_image = wallpaper.generate_wallpaper( + wallpaper_resolution, + temp_poster_paths, # Pass the list of temporary file paths + wallpaper_bg_color + ) + # Need to come up with a scheme for naming generated files + wallpaper_save_path = os.path.join(conf.POSTERS_DIR, "generated_wallpaper.png") + wallpaper_image.save(wallpaper_save_path) + print(f"\nWallpaper created successfully and saved as {wallpaper_save_path}") + + # Clean up temporary poster files + for path in temp_poster_paths: + try: + os.remove(path) + except FileNotFoundError: + pass + + except ValueError as e: + print(f"Error during wallpaper generation: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + else: + print("Invalid number of posters.") + else: + print("Invalid choice.") + except KeyboardInterrupt: exutils.clear() print("👋 Alright, no problem! See you next time.")