voussoirkit/voussoirkit/imagetools.py
2022-02-12 20:09:14 -08:00

138 lines
4.2 KiB
Python

import copy
import PIL.ExifTags
import PIL.Image
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 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)
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)