diff --git a/optima/__init__.py b/optima/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/optima/image_handler.py b/optima/image_handler.py deleted file mode 100644 index 33f47a0..0000000 --- a/optima/image_handler.py +++ /dev/null @@ -1,199 +0,0 @@ -from PIL import Image, ImageDraw, ImageFont, ImageEnhance -import piexif -from fractions import Fraction - -class ImageProcessor: - """Functions using pillow are in here.""" - def __init__(self): - pass - - def open_image(self, path): - """Open an image from path, returns image object.""" - return Image.open(path) - - def get_image_size(self, image): - """Simply get image size.""" - return image.size - - def grayscale(self, image): - """Change to grayscale""" - return image.convert("L") - - def change_contrast(self, image, change): - """Change contrast by percent.""" - enhancer = ImageEnhance.Contrast(image) - new_img = enhancer.enhance(1 + (change/100)) - return new_img - - def change_brightness(self, image, change): - """Changes brightness by percent""" - enhancer = ImageEnhance.Brightness(image) - new_img = enhancer.enhance(1 + (change/100)) - return new_img - - def resize_image(self, image, percent, resample = True): - """Resize an image by giving a percent.""" - new_size = tuple(int(x * (percent / 100)) for x in image.size) - if resample: - resized_image = image.resize(new_size) - else: - resized_image = image.resize((new_size),resample=Image.Resampling.NEAREST) - return resized_image - - def add_watermark(self, image, text, font_size_percentage): - """Addes a watermark to the image using default os font.""" - drawer = ImageDraw.Draw(image) - imagewidth, imageheight = image.size - margin = (imageheight / 100 ) * 2 # margin dynamic, 2% of image size - font_size = imagewidth * (font_size_percentage / 100) - - try: # Try loading front, if notaviable return unmodified image - font = ImageFont.load_default(font_size) - except Exception as e: - print(f"Error {e}\nloading font for watermark, please ensure font is installed...\n") - return image - - c, w, textwidth, textheight, = drawer.textbbox(xy = (0, 0), text = text, font = font) # Getting text size, only need the last two values - x = imagewidth - textwidth - margin - y = imageheight - textheight - margin - - # thin border - drawer.text((x-1, y), text, font = font, fill = (64, 64, 64)) - drawer.text((x+1, y), text, font = font, fill = (64, 64, 64)) - drawer.text((x, y-1), text, font = font, fill = (64, 64, 64)) - drawer.text((x, y+1), text, font = font, fill = (64, 64, 64)) - # Adding text in the desired color - drawer.text((x, y), text, font = font, fill = (255, 255, 255)) - - return image - - def save_image(self, image, path, file_type, jpg_quality, png_compressing, optimize, piexif_exif_data): - # partly optimized by chatGPT - """ - Save an image to the specified path with optional EXIF data. - """ - save_params = {"optimize": optimize} - # Add file-specific parameters - if file_type == "jpg" or "webp": - save_params["quality"] = jpg_quality - elif file_type == "png": - save_params["compress_level"] = png_compressing - elif file_type not in ["webp", "jpg", "png"]: - input(f"Type: {file_type} is not supported. Press Enter to continue...") - return - # Add EXIF data if available - if piexif_exif_data is not None: - save_params["exif"] = piexif.dump(piexif_exif_data) - if file_type == "webp": - print("File format webp does not support all exif features, some information might get lost...\n") - try: - image.save(f"{path}.{file_type}", **save_params) - except Exception as e: - print(f"Failed to save image: {e}") - -class ExifHandler: - """Function using piexif are here.""" - def __init__(self): - pass - - def get_exif_info(self, image): - return(piexif.load(image.info['exif'])) - - def build_exif_dict(self, user_data, imagesize): - """Build a piexif-compatible EXIF dictionary from a dicts.""" - # Mostly made by ChatGPT, some adjustment - zeroth_ifd = { - piexif.ImageIFD.Make: user_data["make"].encode("utf-8"), - piexif.ImageIFD.Model: user_data["model"].encode("utf-8"), - piexif.ImageIFD.Software: user_data["software"].encode("utf-8"), - piexif.ImageIFD.Copyright: user_data["copyright_info"].encode("utf-8"), - piexif.ImageIFD.Artist: user_data["artist"].encode("utf-8"), - piexif.ImageIFD.ImageDescription: user_data["image_description"].encode("utf-8"), - piexif.ImageIFD.XResolution: (72, 1), - piexif.ImageIFD.YResolution: (72, 1), - } - exif_ifd = { - piexif.ExifIFD.UserComment: user_data["user_comment"].encode("utf-8"), - piexif.ExifIFD.ISOSpeedRatings: int(user_data["iso"].encode("utf-8")), - piexif.ExifIFD.PixelXDimension: imagesize[0], - piexif.ExifIFD.PixelYDimension: imagesize[1], - } - if "date_time_original" in user_data: - exif_ifd[piexif.ExifIFD.DateTimeOriginal] = user_data["date_time_original"].encode("utf-8") - - return {"0th": zeroth_ifd, "Exif": exif_ifd} - - def _deg_to_dms(self, decimal_coordinate, cardinal_directions): - """ - This function converts decimal coordinates into the DMS (degrees, minutes and seconds) format. - It also determines the cardinal direction of the coordinates. - - :param decimal_coordinate: the decimal coordinates, such as 34.0522 - :param cardinal_directions: the locations of the decimal coordinate, such as ["S", "N"] or ["W", "E"] - :return: degrees, minutes, seconds and compass_direction - :rtype: int, int, float, string - """ - if decimal_coordinate < 0: - compass_direction = cardinal_directions[0] - elif decimal_coordinate > 0: - compass_direction = cardinal_directions[1] - else: - compass_direction = "" - degrees = int(abs(decimal_coordinate)) - decimal_minutes = (abs(decimal_coordinate) - degrees) * 60 - minutes = int(decimal_minutes) - seconds = Fraction((decimal_minutes - minutes) * 60).limit_denominator(100) - return degrees, minutes, seconds, compass_direction - - def _dms_to_exif_format(self, dms_degrees, dms_minutes, dms_seconds): - """ - This function converts DMS (degrees, minutes and seconds) to values that can - be used with the EXIF (Exchangeable Image File Format). - - :param dms_degrees: int value for degrees - :param dms_minutes: int value for minutes - :param dms_seconds: fractions.Fraction value for seconds - :return: EXIF values for the provided DMS values - :rtype: nested tuple - """ - exif_format = ( - (dms_degrees, 1), - (dms_minutes, 1), - (int(dms_seconds.limit_denominator(100).numerator), int(dms_seconds.limit_denominator(100).denominator)) - ) - return exif_format - - def add_geolocation_to_exif(self, exif_data, latitude, longitude): - """ - https://stackoverflow.com/questions/77015464/adding-exif-gps-data-to-jpg-files-using-python-and-piexif - This function adds GPS values to an image using the EXIF format. - This fumction calls the functions deg_to_dms and dms_to_exif_format. - - :param image_path: image to add the GPS data to - :param latitude: the north–south position coordinate - :param longitude: the east–west position coordinate - """ - # converts the latitude and longitude coordinates to DMS - latitude_dms = self._deg_to_dms(latitude, ["S", "N"]) - longitude_dms = self._deg_to_dms(longitude, ["W", "E"]) - - # convert the DMS values to EXIF values - exif_latitude = self._dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2]) - exif_longitude = self._dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2]) - - try: - # https://exiftool.org/TagNames/GPS.html - # Create the GPS EXIF data - coordinates = { - piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0), - piexif.GPSIFD.GPSLatitude: exif_latitude, - piexif.GPSIFD.GPSLatitudeRef: latitude_dms[3], - piexif.GPSIFD.GPSLongitude: exif_longitude, - piexif.GPSIFD.GPSLongitudeRef: longitude_dms[3] - } - # Update the EXIF data with the GPS information - exif_data["GPS"] = coordinates - - return exif_data - except Exception as e: - print(f"Error: {str(e)}") diff --git a/optima/optima35.py b/optima/optima35.py deleted file mode 100644 index 9646b73..0000000 --- a/optima/optima35.py +++ /dev/null @@ -1,108 +0,0 @@ -import re -import os -from datetime import datetime -from optima.image_handler import ImageProcessor, ExifHandler - -class OPTIMA35: - def __init__(self): - self.name = "OPTIMA-35" - self.version = "0.5.0" - self.image_processor = ImageProcessor() - self.exif_handler = ExifHandler() - - def modify_timestamp_in_exif(self, data_for_exif: dict, filename: str): - """"Takes a dict formated for exif use by piexif and adjusts the date_time_original, changing the minutes and seconds to fit the number of the filname.""" - last_tree = filename[-3:len(filename)] - total_seconds = int(re.sub(r'\D+', '', last_tree)) - minutes = total_seconds // 60 - seconds = total_seconds % 60 - time = datetime.strptime(data_for_exif["date_time_original"], "%Y:%m:%d %H:%M:%S") # change date time string back to an time object for modification - new_time = time.replace(hour=12, minute=minutes, second=seconds) - data_for_exif["date_time_original"] = new_time.strftime("%Y:%m:%d %H:%M:%S") - return data_for_exif - - def process_image(self, - image_input_file, - image_output_file, - file_type, - quality, - compressing, - optimize, - resize = None, - watermark = None, - font_size = 2, - grayscale = False, - brightness = None, - contrast = None, - dict_for_exif = None, - gps = None, - copy_exif = False): - # Partly optimized by ChatGPT - # Open the image file - with self.image_processor.open_image(image_input_file) as img: - processed_img = img - image_name = os.path.basename(image_output_file) # for date adjustment - - # Resize - if resize is not None: - processed_img = self.image_processor.resize_image( - image=processed_img, percent=resize - ) - - # Watermark - if watermark is not None: - processed_img = self.image_processor.add_watermark( - processed_img, watermark, int(font_size) - ) - - # Grayscale - if grayscale: - processed_img = self.image_processor.grayscale(processed_img) - - # Brightness - if brightness is not None: - processed_img = self.image_processor.change_brightness( - processed_img, brightness - ) - - # Contrast - if contrast is not None: - processed_img = self.image_processor.change_contrast( - processed_img, contrast - ) - - # EXIF data handling - exif_piexif_format = None - if dict_for_exif: # todo: maybe move to ui and only accept complete exif dicts.. - selected_exif = dict_for_exif - if "date_time_original" in dict_for_exif: - selected_exif = self.modify_timestamp_in_exif(selected_exif, image_name) - exif_piexif_format = self.exif_handler.build_exif_dict( - selected_exif, self.image_processor.get_image_size(processed_img) - ) - - # GPS data - if gps is not None: - latitude = float(gps[0]) - longitude = float(gps[1]) - exif_piexif_format = self.exif_handler.add_geolocation_to_exif(exif_piexif_format, latitude, longitude) - - # Copy EXIF data if selected, and ensure size is correct in exif data - elif copy_exif: - try: - og_exif = self.exif_handler.get_exif_info(img) - og_exif["Exif"][40962], og_exif["Exif"][40963] = self.image_processor.get_image_size(processed_img) - exif_piexif_format = og_exif - except Exception: - print("Copying EXIF data selected, but no EXIF data is available in the original image file.") - - # Save the processed image - self.image_processor.save_image( - image = processed_img, - path = image_output_file, - piexif_exif_data = exif_piexif_format, - file_type = file_type, - jpg_quality = quality, - png_compressing = compressing, - optimize = optimize - )