voussoirkit/voussoirkit/imagetools.py

219 lines
6.3 KiB
Python

import copy
import datetime
import dateutil.parser
import exifread
import io
import PIL.ExifTags
import PIL.Image
import re
from voussoirkit import pathclass
_exifread = exifread
ORIENTATION_KEY = None
for (ORIENTATION_KEY, val) in PIL.ExifTags.TAGS.items():
if val == 'Orientation':
break
def checkerboard_image(color_1, color_2, image_size, checker_size) -> PIL.Image:
'''
Generate a PIL Image with a checkerboard pattern.
color_1:
The color starting in the top left. Either RGB tuple or a string
that PIL understands.
color_2:
The alternate color
image_size:
Tuple of two integers, the image size in pixels.
checker_size:
Tuple of two integers, the size of each checker in pixels.
'''
image = PIL.Image.new('RGB', image_size, color_1)
checker = PIL.Image.new('RGB', (checker_size, checker_size), color_2)
offset = True
for y in range(0, image_size[1], checker_size):
for x in range(0, image_size[0], checker_size * 2):
x += offset * checker_size
image.paste(checker, (x, y))
offset = not offset
return image
def fit_into_bounds(
image_width,
image_height,
frame_width,
frame_height,
*,
only_shrink=False,
) -> tuple:
'''
Given the w+h of the image and the w+h of the frame,
return new w+h that fits the image into the frame
while maintaining the aspect ratio.
(1920, 1080, 400, 400) -> (400, 225)
'''
width_ratio = frame_width / image_width
height_ratio = frame_height / image_height
ratio = min(width_ratio, height_ratio)
new_width = int(image_width * ratio)
new_height = int(image_height * ratio)
if only_shrink and (new_width > image_width or new_height > image_height):
return (image_width, image_height)
return (new_width, new_height)
def _get_exif_datetime_pil(image):
exif = image.getexif()
if not exif:
return
exif = {
PIL.ExifTags.TAGS[key]: value
for (key, value) in exif.items()
if key in PIL.ExifTags.TAGS
}
exif_date = exif.get('DateTimeOriginal') or exif.get('DateTime') or exif.get('DateTimeDigitized')
if not exif_date:
return None
return exif_date
def _get_exif_datetime_exifread(path):
path = pathclass.Path(path)
exif = _exifread.process_file(path.open('rb'))
exif_date = (
exif.get('EXIF DateTimeOriginal') or
exif.get('Image DateTime') or
exif.get('EXIF DateTimeDigitized')
)
if not exif_date:
return None
exif_date = exif_date.values
return exif_date
def get_exif_datetime(image) -> datetime.datetime:
# Thanks Payne
# https://stackoverflow.com/a/4765242
if isinstance(image, (str, pathclass.Path)):
exif_date = _get_exif_datetime_exifread(image)
elif isinstance(image, PIL.Image.Image):
exif_date = _get_exif_datetime_pil(image)
if not exif_date:
return None
if exif_date.startswith('0000:'):
return None
exif_date = re.sub(r'(\d\d\d\d):(\d\d):(\d\d)', r'\1-\2-\3', exif_date)
return dateutil.parser.parse(exif_date)
def exifread(path) -> dict:
if isinstance(path, PIL.Image.Image):
handle = io.BytesIO()
path.save(handle, format='JPEG', exif=path.getexif(), quality=10)
handle.seek(0)
elif isinstance(path, pathclass.Path):
handle = path.open('rb')
elif isinstance(path, str):
handle = open(path, 'rb')
return _exifread.process_file(handle)
def pad_to_square(image, background_color=None) -> PIL.Image:
'''
If the given image is not already square, return a new, square image with
additional padding on top and bottom or left and right.
'''
if image.size[0] == image.size[1]:
return image
dimension = max(image.size)
diff_w = int((dimension - image.size[0]) / 2)
diff_h = int((dimension - image.size[1]) / 2)
new_image = PIL.Image.new(image.mode, (dimension, dimension), background_color)
new_image.paste(image, (diff_w, diff_h))
return new_image
def replace_color(image, from_color, to_color):
image = image.copy()
pixels = image.load()
for y in range(image.size[1]):
for x in range(image.size[0]):
if pixels[x, y] == from_color:
pixels[x, y] = to_color
return image
def rotate_by_exif(image):
'''
Rotate the image according to its exif data, so that it will display
correctly even if saved without the exif.
Returns (image, exif) where exif has the orientation key set to 1,
the upright position, if the rotation was successful.
You should be able to call image.save('filename.jpg', exif=exif) with
these returned values.
(To my knowledge, I can not put the exif back into the Image object itself.
There is getexif but no setexif or putexif, etc.)
'''
# Thank you Scabbiaza
# https://stackoverflow.com/a/26928142
try:
exif = image.getexif()
except AttributeError:
return (image, exif)
if exif is None:
return (image, exif)
try:
rotation = exif[ORIENTATION_KEY]
except KeyError:
return (image, exif)
fp = getattr(exif, 'fp', None)
if isinstance(fp, io.BufferedReader):
exif.fp = io.BytesIO()
exif.fp.write(fp.read())
exif.fp.seek(0)
exif = copy.deepcopy(exif)
if rotation == 1:
pass
elif rotation == 2:
image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
elif rotation == 3:
image = image.transpose(PIL.Image.ROTATE_180)
elif rotation == 4:
image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
image = image.transpose(PIL.Image.ROTATE_180)
elif rotation == 5:
image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
image = image.transpose(PIL.Image.ROTATE_90)
elif rotation == 6:
image = image.transpose(PIL.Image.ROTATE_270)
elif rotation == 7:
image = image.transpose(PIL.Image.FLIP_LEFT_RIGHT)
image = image.transpose(PIL.Image.ROTATE_270)
elif rotation == 8:
image = image.transpose(PIL.Image.ROTATE_90)
exif[ORIENTATION_KEY] = 1
return (image, exif)
def save_to_bytes(image, *save_args, **save_kwargs):
bio = io.BytesIO()
image.save(bio, *save_args, **save_kwargs)
bio.seek(0)
blob = bio.read()
return blob