Browse Source

Merge branch 'refactor'

pull/87/merge
Simon Conseil 12 years ago
parent
commit
e9d4391389
  1. 11
      docs/changelog.rst
  2. 69
      docs/themes.rst
  3. 4
      sigal/__init__.py
  4. 12
      sigal/compat.py
  5. 602
      sigal/gallery.py
  6. 9
      sigal/settings.py
  7. 56
      sigal/themes/colorbox/templates/index.html
  8. 46
      sigal/themes/galleria/templates/index.html
  9. 142
      sigal/writer.py
  10. 135
      tests/test_gallery.py

11
docs/changelog.rst

@ -7,6 +7,17 @@ Version 0.7.dev
Released on 2014-xx-xx.
- Refactor the way to store album and media informations. Albums, images and
videos are now represented by objects, and these objects are directly
available in the templates. The following template variables have been
renamed:
``albums`` => ``album.albums``, ``breadcrumb`` => ``album.breadcrumb``,
``description`` => ``album.description``, ``index_url`` =>
``album.index_url``, ``medias`` => ``album.medias``, ``title`` =>
``album.title``, ``media.file`` => ``media.filename``, ``media.thumb`` =>
``media.thumbnail``, ``zip_gallery`` => ``album.zip``
Version 0.6.0
~~~~~~~~~~~~~

69
docs/themes.rst

@ -26,59 +26,42 @@ Variables
You can use the following variables in your template:
``albums``
List of ``album`` objects. An ``album`` object has the following attributes:
- ``album.name``
- ``album.title``
- ``album.url``
- ``album.thumb``
``breadcrumb``
List of ``(url, title)`` tuples defining the current breadcrumb path.
``album``
The current album that is rendered in the HTML file, represented by an
:class:`~sigal.gallery.Album` object. ``album.medias`` contains the list
of all medias in the album (represented by the
:class:`~sigal.gallery.Image` and :class:`~sigal.gallery.Video` objects,
inherited from :class:`~sigal.gallery.Media`).
``index_title``
Name of the index. This is either the directory name or the title specified
in the ``index.md``.
``index_url``
URL to the index page.
``medias``
List of ``media`` objects. A ``media`` object has the following attributes:
- ``media.type``: Either ``"img"`` or ``"vid"``.
- ``media.file``: Location of the resized image.
- ``media.thumb``: Location of the corresponding thumbnail image.
- ``media.big``: If not None, location of the unmodified image.
- ``media.exif``: If not None contains a dict with the most common tags. For
more information, see :ref:`simple-exif-data`.
- ``media.raw_exif``: If not ``None``, it contains the raw EXIF tags.
``meta`` and ``description``
Meta data and album description. For details how to annotate your albums
with meta data, see :doc:`album_information`.
``theme.name``
Name of the currently used theme.
in the ``index.md`` of the ``source`` directory.
``settings``
The entire dictionary from ``sigal.conf.py``. For example, you could use
this to output an optional download link for zipped archives:
.. code-block:: jinja
{% if settings.zip_gallery %}
<a href="{{ settings.zip_gallery }}">Download archive</a>
{% endif %}
The entire dictionary from ``sigal.conf.py``.
``sigal_link``
URL to the Sigal homepage.
``zip_gallery``
If not None, it contains the location of a zip archive with all original
images of the corresponding directory.
``theme.name``, ``theme.url``
Name and url of the currently used theme.
.. autoclass:: sigal.gallery.Album
:members:
:undoc-members:
:inherited-members:
.. autoclass:: sigal.gallery.Media
:members:
:undoc-members:
.. autoclass:: sigal.gallery.Image
:members:
:undoc-members:
.. autoclass:: sigal.gallery.Video
:members:
:undoc-members:
.. _simple-exif-data:

4
sigal/__init__.py

