Merge pull request #18 from chmduquesne/master
Adding support for videos
This commit is contained in:
@@ -5,7 +5,7 @@ python:
|
||||
before_install:
|
||||
# Dependencies to build PIL
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -qq libfreetype6-dev libjpeg8-dev zlib1g-dev
|
||||
- sudo apt-get install -qq libfreetype6-dev libjpeg8-dev zlib1g-dev ffmpeg
|
||||
install:
|
||||
- pip install pytest --use-mirrors
|
||||
- pip install . --use-mirrors
|
||||
|
||||
126
sigal/gallery.py
126
sigal/gallery.py
@@ -1,6 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
# Copyright (c) 2009-2013 - Simon Conseil
|
||||
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
@@ -34,12 +35,16 @@ from clint.textui import progress, colored
|
||||
from os.path import join
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from .image import generate_image, generate_thumbnail
|
||||
import sigal.image
|
||||
import sigal.video
|
||||
from .settings import get_thumb
|
||||
from .writer import Writer
|
||||
|
||||
DESCRIPTION_FILE = "index.md"
|
||||
|
||||
class FileExtensionError(Exception):
|
||||
"""Raised if we made an error when handling file extensions"""
|
||||
pass
|
||||
|
||||
class PathsDb(object):
|
||||
"""Container for all the information on the directory structure.
|
||||
@@ -49,8 +54,9 @@ class PathsDb(object):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path, ext_list):
|
||||
self.ext_list = ext_list
|
||||
def __init__(self, path, img_ext_list, vid_ext_list):
|
||||
self.img_ext_list = img_ext_list
|
||||
self.vid_ext_list = vid_ext_list
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# basepath must to be a unicode string so that os.walk will return
|
||||
@@ -92,23 +98,23 @@ class PathsDb(object):
|
||||
|
||||
self.db['paths_list'].append(relpath)
|
||||
self.db[relpath] = {
|
||||
'img': [f for f in filenames
|
||||
if os.path.splitext(f)[1] in self.ext_list],
|
||||
'medias': [f for f in filenames if os.path.splitext(f)[1]
|
||||
in (self.img_ext_list + self.vid_ext_list)],
|
||||
'subdir': dirnames
|
||||
}
|
||||
self.db[relpath].update(get_metadata(path))
|
||||
|
||||
path_im = [path for path in self.db['paths_list']
|
||||
if self.db[path]['img'] and path != '.']
|
||||
path_noim = [path for path in self.db['paths_list']
|
||||
if not self.db[path]['img'] and path != '.']
|
||||
path_media = [path for path in self.db['paths_list']
|
||||
if self.db[path]['medias'] and path != '.']
|
||||
path_nomedia = [path for path in self.db['paths_list']
|
||||
if not self.db[path]['medias'] and path !='.']
|
||||
|
||||
# dir with images: check the thumbnail, and find it if necessary
|
||||
for path in path_im:
|
||||
for path in path_media:
|
||||
self.check_thumbnail(path)
|
||||
|
||||
# dir without images, start with the deepest ones
|
||||
for path in reversed(sorted(path_noim, key=lambda x: x.count('/'))):
|
||||
for path in reversed(sorted(path_nomedia, key=lambda x: x.count('/'))):
|
||||
for subdir in self.get_subdirs(path):
|
||||
# use the thumbnail of their sub-directories
|
||||
if self.db[subdir].get('thumbnail', ''):
|
||||
@@ -136,17 +142,20 @@ class PathsDb(object):
|
||||
return
|
||||
|
||||
# find and return the first landscape image
|
||||
for f in self.db[path]['img']:
|
||||
im = PILImage.open(join(self.basepath, path, f))
|
||||
if im.size[0] > im.size[1]:
|
||||
self.db[path]['thumbnail'] = f
|
||||
return
|
||||
for f in self.db[path]['medias']:
|
||||
base, ext = os.path.splitext(f)
|
||||
if ext in self.img_ext_list:
|
||||
im = PILImage.open(join(self.basepath, path, f))
|
||||
if im.size[0] > im.size[1]:
|
||||
self.db[path]['thumbnail'] = f
|
||||
return
|
||||
|
||||
# else simply return the 1st image
|
||||
if self.db[path]['img']:
|
||||
self.db[path]['thumbnail'] = self.db[path]['img'][0]
|
||||
else:
|
||||
self.db[path]['thumbnail'] = ''
|
||||
# else simply return the 1st media file
|
||||
if self.db[path]['medias']:
|
||||
self.db[path]['thumbnail'] = self.db[path]['medias'][0]
|
||||
return
|
||||
|
||||
self.db[path]['thumbnail'] = ''
|
||||
|
||||
|
||||
class Gallery(object):
|
||||
@@ -162,7 +171,8 @@ class Gallery(object):
|
||||
theme=theme)
|
||||
|
||||
self.paths = PathsDb(self.settings['source'],
|
||||
self.settings['ext_list'])
|
||||
self.settings['img_ext_list'],
|
||||
self.settings['vid_ext_list'])
|
||||
self.paths.build()
|
||||
self.db = self.paths.db
|
||||
|
||||
@@ -179,22 +189,22 @@ class Gallery(object):
|
||||
# loop on directories in reversed order, to process subdirectories
|
||||
# before their parent
|
||||
for path in reversed(self.db['paths_list']):
|
||||
imglist = [os.path.normpath(join(self.settings['source'], path, f))
|
||||
for f in self.db[path]['img']]
|
||||
media_files = [os.path.normpath(join(self.settings['source'], path, f))
|
||||
for f in self.db[path]['medias']]
|
||||
|
||||
# output dir for the current path
|
||||
img_out = os.path.normpath(join(self.settings['destination'],
|
||||
outpath = os.path.normpath(join(self.settings['destination'],
|
||||
path))
|
||||
check_or_create_dir(img_out)
|
||||
check_or_create_dir(outpath)
|
||||
|
||||
if len(imglist) != 0:
|
||||
self.process_dir(imglist, img_out, path,
|
||||
label_width=label_width)
|
||||
if len(media_files) != 0:
|
||||
self.process_dir(media_files, outpath, path,
|
||||
label_width=label_width)
|
||||
|
||||
if self.settings['write_html']:
|
||||
self.writer.write(self.db, path)
|
||||
|
||||
def process_dir(self, imglist, outpath, dirname, label_width=20):
|
||||
def process_dir(self, media_files, outpath, dirname, label_width=20):
|
||||
"""Process a list of images in a directory."""
|
||||
|
||||
# Create thumbnails directory and optionally the one for original img
|
||||
@@ -206,25 +216,36 @@ class Gallery(object):
|
||||
# use progressbar if level is > INFO
|
||||
if self.logger.getEffectiveLevel() > 20:
|
||||
label = colored.green(dirname.ljust(label_width))
|
||||
img_iterator = progress.bar(imglist, label=label)
|
||||
media_iterator = progress.bar(media_files, label=label)
|
||||
else:
|
||||
img_iterator = iter(imglist)
|
||||
media_iterator = iter(media_files)
|
||||
self.logger.info("")
|
||||
self.logger.info(":: Processing '%s' [%i images]",
|
||||
colored.green(dirname), len(imglist))
|
||||
self.logger.info(":: Processing '%s' [%i img/vid]",
|
||||
colored.green(dirname), len(media_files))
|
||||
self.logger.info("")
|
||||
|
||||
try:
|
||||
# loop on images
|
||||
for f in img_iterator:
|
||||
for f in media_iterator:
|
||||
filename = os.path.split(f)[1]
|
||||
outname = join(outpath, filename)
|
||||
base, ext = os.path.splitext(filename)
|
||||
if ext in self.settings['img_ext_list']:
|
||||
outname = join(outpath, filename)
|
||||
elif ext in self.settings['vid_ext_list']:
|
||||
outname = join(outpath, base + '.webm')
|
||||
else:
|
||||
raise FileExtensionError
|
||||
|
||||
if os.path.isfile(outname) and not self.force:
|
||||
self.logger.info("%s exists - skipping", filename)
|
||||
else:
|
||||
self.logger.info(filename)
|
||||
process_image(f, outpath, self.settings)
|
||||
if ext in self.settings['img_ext_list']:
|
||||
process_image(f, outpath, self.settings)
|
||||
elif ext in self.settings['vid_ext_list']:
|
||||
process_video(f, outpath, self.settings)
|
||||
else:
|
||||
raise FileExtensionError
|
||||
|
||||
except KeyboardInterrupt:
|
||||
sys.exit('Interrupted')
|
||||
@@ -247,14 +268,35 @@ def process_image(filepath, outpath, settings):
|
||||
if settings['keep_orig']:
|
||||
shutil.copy(filepath, join(outpath, settings['orig_dir'], filename))
|
||||
|
||||
generate_image(filepath, outname, settings['img_size'], None,
|
||||
options=options, copyright_text=settings['copyright'],
|
||||
method=settings['img_processor'])
|
||||
sigal.image.generate_image(filepath, outname, settings['img_size'],
|
||||
None, options=options, copyright_text=settings['copyright'],
|
||||
method=settings['img_processor'])
|
||||
|
||||
if settings['make_thumbs']:
|
||||
thumb_name = join(outpath, get_thumb(settings, filename))
|
||||
generate_thumbnail(outname, thumb_name, settings['thumb_size'], None,
|
||||
fit=settings['thumb_fit'], options=options)
|
||||
sigal.image.generate_thumbnail(outname, thumb_name,
|
||||
settings['thumb_size'], None, fit=settings['thumb_fit'],
|
||||
options=options)
|
||||
|
||||
def process_video(filepath, outpath, settings):
|
||||
"""Process one image: resize, create thumbnail."""
|
||||
|
||||
filename = os.path.split(filepath)[1]
|
||||
base, ext = os.path.splitext(filename)
|
||||
outname = join(outpath, base + '.webm')
|
||||
|
||||
if settings['keep_orig']:
|
||||
shutil.copy(filepath, join(outpath, settings['orig_dir'], filename))
|
||||
|
||||
# TODO: Add specific video size settings
|
||||
sigal.video.generate_video(filepath, outname, settings['img_size'],
|
||||
settings['webm_options'])
|
||||
|
||||
if settings['make_thumbs']:
|
||||
thumb_name = join(outpath, get_thumb(settings, filename))
|
||||
sigal.video.generate_thumbnail(outname, thumb_name,
|
||||
settings['thumb_size'], None, fit=settings['thumb_fit'],
|
||||
options=settings['jpg_options'])
|
||||
|
||||
|
||||
def get_metadata(path):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
# Copyright (c) 2009-2013 - Simon Conseil
|
||||
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
@@ -37,8 +38,10 @@ _DEFAULT_CONFIG = {
|
||||
'keep_orig': False,
|
||||
'orig_dir': 'original',
|
||||
'jpg_options': {'quality': 85, 'optimize': True, 'progressive': True},
|
||||
'webm_options': {'crf': '10', 'bitrate': '1.6M', 'qmin': '4', 'qmax': '63'},
|
||||
'copyright': '',
|
||||
'ext_list': ['.jpg', '.jpeg', '.JPG', '.JPEG', '.png'],
|
||||
'img_ext_list': ['.jpg', '.jpeg', '.JPG', '.JPEG', '.png'],
|
||||
'vid_ext_list': ['.MOV', '.mov', '.avi', '.mp4', '.webm', '.ogv'],
|
||||
'theme': 'colorbox',
|
||||
'write_html': True,
|
||||
'index_in_url': False,
|
||||
@@ -48,10 +51,23 @@ _DEFAULT_CONFIG = {
|
||||
|
||||
|
||||
def get_thumb(settings, filename):
|
||||
"""Return the path to the thumb."""
|
||||
"""Return the path to the thumb.
|
||||
|
||||
examples:
|
||||
>>> get_thumb(default_settings, "bar/foo.jpg")
|
||||
"bar/thumbnails/foo.jpg"
|
||||
>>> get_thumb(default_settings, "bar/foo.png")
|
||||
"bar/thumbnails/foo.png"
|
||||
|
||||
for videos, it returns a jpg file:
|
||||
>>> get_thumb(default_settings, "bar/foo.webm")
|
||||
"bar/thumbnails/foo.jpg"
|
||||
"""
|
||||
|
||||
path, filen = os.path.split(filename)
|
||||
name, ext = os.path.splitext(filen)
|
||||
if ext in settings['vid_ext_list']:
|
||||
ext = '.jpg'
|
||||
return os.path.join(path, settings['thumb_dir'], settings['thumb_prefix'] +
|
||||
name + settings['thumb_suffix'] + ext)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ source = 'pictures'
|
||||
# - colorbox (default), galleria, or the path to a custom theme directory
|
||||
theme = 'galleria'
|
||||
|
||||
# Size of resized image
|
||||
# Size of resized image (default: (640, 480))
|
||||
img_size = (800, 600)
|
||||
|
||||
# Pilkit processor used to resize the image
|
||||
@@ -55,6 +55,16 @@ thumb_size = (280, 210)
|
||||
# 'optimize': True,
|
||||
# 'progressive': True}
|
||||
|
||||
# Webm options
|
||||
# Options used in ffmpeg to encode the webm video. You may want to read
|
||||
# http://ffmpeg.org/trac/ffmpeg/wiki/vpxEncodingGuide
|
||||
# Be aware of the fact these options need to be passed as strings.
|
||||
# webm_options = {'crf': '10',
|
||||
# 'bitrate': '1.6M',
|
||||
# 'qmin': '4',
|
||||
# 'qmax': '63'}
|
||||
|
||||
|
||||
# Write HTML files. If False, sigal will only process the images.
|
||||
# write_html = True
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=PT+Sans">
|
||||
<link rel="stylesheet" href="{{ theme.url }}/css/style.min.css">
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -67,17 +67,38 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if images %}
|
||||
{% if medias %}
|
||||
<div id="gallery" class="row">
|
||||
{% for image in images %}
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="{{ image.file }}" class="gallery" title="{{ image.file }}"
|
||||
{% if image.big %} data-big="{{ image.big }}"{% endif %}>
|
||||
<img src="{{ image.thumb }}" alt="{{ image.file }}"
|
||||
title="{{ image.file }}" /></a>
|
||||
</div>
|
||||
{% for media in medias %}
|
||||
{% if media.type == "img" %}
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="{{ media.file }}" class="gallery" title="{{ media.file }}"
|
||||
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
title="{{ media.file }}" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if media.type == "vid" %}
|
||||
<div class="four columns thumbnail
|
||||
{% if loop.index % 3 == 1 %}alpha{% endif%}
|
||||
{% if loop.index % 3 == 0 %}omega{% endif%}">
|
||||
<a href="#{{ media.file|replace('.', '') }}" class="gallery"
|
||||
inline='yes' title="{{ media.file }}"
|
||||
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
title="{{ media.file }}" /></a>
|
||||
</div>
|
||||
<!-- This contains the hidden content for the video -->
|
||||
<div style='display:none'>
|
||||
<div id="{{ media.file|replace('.', '') }}">
|
||||
<video controls>
|
||||
<source src='{{ media.file }}' type='video/webm' />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -90,7 +111,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if images %}
|
||||
{% if medias %}
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ theme.url }}/js/jquery-1.10.2.min.js"%3E%3C/script%3E'))</script>
|
||||
<script src="{{ theme.url }}/js/jquery.colorbox.min.js"></script>
|
||||
@@ -109,6 +130,9 @@
|
||||
title += " (full size)".link(this.getAttribute("data-big"));
|
||||
}
|
||||
return title;
|
||||
},
|
||||
inline: function() {
|
||||
return this.hasAttribute("inline");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
BIN
sigal/themes/galleria/static/img/empty.png
Normal file
BIN
sigal/themes/galleria/static/img/empty.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 151 B |
@@ -13,7 +13,7 @@
|
||||
<link rel="stylesheet" href="{{ theme.url }}/css/style.min.css">
|
||||
<link rel="stylesheet" href="{{ theme.url }}/css/galleria.classic.css">
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -57,14 +57,28 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if images %}
|
||||
{% if medias %}
|
||||
<div id="gallery">
|
||||
{% for image in images %}
|
||||
<a href="{{ image.file }}">
|
||||
<img src="{{ image.thumb }}" alt="{{ image.file }}"
|
||||
data-title="{{ image.file }}"
|
||||
{%- if image.big %} data-description="<a href='{{ image.big }}'>Full size</a>"{% endif %}/>
|
||||
</a>
|
||||
{% for media in medias %}
|
||||
{% if media.type == "img" %}
|
||||
<a href="{{ media.file }}">
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
data-title="{{ media.file }}"
|
||||
{%- if media.big %} data-description="<a href='{{ media.big }}'>Full size</a>"{% endif %}/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if media.type == "vid" %}
|
||||
<a href="{{ theme.url }}/img/empty.png">
|
||||
<img src="{{ media.thumb }}" alt="{{ media.file }}"
|
||||
{# TODO: stop using inline css, put this in the main css file #}
|
||||
data-layer="<video style='position:absolute;
|
||||
top:10%;
|
||||
width:100%;
|
||||
margin=0 auto;' controls>
|
||||
<source src={{ media.file }} type='video/webm' />
|
||||
</video>" />
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -82,7 +96,7 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{% if images %}
|
||||
{% if medias %}
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
|
||||
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ theme.url }}/js/jquery-1.8.2.min.js"%3E%3C/script%3E'))</script>
|
||||
<script src="{{ theme.url }}/js/galleria-1.2.9.min.js"></script>
|
||||
|
||||
97
sigal/video.py
Normal file
97
sigal/video.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
from __future__ import with_statement
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sigal.image
|
||||
|
||||
def vid_size(source):
|
||||
"""Returns the dimensions of the video"""
|
||||
pattern = re.compile(r'Stream.*Video.* ([0-9]+)x([0-9]+)')
|
||||
p = subprocess.Popen(['ffmpeg', '-i', source],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
match = pattern.search(stderr)
|
||||
if match:
|
||||
x, y = int(match.groups()[0]), int(match.groups()[1])
|
||||
else:
|
||||
x = y = 0
|
||||
return (x, y)
|
||||
|
||||
|
||||
def generate_video(source, outname, size, options={}):
|
||||
"""Video processor
|
||||
|
||||
:param source: path to an image
|
||||
:param outname: name of the generated video
|
||||
:param options: array of options passed to ffmpeg
|
||||
|
||||
"""
|
||||
# Don't transcode if source is in the required format and
|
||||
# has fitting datedimensions, copy instead.
|
||||
w_src, h_src = vid_size(source)
|
||||
w_dst, h_dst = size
|
||||
base, src_ext = os.path.splitext(source)
|
||||
base, dst_ext = os.path.splitext(outname)
|
||||
if dst_ext == src_ext and w_src <= w_dst and h_src <= w_dst:
|
||||
shutil.copy(source, outname)
|
||||
return
|
||||
|
||||
# http://stackoverflow.com/questions/8218363/maintaining-ffmpeg-aspect-ratio
|
||||
# + I made a drawing on paper to figure this out
|
||||
if h_dst * w_src < h_src * w_dst:
|
||||
# biggest fitting dimension is height
|
||||
resize_opt = ['-vf', "scale=trunc(oh*a/2)*2:%i" % h_dst]
|
||||
else:
|
||||
# biggest fitting dimension is width
|
||||
resize_opt = ['-vf', "scale=%i:trunc(ow/a/2)*2" % w_dst]
|
||||
|
||||
# do not resize if input dimensions are smaller than output dimensions
|
||||
if w_src <= w_dst and h_src <= h_dst:
|
||||
resize_opt = []
|
||||
|
||||
# Encoding options improved, thanks to
|
||||
# http://ffmpeg.org/trac/ffmpeg/wiki/vpxEncodingGuide
|
||||
with open("/dev/null") as devnull:
|
||||
subprocess.call(['ffmpeg', '-i', source, '-y',
|
||||
'-crf', options.get('crf', '10'),
|
||||
'-b:v', options.get('bitrate', '1.6M'),
|
||||
'-qmin', options.get('qmin', '4'),
|
||||
'-qmax', options.get('qmax', '63')] +
|
||||
resize_opt + [outname],
|
||||
stderr=devnull)
|
||||
|
||||
def generate_thumbnail(source, outname, box, format, fit=True, options=None):
|
||||
"Create a thumbnail image"
|
||||
# 1) dump an image of the video
|
||||
tmpfile = outname + ".tmp.jpg"
|
||||
with open("/dev/null") as devnull:
|
||||
subprocess.call(['ffmpeg', '-i', source, '-an', '-r', '1',
|
||||
'-vframes', '1', '-y', tmpfile], stderr=devnull)
|
||||
# 2) use the generate_thumbnail function from sigal.image
|
||||
sigal.image.generate_thumbnail(tmpfile, outname, box, format, fit, options)
|
||||
# 3) remove the image
|
||||
os.unlink(tmpfile)
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
# Copyright (c) 2009-2013 - Simon Conseil
|
||||
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
@@ -33,7 +34,8 @@ from distutils.dir_util import copy_tree
|
||||
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from .image import generate_thumbnail
|
||||
import sigal.image
|
||||
import sigal.video
|
||||
from .settings import get_thumb, get_orig
|
||||
from .pkgmeta import __url__ as sigal_link
|
||||
|
||||
@@ -89,7 +91,7 @@ class Writer(object):
|
||||
self.ctx = {
|
||||
'sigal_link': sigal_link,
|
||||
'theme': {'name': os.path.basename(self.theme)},
|
||||
'images': [],
|
||||
'medias': [],
|
||||
'albums': [],
|
||||
'breadcumb': ''
|
||||
}
|
||||
@@ -134,12 +136,19 @@ class Writer(object):
|
||||
if relpath != '.':
|
||||
ctx['breadcumb'] = self.get_breadcumb(paths, relpath)
|
||||
|
||||
for i in paths[relpath]['img']:
|
||||
img_ctx = {'file': i,
|
||||
'thumb': get_thumb(self.settings, i)}
|
||||
for i in paths[relpath]['medias']:
|
||||
media_ctx = {}
|
||||
base, ext = os.path.splitext(i)
|
||||
if ext in self.settings['img_ext_list']:
|
||||
media_ctx['type'] = 'img'
|
||||
media_ctx['file'] = i
|
||||
else:
|
||||
media_ctx['type'] = 'vid'
|
||||
media_ctx['file'] = base + '.webm'
|
||||
media_ctx['thumb'] = get_thumb(self.settings, i)
|
||||
if self.settings['keep_orig']:
|
||||
img_ctx['big'] = get_orig(self.settings, i)
|
||||
ctx['images'].append(img_ctx)
|
||||
media_ctx['big'] = get_orig(self.settings, i)
|
||||
ctx['medias'].append(media_ctx)
|
||||
|
||||
for d in paths[relpath]['subdir']:
|
||||
dpath = os.path.normpath(os.path.join(relpath, d))
|
||||
@@ -152,9 +161,15 @@ class Writer(object):
|
||||
# settings['make_thumbs'] is False)
|
||||
if not os.path.exists(thumb_path):
|
||||
source = os.path.join(self.output_dir, dpath, alb_thumb)
|
||||
generate_thumbnail(
|
||||
source, thumb_path, self.settings['thumb_size'], None,
|
||||
fit=self.settings['thumb_fit'])
|
||||
base, ext = os.path.splitext(source)
|
||||
if ext in self.settings['img_ext_list']:
|
||||
sigal.image.generate_thumbnail(
|
||||
source, thumb_path, self.settings['thumb_size'],
|
||||
None, fit=self.settings['thumb_fit'])
|
||||
else:
|
||||
sigal.video.generate_thumbnail(
|
||||
source, thumb_path, self.settings['thumb_size'],
|
||||
None, fit=self.settings['thumb_fit'])
|
||||
|
||||
ctx['albums'].append({
|
||||
'url': d + '/' + self.url_ext,
|
||||
|
||||
Binary file not shown.
@@ -13,30 +13,35 @@ REF = {
|
||||
'dir1': {
|
||||
'title': 'An example gallery',
|
||||
'thumbnail': 'test1/11.jpg',
|
||||
'img': '',
|
||||
'medias': [],
|
||||
},
|
||||
'dir1/test1': {
|
||||
'title': 'An example sub-category',
|
||||
'thumbnail': '11.jpg',
|
||||
'img': ['11.jpg', 'archlinux-kiss-1024x640.png'],
|
||||
'medias': ['11.jpg', 'archlinux-kiss-1024x640.png'],
|
||||
},
|
||||
'dir1/test2': {
|
||||
'title': 'Test2',
|
||||
'thumbnail': '21.jpg',
|
||||
'img': ['21.jpg', '22.jpg'],
|
||||
'medias': ['21.jpg', '22.jpg'],
|
||||
},
|
||||
'dir2': {
|
||||
'title': 'Another example gallery',
|
||||
'thumbnail': 'm57_the_ring_nebula-587px.jpg',
|
||||
'img': ['exo20101028-b-full.jpg',
|
||||
'medias': ['exo20101028-b-full.jpg',
|
||||
'm57_the_ring_nebula-587px.jpg',
|
||||
'Hubble ultra deep field.jpg',
|
||||
'Hubble Interacting Galaxy NGC 5257.jpg']
|
||||
'Hubble Interacting Galaxy NGC 5257.jpg'],
|
||||
},
|
||||
u'accentué': {
|
||||
'title': u'Accentué',
|
||||
'thumbnail': u'hélicoïde.jpg',
|
||||
'img': [u'hélicoïde.jpg', 'superdupont_source_wikipedia_en.jpg']
|
||||
'medias': [u'hélicoïde.jpg', 'superdupont_source_wikipedia_en.jpg'],
|
||||
},
|
||||
'video': {
|
||||
'title': 'Video',
|
||||
'thumbnail': 'stallman-software-freedom-day-low.ogv',
|
||||
'medias': ['stallman-software-freedom-day-low.ogv']
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +52,8 @@ def paths():
|
||||
|
||||
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
|
||||
settings = read_settings(default_conf)
|
||||
return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'), settings['ext_list'])
|
||||
return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'),
|
||||
settings['img_ext_list'], settings['vid_ext_list'])
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
@@ -58,14 +64,15 @@ def db(paths):
|
||||
|
||||
def test_filelist(db):
|
||||
assert set(db.keys()) == set(['paths_list', 'skipped_dir', '.',
|
||||
'dir1', 'dir2', 'dir1/test1', 'dir1/test2', u'accentué'])
|
||||
'dir1', 'dir2', 'dir1/test1', 'dir1/test2', u'accentué', 'video'])
|
||||
|
||||
assert set(db['paths_list']) == set(['.', 'dir1', 'dir1/test1',
|
||||
'dir1/test2', 'dir2', u'accentué'])
|
||||
'dir1/test2', 'dir2', u'accentué', 'video'])
|
||||
|
||||
assert set(db['skipped_dir']) == set(['empty', 'dir1/empty'])
|
||||
assert db['.']['img'] == []
|
||||
assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2'])
|
||||
assert db['.']['medias'] == []
|
||||
assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2',
|
||||
'video'])
|
||||
|
||||
|
||||
def test_title(db):
|
||||
@@ -78,15 +85,16 @@ def test_thumbnail(db):
|
||||
assert db[p]['thumbnail'] == REF[p]['thumbnail']
|
||||
|
||||
|
||||
def test_imglist(db):
|
||||
def test_medialist(db):
|
||||
for p in REF.keys():
|
||||
assert set(db[p]['img']) == set(REF[p]['img'])
|
||||
assert set(db[p]['medias']) == set(REF[p]['medias'])
|
||||
|
||||
|
||||
def test_get_subdir(paths):
|
||||
assert set(paths.get_subdirs('dir1')) == set(['dir1/test1', 'dir1/test2'])
|
||||
assert set(paths.get_subdirs('.')) == set(['dir1', 'dir2', 'dir1/test1',
|
||||
'dir1/test2', u'accentué'])
|
||||
'dir1/test2', u'accentué',
|
||||
'video'])
|
||||
|
||||
|
||||
def test_get_metadata():
|
||||
|
||||
56
tests/test_video.py
Normal file
56
tests/test_video.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import os
|
||||
import filecmp
|
||||
import pytest
|
||||
|
||||
from sigal import init_logging
|
||||
from sigal.video import vid_size, generate_video
|
||||
|
||||
CURRENT_DIR = os.path.dirname(__file__)
|
||||
TEST_VIDEO = 'stallman-software-freedom-day-low.ogv'
|
||||
SRCFILE = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'video', TEST_VIDEO)
|
||||
|
||||
|
||||
|
||||
def test_generate_video_fit_height(tmpdir):
|
||||
"""largest fitting dimension is height"""
|
||||
|
||||
base, ext = os.path.splitext(TEST_VIDEO)
|
||||
dstfile = str(tmpdir.join(base + '.webm'))
|
||||
generate_video(SRCFILE, dstfile, (50, 100))
|
||||
|
||||
size_src = vid_size(SRCFILE)
|
||||
size_dst = vid_size(dstfile)
|
||||
|
||||
assert size_dst[0] == 50
|
||||
# less than 2% error on ratio
|
||||
assert abs(size_dst[0]/size_dst[1] - size_src[0]/size_src[1]) < 2e-2
|
||||
|
||||
def test_generate_video_fit_width(tmpdir):
|
||||
"""largest fitting dimension is width"""
|
||||
|
||||
base, ext = os.path.splitext(TEST_VIDEO)
|
||||
dstfile = str(tmpdir.join(base + '.webm'))
|
||||
generate_video(SRCFILE, dstfile, (100, 50))
|
||||
|
||||
size_src = vid_size(SRCFILE)
|
||||
size_dst = vid_size(dstfile)
|
||||
|
||||
assert size_dst[1] == 50
|
||||
# less than 2% error on ratio
|
||||
assert abs(size_dst[0]/size_dst[1] - size_src[0]/size_src[1]) < 2e-2
|
||||
|
||||
def test_generate_video_dont_enlarge(tmpdir):
|
||||
"""video dimensions should not be enlarged"""
|
||||
|
||||
base, ext = os.path.splitext(TEST_VIDEO)
|
||||
dstfile = str(tmpdir.join(base + '.webm'))
|
||||
generate_video(SRCFILE, dstfile, (1000, 1000))
|
||||
|
||||
size_src = vid_size(SRCFILE)
|
||||
size_dst = vid_size(dstfile)
|
||||
|
||||
assert size_src == size_dst
|
||||
Reference in New Issue
Block a user