HexaGrid / utils /image_utils.py
Surn's picture
Fix Template Image selection mismatch
16e4012
# 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
def get_image_from_dict(image_path):
if isinstance(image_path, dict) :
if 'image' in image_path:
image_path = image_path.get('image')
elif 'composite' in image_path:
image_path = image_path.get('composite')
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
else:
image_path = rename_file_to_lowercase_extension(image_path)
import requests
try:
image_path, is_dict = get_image_from_dict(image_path)
# 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 less than or equal to the given limit.
The sorting order matches pre_rendered_maps_paths based on quality and a case-insensitive alphanumeric key.
Args:
quality_limit (int): The quality threshold.
key (str): The key to extract the file path from each map info (default is 'file').
Returns:
list: A list of sorted file paths meeting the quality criteria.
"""
# Sort the PRE_RENDERED_MAPS_JSON_LEVELS items by quality and alphanumeric key
sorted_maps = sorted(
PRE_RENDERED_MAPS_JSON_LEVELS.items(),
key=lambda x: (
x[1]['quality'],
''.join(char.lower() for char in x[0] if char.isalnum())
)
)
# Filter and extract the file paths that meet the quality limit
images_list = [
map_info[key].replace("\\", "/")
for _, map_info in sorted_maps
if map_info['quality'] <= quality_limit
]
return build_prerendered_images(images_list)
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
# 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)
# 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.
"""
alpha = alpha_percent / 100.0
if isinstance(base_image, str):
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)
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.
"""
image_with_alpha, isdict = get_image_from_dict(image_with_alpha)
alpha_multiplier = alpha_percent / 100.0
if isinstance(base_image, str):
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)
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 read_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_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_lut(lut_path)
return img.filter(lut)
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 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
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.
"""
if image_path is None:
raise UserWarning("No image provided.")
return None, None
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)
delete_image(image_path)
else:
new_image_path = image_path
if lut_filename is not None:
try:
img = apply_lut(img, lut_filename)
except Exception as e:
print(f"BAD LUT: Error applying LUT {str(e)}.")
img.save(new_image_path, format='PNG')
return img, str(new_image_path)
############################################# RGBA ###########################################################
# 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():
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 Image.open(input_path), str(output_path)
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}"
# Save the resized image
resized_img.save(output_filename, quality=95)
successful += 1
print(f"Successfully resized: {file_path.name}")
except Exception as e:
failed += 1
print(f"Failed to resize {file_path.name}: {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("}")