photolib/photoapp/thumb.py

111 lines
3.7 KiB
Python
Raw Normal View History

2018-09-08 15:49:16 -07:00
import os
2018-09-09 12:05:13 -07:00
import sys
import traceback
from time import time
from collections import defaultdict
from multiprocessing import Process
from PIL import Image, ImageOps
2019-07-04 18:41:57 -07:00
import tempfile
from shutil import copyfileobj
2019-07-11 19:26:22 -07:00
from contextlib import closing
import logging
2018-09-08 15:49:16 -07:00
2021-08-17 22:26:06 -07:00
# style tuples: max x, max y, rotate ok)
# rotate ok means x and y maxes can be swapped if it fits the image's aspect ratio better
THUMB_STYLES = {"tiny": (80, 80, False),
"small": (100, 100, False),
"feed": (250, 250, False),
"preview": (1024, 768, True),
"big": (2048, 1536, True)}
def gen_thumb(src_img, dest_img, width, height, rotation):
start = time()
# TODO lock around the dir creation
os.makedirs(os.path.split(dest_img)[0], exist_ok=True)
image = Image.open(src_img)
if image.mode != "RGB":
image = image.convert("RGB")
image = image.rotate(90 * rotation, expand=True)
thumb = ImageOps.fit(image, (width, height), Image.ANTIALIAS)
thumb.save(dest_img, 'JPEG')
logging.info("Generated {} in {}s".format(dest_img, round(time() - start, 4)))
return dest_img
def gen_thumb_process(src_img, dest_img, width, height, rotation):
try:
return gen_thumb(src_img, dest_img, width, height, rotation)
except Exception:
traceback.print_exc()
if os.path.exists(dest_img):
os.unlink(dest_img)
sys.exit(1)
def thumb_path(style_name, uuid):
return os.path.join("thumbs", style_name, uuid[0:2], "{}.jpg".format(uuid))
2019-07-04 18:41:57 -07:00
class ThumbGenerator(object):
2019-07-11 19:26:22 -07:00
def __init__(self, library, storage):
2019-07-04 18:41:57 -07:00
self.library = library
2019-07-11 19:26:22 -07:00
self.storage = storage
2018-09-09 12:05:13 -07:00
self._failed_thumbs_cache = defaultdict(dict)
2018-09-08 15:49:16 -07:00
2021-08-17 22:26:06 -07:00
def make_photo_thumb(self, photo, style_name):
2018-09-09 12:05:13 -07:00
"""
Create a thumbnail of the given photo, scaled/cropped to the given named style
:return: local path to thumbnail file or None if creation failed or was blocked
"""
2021-08-17 22:26:06 -07:00
dest = thumb_path(style_name, photo.uuid)
2019-07-11 19:26:22 -07:00
if self.storage.exists(dest):
return self.storage.open(dest, "rb")
2021-08-17 22:26:06 -07:00
if photo.uuid in self._failed_thumbs_cache[style_name]:
2019-07-11 09:16:14 -07:00
return None
2021-08-17 22:26:06 -07:00
# if photo.width is None: # todo better detection of images that PIL can't open
# return None
thumb_width, thumb_height, flip_ok = THUMB_STYLES[style_name]
2019-07-11 09:16:14 -07:00
i_width = photo.width
i_height = photo.height
im_is_rotated = photo.orientation % 2 != 0 or i_height > i_width
2018-09-09 16:47:05 -07:00
2019-07-11 09:16:14 -07:00
if im_is_rotated and flip_ok:
thumb_width, thumb_height = thumb_height, thumb_width
2018-09-09 16:47:05 -07:00
2021-08-17 22:26:06 -07:00
thumb_width = min(thumb_width, i_width if i_width > 0 else 999999999)
thumb_height = min(thumb_height, i_height if i_height > 0 else 999999999)
2018-09-09 16:47:05 -07:00
2019-07-11 09:16:14 -07:00
# TODO have the subprocess download the file
2019-07-11 19:26:22 -07:00
# TODO thundering herd
2019-07-11 09:16:14 -07:00
with tempfile.TemporaryDirectory() as tmpdir:
2019-07-11 19:26:22 -07:00
fthumblocal = tempfile.NamedTemporaryFile(delete=True)
2019-07-11 09:16:14 -07:00
fpath = os.path.join(tmpdir, "image")
with self.library.storage.open(photo.path, 'rb') as fsrc:
2019-07-11 19:26:22 -07:00
with open(fpath, 'wb') as ftmpdest:
copyfileobj(fsrc, ftmpdest)
2019-07-04 18:41:57 -07:00
2021-08-17 22:26:06 -07:00
p = Process(target=gen_thumb_process,
args=(fpath, fthumblocal.name, thumb_width, thumb_height, photo.orientation))
2019-07-11 09:16:14 -07:00
p.start()
p.join()
if p.exitcode != 0:
2021-08-17 22:26:06 -07:00
self._failed_thumbs_cache[style_name][photo.uuid] = True # dont retry failed generations
2019-07-11 09:16:14 -07:00
return None
2019-07-11 19:26:22 -07:00
with closing(self.storage.open(dest, 'wb')) as fdest:
copyfileobj(fthumblocal, fdest)
fthumblocal.seek(0)
return fthumblocal