# utils/image_utils.py import os from io import BytesIO import cairosvg import base64 import numpy as np #from decimal import ROUND_CEILING from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter, ImageDraw, ImageOps, ImageMath from typing import List, Union, is_typeddict #import numpy as np #import math from pathlib import Path from utils.constants import default_lut_example_img, PRE_RENDERED_MAPS_JSON_LEVELS from utils.color_utils import ( detect_color_format, update_color_opacity ) from utils.file_utils import rename_file_to_lowercase_extension, get_file_parts def save_image_to_temp_png(image_source, user_dir: str = None, file_name: str = None): """ Opens an image from a file path, URL, or DataURL and saves it as a PNG in the user's temporary directory. Parameters: image_source (str, dict or PIL.Image.Image): The source of the image to open. Returns: str: The file path of the saved PNG image in the temporary directory. """ import tempfile import uuid # Open the image using the existing utility function img = open_image(image_source) # Ensure the image is in a format that supports PNG (convert if necessary) if img.mode not in ("RGB", "RGBA"): img = img.convert("RGBA") # Generate a unique filename in the system temporary directory if user_dir is None: user_dir = tempfile.gettempdir() if file_name is None: file_name = "{uuid.uuid4()}" temp_filepath = os.path.join(user_dir, file_name.lower() + ".png") os.makedirs(user_dir, exist_ok=True) # Save the image as PNG img.save(temp_filepath, format="PNG") return temp_filepath def get_image_from_dict(image_path): if isinstance(image_path, dict) : if 'composite' in image_path: image_path = image_path.get('composite') elif 'image' in image_path: image_path = image_path.get('image') else: print("\n Unknown image dictionary.\n") raise UserWarning("Unknown image dictionary.") return image_path, True else: return image_path, False def open_image(image_path): """ Opens an image from a file path or URL, or decodes a DataURL string into an image. Supports SVG and ICO by converting them to PNG. Parameters: image_path (str): The file path, URL, or DataURL string of the image to open. Returns: Image: A PIL Image object of the opened image. Raises: Exception: If there is an error opening the image. """ if isinstance(image_path, Image.Image): return image_path elif isinstance(image_path, dict): image_path, is_dict = get_image_from_dict(image_path) image_path = rename_file_to_lowercase_extension(image_path) import requests try: # Strip leading and trailing double quotation marks, if present image_path = image_path.strip('"') if image_path.startswith('http'): response = requests.get(image_path) if image_path.lower().endswith('.svg'): png_data = cairosvg.svg2png(bytestring=response.content) img = Image.open(BytesIO(png_data)) elif image_path.lower().endswith('.ico'): img = Image.open(BytesIO(response.content)).convert('RGBA') else: img = Image.open(BytesIO(response.content)) elif image_path.startswith('data'): encoded_data = image_path.split(',')[1] decoded_data = base64.b64decode(encoded_data) if image_path.lower().endswith('.svg'): png_data = cairosvg.svg2png(bytestring=decoded_data) img = Image.open(BytesIO(png_data)) elif image_path.lower().endswith('.ico'): img = Image.open(BytesIO(decoded_data)).convert('RGBA') else: img = Image.open(BytesIO(decoded_data)) else: if image_path.lower().endswith('.svg'): png_data = cairosvg.svg2png(url=image_path) img = Image.open(BytesIO(png_data)) elif image_path.lower().endswith('.ico'): img = Image.open(image_path).convert('RGBA') else: img = Image.open(image_path) except Exception as e: raise Exception(f'Error opening image: {e}') return img def build_prerendered_images(images_list): """ Opens a list of images from file paths, URLs, or DataURL strings. Parameters: images_list (list): A list of file paths, URLs, or DataURL strings of the images to open. Returns: list: A list of PIL Image objects of the opened images. """ return [open_image(image) for image in images_list] # Example usage # filtered_maps = get_maps_with_quality_less_than(3) # print(filtered_maps) def build_prerendered_images_by_quality(quality_limit, key='file'): """ Retrieve and sort file paths from PRE_RENDERED_MAPS_JSON_LEVELS where quality is <= quality_limit. Sorts by quality and case-insensitive alphanumeric key. Args: quality_limit (int): Maximum quality threshold key (str): Key to extract file path from map info (default: 'file') Returns: tuple: (sorted file paths list, list of corresponding map names) """ # Pre-compute lowercase alphanumeric key once per item def get_sort_key(item): name, info = item return (info['quality'], ''.join(c for c in name.lower() if c.isalnum())) # Single pass: sort and filter filtered_maps = [ (info[key].replace("\\", "/"), name) for name, info in sorted(PRE_RENDERED_MAPS_JSON_LEVELS.items(), key=get_sort_key) if info['quality'] <= quality_limit ] # Split into separate lists efficiently if filtered_maps: #file_paths, map_names = zip(*filtered_maps) #return (build_prerendered_images(file_paths), list(map_names)) return [(open_image(file_path), map_name) for file_path, map_name in filtered_maps] return (None,"") def build_encoded_images(images_list): """ Encodes a list of images to base64 strings. Parameters: images_list (list): A list of file paths, URLs, DataURL strings, or PIL Image objects of the images to encode. Returns: list: A list of base64-encoded strings of the images. """ return [image_to_base64(image) for image in images_list] def image_to_base64(image): """ Encodes an image to a base64 string. Supports ICO files by converting them to PNG with RGBA channels. Parameters: image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to encode. Returns: str: A base64-encoded string of the image. """ buffered = BytesIO() if isinstance(image, str): image = open_image(image) image.save(buffered, format="PNG") return "data:image/png;base64," + base64.b64encode(buffered.getvalue()).decode() def change_color(image, color, opacity=0.75): """ Changes the color of an image by overlaying it with a specified color and opacity. Parameters: image (str or PIL.Image.Image): The file path, URL, DataURL string, or PIL Image object of the image to change. color (str or tuple): The color to overlay on the image. opacity (float): The opacity of the overlay color (0.0 to 1.0). Returns: PIL.Image.Image: The image with the color changed. """ if type(image) is str: image = open_image(image) try: # Convert the color to RGBA format rgba_color = detect_color_format(color) rgba_color = update_color_opacity(rgba_color, opacity) # Convert the image to RGBA mode image = image.convert("RGBA") # Create a new image with the same size and mode new_image = Image.new("RGBA", image.size, rgba_color) # Composite the new image with the original image result = Image.alpha_composite(image, new_image) except Exception as e: print(f"Error changing color: {e}") return image return result def convert_str_to_int_or_zero(value): """ Converts a string to an integer, or returns zero if the conversion fails. Parameters: value (str): The string to convert. Returns: int: The converted integer, or zero if the conversion fails. """ try: return int(value) except ValueError: return 0 def upscale_image(image, scale_factor): """ Upscales an image by a given scale factor using the LANCZOS filter. Parameters: image (PIL.Image.Image): The input image to be upscaled. scale_factor (float): The factor by which to upscale the image. Returns: PIL.Image.Image: The upscaled image. """ # Calculate the new size new_width = int(image.width * scale_factor) new_height = int(image.height * scale_factor) # Upscale the image using the LANCZOS filter upscaled_image = image.resize((new_width, new_height), Image.LANCZOS) return upscaled_image def crop_and_resize_image(image, width, height): """ Crops the image to a centered square and resizes it to the specified width and height. Parameters: image (PIL.Image.Image): The input image to be cropped and resized. width (int): The desired width of the output image. height (int): The desired height of the output image. Returns: PIL.Image.Image: The cropped and resized image. """ # Get original dimensions original_width, original_height = image.size # Determine the smaller dimension to make a square crop min_dim = min(original_width, original_height) # Calculate coordinates for cropping to a centered square left = (original_width - min_dim) // 2 top = (original_height - min_dim) // 2 right = left + min_dim bottom = top + min_dim # Crop the image cropped_image = image.crop((left, top, right, bottom)) # Resize the image to the desired dimensions resized_image = cropped_image.resize((width, height), Image.LANCZOS) return resized_image def resize_image_with_aspect_ratio(image, target_width, target_height): """ Resizes the image to fit within the target dimensions while maintaining aspect ratio. If the aspect ratio does not match, the image will be padded with black pixels. Parameters: image (PIL.Image.Image): The input image to be resized. target_width (int): The target width. target_height (int): The target height. Returns: PIL.Image.Image: The resized image. """ # Calculate aspect ratios original_width, original_height = image.size target_aspect = target_width / target_height original_aspect = original_width / original_height #print(f"Original size: {image.size}\ntarget_aspect: {target_aspect}\noriginal_aspect: {original_aspect}\n") # Decide whether to fit width or height if original_aspect > target_aspect: # Image is wider than target aspect ratio new_width = target_width new_height = int(target_width / original_aspect) else: # Image is taller than target aspect ratio new_height = target_height new_width = int(target_height * original_aspect) # Resize the image resized_image = image.resize((new_width, new_height), Image.LANCZOS) #print(f"Resized size: {resized_image.size}\n") # Create a new image with target dimensions and black background new_image = Image.new("RGB", (target_width, target_height), (0, 0, 0)) # Paste the resized image onto the center of the new image paste_x = (target_width - new_width) // 2 paste_y = (target_height - new_height) // 2 new_image.paste(resized_image, (paste_x, paste_y)) return new_image def lerp_imagemath(img1, img2, alpha_percent: int = 50): """ Performs linear interpolation (LERP) between two images based on the given alpha value. Parameters: img1 (str or PIL.Image.Image): The first image or its file path. img2 (str or PIL.Image.Image): The second image or its file path. alpha (int): The interpolation factor (0 to 100). Returns: PIL.Image.Image: The interpolated image. """ if isinstance(img1, str): img1 = open_image(img1) if isinstance(img2, str): img2 = open_image(img2) # Ensure both images are in the same mode (e.g., RGBA) img1 = img1.convert('RGBA') img2 = img2.convert('RGBA') # Convert images to NumPy arrays arr1 = np.array(img1, dtype=np.float32) arr2 = np.array(img2, dtype=np.float32) # Perform linear interpolation alpha = alpha_percent / 100.0 result_arr = (arr1 * (1 - alpha)) + (arr2 * alpha) # Convert the result back to a PIL image result_img = Image.fromarray(np.uint8(result_arr)) #result_img.show() return result_img def shrink_and_paste_on_blank(current_image, mask_width, mask_height, blank_color:tuple[int, int, int, int] = (0,0,0,0)): """ Decreases size of current_image by mask_width pixels from each side, then adds a mask_width width transparent frame, so that the image the function returns is the same size as the input. Parameters: current_image (PIL.Image.Image): The input image to transform. mask_width (int): Width in pixels to shrink from each side. mask_height (int): Height in pixels to shrink from each side. blank_color (tuple): The color of the blank frame (default is transparent). Returns: PIL.Image.Image: The transformed image. """ # calculate new dimensions width, height = current_image.size new_width = width - (2 * mask_width) new_height = height - (2 * mask_height) # resize and paste onto blank image prev_image = current_image.resize((new_width, new_height)) blank_image = Image.new("RGBA", (width, height), blank_color) blank_image.paste(prev_image, (mask_width, mask_height)) return blank_image def multiply_and_blend_images(base_image, image2, alpha_percent=50): """ Multiplies two images and blends the result with the original image. Parameters: image1 (PIL.Image.Image): The first input image. image2 (PIL.Image.Image): The second input image. alpha (float): The blend factor (0.0 to 100.0) for blending the multiplied result with the original image. Returns: PIL.Image.Image: The blended image. """ name = None directory = None alpha = alpha_percent / 100.0 if isinstance(base_image, str): directory, _, name,_,_ = get_file_parts(base_image) base_image = open_image(base_image) if isinstance(image2, str): image2 = open_image(image2) # Ensure both images are in the same mode and size base_image = base_image.convert('RGBA') image2 = image2.convert('RGBA') image2 = image2.resize(base_image.size) # Multiply the images multiplied_image = ImageChops.multiply(base_image, image2) # Blend the multiplied result with the original blended_image = Image.blend(base_image, multiplied_image, alpha) if name is not None: new_image_path = os.path.join(directory, name + f"_mb{str(alpha_percent)}.png") blended_image.save(new_image_path) return new_image_path return blended_image def alpha_composite_with_control(base_image, image_with_alpha, alpha_percent=100): """ Overlays image_with_alpha onto base_image with controlled alpha transparency. Parameters: base_image (PIL.Image.Image): The base image. image_with_alpha (PIL.Image.Image): The image to overlay with an alpha channel. alpha_percent (float): The multiplier for the alpha channel (0.0 to 100.0). Returns: PIL.Image.Image: The resulting image after alpha compositing. """ name = None directory = None image_with_alpha, isdict = get_image_from_dict(image_with_alpha) alpha_multiplier = alpha_percent / 100.0 if isinstance(base_image, str): directory, _, name,_, new_ext = get_file_parts(base_image) base_image = open_image(base_image) if isinstance(image_with_alpha, str): image_with_alpha = open_image(image_with_alpha) # Ensure both images are in RGBA mode base_image = base_image.convert('RGBA') image_with_alpha = image_with_alpha.convert('RGBA') # Extract the alpha channel and multiply by alpha_multiplier alpha_channel = image_with_alpha.split()[3] alpha_channel = alpha_channel.point(lambda p: p * alpha_multiplier) # Apply the modified alpha channel back to the image image_with_alpha.putalpha(alpha_channel) # Composite the images result = Image.alpha_composite(base_image, image_with_alpha) if name is not None: new_image_path = os.path.join(directory, name + f"_alpha{str(alpha_percent)}.png") result.save(new_image_path) return new_image_path return blended_image return result def apply_alpha_mask(image, mask_image, invert = False): """ Applies a mask image as the alpha channel of the input image. Parameters: image (PIL.Image.Image): The image to apply the mask to. mask_image (PIL.Image.Image): The alpha mask to apply. invert (bool): Whether to invert the mask (default is False). Returns: PIL.Image.Image: The image with the applied alpha mask. """ # Resize the mask to match the current image size mask_image = resize_and_crop_image(mask_image, image.width, image.height).convert('L') # convert to grayscale if invert: mask_image = ImageOps.invert(mask_image) # Apply the mask as the alpha layer of the current image result_image = image.copy() result_image.putalpha(mask_image) return result_image def resize_and_crop_image(image: Image, new_width: int = 512, new_height: int = 512) -> Image: """ Resizes and crops an image to a specified width and height. This ensures that the entire new_width and new_height dimensions are filled by the image, and the aspect ratio is maintained. Parameters: image (PIL.Image.Image): The image to be resized and cropped. new_width (int): The desired width of the new image (default is 512). new_height (int): The desired height of the new image (default is 512). Returns: PIL.Image.Image: The resized and cropped image. """ # Get the dimensions of the original image orig_width, orig_height = image.size # Calculate the aspect ratios of the original and new images orig_aspect_ratio = orig_width / float(orig_height) new_aspect_ratio = new_width / float(new_height) # Calculate the new size of the image while maintaining aspect ratio if orig_aspect_ratio > new_aspect_ratio: # The original image is wider than the new image, so we need to crop the sides resized_width = int(new_height * orig_aspect_ratio) resized_height = new_height left_offset = (resized_width - new_width) // 2 top_offset = 0 else: # The original image is taller than the new image, so we need to crop the top and bottom resized_width = new_width resized_height = int(new_width / orig_aspect_ratio) left_offset = 0 top_offset = (resized_height - new_height) // 2 # Resize the image with Lanczos resampling filter resized_image = image.resize((resized_width, resized_height), resample=Image.Resampling.LANCZOS) # Crop the image to fill the entire height and width of the new image cropped_image = resized_image.crop((left_offset, top_offset, left_offset + new_width, top_offset + new_height)) return cropped_image ##################################################### LUTs ############################################################ def is_3dlut_row(row: List[str]) -> bool: """ Check if one line in the file has exactly 3 numeric values. Parameters: row (list): A list of strings representing the values in a row. Returns: bool: True if the row has exactly 3 numeric values, False otherwise. """ try: row_values = [float(val) for val in row] return len(row_values) == 3 except ValueError: return False def get_lut_type(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> str: with open(path_lut) as f: lines = f.read().splitlines() lut_type = "3D" # Initially assume 3D LUT size = None table = [] # Parse the file for line in lines: line = line.strip() if line.startswith("#") or not line: continue # Skip comments and empty lines parts = line.split() if parts[0] == "LUT_3D_SIZE": size = int(parts[1]) lut_type = "3D" elif parts[0] == "LUT_1D_SIZE": size = int(parts[1]) lut_type = "1D" elif is_3dlut_row(parts): table.append(tuple(float(val) for val in parts)) return lut_type def read_3d_lut(path_lut: Union[str, os.PathLike], num_channels: int = 3) -> ImageFilter.Color3DLUT: """ Read LUT from a raw file. Each line in the file is considered part of the LUT table. The function reads the file, parses the rows, and constructs a Color3DLUT object. Args: path_lut: A string or os.PathLike object representing the path to the LUT file. num_channels: An integer specifying the number of color channels in the LUT (default is 3). Returns: An instance of ImageFilter.Color3DLUT representing the LUT. Raises: FileNotFoundError: If the LUT file specified by path_lut does not exist. """ with open(path_lut) as f: lut_raw = f.read().splitlines() size = round(len(lut_raw) ** (1 / 3)) row2val = lambda row: tuple([float(val) for val in row.split(" ")]) lut_table = [row2val(row) for row in lut_raw if is_3dlut_row(row.split(" "))] return ImageFilter.Color3DLUT(size, lut_table, num_channels) def apply_1d_lut(image, lut_file): # Read the 1D LUT with open(lut_file) as f: lines = f.read().splitlines() table = [] for line in lines: if not line.startswith(("#", "LUT", "TITLE", "DOMAIN")) and line.strip(): values = [float(v) for v in line.split()] table.append(tuple(values)) # Convert image to grayscale if image.mode != 'L': image = image.convert('L') img_array = np.array(image) / 255.0 # Normalize to [0, 1] # Map grayscale values to colors lut_size = len(table) indices = (img_array * (lut_size - 1)).astype(int) colors = np.array(table)[indices] # Create RGB image rgb_image = Image.fromarray((colors * 255).astype(np.uint8), mode='RGB') return rgb_image def apply_3d_lut(img: Image, lut_path: str = "", lut: ImageFilter.Color3DLUT = None) -> Image: """ Apply a LUT to an image and return a PIL Image with the LUT applied. The function applies the LUT to the input image using the filter() method of the PIL Image class. Args: img: A PIL Image object to which the LUT should be applied. lut_path: A string representing the path to the LUT file (optional if lut argument is provided). lut: An instance of ImageFilter.Color3DLUT representing the LUT (optional if lut_path is provided). Returns: A PIL Image object with the LUT applied. Raises: ValueError: If both lut_path and lut arguments are not provided. """ if lut is None: if lut_path == "": raise ValueError("Either lut_path or lut argument must be provided.") lut = read_3d_lut(lut_path) return img.filter(lut) def apply_lut(image, lut_filename: str) -> Image: """ Apply a LUT to an image and return the result. Args: image (str or PIL.Image.Image): The image to apply the LUT to. lut_filename (str): The path to the LUT file. Returns: PIL.Image.Image: The image with the LUT applied. """ if isinstance(image, str): image = open_image(image) if lut_filename is not None: if (get_lut_type(lut_filename) == "3D"): lut = read_3d_lut(lut_filename) image = apply_3d_lut(image, lut=lut) else: image = apply_1d_lut(image, lut_filename) return image def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img) -> Image: if lut_filename is not None: try: lut_example_image = apply_lut(lut_example_image, lut_filename) except Exception as e: print(f"BAD LUT: Error applying LUT {str(e)}.") else: lut_example_image = open_image(default_lut_example_img) return lut_example_image def apply_1d_lut(image, lut_file): # Read the 1D LUT with open(lut_file) as f: lines = f.read().splitlines() table = [] for line in lines: if not line.startswith(("#", "LUT", "TITLE", "DOMAIN")) and line.strip(): values = [float(v) for v in line.split()] table.append(tuple(values)) # Convert image to grayscale if image.mode != 'L': image = image.convert('L') img_array = np.array(image) / 255.0 # Normalize to [0, 1] # Map grayscale values to colors lut_size = len(table) indices = (img_array * (lut_size - 1)).astype(int) colors = np.array(table)[indices] # Create RGB image rgb_image = Image.fromarray((colors * 255).astype(np.uint8), mode='RGB') return rgb_image def apply_lut_to_image_path(lut_filename: str, image_path: str) -> tuple[Image, str]: """ Apply a LUT to an image and return the result. Supports ICO files by converting them to PNG with RGBA channels. Args: lut_filename: A string representing the path to the LUT file. image_path: A string representing the path to the input image. Returns: tuple: A tuple containing the PIL Image object with the LUT applied and the new image path as a string. """ import gradio as gr img_lut = None if image_path is None: raise UserWarning("No image provided.") return None, None # Split the path into directory and filename directory, file_name = os.path.split(image_path) lut_directory, lut_file_name = os.path.split(lut_filename) # Split the filename into name and extension name, ext = os.path.splitext(file_name) lut_name, lut_ext = os.path.splitext(lut_file_name) # Convert the extension to lowercase new_ext = ext.lower() path = Path(image_path) img = open_image(image_path) if not ((path.suffix.lower() == '.png' and img.mode == 'RGBA')): if image_path.lower().endswith(('.jpg', '.jpeg')): img, new_image_path = convert_jpg_to_rgba(path) elif image_path.lower().endswith('.ico'): img, new_image_path = convert_to_rgba_png(image_path) elif image_path.lower().endswith(('.gif', '.webp')): img, new_image_path = convert_to_rgba_png(image_path) else: img, new_image_path = convert_to_rgba_png(image_path) if image_path != new_image_path: delete_image(image_path) else: # ensure the file extension is lower_case, otherwise leave as is new_filename = name + new_ext new_image_path = os.path.join(directory, new_filename) # Apply the LUT to the image if (lut_filename is not None and img is not None): try: img_lut = apply_lut(img, lut_filename) except Exception as e: print(f"BAD LUT: Error applying LUT {str(e)}.") if img_lut is not None: new_filename = name + "_"+ lut_name + new_ext new_image_path = os.path.join(directory, new_filename) #delete_image(image_path) - renamed with lut name img = img_lut img.save(new_image_path, format='PNG') print(f"Image with LUT saved as {new_image_path}") return img, gr.update(value=str(new_image_path)) ############################################# RGBA ########################################################### def convert_rgb_to_rgba_safe(image: Image) -> Image: """ Converts an RGB image to RGBA by adding an alpha channel. Ensures that the original image remains unaltered. Parameters: image (PIL.Image.Image): The RGB image to convert. Returns: PIL.Image.Image: The converted RGBA image. """ if image.mode != 'RGB': if image.mode == 'RGBA': return image elif image.mode == 'P': # Convert palette image to RGBA image = image.convert('RGB') else: raise ValueError("Unsupported image mode for conversion to RGBA.") # Create a copy of the image to avoid modifying the original rgba_image = image.copy() # Optionally, set a default alpha value (e.g., fully opaque) alpha = Image.new('L', rgba_image.size, 255) # 255 for full opacity rgba_image.putalpha(alpha) return rgba_image # Example usage # convert_jpg_to_rgba('input.jpg', 'output.png') def convert_jpg_to_rgba(input_path) -> tuple[Image, str]: """ Convert a JPG image to RGBA format and save it as a PNG. Args: input_path (str or Path): Path to the input JPG image file. Raises: FileNotFoundError: If the input file does not exist. ValueError: If the input file is not a JPG. OSError: If there's an error reading or writing the file. Returns: tuple: A tuple containing the RGBA image and the output path as a string. """ try: # Convert input_path to Path object if it's a string input_path = Path(input_path) output_path = input_path.with_suffix('.png') # Check if the input file exists if not input_path.exists(): #if file was renamed to lower case, update the input path input_path = output_path if not input_path.exists(): raise FileNotFoundError(f"The file {input_path} does not exist.") # Check file extension first to skip unnecessary processing if input_path.suffix.lower() not in ('.jpg', '.jpeg'): print(f"Skipping conversion: {input_path} is not a JPG or JPEG file.") return None, None print(f"Converting to PNG: {input_path} is a JPG or JPEG file.") # Open the image file with Image.open(input_path) as img: # Convert the image to RGBA mode rgba_img = img.convert('RGBA') # Ensure the directory exists for the output file output_path.parent.mkdir(parents=True, exist_ok=True) # Save the image with RGBA mode as PNG rgba_img.save(output_path) except FileNotFoundError as e: print(f"Error: {e}") except ValueError as e: print(f"Error: {e}") except OSError as e: print(f"Error: An OS error occurred while processing the image - {e}") except Exception as e: print(f"An unexpected error occurred: {e}") return rgba_img, str(output_path) def convert_to_rgba_png(file_path: str) -> tuple[Image, str]: """ Converts an image to RGBA PNG format and saves it with the same base name and a .png extension. Supports ICO files. Args: file_path (str): The path to the input image file. Returns: tuple: A tuple containing the RGBA image and the new file path as a string. """ new_file_path = None rgba_img = None img = None if file_path is None: raise UserWarning("No image provided.") return None, None try: file_path, is_dict = get_image_from_dict(file_path) img = open_image(file_path) print(f"Opened image: {file_path}\n") # Handle ICO files if file_path.lower().endswith(('.ico','.webp','.gif')): rgba_img = img.convert('RGBA') new_file_path = Path(file_path).with_suffix('.png') rgba_img.save(new_file_path, format='PNG') print(f"Converted ICO to PNG: {new_file_path}") else: rgba_img, new_file_path = convert_jpg_to_rgba(file_path) if rgba_img is None: rgba_img = convert_rgb_to_rgba_safe(img) new_file_path = Path(file_path).with_suffix('.png') rgba_img.save(new_file_path, format='PNG') print(f"Image saved as {new_file_path}") except ValueError as ve: print(f"ValueError: {ve}") except Exception as e: print(f"Error converting image: {e}") return rgba_img if rgba_img else img, str(new_file_path) def delete_image(file_path: str) -> None: """ Deletes the specified image file. Parameters: file_path (str): The path to the image file to delete. Raises: FileNotFoundError: If the file does not exist. Exception: If there is an error deleting the file. """ try: path = Path(file_path) path.unlink() print(f"Deleted original image: {file_path}") except FileNotFoundError: print(f"File not found: {file_path}") except Exception as e: print(f"Error deleting image: {e}") def resize_all_images_in_folder(target_width: int, output_folder: str = "resized", file_prefix: str = "resized_") -> tuple[int, int]: """ Resizes all images in the current folder to a specified width while maintaining aspect ratio. Creates a new folder for the resized images. Parameters: target_width (int): The desired width for all images output_folder (str): Name of the folder to store resized images (default: "resized") file_prefix (str): Prefix for resized files (default: "resized_") Returns: tuple[int, int]: (number of successfully resized images, number of failed attempts) Example Usage: successful_count, failed_count = resize_all_images_in_folder(target_width=800, output_folder="th", file_prefix="th_") """ # Supported image extensions valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff') # Create output folder if it doesn't exist output_path = Path(output_folder) output_path.mkdir(exist_ok=True) successful = 0 failed = 0 # Get current directory current_dir = Path.cwd() # Iterate through all files in current directory for file_path in current_dir.iterdir(): if file_path.is_file() and file_path.suffix.lower() in valid_extensions: try: # Open the image with Image.open(file_path) as img: # Convert to RGB if needed (handles RGBA, CMYK, etc.) if img.mode != 'RGB': img = img.convert('RGB') # Calculate target height maintaining aspect ratio original_width, original_height = img.size aspect_ratio = original_height / original_width target_height = int(target_width * aspect_ratio) # Resize using the reference function resized_img = resize_image_with_aspect_ratio(img, target_width, target_height) # Create output filename output_filename = output_path / f"{file_prefix}{file_path.name.lower()}" # Save the resized image resized_img.save(output_filename, quality=95) successful += 1 print(f"Successfully resized: {file_path.name.lower()}") except Exception as e: failed += 1 print(f"Failed to resize {file_path.name.lower()}: {str(e)}") print(f"\nResizing complete. Successfully processed: {successful}, Failed: {failed}") return successful, failed def get_image_quality(file_path): """Determine quality based on image width.""" try: with Image.open(file_path) as img: width, _ = img.size if width < 1025: return 0 elif width < 1537: return 1 elif width < 2680: return 2 else: # width >= 2680 return 3 except Exception as e: print(f"Error opening {file_path}: {e}") return 0 # Default to 0 if there's an error def update_quality(): """Update quality for each file in PRE_RENDERED_MAPS_JSON_LEVELS.""" possible_paths = ["./", "./images/prerendered/"] for key, value in PRE_RENDERED_MAPS_JSON_LEVELS.items(): file_path = value['file'] found = False # Check both possible locations for base_path in possible_paths: full_path = os.path.join(base_path, os.path.basename(file_path)) if os.path.exists(full_path): quality = get_image_quality(full_path) PRE_RENDERED_MAPS_JSON_LEVELS[key]['quality'] = quality print(f"Updated {key}: Quality set to {quality} (Width checked at {full_path})") found = True break if not found: print(f"Warning: File not found for {key} at any location. Keeping quality as {value['quality']}") def print_json(): """Print the updated PRE_RENDERED_MAPS_JSON_LEVELS in a formatted way.""" print("\nUpdated PRE_RENDERED_MAPS_JSON_LEVELS = {") for key, value in PRE_RENDERED_MAPS_JSON_LEVELS.items(): print(f" '{key}': {{'file': '{value['file']}', 'thumbnail': '{value['thumbnail']}', 'quality': {value['quality']}}},") print("}") def calculate_optimal_fill_dimensions(image: Image.Image): # Extract the original dimensions original_width, original_height = image.size # Set constants MIN_ASPECT_RATIO = 9 / 16 MAX_ASPECT_RATIO = 16 / 9 FIXED_DIMENSION = 1024 # Calculate the aspect ratio of the original image original_aspect_ratio = original_width / original_height # Determine which dimension to fix if original_aspect_ratio > 1: # Wider than tall width = FIXED_DIMENSION height = round(FIXED_DIMENSION / original_aspect_ratio) else: # Taller than wide height = FIXED_DIMENSION width = round(FIXED_DIMENSION * original_aspect_ratio) # Ensure dimensions are multiples of 8 width = (width // 8) * 8 height = (height // 8) * 8 # Enforce aspect ratio limits calculated_aspect_ratio = width / height if calculated_aspect_ratio > MAX_ASPECT_RATIO: width = (height * MAX_ASPECT_RATIO // 8) * 8 elif calculated_aspect_ratio < MIN_ASPECT_RATIO: height = (width / MIN_ASPECT_RATIO // 8) * 8 # Ensure width and height remain above the minimum dimensions width = max(width, 576) if width == FIXED_DIMENSION else width height = max(height, 576) if height == FIXED_DIMENSION else height return width, height