@ -105,8 +105,8 @@ def build(source, destination, debug=False, verbose=False, force=False,
sys.exit(1)
locale.setlocale(locale.LC_ALL, settings['locale'])
gal = Gallery(settings, force=force, theme=theme, ncpu=ncpu)
gal.build()
gal = Gallery(settings, theme=theme, ncpu=ncpu)
gal.build(force=force)
# copy extra files
for src, dst in settings['files_to_copy']:

12
sigal/compat.py

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import locale
import sys
PY2 = sys.version_info[0] == 2
@ -9,12 +10,23 @@ if not PY2:
string_types = (str,)
unichr = chr
from functools import cmp_to_key
alpha_sort = {'key': cmp_to_key(locale.strcoll)}
from http import server
import socketserver
else:
text_type = unicode # NOQA
string_types = (str, unicode) # NOQA
unichr = unichr
alpha_sort = {'cmp': locale.strcoll}
import SimpleHTTPServer as server
import SocketServer as socketserver
class UnicodeMixin(object):
if not PY2:
__str__ = lambda x: x.__unicode__()
else:
__str__ = lambda x: unicode(x).encode('utf-8')

602
sigal/gallery.py

@ -24,7 +24,6 @@
from __future__ import absolute_import, print_function
import codecs
import locale
import logging
import markdown
import multiprocessing
@ -32,163 +31,364 @@ import os
import sys
import zipfile
from os.path import join, normpath
from collections import defaultdict
from os.path import isfile, join
from PIL import Image as PILImage
from pprint import pformat
from . import compat
from .image import process_image
from . import image, video
from .compat import UnicodeMixin, alpha_sort
from .image import process_image, get_exif_tags
from .log import colored, BLUE
from .settings import get_thumb, get_orig
from .utils import copy, check_or_create_dir
from .video import process_video
from .writer import Writer
DESCRIPTION_FILE = "index.md"
class Media(UnicodeMixin):
"""Base Class for media files.
class FileExtensionError(Exception):
"""Raised if we made an error when handling file extensions"""
pass
Attributes:
- ``type``: ``"image"`` or ``"video"``.
- ``filename``: Filename of the resized image.
- ``thumbnail``: Location of the corresponding thumbnail image.
- ``big``: If not None, location of the unmodified image.
- ``exif``: If not None contains a dict with the most common tags. For more
information, see :ref:`simple-exif-data`.
- ``raw_exif``: If not ``None``, it contains the raw EXIF tags.
class PathsDb(object):
"""Container for all the information on the directory structure.
"""
All the info is stored in a dictionnary, `self.db`. This class also has
methods to build this dictionnary.
type = ''
extensions = ()
"""
def __init__(self, filename, path, settings):
self.filename = filename
self.settings = settings
self.file_path = join(path, filename)
self.src_path = join(settings['source'], path, filename)
self.dst_path = join(settings['destination'], path, filename)
self.thumb_name = get_thumb(self.settings, self.filename)
self.thumb_path = join(settings['destination'], path, self.thumb_name)
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.ext_list = self.img_ext_list + self.vid_ext_list
self.logger = logging.getLogger(__name__)
self.raw_exif = None
self.exif = None
def __repr__(self):
return "<%s>(%r)" % (self.__class__.__name__, self.file_path)
# The dict containing all information
self.db = {
'paths_list': [],
'skipped_dir': []
}
# basepath must to be a unicode string so that os.walk will return
# unicode dirnames and filenames. If basepath is a str, we must
# convert it to unicode.
if compat.PY2 and isinstance(path, str):
enc = locale.getpreferredencoding()
self.basepath = path.decode(enc)
def __unicode__(self):
return self.file_path
@property
def big(self):
"""Path to the original image, if ``keep_orig`` is set (relative to the
album directory).
"""
if self.settings['keep_orig']:
return get_orig(self.settings, self.filename)
else:
self.basepath = path
return None
self.build()
@property
def thumbnail(self):
"""Path to the thumbnail image (relative to the album directory)."""
def get_subdirs(self, path):
"""Return the list of all sub-directories of path."""
if not os.path.isfile(self.thumb_path):
# if thumbnail is missing (if settings['make_thumbs'] is False)
if self.type == 'image':
generator = image.generate_thumbnail
elif self.type == 'video':
generator = video.generate_thumbnail
self.logger.debug('Generating thumbnail for %r', self)
generator(self.src_path, self.thumb_path,
self.settings['thumb_size'],
fit=self.settings['thumb_fit'])
return self.thumb_name
class Image(Media):
"""Gather all informations on an image file."""
type = 'image'
extensions = ('.jpg', '.jpeg', '.JPG', '.JPEG', '.png')
def __init__(self, filename, path, settings):
super(Image, self).__init__(filename, path, settings)
self.raw_exif, self.exif = get_exif_tags(self.src_path)
class Video(Media):
"""Gather all informations on a video file."""
type = 'video'
extensions = ('.MOV', '.mov', '.avi', '.mp4', '.webm', '.ogv')
def __init__(self, filename, path, settings):
super(Video, self).__init__(filename, path, settings)
base = os.path.splitext(filename)[0]
self.file_path = join(path, base + '.webm')
self.dst_path = join(settings['destination'], path, base + '.webm')
class Album(UnicodeMixin):
"""Gather all informations on an album.
Attributes:
for name in self.db[path].get('subdir', []):
subdir = normpath(join(path, name))
yield subdir
for subname in self.get_subdirs(subdir):
yield subname
:var description_file: Name of the Markdown file which gives information
on an album
:ivar index_url: URL to the index page.
:ivar output_file: Name of the output HTML file
:ivar meta: Meta data from the Markdown file.
:ivar description: description from the Markdown file.
def build(self):
"Build the list of directories with images"
For details how to annotate your albums with meta data, see
:doc:`album_information`.
if compat.PY2:
sort_args = {'cmp': locale.strcoll}
"""
description_file = "index.md"
output_file = 'index.html'
def __init__(self, path, settings, dirnames, filenames, gallery):
self.path = path
self.name = path.split(os.path.sep)[-1]
self.gallery = gallery
self.settings = settings
self.orig_path = None
self._thumbnail = None
if path == '.':
self.src_path = settings['source']
self.dst_path = settings['destination']
else:
from functools import cmp_to_key
sort_args = {'key': cmp_to_key(locale.strcoll)}
# get information for each directory
for path, dirnames, filenames in os.walk(self.basepath,
followlinks=True):
relpath = os.path.relpath(path, self.basepath)
# sort images and sub-albums by name
filenames.sort(**sort_args)
dirnames.sort(**sort_args)
self.db['paths_list'].append(relpath)
self.db[relpath] = {
'medias': [f for f in filenames
if os.path.splitext(f)[1] in self.ext_list],
'subdir': dirnames
}
self.db[relpath].update(get_metadata(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_media:
self.check_thumbnail(path)
# dir without images, start with the deepest ones
for path in reversed(sorted(path_nomedia, key=lambda x: x.count('/'))):
# stop if it is already set and a valid file
alb_thumb = self.db[path].setdefault('thumbnail', '')
if alb_thumb and os.path.isfile(join(self.basepath, path,
alb_thumb)):
self.logger.debug("Thumb for %s : %s", path, alb_thumb)
self.src_path = join(settings['source'], path)
self.dst_path = join(settings['destination'], path)
self.logger = logging.getLogger(__name__)
self._get_metadata()
# Create thumbnails directory and optionally the one for original img
check_or_create_dir(self.dst_path)
check_or_create_dir(join(self.dst_path, settings['thumb_dir']))
if settings['keep_orig']:
self.orig_path = join(self.dst_path, settings['orig_dir'])
check_or_create_dir(self.orig_path)
# optionally add index.html to the URLs
self.url_ext = self.output_file if settings['index_in_url'] else ''
self.url = self.name + '/' + self.url_ext
self.index_url = os.path.relpath(settings['destination'],
self.dst_path) + '/' + self.url_ext
# sort images and sub-albums by name
filenames.sort(**alpha_sort)
dirnames.sort(**alpha_sort)
self.subdirs = dirnames
#: List of all medias in the album (:class:`~sigal.gallery.Image` and
#: :class:`~sigal.gallery.Video`).
self.medias = []
self.medias_count = defaultdict(int)
for f in filenames:
ext = os.path.splitext(f)[1]
if ext in Image.extensions:
media = Image(f, self.path, settings)
elif ext in Video.extensions:
media = Video(f, self.path, settings)
else:
continue
for subdir in self.get_subdirs(path):
# use the thumbnail of their sub-directories
if self.db[subdir].get('thumbnail', ''):
self.db[path]['thumbnail'] = join(
os.path.relpath(subdir, path),
self.db[subdir]['thumbnail'])
self.logger.debug("Found thumb for %s : %s", path,
self.db[path]['thumbnail'])
break
if not self.db[path].get('thumbnail', ''):
# else remove all info about this directory
self.logger.info("Directory '%s' is empty", path)
self.db['skipped_dir'].append(path)
self.db['paths_list'].remove(path)
del self.db[path]
parent = normpath(join(path, '..'))
child = os.path.relpath(path, parent)
self.db[parent]['subdir'].remove(child)
self.logger.debug('Database:\n%s', pformat(self.db, width=120))
def check_thumbnail(self, path):
"Find the thumbnail image for a given path."
# stop if it is already set and a valid file
alb_thumb = self.db[path].setdefault('thumbnail', '')
if alb_thumb and os.path.isfile(join(self.basepath, path, alb_thumb)):
return
# find and return the first landscape image
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 media file
if self.db[path]['medias']:
self.db[path]['thumbnail'] = self.db[path]['medias'][0]
self.medias_count[media.type] += 1
self.medias.append(media)
def __repr__(self):
return "<%s>(%r)" % (self.__class__.__name__, self.path)
def __unicode__(self):
return (u"{} : ".format(self.path) +
', '.join("{} {}s".format(count, _type)
for _type, count in self.medias_count.items()))
def __len__(self):
return sum(self.medias_count.values())
def __iter__(self):
return iter(self.medias)
def _get_metadata(self):
"""Get album metadata from `description_file` (`index.md`):
-> title, thumbnail image, description
"""
descfile = join(self.src_path, self.description_file)
if isfile(descfile):
with codecs.open(descfile, "r", "utf-8") as f:
text = f.read()
md = markdown.Markdown(extensions=['meta'])
html = md.convert(text)
self.title = md.Meta.get('title', [''])[0]
self.description = html
self.meta = md.Meta.copy()
else:
# default: get title from directory name
self.title = os.path.basename(self.path).replace('_', ' ')\
.replace('-', ' ').capitalize()
self.description = ''
self.meta = {}
@property
def images(self):
"""List of images (:class:`~sigal.gallery.Image`)."""
for media in self.medias:
if media.type == 'image':
yield media
@property
def videos(self):
"""List of videos (:class:`~sigal.gallery.Video`)."""
for media in self.medias:
if media.type == 'video':
yield media
@property
def albums(self):
"""List of :class:`~sigal.gallery.Album` objects for each
sub-directory.
"""
root_path = self.path if self.path != '.' else ''
return [self.gallery.albums[join(root_path, path)]
for path in self.subdirs]
@property
def thumbnail(self):
"""Path to the thumbnail of the album."""
if self._thumbnail:
# stop if it is already set
return self._thumbnail
# Test the thumbnail from the Markdown file.
thumbnail = self.meta.get('thumbnail', [''])[0]
if thumbnail and isfile(join(self.src_path, thumbnail)):
self._thumbnail = join(self.name, get_thumb(self.settings,
thumbnail))
self.logger.debug("Thumbnail for %r : %s", self, self._thumbnail)
return self._thumbnail
else:
# find and return the first landscape image
for f in self.medias:
ext = os.path.splitext(f.filename)[1]
if ext in Image.extensions:
im = PILImage.open(f.src_path)
if im.size[0] > im.size[1]:
self._thumbnail = join(self.name, f.thumbnail)
self.logger.debug(
"Use 1st landscape image as thumbnail for %r : %s",
self, self._thumbnail)
return self._thumbnail
# else simply return the 1st media file
if not self._thumbnail and self.medias:
self._thumbnail = join(self.name, self.medias[0].thumbnail)
self.logger.debug("Use the 1st image as thumbnail for %r : %s",
self, self._thumbnail)
return self._thumbnail
# use the thumbnail of their sub-directories
if not self._thumbnail:
for path, album in self.gallery.get_albums(self.path):
if album.thumbnail:
self._thumbnail = join(self.name, album.thumbnail)
self.logger.debug(
"Using thumbnail from sub-directory for %r : %s",
self, self._thumbnail)
return self._thumbnail
self.logger.error('Thumbnail not found for %r', self)
return None
@property
def breadcrumb(self):
"""List of ``(url, title)`` tuples defining the current breadcrumb
path.
"""
if self.path == '.':
return []
path = self.path
breadcrumb = [((self.url_ext or '.'), self.title)]
while True:
path = os.path.normpath(os.path.join(path, '..'))
if path == '.':
break
url = os.path.relpath(path, self.path) + '/' + self.url_ext
breadcrumb.append((url, self.gallery.albums[path].title))
breadcrumb.reverse()
return breadcrumb
@property
def zip(self):
"""Make a ZIP archive with all media files and return its path.
If the ``zip_gallery`` setting is set,it contains the location of a zip
archive with all original images of the corresponding directory.
"""
zip_gallery = self.settings['zip_gallery']
if zip_gallery:
archive_path = join(self.dst_path, zip_gallery)
archive = zipfile.ZipFile(archive_path, 'w')
for p in self:
archive.write(p.dst_path, os.path.split(p.dst_path)[1])
archive.close()
self.logger.debug('Created ZIP archive %s', archive_path)
return zip_gallery
else:
return None
class Gallery(object):
def __init__(self, settings, force=False, theme=None, ncpu=None):
def __init__(self, settings, theme=None, ncpu=None):
self.settings = settings
self.force = force
self.theme = theme
self.logger = logging.getLogger(__name__)
self.stats = {'image': 0, 'image_skipped': 0,
'video': 0, 'video_skipped': 0}
self.init_pool(ncpu)
check_or_create_dir(settings['destination'])
# Build the list of directories with images
albums = self.albums = {}
src_path = self.settings['source']
for path, dirs, files in os.walk(src_path, followlinks=True):
relpath = os.path.relpath(path, src_path)
albums[relpath] = Album(relpath, self.settings, dirs, files, self)
self.logger.debug('Albums:\n%r', albums.values())
def init_pool(self, ncpu):
try:
cpu_count = multiprocessing.cpu_count()
except NotImplementedError:
@ -203,33 +403,41 @@ class Gallery(object):
self.logger.error('ncpu should be an integer value')
ncpu = cpu_count
self.logger.info("Using %s cores", ncpu)
if ncpu > 1:
self.pool = multiprocessing.Pool(processes=ncpu)
else:
self.pool = None
self.logger.info("Using %s cores", ncpu)
def get_albums(self, path):
"""Return the list of all sub-directories of path."""
paths = PathsDb(self.settings['source'], self.settings['img_ext_list'],
self.settings['vid_ext_list'])
self.db = paths.db
for name in self.albums[path].subdirs:
subdir = os.path.normpath(join(path, name))
yield subdir, self.albums[subdir]
for subname, album in self.get_albums(subdir):
yield subname, self.albums[subdir]
def build(self):
def build(self, force=False):
"Create the image gallery"
check_or_create_dir(self.settings['destination'])
# loop on directories in reversed order, to process subdirectories
# before their parent
if self.pool:
media_list = []
processor = media_list.append
else:
processor = process_file
for path in reversed(self.db['paths_list']):
if len(self.db[path]['medias']) != 0:
for files in self.process_dir(path):
media_list.append(files)
try:
for album in self.albums.values():
if len(album) > 0:
for files in self.process_dir(album, force=force):
processor(files)
except KeyboardInterrupt:
sys.exit('Interrupted')
if self.pool:
try:
# map_async is needed to handle KeyboardInterrupt correctly
self.pool.map_async(worker, media_list).get(9999)
@ -240,86 +448,48 @@ class Gallery(object):
sys.exit('Interrupted')
print('')
else:
try:
for path in reversed(self.db['paths_list']):
if len(self.db[path]['medias']) != 0:
for files in self.process_dir(path):
process_file(files)
print('')
except KeyboardInterrupt:
sys.exit('Interrupted')
if self.settings['write_html']:
self.writer = Writer(self.settings, self.settings['destination'],
theme=self.theme)
self.writer = Writer(self.settings, theme=self.theme,
index_title=self.albums['.'].title)
for path in reversed(self.db['paths_list']):
self.writer.write(self.db, path)
for album in self.albums.values():
self.writer.write(album)
def process_dir(self, path):
def process_dir(self, album, force=False):
"""Process a list of images in a directory."""
media_files = [normpath(join(self.settings['source'], path, f))
for f in self.db[path]['medias']]
# output dir for the current path
outpath = normpath(join(self.settings['destination'], path))
check_or_create_dir(outpath)
# Create thumbnails directory and optionally the one for original img
check_or_create_dir(join(outpath, self.settings['thumb_dir']))
if self.settings['keep_orig']:
check_or_create_dir(join(outpath, self.settings['orig_dir']))
if sys.stdout.isatty():
print(colored('->', BLUE),
u"{} : {} files".format(path, len(media_files)))
print(colored('->', BLUE), str(album))
else:
self.logger.warn("%s : %d files", path, len(media_files))
# loop on images
if self.settings['zip_gallery']:
zip_files(join(outpath, self.settings['zip_gallery']), media_files)
for f in media_files:
filename = os.path.split(f)[1]
base, ext = os.path.splitext(filename)
if ext in self.settings['img_ext_list']:
outname = join(outpath, filename)
filetype = 'image'
elif ext in self.settings['vid_ext_list']:
outname = join(outpath, base + '.webm')
filetype = 'video'
else:
raise FileExtensionError
self.logger.warn(album)
if os.path.isfile(outname) and not self.force:
self.logger.info("%s exists - skipping", filename)
self.stats[filetype + '_skipped'] += 1
for f in album:
if isfile(f.dst_path) and not force:
self.logger.info("%s exists - skipping", f.filename)
self.stats[f.type + '_skipped'] += 1
else:
if self.settings['keep_orig']:
copy(f, join(outpath, self.settings['orig_dir'], filename),
copy(f.src_path, join(album.orig_path, f.filename),
symlink=self.settings['orig_link'])
self.stats[filetype] += 1
yield filetype, f, outpath, self.settings
self.stats[f.type] += 1
yield f.type, f.src_path, album.dst_path, self.settings
def process_file(args):
ftype, src_path, dst_path, settings = args
logger = logging.getLogger(__name__)
logger.info('Processing %s', args[1])
logger.info('Processing %s', src_path)
if logger.getEffectiveLevel() > 20:
print('.', end='')
sys.stdout.flush()
if args[0] == 'image':
return process_image(*args[1:])
elif args[0] == 'video':
return process_video(*args[1:])
if ftype == 'image':
return process_image(src_path, dst_path, settings)
elif ftype == 'video':
return process_video(src_path, dst_path, settings)
def worker(args):
@ -327,49 +497,3 @@ def worker(args):
process_file(args)
except KeyboardInterrupt:
return 'KeyboardException'
def get_metadata(path):
""" Get album metadata from DESCRIPTION_FILE:
- title
- thumbnail image
- description
"""
descfile = join(path, DESCRIPTION_FILE)
if not os.path.isfile(descfile):
# default: get title from directory name
meta = {
'title': os.path.basename(path).replace('_', ' ')
.replace('-', ' ').capitalize(),
'description': '',
'thumbnail': '',
'meta': {}
}
else:
with codecs.open(descfile, "r", "utf-8") as f:
text = f.read()
md = markdown.Markdown(extensions=['meta'])
html = md.convert(text)
meta = {
'title': md.Meta.get('title', [''])[0],
'description': html,
'thumbnail': md.Meta.get('thumbnail', [''])[0],
'meta': md.Meta.copy()
}
return meta
def zip_files(archive_path, filepaths):
archive = zipfile.ZipFile(archive_path, 'w')
for p in filepaths:
filename = os.path.split(p)[1]
archive.write(p, filename)
archive.close()

9
sigal/settings.py

@ -21,10 +21,13 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import locale
import logging
import os
from pprint import pformat
from .compat import PY2
_DEFAULT_CONFIG = {
'adjust_options': {'color': 1.0, 'brightness': 1.0,
'contrast': 1.0, 'sharpness': 1.0},
@ -122,6 +125,12 @@ def read_settings(filename=None):
settings_path, path)))
logger.debug("Rewrite %s : %s -> %s", p, path, settings[p])
# paths must to be unicode strings so that os.walk will return
# unicode dirnames and filenames
if PY2 and isinstance(settings[p], str):
enc = locale.getpreferredencoding()
settings[p] = settings[p].decode(enc)
for key in ('img_size', 'thumb_size', 'video_size'):
w, h = settings[key]
if h > w:

56
sigal/themes/colorbox/templates/index.html

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ title }}</title>
<title>{{ album.title }}</title>
<meta name="description" content="">
<meta name="author" content="{{ author }}">
<meta name="viewport" content="width=device-width">
@ -20,7 +20,7 @@
<div class="four columns">
<div class="sidebar">
<h1><a href="{{ index_url }}">{{ index_title }}</a></h1>
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
{% if settings.links %}
<nav id="menu">
@ -41,9 +41,9 @@
<div id="main" role="main" class="twelve columns offset-by-four">
<header>
{% if breadcrumb %}
{% if album.breadcrumb %}
<h2>
{%- for url, title in breadcrumb -%}
{%- for url, title in album.breadcrumb -%}
<a href="{{ url }}">{{ title }}</a>
{%- if not loop.last %} » {% endif -%}
{% endfor -%}
@ -52,8 +52,8 @@
{% endif %}
</header>
{% if albums %}
{% for album in albums %}
{% if album.albums %}
{% for alb in album.albums %}
{% if loop.index % 3 == 1 %}
<div id="albums" class="row">
{% endif%}
@ -61,10 +61,10 @@
<div class="four columns thumbnail
{% if loop.index % 3 == 1 %}alpha{% endif%}
{% if loop.index % 3 == 0 %}omega{% endif%}">
<a href="{{ album.url }}">
<img src="{{ album.thumb }}" class="album_thumb"
alt="{{ album.name }}" title="{{ album.name }}" /></a>
<span class="album_title">{{ album.title }}</span>
<a href="{{ alb.url }}">
<img src="{{ alb.thumbnail }}" class="album_thumb"
alt="{{ alb.name }}" title="{{ alb.name }}" /></a>
<span class="album_title">{{ alb.title }}</span>
</div>
{% if loop.last or loop.index % 3 == 0 %}
@ -73,7 +73,7 @@
{% endfor %}
{% endif %}
{% if medias %}
{% if album.medias %}
{% macro img_description(media) -%}
{% if media.big %} data-big="{{ media.big }}"{% endif %}
{% if media.exif %}
@ -83,31 +83,31 @@
{% endif %}
{%- endmacro %}
<div id="gallery" class="row">
{% for media in medias %}
{% if media.type == "img" %}
{% for media in album.medias %}
{% if media.type == "image" %}
<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 }}" {{ img_description(media) }}>
<img src="{{ media.thumb }}" alt="{{ media.file }}"
title="{{ media.file }}" /></a>
<a href="{{ media.filename }}" class="gallery" title="{{ media.filename }}" {{ img_description(media) }}>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.filename }}" /></a>
</div>
{% endif %}
{% if media.type == "vid" %}
{% if media.type == "video" %}
<div class="four columns thumbnail
{% if loop.index % 3 == 1 %}alpha{% endif%}
{% if loop.index % 3 == 0 %}omega{% endif%}">
<a href="#{{ media.file|replace('.', '')|replace(' ', '') }}"
class="gallery" inline='yes' title="{{ media.file }}"
<a href="#{{ media.filename|replace('.', '')|replace(' ', '') }}"
class="gallery" inline='yes' title="{{ media.filename }}"
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
<img src="{{ media.thumb }}" alt="{{ media.file }}"
title="{{ media.file }}" /></a>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.filename }}" /></a>
</div>
<!-- This contains the hidden content for the video -->
<div style='display:none'>
<div id="{{ media.file|replace('.', '')|replace(' ', '') }}">
<div id="{{ media.filename|replace('.', '')|replace(' ', '') }}">
<video controls>
<source src='{{ media.file }}' type='video/webm' />
<source src='{{ media.filename }}' type='video/webm' />
</video>
</div>
</div>
@ -116,24 +116,24 @@
</div>
{% endif %}
{% if zip_gallery %}
{% if album.zip %}
<div id="additionnal-infos" class="row">
<p>
<a href="{{ zip_gallery }}"
<a href="{{ album.zip }}"
title="Download a zip archive with all images">Download ZIP</a>
</p>
</div>
{% endif %}
{% if description %}
{% if album.description %}
<div id="description" class="row">
{{ description }}
{{ album.description }}
</div>
{% endif %}
</div>
</div>
{% if medias %}
{% if album.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>

46
sigal/themes/galleria/templates/index.html

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ title|striptags }}</title>
<title>{{ album.title|striptags }}</title>
<meta name="description" content="">
<meta name="author" content="{{ author }}">
<meta name="viewport" content="width=device-width">
@ -19,7 +19,7 @@
<body>
<div class="container">
<header>
<h1><a href="{{ index_url }}">{{ index_title }}</a></h1>
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
{% if settings.links %}
<nav id="menu">
@ -31,9 +31,9 @@
</nav>
{% endif %}
{% if breadcrumb %}
{% if album.breadcrumb %}
<h2>
{%- for url, title in breadcrumb -%}
{%- for url, title in album.breadcrumb -%}
<a href="{{ url }}">{{ title }}</a>
{%- if not loop.last %} » {% endif -%}
{% endfor -%}
@ -43,21 +43,21 @@
</header>
<div id="main" role="main">
{% if albums %}
{% if album.albums %}
<div id="albums">
<!-- <h1>Albums</h1> -->
<ul>
{% for album in albums %}
<li><a href="{{ album.url }}">
<img src="{{ album.thumb }}" class="album_thumb" alt="{{ album.name }}" title="{{ album.name }}" /></a>
<span class="album_title">{{ album.title }}</span>
{% for alb in album.albums %}
<li><a href="{{ alb.url }}">
<img src="{{ alb.thumbnail }}" class="album_thumb" alt="{{ alb.name }}" title="{{ alb.name }}" /></a>
<span class="album_title">{{ alb.title }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if medias %}
{% if album.medias %}
{% macro img_description(media) -%}
{%- if media.big %}<a href='{{ media.big }}'>Full size</a>{% endif %}
{%- if media.exif %}
@ -72,19 +72,19 @@
{% endif %}
{%- endmacro %}
<div id="gallery">
{% for media in medias %}
{% if media.type == "img" %}
<a href="{{ media.file }}">
<img src="{{ media.thumb }}" alt="{{ media.file }}"
data-title="{{ media.file }}"
{% for media in album.medias %}
{% if media.type == "image" %}
<a href="{{ media.filename }}">
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
data-title="{{ media.filename }}"
data-description="{{ img_description(media) }}"/>
</a>
{% endif %}
{% if media.type == "vid" %}
{% if media.type == "video" %}
<a href="{{ theme.url }}/img/empty.png">
<img src="{{ media.thumb }}" alt="{{ media.file }}"
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
data-layer="<video controls>
<source src='{{ media.file }}' type='video/webm' />
<source src='{{ media.filename }}' type='video/webm' />
</video>" />
</a>
{% endif %}
@ -92,18 +92,18 @@
</div>
{% endif %}
{% if zip_gallery %}
{% if album.zip %}
<div id="additionnal-infos" class="row">
<p>
<a href="{{ zip_gallery }}"
<a href="{{ album.zip }}"
title="Download a zip archive with all images">Download ZIP</a>
</p>
</div>
{% endif %}
{% if description %}
{% if album.description %}
<div id="description">
{{ description }}
{{ album.description }}
</div>
{% endif %}
</div>
@ -114,7 +114,7 @@
</footer>
</div>
{% if medias %}
{% if album.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>

142
sigal/writer.py

@ -24,7 +24,6 @@
from __future__ import absolute_import
import codecs
import copy
import jinja2
import logging
import os
@ -34,9 +33,6 @@ from distutils.dir_util import copy_tree
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader
from jinja2.exceptions import TemplateNotFound
import sigal.image
import sigal.video
from .settings import get_thumb, get_orig
from .pkgmeta import __url__ as sigal_link
THEMES_PATH = os.path.normpath(os.path.join(
@ -46,18 +42,15 @@ THEMES_PATH = os.path.normpath(os.path.join(
class Writer(object):
"""Generate html pages for each directory of images."""
def __init__(self, settings, output_dir, theme=None,
template_file="index.html", output_file="index.html"):
template_file = 'index.html'
def __init__(self, settings, theme=None, index_title=''):
self.settings = settings
self.output_dir = os.path.abspath(output_dir)
self.output_dir = settings['destination']
self.theme = theme or settings['theme']
self.template_file = template_file
self.output_file = output_file
self.index_title = index_title
self.logger = logging.getLogger(__name__)
# optionally add index.html to the URLs
self.url_ext = self.output_file if settings['index_in_url'] else ''
# search the theme in sigal/theme if the given one does not exists
if not os.path.exists(self.theme):
self.theme = os.path.join(THEMES_PATH, self.theme)
@ -66,8 +59,8 @@ class Writer(object):
self.logger.info("Theme : %s", self.theme)
theme_relpath = os.path.join(self.theme, 'templates')
default_loader = FileSystemLoader(
os.path.join(THEMES_PATH, 'default', 'templates'))
default_loader = FileSystemLoader(os.path.join(THEMES_PATH, 'default',
'templates'))
# setup jinja env
env_options = {'trim_blocks': True}
@ -92,121 +85,28 @@ class Writer(object):
self.logger.error('The index.html template was not found.')
sys.exit(1)
self.copy_assets()
self.ctx = {
'sigal_link': sigal_link,
'theme': {'name': os.path.basename(self.theme)},
'medias': [],
'albums': [],
'breadcrumb': ''
}
def copy_assets(self):
"""Copy the theme files in the output dir."""
# Copy the theme files in the output dir
self.theme_path = os.path.join(self.output_dir, 'static')
copy_tree(os.path.join(self.theme, 'static'), self.theme_path)
def get_breadcrumb(self, paths, relpath):
"""Paths to upper directories (with titles and links)."""
tmp_path = relpath
breadcrumb = [((self.url_ext or '.'), paths[tmp_path]['title'])]
while True:
tmp_path = os.path.normpath(os.path.join(tmp_path, '..'))
if tmp_path == '.':
break
url = os.path.relpath(tmp_path, relpath) + '/' + self.url_ext
breadcrumb.append((url, paths[tmp_path]['title']))
return reversed(breadcrumb)
def generate_context(self, paths, relpath):
def generate_context(self, album):
"""Generate the context dict for the given path."""
path = os.path.normpath(os.path.join(self.output_dir, relpath))
index_url = os.path.relpath(self.output_dir, path) + '/' + self.url_ext
self.logger.info("Output path : %s", path)
ctx = copy.deepcopy(self.ctx)
ctx.update({
self.logger.info("Output album : %s", album.dst_path)
return {
'album': album,
'index_title': self.index_title,
'settings': self.settings,
'index_url': index_url,
'index_title': paths['.']['title']
})
ctx['theme']['url'] = os.path.relpath(self.theme_path, path)
if relpath != '.':
ctx['breadcrumb'] = self.get_breadcrumb(paths, relpath)
if len(paths[relpath]['medias']) > 0 and self.settings['zip_gallery']:
ctx['zip_gallery'] = self.settings['zip_gallery']
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
file_path = os.path.join(path, i)
raw, simple = sigal.image.get_exif_tags(file_path)
if raw is not None:
media_ctx['raw_exif'] = raw
if simple is not None:
media_ctx['exif'] = simple
else:
media_ctx['type'] = 'vid'
media_ctx['file'] = base + '.webm'
media_ctx['thumb'] = get_thumb(self.settings, i)
if self.settings['keep_orig']:
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))
alb_thumb = paths[dpath]['thumbnail']
thumb_name = get_thumb(self.settings, alb_thumb)
thumb_path = os.path.join(self.output_dir, dpath, thumb_name)
self.logger.debug("Thumbnail path : %s", thumb_path)
# generate the thumbnail if it is missing (if
# settings['make_thumbs'] is False)
if not os.path.isfile(thumb_path):
source = os.path.join(self.output_dir, dpath, alb_thumb)
ext = os.path.splitext(source)[1]
self.logger.debug("Generating thumbnail for %s", source)
if ext in self.settings['img_ext_list']:
generator = sigal.image.generate_thumbnail
elif ext in self.settings['vid_ext_list']:
generator = sigal.video.generate_thumbnail
else:
generator = None
self.logger.error('Unsupported file type for %s', source)
if generator:
generator(source, thumb_path, self.settings['thumb_size'],
fit=self.settings['thumb_fit'])
ctx['albums'].append({
'url': d + '/' + self.url_ext,
'title': paths[dpath]['title'],
'thumb': os.path.join(d, thumb_name)
})
return ctx
def write(self, paths, relpath):
'sigal_link': sigal_link,
'theme': {'name': os.path.basename(self.theme),
'url': os.path.relpath(self.theme_path, album.dst_path)},
}
def write(self, album):
"""Generate the HTML page and save it."""
ctx = self.generate_context(paths, relpath)
page = self.template.render(paths[relpath], **ctx)
page = self.template.render(**self.generate_context(album))
output_file = os.path.join(album.dst_path, album.output_file)
output_file = os.path.join(self.output_dir, relpath, self.output_file)
with codecs.open(output_file, 'w', 'utf-8') as f:
f.write(page)

135
tests/test_gallery.py

@ -3,7 +3,8 @@
import os
import pytest
from sigal.gallery import Gallery, PathsDb, get_metadata
from os.path import join
from sigal.gallery import Album, Media, Image, Video, Gallery
from sigal.settings import read_settings
CURRENT_DIR = os.path.dirname(__file__)
@ -12,23 +13,31 @@ SAMPLE_DIR = os.path.join(CURRENT_DIR, 'sample')
REF = {
'dir1': {
'title': 'An example gallery',
'thumbnail': 'test1/11.jpg',
'name': 'dir1',
'thumbnail': 'dir1/test1/thumbnails/11.tn.jpg',
'subdirs': ['test1', 'test2'],
'medias': [],
},
'dir1/test1': {
'title': 'An example sub-category',
'thumbnail': '11.jpg',
'name': 'test1',
'thumbnail': 'test1/thumbnails/11.tn.jpg',
'subdirs': [],
'medias': ['11.jpg', 'archlinux-kiss-1024x640.png',
'flickr_jerquiaga_2394751088_cc-by-nc.jpg'],
},
'dir1/test2': {
'title': 'Test2',
'thumbnail': '21.jpg',
'name': 'test2',
'thumbnail': 'test2/thumbnails/21.tn.jpg',
'subdirs': [],
'medias': ['21.jpg', '22.jpg'],
},
'dir2': {
'title': 'Another example gallery with a very long name',
'thumbnail': 'm57_the_ring_nebula-587px.jpg',
'name': 'dir2',
'thumbnail': 'dir2/thumbnails/m57_the_ring_nebula-587px.tn.jpg',
'subdirs': [],
'medias': ['exo20101028-b-full.jpg',
'm57_the_ring_nebula-587px.jpg',
'Hubble ultra deep field.jpg',
@ -36,86 +45,107 @@ REF = {
},
u'accentué': {
'title': u'Accentué',
'thumbnail': u'hélicoïde.jpg',
'name': u'accentué',
'thumbnail': u'accentué/thumbnails/hélicoïde.tn.jpg',
'subdirs': [],
'medias': [u'hélicoïde.jpg', 'superdupont_source_wikipedia_en.jpg'],
},
'video': {
'title': 'Video',
'thumbnail': 'stallman software-freedom-day-low.ogv',
'name': 'video',
'thumbnail': 'video/thumbnails/stallman software-freedom-day-low.tn.jpg',
'subdirs': [],
'medias': ['stallman software-freedom-day-low.ogv']
}
}
@pytest.fixture(scope='module')
def paths():
"""Read the sample config file and build the PathsDb object."""
def settings():
"""Read the sample config file."""
return read_settings(os.path.join(SAMPLE_DIR, 'sigal.conf.py'))
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
settings = read_settings(default_conf)
return PathsDb(os.path.join(SAMPLE_DIR, 'pictures'),
settings['img_ext_list'], settings['vid_ext_list'])
def test_media(settings):
m = Media('11.jpg', 'dir1/test1', settings)
path = join('dir1', 'test1')
file_path = join(path, '11.jpg')
thumb = join('thumbnails', '11.tn.jpg')
@pytest.fixture(scope='module')
def db(paths):
paths.build()
return paths.db
assert m.filename == '11.jpg'
assert m.file_path == file_path
assert m.src_path == join(settings['source'], file_path)
assert m.dst_path == join(settings['destination'], file_path)
assert m.thumb_name == thumb
assert m.thumb_path == join(settings['destination'], path, thumb)
assert repr(m) == "<Media>('{}')".format(file_path)
assert str(m) == file_path
def test_filelist(db):
assert set(db.keys()) == set([
'paths_list', 'skipped_dir', '.', 'dir1', 'dir2', 'dir1/test1',
'dir1/test2', u'accentué', 'video'])
assert set(db['paths_list']) == set([
'.', 'dir1', 'dir1/test1', 'dir1/test2', 'dir2', u'accentué', 'video'])
def test_media_orig(settings):
settings['keep_orig'] = False
m = Media('11.jpg', 'dir1/test1', settings)
assert m.big is None
assert set(db['skipped_dir']) == set(['empty', 'dir1/empty'])
assert db['.']['medias'] == []
assert set(db['.']['subdir']) == set([u'accentué', 'dir1', 'dir2',
'video'])
settings['keep_orig'] = True
m = Media('11.jpg', 'dir1/test1', settings)
assert m.big == 'original/11.jpg'
def test_title(db):
for p in REF.keys():
assert db[p]['title'] == REF[p]['title']
def test_image(settings, tmpdir):
settings['destination'] = str(tmpdir)
m = Image('11.jpg', 'dir1/test1', settings)
assert m.exif['datetime'] == u'Sunday, 22. January 2006'
os.makedirs(join(settings['destination'], 'dir1', 'test1', 'thumbnails'))
assert m.thumbnail == join('thumbnails', '11.tn.jpg')
assert os.path.isfile(m.thumb_path)
def test_thumbnail(db):
for p in REF.keys():
assert db[p]['thumbnail'] == REF[p]['thumbnail']
def test_video(settings, tmpdir):
settings['destination'] = str(tmpdir)
m = Video('stallman software-freedom-day-low.ogv', 'video', settings)
file_path = join('video', 'stallman software-freedom-day-low.webm')
assert m.file_path == file_path
assert m.dst_path == join(settings['destination'], file_path)
def test_medialist(db):
for p in REF.keys():
assert set(db[p]['medias']) == set(REF[p]['medias'])
os.makedirs(join(settings['destination'], 'video', 'thumbnails'))
assert m.thumbnail == join('thumbnails',
'stallman software-freedom-day-low.tn.jpg')
assert os.path.isfile(m.thumb_path)
def test_get_subdir(paths):
assert set(paths.get_subdirs('dir1/test1')) == set()
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é', 'video'])
@pytest.mark.parametrize("path,album", REF.items())
def test_album(path, album, settings, tmpdir):
gal = Gallery(settings, ncpu=1)
a = Album(path, settings, album['subdirs'], album['medias'], gal)
assert a.title == album['title']
assert a.name == album['name']
assert a.subdirs == album['subdirs']
assert a.thumbnail == album['thumbnail']
assert [m.filename for m in a.medias] == album['medias']
assert len(a) == len(album['medias'])
def test_get_metadata():
"Test the get_metadata function."
m = get_metadata(os.path.join(SAMPLE_DIR, 'pictures', 'dir1'))
assert m['title'] == REF['dir1']['title']
assert m['thumbnail'] == ''
def test_album_medias(settings, tmpdir):
gal = Gallery(settings, ncpu=1)
album = REF['dir1/test1']
a = Album('dir1/test1', settings, album['subdirs'], album['medias'], gal)
assert list(im.filename for im in a.images) == album['medias']
assert list(a.videos) == []
m = get_metadata(os.path.join(SAMPLE_DIR, 'pictures', 'dir2'))
assert m['title'] == REF['dir2']['title']
assert m['thumbnail'] == REF['dir2']['thumbnail']
album = REF['video']
a = Album('video', settings, album['subdirs'], album['medias'], gal)
assert list(im.filename for im in a.videos) == album['medias']
assert list(a.images) == []
def test_gallery(tmpdir):
def test_gallery(settings, tmpdir):
"Test the Gallery class."
default_conf = os.path.join(SAMPLE_DIR, 'sigal.conf.py')
settings = read_settings(default_conf)
settings['destination'] = str(tmpdir)
gal = Gallery(settings, ncpu=1)
gal.build()
@ -127,4 +157,3 @@ def test_gallery(tmpdir):
html = f.read()
assert '<title>Sigal test gallery</title>' in html

Loading…
Cancel
Save