138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
import io
|
|
import math
|
|
from datetime import datetime
|
|
from typing import Optional, Tuple
|
|
import exif
|
|
from PIL import Image
|
|
from PIL.ImageEnhance import Brightness, Contrast, Color, Sharpness
|
|
from utils.file import check_directories
|
|
from utils.gps import gps_to_decimal
|
|
|
|
|
|
async def parse_exif_info(path: str, filename: str) -> dict:
|
|
with open(f"{path}/{filename}", "rb") as f:
|
|
img = exif.Image(f)
|
|
if not img.has_exif:
|
|
return {}
|
|
|
|
exif_info = img.get_all()
|
|
|
|
for datetime_field in ("datetime", "datetime_original", "datetime_digitized"):
|
|
if exif_info.get(datetime_field):
|
|
exif_info[datetime_field] = datetime.strptime(exif_info[datetime_field], "%Y:%m:%d %H:%M:%S")
|
|
|
|
if exif_info.get("gps_latitude"):
|
|
exif_info["gps_latitude"] = gps_to_decimal(exif_info["gps_latitude"])
|
|
|
|
if exif_info.get("gps_longitude"):
|
|
exif_info["gps_longitude"] = gps_to_decimal(exif_info["gps_longitude"])
|
|
|
|
return exif_info
|
|
|
|
|
|
class PhotoEditor:
|
|
def __init__(self, path: str, filename: str):
|
|
self.path = path
|
|
self.filename = filename
|
|
|
|
self.img = Image.open(f"{path}/{filename}")
|
|
self.img_size = self.img.size
|
|
|
|
def resize(self, new_width: Optional[int] = None, new_height: Optional[int] = None):
|
|
if not new_width and not new_height:
|
|
raise ValueError("Set either new_width or new_height")
|
|
|
|
w, h = self.img.size
|
|
aspect_ratio = w / h
|
|
|
|
if not new_width:
|
|
# TODO: tohle bude asi blbe, tu se bude muset asi delit?
|
|
new_width = int(new_height * aspect_ratio)
|
|
|
|
if not new_height:
|
|
new_height = int(new_width / aspect_ratio)
|
|
|
|
self.img = self.img.resize((new_width, new_height), Image.BICUBIC)
|
|
self.img_size = self.img.size
|
|
|
|
return self
|
|
|
|
def _get_cropbox_after_rotate(self, degrees: float, rotated_width: int, rotated_height: int) -> Tuple[int, int]:
|
|
original_width, original_height = self.img_size
|
|
aspect_ratio = float(original_width) / original_height
|
|
rotated_aspect_ratio = float(rotated_width) / rotated_height
|
|
angle = math.fabs(degrees) * math.pi / 180
|
|
|
|
if aspect_ratio < 1:
|
|
total_height = float(original_width) / rotated_aspect_ratio
|
|
else:
|
|
total_height = float(original_height)
|
|
|
|
h = total_height / (aspect_ratio * math.sin(angle) + math.cos(angle))
|
|
w = h * aspect_ratio
|
|
|
|
return round(w), round(h)
|
|
|
|
def rotate(self, degrees: float, crop_after_rotate: bool = False):
|
|
degrees *= -1
|
|
self.img = self.img.rotate(degrees, resample=Image.Resampling.BICUBIC, expand=True)
|
|
|
|
if crop_after_rotate:
|
|
rotated_width, rotated_height = self.img.size
|
|
new_width, new_height = self._get_cropbox_after_rotate(degrees, rotated_width, rotated_height)
|
|
|
|
left = round((rotated_width - new_width) / 2)
|
|
top = round((rotated_height - new_height) / 2)
|
|
|
|
self.img = self.img.crop((left, top, new_width, new_height))
|
|
|
|
self.img_size = self.img.size
|
|
return self
|
|
|
|
def crop(self, left: float, top: float, width: float, height: float):
|
|
w, h = self.img_size
|
|
left_px = int(left * w)
|
|
top_px = int(top * h)
|
|
width_px = int(width * w)
|
|
height_px = int(height * h)
|
|
|
|
self.img = self.img.crop((left_px, top_px, left_px + width_px, top_px + height_px))
|
|
self.img_size = self.img.size
|
|
return self
|
|
|
|
def adjust(
|
|
self,
|
|
brightness: Optional[float] = None,
|
|
contrast: Optional[float] = None,
|
|
saturation: Optional[float] = None,
|
|
sharpness: Optional[float] = None
|
|
):
|
|
adjustments = [
|
|
(Brightness, brightness),
|
|
(Contrast, contrast),
|
|
(Color, saturation),
|
|
(Sharpness, sharpness)
|
|
]
|
|
for adjustment, value in adjustments:
|
|
if value is not None:
|
|
self.img = adjustment(self.img).enhance(value)
|
|
|
|
return self
|
|
|
|
def get_as_stream(self):
|
|
img_io = io.BytesIO()
|
|
self.img.save(img_io, 'JPEG')
|
|
img_io.seek(0)
|
|
|
|
return img_io
|
|
|
|
def write_to_file(
|
|
self, quality: int = 90, dest_path: Optional[str] = None, dest_filename: Optional[str] = None
|
|
) -> str:
|
|
check_directories(dest_path or self.path)
|
|
|
|
dest = f"{dest_path or self.path}/{dest_filename or self.filename}"
|
|
self.img.save(dest, 'JPEG', quality=quality)
|
|
|
|
return dest
|