Thumbnails for images and videos #87

Merged
Anna Sirota merged 28 commits from thumbnails into main 2024-04-25 17:50:58 +02:00
3 changed files with 35 additions and 26 deletions
Showing only changes of commit 728fb4a25c - Show all commits

View File

@ -41,18 +41,18 @@ def make_thumbnails(file_id: int) -> None:
assert file.validation.is_ok, f'File pk={file_id} is flagged' assert file.validation.is_ok, f'File pk={file_id} is flagged'
source_path = os.path.join(settings.MEDIA_ROOT, file.source.path) source_path = os.path.join(settings.MEDIA_ROOT, file.source.path)
thumbnail_field = file.thumbnail
if file.is_video: if file.is_video:
f_path = os.path.join(settings.MEDIA_ROOT, files.utils.get_thumbnail_upload_to(file.hash))
# For a video, source of the thumbnail is some frame fetched with ffpeg # For a video, source of the thumbnail is some frame fetched with ffpeg
output_path = os.path.join(settings.MEDIA_ROOT, thumbnail_field.upload_to(file, 'x.png')) files.utils.extract_frame(source_path, f_path)
files.utils.extract_frame(source_path, output_path) source_path = f_path
source_path = output_path
thumbnails = files.utils.make_thumbnails(source_path, file.hash) thumbnails = files.utils.make_thumbnails(source_path, file.hash)
thumbnail_default_path = thumbnails['l']['path'] thumbnail_default_path = thumbnails['l']['path']
update_fields = set() update_fields = set()
thumbnail_field = file.thumbnail
if thumbnail_default_path != thumbnail_field.name: if thumbnail_default_path != thumbnail_field.name:
thumbnail_field.name = thumbnail_default_path thumbnail_field.name = thumbnail_default_path
update_fields.add('thumbnail') update_fields.add('thumbnail')

View File

@ -1,6 +1,11 @@
from django.test import TestCase from django.test import TestCase
from files.utils import find_path_by_name, find_exact_path, filter_paths_by_ext from files.utils import (
filter_paths_by_ext,
find_exact_path,
find_path_by_name,
get_thumbnail_upload_to,
)
class UtilsTest(TestCase): class UtilsTest(TestCase):
@ -98,3 +103,12 @@ class UtilsTest(TestCase):
] ]
paths = filter_paths_by_ext(name_list, '.md') paths = filter_paths_by_ext(name_list, '.md')
self.assertEqual(list(paths), []) self.assertEqual(list(paths), [])
def test_get_thumbnail_upload_to(self):
for file_hash, kwargs, expected in (
('foobar', {}, 'thumbnails/fo/foobar.png'),
('deadbeaf', {'width': None, 'height': None}, 'thumbnails/de/deadbeaf.png'),
('deadbeaf', {'width': 640, 'height': 360}, 'thumbnails/de/deadbeaf_640x360.png'),
):
with self.subTest(file_hash=file_hash, kwargs=kwargs):
self.assertEqual(get_thumbnail_upload_to(file_hash, **kwargs), expected)

View File

@ -202,21 +202,23 @@ def delete_thumbnails(file_metadata: dict) -> None:
delete_file_in_storage(path) delete_file_in_storage(path)
def get_base_path(file_path: str) -> str: def get_thumbnail_upload_to(file_hash: str, width: int = None, height: int = None) -> str:
"""Return the given path without its extension.""" """Return a full media path of a thumbnail.
base_path, _ = os.path.splitext(file_path)
return base_path
Optionally, append thumbnail dimensions to the file name.
def get_thumbnail_upload_to(file_hash: str): """
prefix = 'thumbnails/' prefix = 'thumbnails/'
_hash = file_hash.split(':')[-1] _hash = file_hash.split(':')[-1]
extension = f'.{THUMBNAIL_FORMAT.lower()}' thumbnail_ext = THUMBNAIL_FORMAT.lower()
path = Path(prefix, _hash[:2], _hash).with_suffix(extension) if thumbnail_ext == 'jpeg':
return path thumbnail_ext = 'jpg'
suffix = f'.{thumbnail_ext}'
size_suffix = f'_{width}x{height}' if width and height else ''
path = Path(prefix, _hash[:2], f'{_hash}{size_suffix}').with_suffix(suffix)
return str(path)
def _resize(image: Image, size: tuple, output, output_format: str = 'PNG', **output_params): def resize_image(image: Image, size: tuple, output, output_format: str = 'PNG', **output_params):
"""Resize a models.ImageField to a given size and write it into output file.""" """Resize a models.ImageField to a given size and write it into output file."""
source_image = image.convert('RGBA' if output_format == 'PNG' else 'RGB') source_image = image.convert('RGBA' if output_format == 'PNG' else 'RGB')
source_image.thumbnail(size, Image.LANCZOS) source_image.thumbnail(size, Image.LANCZOS)
@ -229,18 +231,14 @@ def make_thumbnails(file_path: str, file_hash: str, output_format: str = THUMBNA
Resulting thumbnail paths a derived from the given file hash and thumbnail sizes. Resulting thumbnail paths a derived from the given file hash and thumbnail sizes.
Return a dict of size keys to output paths of generated thumbnail images. Return a dict of size keys to output paths of generated thumbnail images.
""" """
thumbnail_ext = output_format.lower()
if thumbnail_ext == 'jpeg':
thumbnail_ext = 'jpg'
base_path = get_base_path(get_thumbnail_upload_to(file_hash))
thumbnails = {} thumbnails = {}
for size_key, (w, h) in THUMBNAIL_SIZES.items(): for size_key, (w, h) in THUMBNAIL_SIZES.items():
output_path = f'{base_path}_{w}x{h}.{thumbnail_ext}' output_path = get_thumbnail_upload_to(file_hash, width=w, height=h)
size = (w, h) size = (w, h)
with tempfile.TemporaryFile() as f: with tempfile.TemporaryFile() as f:
logger.info('Resizing %s to %s (%s)', file_path, size, output_format) logger.info('Resizing %s to %s (%s)', file_path, size, output_format)
image = Image.open(file_path) image = Image.open(file_path)
_resize( resize_image(
image, image,
size, size,
f, f,
@ -259,17 +257,14 @@ def make_thumbnails(file_path: str, file_hash: str, output_format: str = THUMBNA
return thumbnails return thumbnails
def extract_frame(source_path: str, output_path: str): def extract_frame(source_path: str, output_path: str, at_time: str = '00:00:00.01'):
"""Extract a single frame of a video at a given path, write it to the given output path.""" """Extract a single frame of a video at a given path, write it to the given output path."""
try: try:
ffmpeg = ( ffmpeg = (
FFmpeg() FFmpeg()
.option('y') .option('y')
.input(source_path) .input(source_path)
.output( .output(output_path, {'ss': at_time, 'frames:v': 1, 'update': 'true'})
output_path,
{'ss': '00:00:00.01', 'frames:v': 1, 'update': 'true'},
)
) )
output_dir = os.path.dirname(output_path) output_dir = os.path.dirname(output_path)
if not os.path.isdir(output_dir): if not os.path.isdir(output_dir):