Thumbnails for images and videos #87
@ -106,6 +106,6 @@ ABUSE_TYPE = Choices(
|
|||||||
# the code expecting thumbnails of new dimensions can be deployed!
|
# the code expecting thumbnails of new dimensions can be deployed!
|
||||||
THUMBNAIL_SIZE_L = (1920, 1080)
|
THUMBNAIL_SIZE_L = (1920, 1080)
|
||||||
THUMBNAIL_SIZE_S = (640, 360)
|
THUMBNAIL_SIZE_S = (640, 360)
|
||||||
THUMBNAIL_SIZES = [THUMBNAIL_SIZE_L, THUMBNAIL_SIZE_S]
|
THUMBNAIL_SIZES = {'l': THUMBNAIL_SIZE_L, 's': THUMBNAIL_SIZE_S}
|
||||||
THUMBNAIL_FORMAT = 'PNG'
|
THUMBNAIL_FORMAT = 'PNG'
|
||||||
THUMBNAIL_QUALITY = 83
|
THUMBNAIL_QUALITY = 83
|
||||||
|
@ -199,7 +199,11 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def thumbnail_l_url(self) -> str:
|
def thumbnail_l_url(self) -> str:
|
||||||
"""Log absence of the thumbnail file instead of exploding somewhere in the templates."""
|
"""Return absolute path portion of the URL of the large thumbnail of this file.
|
||||||
|
|
||||||
|
Fall back to the source file, if no thumbnail is stored.
|
||||||
|
Log absence of the thumbnail file instead of exploding somewhere in the templates.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self.thumbnail.url
|
return self.thumbnail.url
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -208,11 +212,16 @@ class File(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def thumbnail_s_url(self) -> str:
|
def thumbnail_s_url(self) -> str:
|
||||||
"""Log absence of the thumbnail file instead of exploding somewhere in the templates."""
|
"""Return absolute path portion of the URL of the small thumbnail of this file.
|
||||||
|
|
||||||
|
Fall back to the source file, if no thumbnail is stored.
|
||||||
|
Log absence of the thumbnail file instead of exploding somewhere in the templates.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self.metadata['thumbnails']['s']['path']
|
s = self.metadata['thumbnails']['s']['path']
|
||||||
except KeyError:
|
return self.thumbnail.storage.url(s)
|
||||||
log.exception(f'File pk={self.pk} is missing a small thumbnail')
|
except (KeyError, TypeError):
|
||||||
|
log.exception(f'File pk={self.pk} is missing a small thumbnail: {self.metadata}')
|
||||||
return self.source.url
|
return self.source.url
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ from background_task import background
|
|||||||
from background_task.tasks import TaskSchedule
|
from background_task.tasks import TaskSchedule
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from constants.base import THUMBNAIL_SIZE_L
|
|
||||||
import files.models
|
import files.models
|
||||||
import files.utils
|
import files.utils
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ def clamdscan(file_id: int):
|
|||||||
|
|
||||||
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
@background(schedule={'action': TaskSchedule.RESCHEDULE_EXISTING})
|
||||||
def make_thumbnails(file_id: int) -> None:
|
def make_thumbnails(file_id: int) -> None:
|
||||||
"""Generate thumbnails for a given file."""
|
"""Generate thumbnails for a given file, store them in thumbnail and metadata columns."""
|
||||||
file = files.models.File.objects.get(pk=file_id)
|
file = files.models.File.objects.get(pk=file_id)
|
||||||
args = {'pk': file_id, 'type': file.get_type_display()}
|
args = {'pk': file_id, 'type': file.get_type_display()}
|
||||||
if not file.is_image and not file.is_video:
|
if not file.is_image and not file.is_video:
|
||||||
@ -51,7 +50,7 @@ def make_thumbnails(file_id: int) -> None:
|
|||||||
source_path = output_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 = next(_['path'] for _ in thumbnails if _['size'] == THUMBNAIL_SIZE_L)
|
thumbnail_default_path = thumbnails['l']['path']
|
||||||
|
|
||||||
update_fields = set()
|
update_fields = set()
|
||||||
if thumbnail_default_path != thumbnail_field.name:
|
if thumbnail_default_path != thumbnail_field.name:
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
<div class="file-thumbnails">
|
<div class="file-thumbnails">
|
||||||
{% for thumb in file.metadata.thumbnails %}
|
{% for size_key, thumb in file.metadata.thumbnails.items %}
|
||||||
<div class="file-thumbnail">
|
<div class="file-thumbnail">
|
||||||
<span class="file-thumbnail-size">{{ thumb.size.0 }}x{{ thumb.size.1 }}px</span>
|
<span class="file-thumbnail-size">{{ thumb.size.0 }}x{{ thumb.size.1 }}px</span>
|
||||||
<img height="{% widthratio thumb.size.1 10 1 %}" src="{{ MEDIA_URL }}{{ thumb.path }}" title={{ thumb.path }}>
|
<img height="{% widthratio thumb.size.1 10 1 %}" src="{{ MEDIA_URL }}{{ thumb.path }}" title={{ thumb.path }}>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{# alert about unexpected size of the default thumbnail, which might happen if source file was smaller than 1080p #}
|
||||||
|
{# TODO: do we want to make minimum resolusion a validation criteria during upload instead? #}
|
||||||
{% if file.thumbnail.width != THUMBNAIL_SIZE_L.0 or file.thumbnail.height != THUMBNAIL_SIZE_L.1 %}
|
{% if file.thumbnail.width != THUMBNAIL_SIZE_L.0 or file.thumbnail.height != THUMBNAIL_SIZE_L.1 %}
|
||||||
<p>
|
<p>
|
||||||
<b class="icon-alert">⚠</b> Expected {{ THUMBNAIL_SIZE_L.0 }}x{{ THUMBNAIL_SIZE_L.1 }}px,
|
<b class="icon-alert">⚠</b> Expected {{ THUMBNAIL_SIZE_L.0 }}x{{ THUMBNAIL_SIZE_L.1 }}px,
|
||||||
|
@ -227,14 +227,14 @@ def make_thumbnails(file_path: str, file_hash: str, output_format: str = THUMBNA
|
|||||||
"""Generate thumbnail files for given file and a predefined list of dimensions.
|
"""Generate thumbnail files for given file and a predefined list of dimensions.
|
||||||
|
|
||||||
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 list of sizes and output paths of generated thumbnail images.
|
Return a dict of size keys to output paths of generated thumbnail images.
|
||||||
"""
|
"""
|
||||||
thumbnail_ext = output_format.lower()
|
thumbnail_ext = output_format.lower()
|
||||||
if thumbnail_ext == 'jpeg':
|
if thumbnail_ext == 'jpeg':
|
||||||
thumbnail_ext = 'jpg'
|
thumbnail_ext = 'jpg'
|
||||||
base_path = get_base_path(get_thumbnail_upload_to(file_hash))
|
base_path = get_base_path(get_thumbnail_upload_to(file_hash))
|
||||||
thumbnails = []
|
thumbnails = {}
|
||||||
for w, h in THUMBNAIL_SIZES:
|
for size_key, (w, h) in THUMBNAIL_SIZES.items():
|
||||||
output_path = f'{base_path}_{w}x{h}.{thumbnail_ext}'
|
output_path = f'{base_path}_{w}x{h}.{thumbnail_ext}'
|
||||||
size = (w, h)
|
size = (w, h)
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
@ -255,7 +255,7 @@ def make_thumbnails(file_path: str, file_hash: str, output_format: str = THUMBNA
|
|||||||
logger.warning('%s exists, overwriting', output_path)
|
logger.warning('%s exists, overwriting', output_path)
|
||||||
default_storage.delete(output_path)
|
default_storage.delete(output_path)
|
||||||
default_storage.save(output_path, f)
|
default_storage.save(output_path, f)
|
||||||
thumbnails.append({'size': size, 'path': output_path})
|
thumbnails[size_key] = {'size': size, 'path': output_path}
|
||||||
return thumbnails
|
return thumbnails
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user