2015-09-11 15:04:25 +02:00
|
|
|
import os
|
2015-09-24 15:45:57 +02:00
|
|
|
import json
|
|
|
|
import subprocess
|
2015-09-11 15:04:25 +02:00
|
|
|
from PIL import Image
|
|
|
|
from application import app
|
|
|
|
|
|
|
|
|
|
|
|
def generate_local_thumbnails(src, return_image_stats=False):
|
|
|
|
"""Given a source image, use Pillow to generate thumbnails according to the
|
|
|
|
application settings.
|
|
|
|
|
|
|
|
args:
|
|
|
|
src: the path of the image to be thumbnailed
|
|
|
|
return_image_stats: if True, return a dict object which contains length,
|
|
|
|
resolution, format and path of the thumbnailed image
|
|
|
|
"""
|
|
|
|
|
|
|
|
thumbnail_settings = app.config['UPLOADS_LOCAL_STORAGE_THUMBNAILS']
|
|
|
|
thumbnails = {}
|
|
|
|
for size, settings in thumbnail_settings.iteritems():
|
|
|
|
root, ext = os.path.splitext(src)
|
|
|
|
dst = "{0}-{1}{2}".format(root, size, '.jpg')
|
|
|
|
if os.path.isfile(dst):
|
|
|
|
# If the thumbnail already exists we require stats about it
|
|
|
|
if return_image_stats:
|
|
|
|
thumbnails[size] = dict(exists=True)
|
|
|
|
continue
|
|
|
|
if settings['crop']:
|
|
|
|
resize_and_crop(src, dst, settings['size'])
|
|
|
|
else:
|
|
|
|
im = Image.open(src)
|
|
|
|
im.thumbnail(settings['size'])
|
|
|
|
im.save(dst, "JPEG")
|
|
|
|
|
|
|
|
if return_image_stats:
|
|
|
|
# Get file size
|
|
|
|
st = os.stat(dst)
|
|
|
|
length = st.st_size
|
|
|
|
# Get resolution
|
|
|
|
im = Image.open(dst)
|
|
|
|
width = im.size[0]
|
|
|
|
height = im.size[1]
|
|
|
|
format = im.format.lower()
|
|
|
|
# Get format
|
|
|
|
thumbnails[size] = dict(
|
2015-11-05 18:47:36 +01:00
|
|
|
file_path=dst, # Full path, to be processed before storage
|
2015-09-11 15:04:25 +02:00
|
|
|
length=length,
|
|
|
|
width=width,
|
|
|
|
height=height,
|
|
|
|
md5='--',
|
|
|
|
content_type='image/' + format,
|
|
|
|
)
|
|
|
|
|
|
|
|
if return_image_stats:
|
|
|
|
return thumbnails
|
|
|
|
|
|
|
|
|
|
|
|
def resize_and_crop(img_path, modified_path, size, crop_type='middle'):
|
|
|
|
"""
|
|
|
|
Resize and crop an image to fit the specified size. Thanks to:
|
|
|
|
https://gist.github.com/sigilioso/2957026
|
|
|
|
|
|
|
|
args:
|
|
|
|
img_path: path for the image to resize.
|
|
|
|
modified_path: path to store the modified image.
|
|
|
|
size: `(width, height)` tuple.
|
|
|
|
crop_type: can be 'top', 'middle' or 'bottom', depending on this
|
|
|
|
value, the image will cropped getting the 'top/left', 'middle' or
|
|
|
|
'bottom/right' of the image to fit the size.
|
|
|
|
raises:
|
|
|
|
Exception: if can not open the file in img_path of there is problems
|
|
|
|
to save the image.
|
|
|
|
ValueError: if an invalid `crop_type` is provided.
|
|
|
|
|
|
|
|
"""
|
|
|
|
# If height is higher we resize vertically, if not we resize horizontally
|
|
|
|
img = Image.open(img_path)
|
|
|
|
# Get current and desired ratio for the images
|
|
|
|
img_ratio = img.size[0] / float(img.size[1])
|
|
|
|
ratio = size[0] / float(size[1])
|
|
|
|
#The image is scaled/cropped vertically or horizontally depending on the ratio
|
|
|
|
if ratio > img_ratio:
|
|
|
|
img = img.resize((size[0], int(round(size[0] * img.size[1] / img.size[0]))),
|
|
|
|
Image.ANTIALIAS)
|
|
|
|
# Crop in the top, middle or bottom
|
|
|
|
if crop_type == 'top':
|
|
|
|
box = (0, 0, img.size[0], size[1])
|
|
|
|
elif crop_type == 'middle':
|
|
|
|
box = (0, int(round((img.size[1] - size[1]) / 2)), img.size[0],
|
|
|
|
int(round((img.size[1] + size[1]) / 2)))
|
|
|
|
elif crop_type == 'bottom':
|
|
|
|
box = (0, img.size[1] - size[1], img.size[0], img.size[1])
|
|
|
|
else :
|
|
|
|
raise ValueError('ERROR: invalid value for crop_type')
|
|
|
|
img = img.crop(box)
|
|
|
|
elif ratio < img_ratio:
|
|
|
|
img = img.resize((int(round(size[1] * img.size[0] / img.size[1])), size[1]),
|
|
|
|
Image.ANTIALIAS)
|
|
|
|
# Crop in the top, middle or bottom
|
|
|
|
if crop_type == 'top':
|
|
|
|
box = (0, 0, size[0], img.size[1])
|
|
|
|
elif crop_type == 'middle':
|
|
|
|
box = (int(round((img.size[0] - size[0]) / 2)), 0,
|
|
|
|
int(round((img.size[0] + size[0]) / 2)), img.size[1])
|
|
|
|
elif crop_type == 'bottom':
|
|
|
|
box = (img.size[0] - size[0], 0, img.size[0], img.size[1])
|
|
|
|
else :
|
|
|
|
raise ValueError('ERROR: invalid value for crop_type')
|
|
|
|
img = img.crop(box)
|
|
|
|
else :
|
|
|
|
img = img.resize((size[0], size[1]),
|
|
|
|
Image.ANTIALIAS)
|
|
|
|
# If the scale is the same, we do not need to crop
|
|
|
|
img.save(modified_path, "JPEG")
|
2015-09-24 15:45:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_video_data(filepath):
|
2016-02-08 12:46:23 +01:00
|
|
|
"""Return video duration and resolution given an input file path"""
|
|
|
|
outdata = None
|
2016-03-17 12:21:13 +01:00
|
|
|
ffprobe_inspect = [
|
|
|
|
app.config['BIN_FFPROBE'],
|
|
|
|
'-loglevel',
|
|
|
|
'error',
|
|
|
|
'-show_streams',
|
|
|
|
filepath,
|
|
|
|
'-print_format',
|
|
|
|
'json']
|
|
|
|
|
|
|
|
ffprobe_ouput = json.loads(subprocess.check_output(ffprobe_inspect))
|
2015-09-24 15:45:57 +02:00
|
|
|
|
2016-02-08 12:46:23 +01:00
|
|
|
video_stream = None
|
|
|
|
# Loop throught audio and video streams searching for the video
|
|
|
|
for stream in ffprobe_ouput['streams']:
|
|
|
|
if stream['codec_type'] == 'video':
|
|
|
|
video_stream = stream
|
|
|
|
|
|
|
|
if video_stream:
|
2016-03-17 12:21:13 +01:00
|
|
|
# If video is webm we can't get the duration (seems to be an ffprobe
|
|
|
|
# issue)
|
2016-02-08 12:46:23 +01:00
|
|
|
if video_stream['codec_name'] == 'vp8':
|
2015-09-24 15:45:57 +02:00
|
|
|
duration = None
|
|
|
|
else:
|
2016-02-08 12:46:23 +01:00
|
|
|
duration = int(float(video_stream['duration']))
|
2015-09-24 15:45:57 +02:00
|
|
|
outdata = dict(
|
2016-03-17 12:21:13 +01:00
|
|
|
duration=duration,
|
|
|
|
res_x=video_stream['width'],
|
|
|
|
res_y=video_stream['height'],
|
2015-09-24 15:45:57 +02:00
|
|
|
)
|
2016-02-08 12:46:23 +01:00
|
|
|
if video_stream['sample_aspect_ratio'] != '1:1':
|
2015-09-24 15:45:57 +02:00
|
|
|
print '[warning] Pixel aspect ratio is not square!'
|
|
|
|
|
|
|
|
return outdata
|
|
|
|
|
|
|
|
|
|
|
|
def ffmpeg_encode(src, format, res_y=720):
|
|
|
|
# The specific FFMpeg command, called multiple times
|
|
|
|
args = []
|
|
|
|
args.append("-i")
|
|
|
|
args.append(src)
|
|
|
|
|
|
|
|
if format == 'mp4':
|
|
|
|
# Example mp4 encoding
|
|
|
|
# ffmpeg -i INPUT -vcodec libx264 -pix_fmt yuv420p -preset fast -crf 20
|
|
|
|
# -acodec libfdk_aac -ab 112k -ar 44100 -movflags +faststart OUTPUT
|
|
|
|
args.extend([
|
|
|
|
'-threads', '1',
|
|
|
|
'-vf', 'scale=-2:{0}'.format(res_y),
|
|
|
|
'-vcodec', 'libx264',
|
|
|
|
'-pix_fmt', 'yuv420p',
|
|
|
|
'-preset', 'fast',
|
|
|
|
'-crf', '20',
|
|
|
|
'-acodec', 'libfdk_aac', '-ab', '112k', '-ar', '44100',
|
|
|
|
'-movflags', '+faststart'])
|
|
|
|
elif format == 'webm':
|
|
|
|
# Example webm encoding
|
|
|
|
# ffmpeg -i INPUT -vcodec libvpx -g 120 -lag-in-frames 16 -deadline good
|
|
|
|
# -cpu-used 0 -vprofile 0 -qmax 51 -qmin 11 -slices 4 -b:v 2M -f webm
|
|
|
|
|
|
|
|
args.extend([
|
|
|
|
'-vf', 'scale=-2:{0}'.format(res_y),
|
|
|
|
'-vcodec', 'libvpx',
|
|
|
|
'-g', '120',
|
|
|
|
'-lag-in-frames', '16',
|
|
|
|
'-deadline', 'good',
|
|
|
|
'-cpu-used', '0',
|
|
|
|
'-vprofile', '0',
|
|
|
|
'-qmax', '51', '-qmin', '11', '-slices', '4','-b:v', '2M',
|
|
|
|
#'-acodec', 'libmp3lame', '-ab', '112k', '-ar', '44100',
|
|
|
|
'-f', 'webm'])
|
|
|
|
|
|
|
|
if not os.environ.get('VERBOSE'):
|
|
|
|
args.extend(['-loglevel', 'quiet'])
|
|
|
|
|
|
|
|
dst = os.path.splitext(src)
|
|
|
|
dst = "{0}-{1}p.{2}".format(dst[0], res_y, format)
|
|
|
|
args.append(dst)
|
|
|
|
print "Encoding {0} to {1}".format(src, format)
|
|
|
|
returncode = subprocess.call([app.config['BIN_FFMPEG']] + args)
|
|
|
|
if returncode == 0:
|
|
|
|
print "Successfully encoded {0}".format(dst)
|
|
|
|
else:
|
|
|
|
print "Error during encode"
|
|
|
|
print "Code: {0}".format(returncode)
|
|
|
|
print "Command: {0}".format(app.config['BIN_FFMPEG'] + " " + " ".join(args))
|
|
|
|
dst = None
|
|
|
|
# return path of the encoded video
|
|
|
|
return dst
|
|
|
|
|