Spaces:
Running
on
Zero
Running
on
Zero
| # 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, BASE_HEIGHT | |
| 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') | |
| elif 'background' in image_path: | |
| image_path = image_path.get('background') | |
| 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 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, intensity: int = 100, lut_scale: float = 1.0, lut_offset: float = 0.0): | |
| """ | |
| Apply a 1D LUT to an image with intensity, scale, and offset control. | |
| Args: | |
| image: PIL Image object. | |
| lut_file: Path to the 1D LUT file. | |
| intensity: Integer from -200 to 200 controlling LUT strength (default 100). | |
| lut_scale: Float to scale LUT colors (default 1.0). | |
| lut_offset: Float to offset LUT colors (default 0.0). | |
| Returns: | |
| PIL Image object with the adjusted LUT applied. | |
| """ | |
| import numpy as np | |
| # Compute blending factor | |
| alpha = intensity / 100.0 # Ranges from -2.0 to 2.0 | |
| # 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)) | |
| # Adjust LUT table with scale and offset | |
| adjusted_table = [ | |
| tuple(np.clip(val * lut_scale + lut_offset, 0, 1) for val in color) | |
| for color in table | |
| ] | |
| # If intensity is negative, use inverted LUT | |
| if alpha < 0: | |
| adjusted_table = [(1 - r, 1 - g, 1 - b) for r, g, b in adjusted_table] | |
| alpha = -alpha # Use positive alpha for blending | |
| # 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(adjusted_table) | |
| indices = (img_array * (lut_size - 1)).astype(int) | |
| colors = np.array(adjusted_table)[indices] # LUT-mapped colors, shape (H, W, 3) | |
| # Create original colors (grayscale replicated across RGB) | |
| original_colors = np.repeat(img_array[:, :, np.newaxis], 3, axis=2) # Shape (H, W, 3) | |
| # Blend original and LUT-mapped colors | |
| blended_colors = original_colors * (1 - alpha) + colors * alpha | |
| blended_colors = np.clip(blended_colors, 0, 1) # Ensure values stay in [0, 1] | |
| # Create RGB image | |
| rgb_image = Image.fromarray((blended_colors * 255).astype(np.uint8), mode='RGB') | |
| return rgb_image | |
| def invert_lut(original_lut: ImageFilter.Color3DLUT) -> ImageFilter.Color3DLUT: | |
| """ | |
| Create an inverted LUT by reversing the order of entries to simulate a 180-degree rotation. | |
| Args: | |
| original_lut: The original Color3DLUT object. | |
| Returns: | |
| A new Color3DLUT object with inverted entries. | |
| """ | |
| # Extract the table and size from the original LUT | |
| size = original_lut.size[0] # Assuming cubic LUT | |
| table = original_lut.table | |
| # Reverse the table to simulate a 180-degree rotation | |
| inverted_table = table[::-1] | |
| # Create and return the inverted LUT with the same number of channels | |
| return ImageFilter.Color3DLUT(size, inverted_table, original_lut.channels) | |
| 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_simple(image, lut_filename: str, intensity: int = 100) -> Image: | |
| """ | |
| Apply a LUT to an image with intensity control. | |
| Args: | |
| image: PIL Image object or path to image file. | |
| lut_filename: Path to the LUT file. | |
| intensity: Integer from -200 to 200 controlling LUT strength (default 100). | |
| Returns: | |
| PIL Image object with the adjusted LUT applied. | |
| """ | |
| import numpy as np | |
| # Handle image input as string | |
| if isinstance(image, str): | |
| image = open_image(image) | |
| if lut_filename is not None: | |
| lut_type = get_lut_type(lut_filename) | |
| if lut_type == "3D": | |
| # Read the original 3D LUT | |
| original_lut = read_3d_lut(lut_filename) | |
| # Apply the original LUT to the image | |
| lutted_image = image.filter(original_lut) | |
| # Compute blending factor | |
| alpha = intensity / 100.0 # Ranges from -2.0 to 2.0 | |
| # Handle special cases | |
| if alpha == 0: | |
| return image | |
| elif alpha == 1: | |
| return lutted_image | |
| else: | |
| # Convert images to NumPy arrays for blending | |
| img_array = np.array(image).astype(float) / 255.0 | |
| lutted_array = np.array(lutted_image).astype(float) / 255.0 | |
| blended_array = img_array * (1 - alpha) + lutted_array * alpha | |
| blended_array = np.clip(blended_array, 0, 1) | |
| blended_image = Image.fromarray((blended_array * 255).astype(np.uint8)) | |
| return blended_image | |
| else: | |
| # Apply 1D LUT with intensity (already correct) | |
| image = apply_1d_lut(image, lut_filename, intensity) | |
| return image | |
| def apply_lut(image, lut_filename: str, intensity: int = 100, lut_scale: float = 1.0, lut_offset: float = 0.0) -> Image: | |
| """ | |
| Apply a LUT to an image with intensity, scale, and offset adjustments. | |
| Args: | |
| image: PIL Image object or path to image file. | |
| lut_filename: Path to the LUT file (.cube for 3D LUT or text file for 1D LUT). | |
| intensity: Integer from -200 to 200 controlling LUT strength (default 100). | |
| lut_scale: Float to scale LUT colors (default 1.0). | |
| lut_offset: Float to offset LUT colors (default 0.0). | |
| Returns: | |
| PIL Image object with the adjusted LUT applied. | |
| """ | |
| # Handle image input as string | |
| if isinstance(image, str): | |
| image = Image.open(image).convert('RGB') | |
| if lut_filename is None: | |
| return image | |
| lut_type = get_lut_type(lut_filename) | |
| if lut_type == "3D": | |
| # Read the original 3D LUT | |
| # Read the original 3D LUT using the external function | |
| original_lut = read_3d_lut(lut_filename) | |
| # Create the inverted LUT | |
| inverted_lut = invert_lut(original_lut) | |
| # Define a function to adjust LUT entries with scale and offset | |
| def adjust_entry(r, g, b): | |
| r = np.clip(r * lut_scale + lut_offset, 0, 1) | |
| g = np.clip(g * lut_scale + lut_offset, 0, 1) | |
| b = np.clip(b * lut_scale + lut_offset, 0, 1) | |
| return (r, g, b) | |
| # Apply scale and offset adjustments to both LUTs | |
| adjusted_original_lut = original_lut.transform(adjust_entry) | |
| adjusted_inverted_lut = inverted_lut.transform(adjust_entry) | |
| # Compute blending factor from intensity | |
| alpha = intensity / 100.0 # Ranges from -2.0 to 2.0 | |
| # Select the appropriate LUT based on intensity | |
| if alpha >= 0: | |
| lut_to_use = adjusted_original_lut | |
| else: | |
| lut_to_use = adjusted_inverted_lut | |
| alpha = -alpha # Use positive alpha for blending with inverted LUT | |
| # Apply the selected LUT to the image | |
| lutted_image = image.filter(lut_to_use) | |
| # Convert images to NumPy arrays for blending | |
| original_array = np.array(image, dtype=np.float32) / 255.0 | |
| lutted_array = np.array(lutted_image, dtype=np.float32) / 255.0 | |
| # Blend the original and LUT-applied images | |
| blended_array = original_array * (1 - alpha) + lutted_array * alpha | |
| blended_array = np.clip(blended_array, 0, 1) | |
| # Convert back to PIL Image | |
| final_image = Image.fromarray((blended_array * 255).astype(np.uint8)) | |
| return final_image | |
| else: # 1D LUT | |
| # Delegate to the modified apply_1d_lut function | |
| return apply_1d_lut(image, lut_filename, intensity, lut_scale, lut_offset) | |
| def show_lut(lut_filename: str, lut_example_image: Image = default_lut_example_img, intensity: int = 100) -> Image: | |
| if lut_example_image is None: | |
| lut_example_image = default_lut_example_img | |
| if lut_filename is not None: | |
| try: | |
| lut_example_image = apply_lut(lut_example_image, lut_filename, intensity) | |
| 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_simple(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, intensity: int = 100 ) -> 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, intensity) | |
| 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)) | |
| def png_to_cube(input_png_path, output_cube_path, lut_size=17): | |
| # Example usage | |
| # png_to_cube(input_file, output_file, lut_size=17) | |
| # Open the PNG file | |
| img = Image.open(input_png_path) | |
| # Ensure the image is 512x512 | |
| if img.size != (512, 512): | |
| raise ValueError("Input PNG must be 512x512 pixels.") | |
| # Convert to RGB and normalize to 0-1 range | |
| pixels = np.array(img.convert("RGB")) / 255.0 | |
| # Calculate the step size for sampling (512 / 17 ≈ 30 pixels per step) | |
| step = 512 // lut_size | |
| # Open the output .cube file | |
| with open(output_cube_path, "w") as f: | |
| # Write .cube header | |
| f.write('# Charles Fettinger by PNG to LUT converter\n') | |
| f.write('TITLE "Converted LUT"\n') | |
| f.write(f'LUT_3D_SIZE {lut_size}\n') | |
| f.write('DOMAIN_MIN 0.0 0.0 0.0\n') | |
| f.write('DOMAIN_MAX 1.0 1.0 1.0\n') | |
| # Iterate over the 3D LUT grid (R, G, B) | |
| for b in range(lut_size): # Blue channel | |
| for g in range(lut_size): # Green channel | |
| for r in range(lut_size): # Red channel | |
| # Map LUT coordinates to PNG coordinates | |
| # Assume the PNG is laid out with: | |
| # - X-axis (horizontal) = Red | |
| # - Y-axis (vertical) = Green + Blue slices | |
| x = r * step + step // 2 # Center of each Red step | |
| y = (g + b * lut_size) * step + step // 2 # Green + Blue offset | |
| # Ensure coordinates stay within bounds | |
| x = min(x, 511) | |
| y = min(y, 511) | |
| # Get RGB value from the PNG | |
| rgb = pixels[y, x] | |
| # Write RGB values to .cube file (normalized 0-1) | |
| f.write(f"{rgb[0]:.6f} {rgb[1]:.6f} {rgb[2]:.6f}\n") | |
| print(f"Conversion complete. LUT saved to {output_cube_path}") | |
| def png_8x8_to_3d_cube(input_png_path, output_cube_path, lut_size=8): | |
| # Example usage: png_8x8_to_3d_cube(input_file, output_file, lut_size=8) | |
| # Open the PNG file | |
| img = Image.open(input_png_path) | |
| # Ensure the image is 512x512 | |
| if img.size != (512, 512): | |
| raise ValueError("Input PNG must be 512x512 pixels.") | |
| # Convert to RGB and normalize to 0-1 range | |
| pixels = np.array(img.convert("RGB")) / 255.0 | |
| # Grid parameters | |
| grid_size = 8 # 8x8 boxes | |
| box_size = 512 // grid_size # 64 pixels per box | |
| step = box_size // lut_size # 64 ÷ 8 = 8 pixels per step | |
| # Open the output .cube file | |
| with open(output_cube_path, "w") as f: | |
| # Write .cube header for 3D LUT | |
| f.write('# Charles Fettinger 3D LUT from 8x8 PNG\n') | |
| f.write('TITLE "Converted 8x8x8 LUT"\n') | |
| f.write(f'LUT_3D_SIZE {lut_size}\n') | |
| f.write('DOMAIN_MIN 0.0 0.0 0.0\n') | |
| f.write('DOMAIN_MAX 1.0 1.0 1.0\n') | |
| # Iterate over the 3D LUT (R, G, B) | |
| for b in range(lut_size): # Blue axis (rows of the 8x8 grid) | |
| for g in range(lut_size): # Green axis (columns of the 8x8 grid) | |
| for r in range(lut_size): # Red axis (within each box) | |
| # Map to the 8x8 grid | |
| box_row = b # Blue selects the row | |
| box_col = g # Green selects the column | |
| # Starting coordinates of the current box | |
| box_x = box_col * box_size | |
| box_y = box_row * box_size | |
| # Sample within the box (R varies horizontally, G vertically) | |
| x = box_x + r * step + step // 2 # Center of Red step | |
| y = box_y + g * step + step // 2 # Center of Green step (reuse g for consistency) | |
| # Ensure coordinates stay within bounds | |
| x = min(x, 511) | |
| y = min(y, 511) | |
| # Get RGB value from the PNG | |
| rgb = pixels[y, x] | |
| # Write RGB values to .cube file | |
| f.write(f"{rgb[0]:.6f} {rgb[1]:.6f} {rgb[2]:.6f}\n") | |
| print(f"3D LUT conversion complete. Saved to {output_cube_path}") | |
| def png_8x8_to_3d_cube_inverted(input_png_path, output_cube_path, lut_size=8): | |
| # Open the PNG file | |
| img = Image.open(input_png_path) | |
| if img.size != (512, 512): | |
| raise ValueError("Input PNG must be 512x512 pixels.") | |
| # Convert to RGB and normalize to 0-1 range | |
| pixels = np.array(img.convert("RGB")) / 255.0 | |
| # Grid parameters | |
| grid_size = 8 | |
| box_size = 512 // grid_size # 64 pixels per box | |
| step = box_size // lut_size # 8 pixels per step | |
| # Write the .cube file | |
| with open(output_cube_path, "w") as f: | |
| f.write('# Charles Fettinger 3D LUT from 8x8 PNG (inverted)\n') | |
| f.write('TITLE "Converted 8x8x8 LUT (inverted)"\n') | |
| f.write(f'LUT_3D_SIZE {lut_size}\n') | |
| f.write('DOMAIN_MIN 0.0 0.0 0.0\n') | |
| f.write('DOMAIN_MAX 1.0 1.0 1.0\n') | |
| for b in range(lut_size): | |
| for g in range(lut_size): | |
| for r in range(lut_size): | |
| box_row = b | |
| box_col = g | |
| box_x = box_col * box_size | |
| box_y = box_row * box_size | |
| x = box_x + r * step + step // 2 | |
| y = box_y + g * step + step // 2 | |
| # Sample from the rotated position | |
| x_rev = 511 - x | |
| y_rev = 511 - y | |
| rgb = pixels[y_rev, x_rev] | |
| f.write(f"{rgb[0]:.6f} {rgb[1]:.6f} {rgb[2]:.6f}\n") | |
| print(f"Inverted 3D LUT conversion complete. Saved to {output_cube_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 16 | |
| width = (width // 16) * 16 | |
| height = (height // 16) * 16 | |
| # Enforce aspect ratio limits | |
| calculated_aspect_ratio = width / height | |
| if calculated_aspect_ratio > MAX_ASPECT_RATIO: | |
| width = (height * MAX_ASPECT_RATIO // 16) * 16 | |
| elif calculated_aspect_ratio < MIN_ASPECT_RATIO: | |
| height = (width / MIN_ASPECT_RATIO // 16) * 16 | |
| # Ensure width and height remain above the minimum dimensions | |
| width = max(width, BASE_HEIGHT) if width == FIXED_DIMENSION else width | |
| height = max(height, BASE_HEIGHT) if height == FIXED_DIMENSION else height | |
| return width, height |