initial commit - siglican 0.0.1a

This commit is contained in:
sawall 2014-09-29 14:12:00 -05:00
parent bee40aebf9
commit e5adbe28df
37 changed files with 3500 additions and 1 deletions

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 - Scott Boone
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.

View file

@ -1,4 +1,25 @@
siglican siglican
======== ========
a port of the Sigal gallery generator as a Pelican plugin A static gallery generator plugin for Pelican, based on the Sigal
Colorbox/Galleria generator by Simon Conseil.
##Notes
* The bulk of the code is ported from [Sigal v0.8.0](http://sigal.saimon.org/).
* Removal of Sigal process handling, rewriting Sigal settings variables, and
integration as a Pelican Generator plugin by Scott Boone.
* The core python code used to generate gallery directories and images as well
as to populate the Jinja environment with album metadata is in beta. Jinja
templates are incomplete.
## To Do
1. Determine the best approach for merging Pelican and Sigal web themes. This
will probably require putting Sigal theme information into the Pelican theme
in order to facilitate loading Javascript and CSS in the html headers.
2. Revise Sigal colorbox and galleria themes for easy inclusion into Pelican
themes.
3. Change settings names to something other than SIGAL_*
4. Unit tests.
5. Logging cleanup.
6. General code and documentation cleanup.

1
__init__.py Normal file
View file

@ -0,0 +1 @@
from .siglican import *

490
album.py Normal file
View file

@ -0,0 +1,490 @@
# -*- coding:utf-8 -*-
# Album classes for use with siglican plugin along with some helper
# methods for context building. This code is largely a copy of gallery.py
# from Sigal.
# Copyright (c) 2009-2014 - 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
# 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.
# siglican:
# Copyright (c) 2014 - Scott Boone (https://github.com/sawall/)
# Minor updates from Sigal.
import os
import logging
from collections import defaultdict
from .compat import strxfrm, UnicodeMixin, url_quote
from .utils import read_markdown, url_from_path
# TODO ** move logger out to here
class Media(UnicodeMixin):
"""Base Class for media files.
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.
"""
type = ''
extensions = ()
def __init__(self, filename, path, settings):
self.src_filename = self.filename = self.url = filename
self.path = path
self.settings = settings
self.src_path = os.path.join(settings['SIGAL_SOURCE'], path, filename)
self.dst_path = os.path.join(settings['SIGAL_DESTINATION'], path, filename)
self.thumb_name = get_thumb(self.settings, self.filename)
self.thumb_path = os.path.join(settings['SIGAL_DESTINATION'], path, self.thumb_name)
self.logger = logging.getLogger(__name__)
self.raw_exif = None
self.exif = None
self.date = None
self._get_metadata()
#signals.media_initialized.send(self)
def __repr__(self):
return "<%s>(%r)" % (self.__class__.__name__, str(self))
def __unicode__(self):
return os.path.join(self.path, self.filename)
@property
def thumbnail(self):
"""Path to the thumbnail image (relative to the album directory)."""
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('siglican: Generating thumbnail for %r', self)
try:
generator(self.src_path, self.thumb_path,
self.settings['SIGAL_THUMB_SIZE'],
fit=self.settings['SIGAL_THUMB_FIT'])
except Exception as e:
self.logger.error('siglican: Failed to generate thumbnail: %s', e)
return
return url_from_path(self.thumb_name)
def _get_metadata(self):
""" Get image metadata from filename.md: title, description, meta."""
self.description = ''
self.meta = {}
self.title = ''
descfile = os.path.splitext(self.src_path)[0] + '.md'
if os.path.isfile(descfile):
meta = read_markdown(descfile)
for key, val in meta.items():
setattr(self, key, val)
class Image(Media):
"""Gather all informations on an image file."""
type = 'image'
extensions = ('.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)
if self.exif is not None and 'dateobj' in self.exif:
self.date = self.exif['dateobj']
class Video(Media):
"""Gather all informations on a video file."""
type = 'video'
extensions = ('.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.src_filename = filename
self.filename = self.url = base + '.webm'
self.dst_path = os.path.join(settings['SIGAL_DESTINATION'], path, base + '.webm')
# minimally modified from Sigal's gallery.Album class
class Album(object):
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
# set up source and destination paths
if path == '.':
self.src_path = settings['SIGAL_SOURCE']
self.dst_path = settings['SIGAL_DESTINATION']
else:
self.src_path = os.path.join(settings['SIGAL_SOURCE'], path)
self.dst_path = os.path.join(settings['SIGAL_DESTINATION'], path)
self.logger = logging.getLogger(__name__)
self._get_metadata() # this reads the index.md file
# optionally add index.html to the URLs
# ** don't understand purpose of this; default is False
self.url_ext = self.output_file if settings['SIGAL_INDEX_IN_URL'] else ''
# creates appropriate subdirectory for the album
self.index_url = url_from_path(os.path.relpath(
settings['SIGAL_DESTINATION'], self.dst_path)) + '/' + self.url_ext
# sort sub-albums
dirnames.sort(key=strxfrm, reverse=settings['SIGAL_ALBUMS_SORT_REVERSE'])
self.subdirs = dirnames
#: List of all medias in the album (:class:`~sigal.gallery.Image` and
#: :class:`~sigal.gallery.Video`).
self.medias = medias = []
self.medias_count = defaultdict(int)
# create Media objects
for f in filenames:
ext = os.path.splitext(f)[1]
if ext.lower() in Image.extensions:
media = Image(f, self.path, settings)
elif ext.lower() in Video.extensions:
media = Video(f, self.path, settings)
else:
continue
self.medias_count[media.type] += 1
medias.append(media)
# sort images
if medias:
medias_sort_attr = settings['SIGAL_MEDIAS_SORT_ATTR']
if medias_sort_attr == 'date':
key = lambda s: s.date or datetime.now()
else:
key = lambda s: strxfrm(getattr(s, medias_sort_attr))
medias.sort(key=key, reverse=settings['SIGAL_MEDIAS_SORT_REVERSE'])
#signals.album_initialized.send(self)
# string representation of Album for debug statements
def __repr__(self):
return "<%s>(path=%r, title=%r, assets:%r)" % (self.__class__.__name__,
self.path, self.title, self.medias)
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 len(self.medias)
def __iter__(self):
return iter(self.medias)
def _get_metadata(self):
"""Get album metadata from `description_file` (`index.md`):
-> title, thumbnail image, description
"""
descfile = os.path.join(self.src_path, self.description_file)
self.description = ''
self.meta = {}
# default: get title from directory name
self.title = os.path.basename(self.path if self.path != '.'
else self.src_path)
if os.path.isfile(descfile):
meta = read_markdown(descfile)
for key, val in meta.items():
setattr(self, key, val)
def create_output_directories(self):
"""Create output directories for thumbnails and original images."""
def check_or_create_dir(path):
if not os.path.isdir(path):
os.makedirs(path)
check_or_create_dir(self.dst_path)
if self.medias:
check_or_create_dir(os.path.join(self.dst_path,
self.settings['SIGAL_THUMB_DIR']))
#if self.medias and self.settings['SIGAL_KEEP_ORIG']:
# self.orig_path = os.path.join(self.dst_path, self.settings['SIGAL_ORIG_DIR'])
# check_or_create_dir(self.orig_path)
@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[os.path.join(root_path, path)]
for path in self.subdirs]
@property
def url(self):
"""URL of the album, relative to its parent."""
url = self.name.encode('utf-8')
return url_quote(url) + '/' + self.url_ext
@property
def thumbnail(self):
"""Path to the thumbnail of the album."""
if self._thumbnail:
# stop if it is already set
return url_from_path(self._thumbnail)
# Test the thumbnail from the Markdown file.
thumbnail = self.meta.get('thumbnail', [''])[0]
if thumbnail and os.path.isfile(os.path.join(self.src_path, thumbnail)):
self._thumbnail = os.path.join(self.name, get_thumb(self.settings,
thumbnail))
self.logger.debug("Thumbnail for %r : %s", self, self._thumbnail)
return url_from_path(self._thumbnail)
else:
# find and return the first landscape image
for f in self.medias:
ext = os.path.splitext(f.filename)[1]
if ext.lower() in Image.extensions:
im = PILImage.open(f.src_path)
if im.size[0] > im.size[1]:
self._thumbnail = os.path.join(self.name, f.thumbnail)
self.logger.debug(
"Use 1st landscape image as thumbnail for %r : %s",
self, self._thumbnail)
return url_from_path(self._thumbnail)
# else simply return the 1st media file
if not self._thumbnail and self.medias:
self._thumbnail = os.path.join(self.name, self.medias[0].thumbnail)
self.logger.debug("Use the 1st image as thumbnail for %r : %s",
self, self._thumbnail)
return url_from_path(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 = os.path.join(self.name, album.thumbnail)
self.logger.debug(
"Using thumbnail from sub-directory for %r : %s",
self, self._thumbnail)
return url_from_path(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 = (url_from_path(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['SIGAL_ZIP_GALLERY']
if zip_gallery and len(self) > 0:
archive_path = os.path.join(self.dst_path, zip_gallery)
archive = zipfile.ZipFile(archive_path, 'w')
if self.settings['SIGAL_ZIP_MEDIA_FORMAT'] == 'orig':
for p in self:
archive.write(p.src_path, os.path.split(p.src_path)[1])
else:
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
# ** TODO: move as part of utils cleanup
def get_thumb(settings, filename):
"""Return the path to the thumb.
examples:
>>> default_settings = create_settings()
>>> 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.lower() in Video.extensions:
ext = '.jpg'
return os.path.join(path, settings['SIGAL_THUMB_DIR'], settings['SIGAL_THUMB_PREFIX'] +
name + settings['SIGAL_THUMB_SUFFIX'] + ext)
def get_exif_tags(source):
"""Read EXIF tags from file @source and return a tuple of two dictionaries,
the first one containing the raw EXIF data, the second one a simplified
version with common tags.
"""
logger = logging.getLogger(__name__)
if os.path.splitext(source)[1].lower() not in ('.jpg', '.jpeg'):
return (None, None)
try:
data = _get_exif_data(source)
except (IOError, IndexError, TypeError, AttributeError):
logger.warning(u'Could not read EXIF data from %s', source)
return (None, None)
simple = {}
# Provide more accessible tags in the 'simple' key
if 'FNumber' in data:
fnumber = data['FNumber']
simple['fstop'] = float(fnumber[0]) / fnumber[1]
if 'FocalLength' in data:
focal = data['FocalLength']
simple['focal'] = round(float(focal[0]) / focal[1])
if 'ExposureTime' in data:
if isinstance(data['ExposureTime'], tuple):
simple['exposure'] = '{0}/{1}'.format(*data['ExposureTime'])
elif isinstance(data['ExposureTime'], int):
simple['exposure'] = str(data['ExposureTime'])
else:
logger.warning('Unknown format for ExposureTime: %r (%s)',
data['ExposureTime'], source)
if 'ISOSpeedRatings' in data:
simple['iso'] = data['ISOSpeedRatings']
if 'DateTimeOriginal' in data:
try:
# Remove null bytes at the end if necessary
date = data['DateTimeOriginal'].rsplit('\x00')[0]
simple['dateobj'] = datetime.strptime(date, '%Y:%m:%d %H:%M:%S')
dt = simple['dateobj'].strftime('%A, %d. %B %Y')
if compat.PY2:
simple['datetime'] = dt.decode('utf8')
else:
simple['datetime'] = dt
except (ValueError, TypeError) as e:
logger.warning(u'Could not parse DateTimeOriginal of %s: %s',
source, e)
if 'GPSInfo' in data:
info = data['GPSInfo']
lat_info = info.get('GPSLatitude')
lon_info = info.get('GPSLongitude')
lat_ref_info = info.get('GPSLatitudeRef')
lon_ref_info = info.get('GPSLongitudeRef')
if lat_info and lon_info and lat_ref_info and lon_ref_info:
try:
lat = dms_to_degrees(lat_info)
lon = dms_to_degrees(lon_info)
except (ZeroDivisionError, ValueError):
logger.warning('Failed to read GPS info for %s', source)
lat = lon = None
if lat and lon:
simple['gps'] = {
'lat': - lat if lat_ref_info != 'N' else lat,
'lon': - lon if lon_ref_info != 'E' else lon,
}
return (data, simple)

54
compat.py Normal file
View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2013-2014 - Simon Conseil
# 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.
## ** TODO: use pelican's compatibility features in utils.py (which uses
## Django calls)
import locale
import sys
# the following appears to be from Django and/or Jinja
PY2 = sys.version_info[0] == 2
if not PY2:
text_type = str
string_types = (str,)
unichr = chr
strxfrm = locale.strxfrm
from urllib.parse import quote as url_quote
else:
text_type = unicode # NOQA
string_types = (str, unicode) # NOQA
unichr = unichr
def strxfrm(s):
return locale.strxfrm(s.encode('utf-8'))
from urllib import quote as url_quote # NOQA
# the following appears to be from
# http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/
class UnicodeMixin(object):
if not PY2:
__str__ = lambda x: x.__unicode__()
else:
__str__ = lambda x: unicode(x).encode('utf-8')

202
image.py Normal file
View file

@ -0,0 +1,202 @@
# -*- coding:utf-8 -*-
# Copyright (c) 2009-2014 - Simon Conseil
# 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.
# Additional copyright notice:
#
# Several lines of code concerning extraction of GPS data from EXIF tags where
# taken from a GitHub Gist by Eran Sandler at
#
# https://gist.github.com/erans/983821
#
# and partially modified. The code in question is licensed under MIT license.
# Copyright (c) 2014 - Scott Boone (https://github.com/sawall/)
# Minor updates to image.py from Sigal.
# TODO: merge with video.py
import logging
import os
import pilkit.processors
import sys
from copy import deepcopy
from datetime import datetime
from PIL.ExifTags import TAGS, GPSTAGS
from PIL import Image as PILImage
from PIL import ImageOps
from pilkit.processors import Transpose
from pilkit.utils import save_image
from . import compat #, signals
from .album import Video
def _has_exif_tags(img):
return hasattr(img, 'info') and 'exif' in img.info
def generate_image(source, outname, settings, options=None):
"""Image processor, rotate and resize the image.
:param source: path to an image
:param outname: output filename
:param settings: settings dict
:param options: dict with PIL options (quality, optimize, progressive)
"""
logger = logging.getLogger(__name__)
img = PILImage.open(source)
original_format = img.format
if settings['SIGAL_COPY_EXIF_DATA'] and settings['SIGAL_AUTOROTATE_IMAGES']:
logger.warning("The 'autorotate_images' and 'copy_exif_data' settings "
"are not compatible because Sigal can't save the "
"modified Orientation tag.")
# Preserve EXIF data
if settings['SIGAL_COPY_EXIF_DATA'] and _has_exif_tags(img):
if options is not None:
options = deepcopy(options)
else:
options = {}
options['exif'] = img.info['exif']
# Rotate the img, and catch IOError when PIL fails to read EXIF
if settings['SIGAL_AUTOROTATE_IMAGES']:
try:
img = Transpose().process(img)
except (IOError, IndexError):
pass
# Resize the image
if settings['SIGAL_IMG_PROCESSOR']:
try:
logger.debug('Processor: %s', settings['SIGAL_IMG_PROCESSOR'])
processor_cls = getattr(pilkit.processors,
settings['SIGAL_IMG_PROCESSOR'])
except AttributeError:
logger.error('Wrong processor name: %s', settings['SIGAL_IMG_PROCESSOR'])
sys.exit()
processor = processor_cls(*settings['SIGAL_IMG_SIZE'], upscale=False)
img = processor.process(img)
# TODO ** delete (maintained from Sigal for reference)
# signal.send() does not work here as plugins can modify the image, so we
# iterate other the receivers to call them with the image.
#for receiver in signals.img_resized.receivers_for(img):
# img = receiver(img, settings=settings)
outformat = img.format or original_format or 'JPEG'
logger.debug(u'Save resized image to {0} ({1})'.format(outname, outformat))
save_image(img, outname, outformat, options=options, autoconvert=True)
def generate_thumbnail(source, outname, box, fit=True, options=None):
"""Create a thumbnail image."""
logger = logging.getLogger(__name__)
img = PILImage.open(source)
original_format = img.format
if fit:
img = ImageOps.fit(img, box, PILImage.ANTIALIAS)
else:
img.thumbnail(box, PILImage.ANTIALIAS)
outformat = img.format or original_format or 'JPEG'
logger.debug(u'Save thumnail image: {0} ({1})'.format(outname, outformat))
save_image(img, outname, outformat, options=options, autoconvert=True)
def process_image(filepath, outpath, settings):
"""Process one image: resize, create thumbnail."""
logger = logging.getLogger(__name__)
filename = os.path.split(filepath)[1]
outname = os.path.join(outpath, filename)
ext = os.path.splitext(filename)[1]
if ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
options = settings['SIGAL_JPG_OPTIONS']
elif ext == '.png':
options = {'optimize': True}
else:
options = {}
try:
generate_image(filepath, outname, settings, options=options)
except Exception as e:
logger.error('Failed to process image: %s', e)
return
if settings['SIGAL_MAKE_THUMBS']:
thumb_name = os.path.join(outpath, get_thumb(settings, filename))
generate_thumbnail(outname, thumb_name, settings['SIGAL_THUMB_SIZE'],
fit=settings['SIGAL_THUMB_FIT'], options=options)
def _get_exif_data(filename):
"""Return a dict with EXIF data."""
img = PILImage.open(filename)
exif = img._getexif() or {}
data = {TAGS.get(tag, tag): value for tag, value in exif.items()}
if 'GPSInfo' in data:
data['GPSInfo'] = {GPSTAGS.get(tag, tag): value
for tag, value in data['GPSInfo'].items()}
return data
def dms_to_degrees(v):
"""Convert degree/minute/second to decimal degrees."""
d = float(v[0][0]) / float(v[0][1])
m = float(v[1][0]) / float(v[1][1])
s = float(v[2][0]) / float(v[2][1])
return d + (m / 60.0) + (s / 3600.0)
def get_thumb(settings, filename):
"""Return the path to the thumb.
examples:
>>> default_settings = create_settings()
>>> 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.lower() in Video.extensions:
ext = '.jpg'
return os.path.join(path, settings['SIGAL_THUMB_DIR'], settings['SIGAL_THUMB_PREFIX'] +
name + settings['SIGAL_THUMB_SUFFIX'] + ext)

28
pkgmeta.py Normal file
View file

@ -0,0 +1,28 @@
# -*- coding:utf-8 -*-
# Copyright (c) 2014 - Scott Boone
# 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.
__title__ = 'siglican'
__author__ = 'Scott Boone'
__version__ = '0.0.1-alpha'
__license__ = 'MIT'
__url__ = 'https://github.com/sawall/siglican'
__all__ = ['__title__', '__author__', '__version__', '__license__', '__url__']

284
siglican.py Normal file
View file

@ -0,0 +1,284 @@
# -*- coding:utf-8 -*-
# Copyright (c) 2014 - Scott Boone
#
# 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.
import os
import sys
import locale
import logging
import fnmatch
from pelican import signals
from pelican.generators import Generator
from .compat import PY2
from .album import Album
from .image import process_image
from .video import process_video
from .writer import Writer
logger = logging.getLogger(__name__)
# Default config from Sigal's settings module. These have been changed to
# upper case because Pelican does not recognize lower case configuration names.
_DEFAULT_SIGAL_SETTINGS = {
'SIGAL_ALBUMS_SORT_REVERSE': False,
'SIGAL_AUTOROTATE_IMAGES': True,
'SIGAL_COLORBOX_COLUMN_SIZE': 4,
'SIGAL_COPY_EXIF_DATA': False,
'SIGAL_DESTINATION': 'gallery',
'SIGAL_FILES_TO_COPY': (),
# 'GOOGLE_ANALYTICS': '',
'SIGAL_IGNORE_DIRECTORIES': ['.'], # using a pelican theme template as base
'SIGAL_IGNORE_FILES': [],
'SIGAL_IMG_PROCESSOR': 'ResizeToFit',
'SIGAL_IMG_SIZE': (640, 480),
'SIGAL_INDEX_IN_URL': False,
'SIGAL_JPG_OPTIONS': {'quality': 85, 'optimize': True, 'progressive': True},
# 'SIGAL_KEEP_ORIG': False,
'SIGAL_LINKS': '',
'SIGAL_LOCALE': '',
'SIGAL_MEDIAS_SORT_ATTR': 'filename',
'SIGAL_MEDIAS_SORT_REVERSE': False,
'SIGAL_MAKE_THUMBS': True,
'SIGAL_ORIG_DIR': 'original',
'SIGAL_ORIG_LINK': False,
# 'PLUGINS': [],
# 'PLUGIN_PATHS': [],
'SIGAL_SOURCE': 'sigal',
'SIGAL_THEME': 'colorbox',
'SIGAL_THUMB_DIR': 'thumbs',
'SIGAL_THUMB_FIT': True,
'SIGAL_THUMB_PREFIX': '',
'SIGAL_THUMB_SIZE': (200, 150),
'SIGAL_THUMB_SUFFIX': '',
'SIGAL_VIDEO_SIZE': (480, 360),
'SIGAL_WEBM_OPTIONS': ['-crf', '10', '-b:v', '1.6M',
'-qmin', '4', '-qmax', '63'],
'SIGAL_WRITE_HTML': True,
'SIGAL_ZIP_GALLERY': False,
'SIGAL_ZIP_MEDIA_FORMAT': 'resized',
}
# Generator class used to generate plugin context and write.
# TODO: consider usinge CachingGenerator instead?
class SigalGalleryGenerator(Generator):
# reference: methods provided by Pelican Generator:
# def _update_context(self, items): adds more items to the context dict
# def get_template(self, name): returns templates from theme based on theme
# def get_files(self, paths, exclude=[], extensions=None): paths to search,
# exclude, allowed extensions
def __init__(self, *args, **kwargs):
"""Initialize gallery dict and load in custom Sigal settings."""
logger.debug("siglican: entering SigalGalleryGenerator.__init__")
self.albums = {}
# this needs to be first to establish pelican settings:
super(SigalGalleryGenerator, self).__init__(*args, **kwargs)
# add default sigal settings to generator settings:
for k in _DEFAULT_SIGAL_SETTINGS.keys()[:]:
self.settings[k] = self.settings.get(k, _DEFAULT_SIGAL_SETTINGS[k])
logger.debug("sigal.pelican: setting %s: %s",k,self.settings[k])
self._clean_settings()
# this is where we could create a signal if we wanted to, e.g.:
# signals.gallery_generator_init.send(self)
def _clean_settings(self):
"""Checks existence of directories and normalizes image size settings."""
# create absolute paths to source, theme and destination directories:
init_source = self.settings['SIGAL_SOURCE']
self.settings['SIGAL_SOURCE'] = os.path.normpath(self.settings['PATH'] +
"/../" + self.settings['SIGAL_SOURCE'] + '/images')
self.settings['SIGAL_THEME'] = os.path.normpath(self.settings['PATH'] +
"/../" + init_source + "/" + self.settings['SIGAL_THEME'])
self.settings['SIGAL_DESTINATION'] = os.path.normpath(
self.settings['OUTPUT_PATH'] + "/" + self.settings['SIGAL_DESTINATION'])
enc = locale.getpreferredencoding() if PY2 else None
# test for existence of source directories
pathkeys = ['SIGAL_SOURCE', 'SIGAL_THEME']
for k in pathkeys:
if os.path.isdir(self.settings[k]):
# convert to unicode for os.walk dirname/filename
if PY2 and isinstance(self.settings[k], str):
self.settings[k] = self.settings[k].decode(enc)
logger.debug("siglican: %s = %s",k,self.settings[k])
else:
logger.error("siglican: missing source directory %s: %s",
k,self.settings[k])
sys.exit(1)
# normalize sizes as e landscape
for key in ('SIGAL_IMG_SIZE', 'SIGAL_THUMB_SIZE', 'SIGAL_VIDEO_SIZE'):
w, h = self.settings[key]
if h > w:
self.settings[key] = (h, w)
logger.warning("siglican: The %s setting should be specified "
"with the largest value first.", key)
if not self.settings['SIGAL_IMG_PROCESSOR']:
logger.info('No Processor, images will not be resized')
# based on Sigal's Gallery.__init__() method:
def generate_context(self):
""""Update the global Pelican context that's shared between generators."""
logger.debug("siglican: in generate_context()")
locale.setlocale(locale.LC_ALL, self.settings['SIGAL_LOCALE'])
self.stats = {'image': 0, 'image_skipped': 0,
'video': 0, 'video_skipped': 0}
# build the list of directories with images
# ** TODO: add error checking, consider use of get(), etc.
src_path = self.settings['SIGAL_SOURCE']
ignore_dirs = self.settings['SIGAL_IGNORE_DIRECTORIES']
ignore_files = self.settings['SIGAL_IGNORE_FILES']
for path, dirs, files in os.walk(src_path, followlinks=True,
topdown=False):
relpath = os.path.relpath(path, src_path)
if ignore_dirs and any(fnmatch.fnmatch(relpath, ignore)
for ignore in ignore_dirs):
logger.info('siglican: ignoring %s', relpath)
continue
if ignore_files: # << ** BUG: if no ignore_files, then no files?
files_path = {os.path.join(relpath, f) for f in files}
for ignore in ignore_files:
files_path -= set(fnmatch.filter(files_path, ignore))
## ** BUG? unicode in list may cause mismatch
logger.debug('siglican: Files before filtering: %r', files)
files = [os.path.split(f)[1] for f in files_path]
logger.debug('siglican: Files after filtering: %r', files)
# Remove sub-directories that have been ignored in a previous
# iteration (as topdown=False, sub-directories are processed before
# their parent
for d in dirs[:]:
path = os.path.join(relpath, d) if relpath != '.' else d
if path not in self.albums.keys():
dirs.remove(d)
album = Album(relpath, self.settings, dirs, files, self)
if not album.medias and not album.albums:
logger.info('siglican: Skip empty album: %r', album)
else:
self.albums[relpath] = album
# done generating context (self.albums) now
logger.debug('siglican: albums:\n%r', self.albums.values())
# BUG ** : albums appears to contain one extra empty entry at this point
# <Album>(path='.', title=u'images', assets:[])]
# update the jinja context so that templates can access it:
self._update_context(('albums', )) # ** not 100% certain this is needed
self.context['ALBUMS'] = self.albums
# update the jinja context with the default sigal settings:
for k,v in _DEFAULT_SIGAL_SETTINGS.iteritems():
if not k in self.context:
self.context[k] = v
def generate_output(self, writer):
""" Creates gallery destination directories, thumbnails, resized
images, and moves everything into the destination."""
# ignore the writer because it might be from another plugin
# see https://github.com/getpelican/pelican/issues/1459
# DELETE ** I don't think I need to do anything like this:
# create a gallery in pages so that we can reference it from pelican
#for page in self.pages:
# if page.metadata.get('template') == 'sigal_gallery':
# page.gallery=gallery
# create destination directory
if not os.path.isdir(self.settings['SIGAL_DESTINATION']):
os.makedirs(self.settings['SIGAL_DESTINATION'])
# TODO ** add lots of error/exception catching
# TODO ** re-integrate multiprocessing logic from Sigal
# generate thumbnails, process images, and move them to the destination
albums = self.albums
for a in albums:
logger.debug("siglican: creating directory for %s",a)
albums[a].create_output_directories()
for media in albums[a].medias:
logger.debug("siglican: processing %r , source: %s, dst: %s",
media,media.src_path,media.dst_path)
if os.path.isfile(media.dst_path):
logger.info("siglican: %s exists - skipping", media.filename)
self.stats[media.type + '_skipped'] += 1
else:
self.stats[media.type] += 1
logger.debug("MEDIA TYPE: %s",media.type)
# create/move resized images and thumbnails to output dirs:
if media.type == 'image':
process_image(media.src_path,os.path.dirname(
media.dst_path),self.settings)
elif media.type == 'video':
process_video(media.src_path,os.path.dirname(
media.dst_path),self.settings)
# generate the index.html files for the albums
if self.settings['SIGAL_WRITE_HTML']: # defaults to True
# locate the theme; check for a custom theme in ./sigal/themes, if not
# found, look for a default in siglican/themes
self.theme = self.settings['SIGAL_THEME']
default_themes = os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'themes'))
logger.debug("siglican: custom theme: %s", self.theme)
logger.debug("siglican: default themedir: %s", default_themes)
if not os.path.exists(self.theme):
self.theme = os.path.join(default_themes, os.path.basename(self.theme))
if not os.path.exists(self.theme):
raise Exception("siglican: unable to find theme: %s" %
os.path.basename(self.theme))
logger.info("siglican theme: %s", self.theme)
theme_relpath = os.path.join(self.theme, 'templates')
## note 1: it's impossible to add additional templates to jinja
## after the initial init, which means we either need to put plugin
## templates in with the rest of the pelican templates or use a
## plugin-specific jinja environment and writer
# note 2: when Pelican calls generate_output() on a Generator plugin,
# it's uncertain which Writer will be sent; if other plugins with
# Writers are loaded, it might be one of those Writers instead of
# one of the core Pelican writers. thus this plugin explicitly calls
# a Writer so that it doesn't get any nasty surprises due to plugin
# conflicts. I logged a feature request to Pelican here:
# https://github.com/getpelican/pelican/issues/1459
self.writer = Writer(self.context, self.theme, 'album')
for album in self.albums.values():
self.writer.write(album)
## possible cleanup:
## - missing some writer options that Sigal had, bring back?
## - make sure all necessary template info is accessible by the writer
## - make sure thumbnails don't break in some cases
def get_generators(generators):
return SigalGalleryGenerator
def register():
signals.get_generators.connect(get_generators)

16
themes/Makefile Normal file
View file

@ -0,0 +1,16 @@
COLORBOX_PATH=colorbox/static/css
GALLERIA_PATH=galleria/static/css
all:
sass $(COLORBOX_PATH)/style.scss:$(COLORBOX_PATH)/style.min.css --style compressed
sass $(GALLERIA_PATH)/style.scss:$(GALLERIA_PATH)/style.min.css --style compressed
colorbox:
sass --watch $(COLORBOX_PATH)/style.scss:$(COLORBOX_PATH)/style.min.css \
--style compressed
galleria:
sass --watch $(GALLERIA_PATH)/style.scss:$(GALLERIA_PATH)/style.min.css \
--style compressed
.PHONY: colorbox galleria

View file

@ -0,0 +1,269 @@
/*
* Skeleton V1.2
* Copyright 2011, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 6/20/2012
*/
/* Table of Content
==================================================
#Reset & Basics
#Basic Styles
#Site Styles
#Typography
#Links
#Lists
#Images
#Buttons
#Forms
#Misc */
/* #Reset & Basics (Inspired by E. Meyers)
================================================== */
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline; }
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {
display: block; }
body {
line-height: 1; }
ol, ul {
list-style: none; }
blockquote, q {
quotes: none; }
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none; }
table {
border-collapse: collapse;
border-spacing: 0; }
/* #Basic Styles
================================================== */
body {
background: #fff;
font: 14px/21px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #444;
-webkit-font-smoothing: antialiased; /* Fix for webkit rendering */
-webkit-text-size-adjust: 100%;
}
/* #Typography
================================================== */
h1, h2, h3, h4, h5, h6 {
color: #181818;
font-family: "Georgia", "Times New Roman", serif;
font-weight: normal; }
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
h1 { font-size: 46px; line-height: 50px; margin-bottom: 14px;}
h2 { font-size: 35px; line-height: 40px; margin-bottom: 10px; }
h3 { font-size: 28px; line-height: 34px; margin-bottom: 8px; }
h4 { font-size: 21px; line-height: 30px; margin-bottom: 4px; }
h5 { font-size: 17px; line-height: 24px; }
h6 { font-size: 14px; line-height: 21px; }
.subheader { color: #777; }
p { margin: 0 0 20px 0; }
p img { margin: 0; }
p.lead { font-size: 21px; line-height: 27px; color: #777; }
em { font-style: italic; }
strong { font-weight: bold; color: #333; }
small { font-size: 80%; }
/* Blockquotes */
blockquote, blockquote p { font-size: 17px; line-height: 24px; color: #777; font-style: italic; }
blockquote { margin: 0 0 20px; padding: 9px 20px 0 19px; border-left: 1px solid #ddd; }
blockquote cite { display: block; font-size: 12px; color: #555; }
blockquote cite:before { content: "\2014 \0020"; }
blockquote cite a, blockquote cite a:visited, blockquote cite a:visited { color: #555; }
hr { border: solid #ddd; border-width: 1px 0 0; clear: both; margin: 10px 0 30px; height: 0; }
/* #Links
================================================== */
a, a:visited { color: #333; text-decoration: underline; outline: 0; }
a:hover, a:focus { color: #000; }
p a, p a:visited { line-height: inherit; }
/* #Lists
================================================== */
ul, ol { margin-bottom: 20px; }
ul { list-style: none outside; }
ol { list-style: decimal; }
ol, ul.square, ul.circle, ul.disc { margin-left: 30px; }
ul.square { list-style: square outside; }
ul.circle { list-style: circle outside; }
ul.disc { list-style: disc outside; }
ul ul, ul ol,
ol ol, ol ul { margin: 4px 0 5px 30px; font-size: 90%; }
ul ul li, ul ol li,
ol ol li, ol ul li { margin-bottom: 6px; }
li { line-height: 18px; margin-bottom: 12px; }
ul.large li { line-height: 21px; }
li p { line-height: 21px; }
/* #Images
================================================== */
img.scale-with-grid {
max-width: 100%;
height: auto; }
/* #Buttons
================================================== */
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
background: #eee; /* Old browsers */
background: #eee -moz-linear-gradient(top, rgba(255,255,255,.2) 0%, rgba(0,0,0,.2) 100%); /* FF3.6+ */
background: #eee -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.2)), color-stop(100%,rgba(0,0,0,.2))); /* Chrome,Safari4+ */
background: #eee -webkit-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Chrome10+,Safari5.1+ */
background: #eee -o-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* Opera11.10+ */
background: #eee -ms-linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* IE10+ */
background: #eee linear-gradient(top, rgba(255,255,255,.2) 0%,rgba(0,0,0,.2) 100%); /* W3C */
border: 1px solid #aaa;
border-top: 1px solid #ccc;
border-left: 1px solid #ccc;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
color: #444;
display: inline-block;
font-size: 11px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px rgba(255, 255, 255, .75);
cursor: pointer;
margin-bottom: 20px;
line-height: normal;
padding: 8px 10px;
font-family: "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover {
color: #222;
background: #ddd; /* Old browsers */
background: #ddd -moz-linear-gradient(top, rgba(255,255,255,.3) 0%, rgba(0,0,0,.3) 100%); /* FF3.6+ */
background: #ddd -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.3)), color-stop(100%,rgba(0,0,0,.3))); /* Chrome,Safari4+ */
background: #ddd -webkit-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Chrome10+,Safari5.1+ */
background: #ddd -o-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* Opera11.10+ */
background: #ddd -ms-linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* IE10+ */
background: #ddd linear-gradient(top, rgba(255,255,255,.3) 0%,rgba(0,0,0,.3) 100%); /* W3C */
border: 1px solid #888;
border-top: 1px solid #aaa;
border-left: 1px solid #aaa; }
.button:active,
button:active,
input[type="submit"]:active,
input[type="reset"]:active,
input[type="button"]:active {
border: 1px solid #666;
background: #ccc; /* Old browsers */
background: #ccc -moz-linear-gradient(top, rgba(255,255,255,.35) 0%, rgba(10,10,10,.4) 100%); /* FF3.6+ */
background: #ccc -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,.35)), color-stop(100%,rgba(10,10,10,.4))); /* Chrome,Safari4+ */
background: #ccc -webkit-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Chrome10+,Safari5.1+ */
background: #ccc -o-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* Opera11.10+ */
background: #ccc -ms-linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* IE10+ */
background: #ccc linear-gradient(top, rgba(255,255,255,.35) 0%,rgba(10,10,10,.4) 100%); /* W3C */ }
.button.full-width,
button.full-width,
input[type="submit"].full-width,
input[type="reset"].full-width,
input[type="button"].full-width {
width: 100%;
padding-left: 0 !important;
padding-right: 0 !important;
text-align: center; }
/* Fix for odd Mozilla border & padding issues */
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/* #Forms
================================================== */
form {
margin-bottom: 20px; }
fieldset {
margin-bottom: 20px; }
input[type="text"],
input[type="password"],
input[type="email"],
textarea,
select {
border: 1px solid #ccc;
padding: 6px 4px;
outline: none;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
font: 13px "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
margin: 0;
width: 210px;
max-width: 100%;
display: block;
margin-bottom: 20px;
background: #fff; }
select {
padding: 0; }
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
textarea:focus {
border: 1px solid #aaa;
color: #444;
-moz-box-shadow: 0 0 3px rgba(0,0,0,.2);
-webkit-box-shadow: 0 0 3px rgba(0,0,0,.2);
box-shadow: 0 0 3px rgba(0,0,0,.2); }
textarea {
min-height: 60px; }
label,
legend {
display: block;
font-weight: bold;
font-size: 13px; }
select {
width: 220px; }
input[type="checkbox"] {
display: inline; }
label span,
legend span {
font-weight: normal;
font-size: 13px;
color: #444; }
/* #Misc
================================================== */
.remove-bottom { margin-bottom: 0 !important; }
.half-bottom { margin-bottom: 10px !important; }
.add-bottom { margin-bottom: 20px !important; }

View file

@ -0,0 +1,47 @@
/*
ColorBox Core Style:
The following CSS is consistent between example themes and should not be altered.
*/
#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
#cboxOverlay{position:fixed; width:100%; height:100%;}
#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
#cboxContent{position:relative;}
#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;}
#cboxTitle{margin:0;}
#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none;}
.cboxIframe{width:100%; height:100%; display:block; border:0;}
#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;}
/*
User Style:
Change the following styles to modify the appearance of ColorBox. They are
ordered & tabbed in a way that represents the nesting of the generated HTML.
*/
#cboxOverlay{background:#000;}
#colorbox{outline:0;}
#cboxContent{margin-top:20px;background:#000;}
.cboxIframe{background:#fff;}
#cboxError{padding:50px; border:1px solid #ccc;}
#cboxLoadedContent{border:5px solid #000; background:#fff;}
#cboxTitle{position:absolute; top:-20px; left:0; color:#ccc;}
#cboxCurrent{position:absolute; top:-20px; right:0px; color:#ccc;}
#cboxLoadingGraphic{background:url(../images/loading.gif) no-repeat center center;}
/* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */
#cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; width:auto; background:none; }
/* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */
#cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;}
#cboxSlideshow{position:absolute; top:-20px; right:90px; color:#fff;}
#cboxPrevious{position:absolute; top:50%; left:5px; margin-top:-32px;
background:url(../images/controls.png) no-repeat top left; width:28px; height:65px; text-indent:-9999px;}
#cboxPrevious:hover{background-position:bottom left;}
#cboxNext{position:absolute; top:50%; right:5px; margin-top:-32px;
background:url(../images/controls.png) no-repeat top right; width:28px; height:65px; text-indent:-9999px;}
#cboxNext:hover{background-position:bottom right;}
#cboxClose{position:absolute; top:5px; right:5px; display:block;
background:url(../images/controls.png) no-repeat top center; width:38px; height:19px; text-indent:-9999px;}
#cboxClose:hover{background-position:bottom center;}

View file

@ -0,0 +1,58 @@
/*
* Skeleton V1.2
* Copyright 2011, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 6/20/2012
*/
/* Table of Content
==================================================
#Site Styles
#Page Styles
#Media Queries
#Font-Face */
/* #Site Styles
================================================== */
/* #Page Styles
================================================== */
/* #Media Queries
================================================== */
/* Smaller than standard 960 (devices and browsers) */
@media only screen and (max-width: 959px) {}
/* Tablet Portrait size to standard 960 (devices and browsers) */
@media only screen and (min-width: 768px) and (max-width: 959px) {}
/* All Mobile Sizes (devices and browser) */
@media only screen and (max-width: 767px) {}
/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */
@media only screen and (min-width: 480px) and (max-width: 767px) {}
/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */
@media only screen and (max-width: 479px) {}
/* #Font-Face
================================================== */
/* This is the proper syntax for an @font-face file
Just create a "fonts" folder at the root,
copy your FontName into code below and remove
comment brackets */
/* @font-face {
font-family: 'FontName';
src: url('../fonts/FontName.eot');
src: url('../fonts/FontName.eot?iefix') format('eot'),
url('../fonts/FontName.woff') format('woff'),
url('../fonts/FontName.ttf') format('truetype'),
url('../fonts/FontName.svg#webfontZam02nTh') format('svg');
font-weight: normal;
font-style: normal; }
*/

242
themes/colorbox/static/css/skeleton.scss vendored Normal file
View file

@ -0,0 +1,242 @@
/*
* Skeleton V1.2
* Copyright 2011, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 6/20/2012
*/
/* Table of Contents
==================================================
#Base 960 Grid
#Tablet (Portrait)
#Mobile (Portrait)
#Mobile (Landscape)
#Clearing */
/* #Base 960 Grid
================================================== */
.container { position: relative; width: 960px; margin: 0 auto; padding: 0; }
.container .column,
.container .columns { float: left; display: inline; margin-left: 10px; margin-right: 10px; }
.row { margin-bottom: 20px; }
/* Nested Column Classes */
.column.alpha, .columns.alpha { margin-left: 0; }
.column.omega, .columns.omega { margin-right: 0; }
/* Base Grid */
.container .one.column,
.container .one.columns { width: 40px; }
.container .two.columns { width: 100px; }
.container .three.columns { width: 160px; }
.container .four.columns { width: 220px; }
.container .five.columns { width: 280px; }
.container .six.columns { width: 340px; }
.container .seven.columns { width: 400px; }
.container .eight.columns { width: 460px; }
.container .nine.columns { width: 520px; }
.container .ten.columns { width: 580px; }
.container .eleven.columns { width: 640px; }
.container .twelve.columns { width: 700px; }
.container .thirteen.columns { width: 760px; }
.container .fourteen.columns { width: 820px; }
.container .fifteen.columns { width: 880px; }
.container .sixteen.columns { width: 940px; }
.container .one-third.column { width: 300px; }
.container .two-thirds.column { width: 620px; }
/* Offsets */
.container .offset-by-one { padding-left: 60px; }
.container .offset-by-two { padding-left: 120px; }
.container .offset-by-three { padding-left: 180px; }
.container .offset-by-four { padding-left: 240px; }
.container .offset-by-five { padding-left: 300px; }
.container .offset-by-six { padding-left: 360px; }
.container .offset-by-seven { padding-left: 420px; }
.container .offset-by-eight { padding-left: 480px; }
.container .offset-by-nine { padding-left: 540px; }
.container .offset-by-ten { padding-left: 600px; }
.container .offset-by-eleven { padding-left: 660px; }
.container .offset-by-twelve { padding-left: 720px; }
.container .offset-by-thirteen { padding-left: 780px; }
.container .offset-by-fourteen { padding-left: 840px; }
.container .offset-by-fifteen { padding-left: 900px; }
/* #Tablet (Portrait)
================================================== */
/* Note: Design for a width of 768px */
@media only screen and (min-width: 768px) and (max-width: 959px) {
.container { width: 768px; }
.container .column,
.container .columns { margin-left: 10px; margin-right: 10px; }
.column.alpha, .columns.alpha { margin-left: 0; margin-right: 10px; }
.column.omega, .columns.omega { margin-right: 0; margin-left: 10px; }
.alpha.omega { margin-left: 0; margin-right: 0; }
.container .one.column,
.container .one.columns { width: 28px; }
.container .two.columns { width: 76px; }
.container .three.columns { width: 124px; }
.container .four.columns { width: 172px; }
.container .five.columns { width: 220px; }
.container .six.columns { width: 268px; }
.container .seven.columns { width: 316px; }
.container .eight.columns { width: 364px; }
.container .nine.columns { width: 412px; }
.container .ten.columns { width: 460px; }
.container .eleven.columns { width: 508px; }
.container .twelve.columns { width: 556px; }
.container .thirteen.columns { width: 604px; }
.container .fourteen.columns { width: 652px; }
.container .fifteen.columns { width: 700px; }
.container .sixteen.columns { width: 748px; }
.container .one-third.column { width: 236px; }
.container .two-thirds.column { width: 492px; }
/* Offsets */
.container .offset-by-one { padding-left: 48px; }
.container .offset-by-two { padding-left: 96px; }
.container .offset-by-three { padding-left: 144px; }
.container .offset-by-four { padding-left: 192px; }
.container .offset-by-five { padding-left: 240px; }
.container .offset-by-six { padding-left: 288px; }
.container .offset-by-seven { padding-left: 336px; }
.container .offset-by-eight { padding-left: 384px; }
.container .offset-by-nine { padding-left: 432px; }
.container .offset-by-ten { padding-left: 480px; }
.container .offset-by-eleven { padding-left: 528px; }
.container .offset-by-twelve { padding-left: 576px; }
.container .offset-by-thirteen { padding-left: 624px; }
.container .offset-by-fourteen { padding-left: 672px; }
.container .offset-by-fifteen { padding-left: 720px; }
}
/* #Mobile (Portrait)
================================================== */
/* Note: Design for a width of 320px */
@media only screen and (max-width: 767px) {
.container { width: 300px; }
.container .columns,
.container .column { margin: 0; }
.container .one.column,
.container .one.columns,
.container .two.columns,
.container .three.columns,
.container .four.columns,
.container .five.columns,
.container .six.columns,
.container .seven.columns,
.container .eight.columns,
.container .nine.columns,
.container .ten.columns,
.container .eleven.columns,
.container .twelve.columns,
.container .thirteen.columns,
.container .fourteen.columns,
.container .fifteen.columns,
.container .sixteen.columns,
.container .one-third.column,
.container .two-thirds.column { width: 300px; }
/* Offsets */
.container .offset-by-one,
.container .offset-by-two,
.container .offset-by-three,
.container .offset-by-four,
.container .offset-by-five,
.container .offset-by-six,
.container .offset-by-seven,
.container .offset-by-eight,
.container .offset-by-nine,
.container .offset-by-ten,
.container .offset-by-eleven,
.container .offset-by-twelve,
.container .offset-by-thirteen,
.container .offset-by-fourteen,
.container .offset-by-fifteen { padding-left: 0; }
}
/* #Mobile (Landscape)
================================================== */
/* Note: Design for a width of 480px */
@media only screen and (min-width: 480px) and (max-width: 767px) {
.container { width: 420px; }
.container .columns,
.container .column { margin: 0; }
.container .one.column,
.container .one.columns,
.container .two.columns,
.container .three.columns,
.container .four.columns,
.container .five.columns,
.container .six.columns,
.container .seven.columns,
.container .eight.columns,
.container .nine.columns,
.container .ten.columns,
.container .eleven.columns,
.container .twelve.columns,
.container .thirteen.columns,
.container .fourteen.columns,
.container .fifteen.columns,
.container .sixteen.columns,
.container .one-third.column,
.container .two-thirds.column { width: 420px; }
}
/* #Clearing
================================================== */
/* Self Clearing Goodness */
.container:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; }
/* Use clearfix class on parent to clear nested columns,
or wrap each row of columns in a <div class="row"> */
.clearfix:before,
.clearfix:after,
.row:before,
.row:after {
content: '\0020';
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0; }
.row:after,
.clearfix:after {
clear: both; }
.row,
.clearfix {
zoom: 1; }
/* You can also use a <br class="clear" /> to clear columns */
.clear {
clear: both;
display: block;
overflow: hidden;
visibility: hidden;
width: 0;
height: 0;
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,98 @@
/* -*- scss-compile-at-save: nil -*- */
@import "base";
@import "skeleton";
@import "colorbox";
body {
font: 100%/1.4 "PT Sans", sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: "PT Sans", sans-serif;
}
ul {
list-style: disc outside;
padding-left: 20px;
}
.container {
margin: 0 0 0 40px;
}
header {
h1 a { text-decoration: none; }
}
.sidebar {
h1 {
font-size: 35px;
line-height: 40px;
margin: 40px 0;
a { text-decoration: none; }
}
footer {
position: absolute;
bottom: 40px;
}
}
.thumbnail {
text-align: center;
img {
max-width: 100%;
height: auto;
margin: 0 5px 5px 0;
border: 10px solid #fff;
border-radius: 2px 2px 2px 2px;
box-shadow: 0 0 3px #B1B1B1;
padding: 0 0 2px;
&:hover {
box-shadow: 0 0 5px #818181;
}
}
}
.album_thumb {}
.album_title {
display: block;
}
#cboxTitle {
max-width: 90%;
a, a:visited {
color: #ccc;
}
}
@media only screen and (min-width: 767px) {
header {
margin-top: 40px;
}
.sidebar {
position: fixed;
height: 100%;
width: 220px;
}
}
@media only screen and (max-width: 767px) {
.container {
padding-bottom: 40px;
}
.sidebar footer {
bottom: 10px;
}
}
@media only screen and (min-width: 768px) and (max-width: 959px) {
.sidebar {
width: 172px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,130 @@
{% extends "base.html" %}
{% block content %}
<link rel="stylesheet" href="{{ SIGAL_THEME_URL }}/css/style.min.css"> <!-- this needs to be in head -->
<div id="main" role="main" class="twelve columns offset-by-four">
<header>
{% if SIGAL_ALBUM.breadcrumb %}
<h2>
{% for url, title in SIGAL_ALBUM.breadcrumb %}
<a href="{{ url }}">{{ title }}</a>{% if not loop.last %} » {% endif %}
{% endfor -%}
</h2>
<hr>
{% endif %}
</header>
{% set numbers = ["zero", "one", "two", "three", "four", "five", "six",
"seven", "eight", "nine", "ten", "eleven", "twelve"] %}
{% set column_size = SIGAL_COLORBOX_COLUMN_SIZE %}
{% set nb_columns = (12 / column_size)|int %}
{% set column_size_t = numbers[column_size] %}
{% if SIGAL_ALBUM.albums %}
{% for alb in SIGAL_ALBUM.albums %}
{% if loop.index % nb_columns == 1 %}
<div id="albums" class="row">
{% endif%}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<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 % nb_columns == 0 %}
</div>
{% endif%}
{% endfor %}
{% endif %}
{% if SIGAL_ALBUM.medias %}
{% macro img_description(media) -%}
{% if media.big %} data-big="{{ media.big }}"{% endif %}
{% if media.exif %}
{% if media.exif.datetime %}
data-date=", {{ media.exif.datetime }}"
{% endif %}
{% endif %}
{%- endmacro %}
<div id="gallery" class="row">
{% for media in SIGAL_ALBUM.medias %}
{% if media.type == "image" %}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<a href="{{ media.filename }}" class="gallery" title="{{ media.filename }}" {{ img_description(media) }}>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.title if media.title else media.filename }}" /></a>
</div>
{% endif %}
{% if media.type == "video" %}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<a href="#{{ media.filename|replace('.', '')|replace(' ', '') }}"
class="gallery" inline='yes' title="{{ media.filename }}"
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.title if media.title else media.filename }}" /></a>
</div>
<!-- This contains the hidden content for the video -->
<div style='display:none'>
<div id="{{ media.filename|replace('.', '')|replace(' ', '') }}">
<video controls>
<source src='{{ media.filename }}' type='video/webm' />
</video>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if SIGAL_ALBUM.zip %}
<div id="additionnal-infos" class="row">
<p><a href="{{ album.zip }}"
title="Download a zip archive with all images">Download ZIP</a></p>
</div>
{% endif %}
{% if SIGAL_ALBUM.description %}
<div id="description" class="row">
{{ SIGAL_ALBUM.description }}
</div>
{% endif %}
</div>
</div>
{% if SIGAL_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="{{ SIGAL_THEME_URL }}/js/jquery-1.10.2.min.js"%3E%3C/script%3E'))</script>
<script src="{{ SIGAL_THEME_URL }}/js/jquery.colorbox.min.js"></script>
<script>
$(".gallery").colorbox({
rel:"gallery",
transition:"none",
maxWidth: "90%",
maxHeight: "90%",
scalePhotos: true,
current: "{current} / {total}",
title: function () {
title = this.title;
if(this.hasAttribute("data-big")) {
title += " (full size)".link(this.getAttribute("data-big"));
}
if(this.hasAttribute("data-date")) {
title += this.getAttribute("data-date");
}
return title;
},
inline: function() {
return this.hasAttribute("inline");
}
});
</script>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,165 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ album.title }}</title>
<meta name="description" content="">
<meta name="author" content="{{ author }}">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=PT+Sans">
<link rel="stylesheet" href="{{ theme.url }}/css/style.min.css">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="four columns">
<div class="sidebar">
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
{% if settings.links %}
<nav id="menu">
<ul>
{% for title, link in settings.links %}
<li><a href="{{ link }}">{{ title }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
<footer>
<p>{% if author %}&copy; {{ author }} - {% endif %}
Generated by <a href="{{ sigal_link }}">sigal</a></p>
</footer>
</div>
</div>
<div id="main" role="main" class="twelve columns offset-by-four">
<header>
{% if album.breadcrumb %}
<h2>
{% for url, title in album.breadcrumb %}
<a href="{{ url }}">{{ title }}</a>{% if not loop.last %} » {% endif %}
{% endfor -%}
</h2>
<hr>
{% endif %}
</header>
{% set numbers = ["zero", "one", "two", "three", "four", "five", "six",
"seven", "eight", "nine", "ten", "eleven", "twelve"] %}
{% set column_size = settings.colorbox_column_size %}
{% set nb_columns = (12 / column_size)|int %}
{% set column_size_t = numbers[column_size] %}
{% if album.albums %}
{% for alb in album.albums %}
{% if loop.index % nb_columns == 1 %}
<div id="albums" class="row">
{% endif%}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<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 % nb_columns == 0 %}
</div>
{% endif%}
{% endfor %}
{% endif %}
{% if album.medias %}
{% macro img_description(media) -%}
{% if media.big %} data-big="{{ media.big }}"{% endif %}
{% if media.exif %}
{% if media.exif.datetime %}
data-date=", {{ media.exif.datetime }}"
{% endif %}
{% endif %}
{%- endmacro %}
<div id="gallery" class="row">
{% for media in album.medias %}
{% if media.type == "image" %}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<a href="{{ media.filename }}" class="gallery" title="{{ media.filename }}" {{ img_description(media) }}>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.title if media.title else media.filename }}" /></a>
</div>
{% endif %}
{% if media.type == "video" %}
<div class="{{ column_size_t }} columns thumbnail
{% if loop.index % nb_columns == 1 %}alpha{% endif%}
{% if loop.index % nb_columns == 0 %}omega{% endif%}">
<a href="#{{ media.filename|replace('.', '')|replace(' ', '') }}"
class="gallery" inline='yes' title="{{ media.filename }}"
{% if media.big %} data-big="{{ media.big }}"{% endif %}>
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
title="{{ media.title if media.title else media.filename }}" /></a>
</div>
<!-- This contains the hidden content for the video -->
<div style='display:none'>
<div id="{{ media.filename|replace('.', '')|replace(' ', '') }}">
<video controls>
<source src='{{ media.filename }}' type='video/webm' />
</video>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if album.zip %}
<div id="additionnal-infos" class="row">
<p><a href="{{ album.zip }}"
title="Download a zip archive with all images">Download ZIP</a></p>
</div>
{% endif %}
{% if album.description %}
<div id="description" class="row">
{{ album.description }}
</div>
{% endif %}
</div>
</div>
{% 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>
<script>
$(".gallery").colorbox({
rel:"gallery",
transition:"none",
maxWidth: "90%",
maxHeight: "90%",
scalePhotos: true,
current: "{current} / {total}",
title: function () {
title = this.title;
if(this.hasAttribute("data-big")) {
title += " (full size)".link(this.getAttribute("data-big"));
}
if(this.hasAttribute("data-date")) {
title += this.getAttribute("data-date");
}
return title;
},
inline: function() {
return this.hasAttribute("inline");
}
});
</script>
{% endif %}
{% include 'analytics.html' %}
</body>
</html>

View file

@ -0,0 +1,13 @@
{% if settings.google_analytics %}
<script>
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{ settings.google_analytics }}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
{% endif %}

View file

@ -0,0 +1,222 @@
/* Galleria Classic Theme 2012-08-07 | https://raw.github.com/aino/galleria/master/LICENSE | (c) Aino */
#galleria-loader{height:1px!important}
.galleria-container {
position: relative;
overflow: hidden;
}
.galleria-container img {
-moz-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
.galleria-stage {
background: #222;
background: rgba( 0, 0, 0, .15 );
position: absolute;
top: 0;
bottom: 80px;
left: 10px;
right: 10px;
overflow:hidden;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
}
.galleria-thumbnails-container {
height: 70px;
bottom: 0;
position: absolute;
left: 10px;
right: 10px;
z-index: 2;
}
.galleria-carousel .galleria-thumbnails-list {
margin-left: 30px;
margin-right: 30px;
}
.galleria-thumbnails .galleria-image {
height: 60px;
width: 90px;
background: transparent;
margin: 0 10px 0 0;
float: left;
cursor: pointer;
}
.galleria-counter {
position: absolute;
bottom: 10px;
left: 10px;
text-align: right;
z-index: 2;
}
.galleria-loader {
width: 20px;
height: 20px;
position: absolute;
top: 10px;
right: 10px;
z-index: 2;
display: none;
background: url('../img/classic-loader.gif') no-repeat 2px 2px;
}
.galleria-info {
width: 50%;
top: 15px;
left: 25px;
z-index: 2;
position: absolute;
}
.galleria-info-text {
background: #000;
background: rgba(0, 0, 0, .4);
padding: 15px;
display: none;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
}
.galleria-info-title {
font-size: 1.4em;
line-height: 1.4em;
font-weight: bold;
margin: 0;
color: #fff;
}
.galleria-info-description {
font-size: 1.2em;
line-height: 1.2em;
margin: 0;
color: #bbb;
}
.galleria-info-title+.galleria-info-description { margin-top: 7px; }
.galleria-info-close {
width: 9px;
height: 9px;
position: absolute;
top: 10px;
right: 10px;
background-position: -753px -11px;
opacity: .5;
filter: alpha(opacity=50);
cursor: pointer;
display: none;
}
.notouch .galleria-info-close:hover{
opacity:1;
filter: alpha(opacity=100);
}
.touch .galleria-info-close:active{
opacity:1;
filter: alpha(opacity=100);
}
.galleria-info-link {
background-position: -669px -5px;
opacity: .7;
filter: alpha(opacity=70);
position: absolute;
width: 20px;
height: 20px;
cursor: pointer;
}
.notouch .galleria-info-link:hover {
opacity: 1;
filter: alpha(opacity=100);
}
.touch .galleria-info-link:active {
opacity: 1;
filter: alpha(opacity=100);
}
.galleria-image-nav {
position: absolute;
top: 50%;
margin-top: -62px;
width: 100%;
height: 62px;
left: 0;
}
.galleria-image-nav-left,
.galleria-image-nav-right {
opacity: .3;
filter: alpha(opacity=30);
cursor: pointer;
width: 62px;
height: 124px;
position: absolute;
left: 15px;
z-index: 2;
background-position: 0 46px;
}
.galleria-image-nav-right {
left: auto;
right: 15px;
background-position: -254px 46px;
z-index: 2;
}
.notouch .galleria-image-nav-left:hover,
.notouch .galleria-image-nav-right:hover {
opacity: 1;
filter: alpha(opacity=100);
}
.touch .galleria-image-nav-left:active,
.touch .galleria-image-nav-right:active {
opacity: 1;
filter: alpha(opacity=100);
}
.galleria-thumb-nav-left,
.galleria-thumb-nav-right {
cursor: pointer;
display: none;
background-position: -495px 5px;
position: absolute;
left: 0;
top: 0;
height: 40px;
width: 23px;
z-index: 3;
opacity: .8;
filter: alpha(opacity=80);
}
.galleria-thumb-nav-right {
background-position: -578px 5px;
border-right: none;
right: 0;
left: auto;
}
.galleria-thumbnails-container .disabled {
opacity: .2;
filter: alpha(opacity=20);
cursor: default;
}
.notouch .galleria-thumb-nav-left:hover,
.notouch .galleria-thumb-nav-right:hover {
opacity: 1;
filter: alpha(opacity=100);
background-color: #111;
}
.touch .galleria-thumb-nav-left:active,
.touch .galleria-thumb-nav-right:active {
opacity: 1;
filter: alpha(opacity=100);
background-color: #111;
}
.notouch .galleria-thumbnails-container .disabled:hover {
opacity: .2;
filter: alpha(opacity=20);
background-color: transparent;
}
.galleria-carousel .galleria-thumb-nav-left,
.galleria-carousel .galleria-thumb-nav-right {
display: block;
}
.galleria-thumb-nav-left,
.galleria-thumb-nav-right,
.galleria-info-link,
.galleria-info-close,
.galleria-image-nav-left,
.galleria-image-nav-right {
background-image: url('../img/classic-map.png');
background-repeat: no-repeat;
}

View file

@ -0,0 +1,425 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View file

@ -0,0 +1 @@
/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}body{font:16px/1.5 Molengo, sans-serif;background-color:#242424;color:#aaa;text-shadow:0 1px 3px rgba(0,0,0,0.7)}.container{margin:0 auto;text-align:left;width:96%}a,a:link,a:visited{color:#999;text-decoration:underline}a:hover{color:#dadada}header{padding:1em 0;color:#eee}header h1,header h2{color:#eee}header h1 a,header h1 a:link,header h1 a:visited,header h2 a,header h2 a:link,header h2 a:visited{color:#eee;text-shadow:0 2px 0 #000;text-decoration:none}header h1 a:hover,header h2 a:hover{color:#fff;text-decoration:none}header h1,header h2{margin:10px 0}header #menu ul{list-style-type:none;margin:0 0 10px;padding:0}header #menu ul li{display:inline-block}header #menu ul li a,header #menu ul li a:link,header #menu ul li a:visited{color:#aaa;border-bottom:1px solid #aaa;padding-bottom:2px;margin-left:5px;text-decoration:none}header #menu ul li a:hover{color:#eee;border-color:#eee;text-decoration:none}#albums ul{list-style-type:none;padding-left:0}#albums ul li{display:inline-block;margin:0 55px 30px 0;text-align:center;vertical-align:top;width:280px}#albums ul li:nth-child(3n+3){margin-right:0}#albums ul li a img{opacity:1;-webkit-transition:opacity 0.2s ease-in;-moz-transition-property:opacity;-moz-transition-duration:0.2s;-moz-box-shadow:0 1px 6px rgba(0,0,0,0.6);-webkit-box-shadow:0 1px 6px rgba(0,0,0,0.6);-o-box-shadow:0 1px 6px rgba(0,0,0,0.6);box-shadow:0 1px 6px rgba(0,0,0,0.6)}#albums ul li a:hover img{opacity:.5}.album_title{display:block;color:#eee;font-size:1.3em;font-variant:small-caps;font-weight:bold}#gallery{line-height:0;width:100%;height:600px}#gallery video{position:absolute;top:10%;width:100%;margin:0 auto}footer{clear:both;display:block;margin:1em 0;text-align:center}footer a:link,footer a:visited{font-weight:bold;text-decoration:none}footer a:hover{border-bottom:1px solid;text-decoration:none}@media only screen and (min-width: 980px){.container{width:960px}#gallery{width:980px;margin:0 0 40px -10px}header h1,header #menu{display:inline-block;width:49.5%}header #menu{text-align:right}}

View file

@ -0,0 +1,174 @@
/* -*- scss-compile-at-save: nil -*- */
@import "normalize";
$textColor: #aaa;
$titleColor: #eee;
$linkColor: #999;
body {
font: 16px/1.5 Molengo, sans-serif;
background-color: #242424;
color: $textColor;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
.container {
margin: 0 auto;
text-align: left;
width: 96%;
}
a, a:link, a:visited {
color: $linkColor;
text-decoration: underline;
}
a:hover {
color: #dadada;
}
/* header */
header {
padding: 1em 0;
color: $titleColor;
h1, h2 {
color: $titleColor;
a, a:link, a:visited {
color: $titleColor;
text-shadow: 0 2px 0 #000;
text-decoration: none;
}
a:hover {
color: #fff;
text-decoration: none;
}
}
h1, h2 {
margin: 10px 0;
}
#menu {
ul {
list-style-type: none;
margin: 0 0 10px;
padding: 0;
li {
display: inline-block;
a, a:link, a:visited {
color: $textColor;
border-bottom: 1px solid $textColor;
padding-bottom: 2px;
margin-left: 5px;
text-decoration: none;
}
a:hover {
color: $titleColor;
border-color: $titleColor;
text-decoration: none;
}
}
}
}
}
/* gallery */
#albums ul {
list-style-type: none;
padding-left: 0;
li {
display: inline-block;
margin: 0 55px 30px 0;
text-align: center;
vertical-align: top;
width: 280px;
}
li:nth-child(3n+3) {
margin-right: 0;
}
li a img {
opacity: 1;
-webkit-transition: opacity 0.2s ease-in;
-moz-transition-property: opacity;
-moz-transition-duration: 0.2s;
-moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
-webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
-o-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
}
li a:hover img {
opacity: .5;
}
}
.album_thumb {}
.album_title {
display: block;
color: $titleColor;
font-size: 1.3em;
font-variant: small-caps;
font-weight: bold;
}
/* galleria */
#gallery {
line-height: 0;
width: 100%;
height: 600px;
video {
position: absolute;
top: 10%;
width: 100%;
margin: 0 auto;
}
}
/* footer */
footer {
clear: both;
display: block;
margin: 1em 0;
text-align: center;
a:link, a:visited {
font-weight: bold;
text-decoration: none;
}
a:hover {
border-bottom: 1px solid;
text-decoration: none;
}
}
@media only screen and (min-width: 980px) {
.container {
width: 960px;
}
#gallery {
width: 980px;
margin: 0 0 40px -10px;
}
header {
h1, #menu {
display: inline-block;
width: 49.5%;
}
#menu {
text-align: right;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
!function(i){Galleria.addTheme({name:"classic",author:"Galleria",css:"galleria.classic.css",defaults:{transition:"slide",thumbCrop:"height",_toggleInfo:true},init:function(t){Galleria.requires(1.33,"This version of Classic theme requires Galleria 1.3.3 or later");this.addElement("info-link","info-close");this.append({info:["info-link","info-close"]});var e=this.$("info-link,info-close,info-text"),s=Galleria.TOUCH;this.$("loader,counter").show().css("opacity",.4);if(!s){this.addIdleState(this.get("image-nav-left"),{left:-50});this.addIdleState(this.get("image-nav-right"),{right:-50});this.addIdleState(this.get("counter"),{opacity:0})}if(t._toggleInfo===true){e.bind("click:fast",function(){e.toggle()})}else{e.show();this.$("info-link, info-close").hide()}this.bind("thumbnail",function(t){if(!s){i(t.thumbTarget).css("opacity",.6).parent().hover(function(){i(this).not(".active").children().stop().fadeTo(100,1)},function(){i(this).not(".active").children().stop().fadeTo(400,.6)});if(t.index===this.getIndex()){i(t.thumbTarget).css("opacity",1)}}else{i(t.thumbTarget).css("opacity",this.getIndex()?1:.6).bind("click:fast",function(){i(this).css("opacity",1).parent().siblings().children().css("opacity",.6)})}});var n=function(t){i(t.thumbTarget).css("opacity",1).parent().siblings().children().css("opacity",.6)};this.bind("loadstart",function(i){if(!i.cached){this.$("loader").show().fadeTo(200,.4)}window.setTimeout(function(){n(i)},s?300:0);this.$("info").toggle(this.hasInfo())});this.bind("loadfinish",function(i){this.$("loader").fadeOut(200)})}})}(jQuery);

View file

@ -0,0 +1 @@
!function(n,e){Galleria.requires(1.25,"The History Plugin requires Galleria version 1.2.5 or later.");Galleria.History=function(){var i=[],t=false,a=e.location,o=e.document,r=Galleria.IE,s="onhashchange"in e&&(o.mode===undefined||o.mode>7),u,c=function(n){if(u&&!s&&Galleria.IE){n=n||u.location}else{n=a}return parseInt(n.hash.substr(2),10)},f=c(a),l=[],h=function(){n.each(l,function(n,i){i.call(e,c())})},d=function(){n.each(i,function(n,e){e()});t=true},y=function(n){return"/"+n};if(s&&r<8){s=false}if(!s){n(function(){var i=e.setInterval(function(){var n=c();if(!isNaN(n)&&n!=f){f=n;a.hash=y(n);h()}},50);if(r){n('<iframe tabindex="-1" title="empty">').hide().attr("src","about:blank").one("load",function(){u=this.contentWindow;d()}).insertAfter(o.body)}else{d()}})}else{d()}return{change:function(n){l.push(n);if(s){e.onhashchange=h}},set:function(n){if(isNaN(n)){return}if(!s&&r){this.ready(function(){var e=u.document;e.open();e.close();u.location.hash=y(n)})}a.hash=y(n)},ready:function(n){if(!t){i.push(n)}else{n()}}}}()}(jQuery,this);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,145 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ album.title|striptags }}</title>
<meta name="description" content="">
<meta name="author" content="{{ author }}">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Molengo">
<link rel="stylesheet" href="{{ theme.url }}/css/galleria.classic.css">
<link rel="stylesheet" href="{{ theme.url }}/css/style.min.css">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<header>
<h1><a href="{{ album.index_url }}">{{ index_title }}</a></h1>
{% if settings.links %}
<nav id="menu">
<ul>
{% for title, link in settings.links %}
<li><a href="{{ link }}">{{ title }}</a></li>
{% endfor %}
</ul>
</nav>
{% endif %}
{% if album.breadcrumb %}
<h2>
{%- for url, title in album.breadcrumb -%}
<a href="{{ url }}">{{ title }}</a>
{%- if not loop.last %} » {% endif -%}
{% endfor -%}
</h2>
<hr>
{% endif %}
</header>
<div id="main" role="main">
{% if album.albums %}
<div id="albums">
<!-- <h1>Albums</h1> -->
<ul>
{% 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 album.medias %}
{% macro img_description(media) -%}
{%- if media.big %}<a href='{{ media.big }}'>Full size</a>{% endif %}
{% if media.description %}<br>{{ media.description }}{% endif %}
{%- if media.exif %}
<br>
{% if media.exif.iso %}ISO: {{ media.exif.iso }}, {% endif %}
{% if media.exif.focal %}Focal: {{ media.exif.focal }}, {% endif %}
{% if media.exif.exposure %}Exposure: {{ media.exif.exposure }}, {% endif %}
{% if media.exif.fstop %}Fstop: {{ media.exif.fstop }}{% endif %}
{% if media.exif.datetime %}
<br>Date: {{ media.exif.datetime }}
{% endif %}
{% endif %}
{%- endmacro %}
<div id="gallery">
{% for media in album.medias %}
{% if media.type == "image" %}
<a href="{{ media.filename }}">
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
data-title="{{ media.title if media.title else media.filename }}"
data-description="{{ img_description(media) }}"/>
</a>
{% endif %}
{% if media.type == "video" %}
<a href="{{ theme.url }}/img/empty.png">
<img src="{{ media.thumbnail }}" alt="{{ media.filename }}"
data-title="{{ media.title if media.title else media.filename }}"
data-description="{{ img_description(media) }}"
data-layer="<video controls>
<source src='{{ media.filename }}' type='video/webm' />
</video>" />
</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if album.zip %}
<div id="additionnal-infos" class="row">
<p>
<a href="{{ album.zip }}"
title="Download a zip archive with all images">Download ZIP</a>
</p>
</div>
{% endif %}
{% if album.description %}
<div id="description">
{{ album.description }}
</div>
{% endif %}
</div>
<footer>
<p>{% if author %}&copy; {{ author }} - {% endif %}
Generated by <a href="{{ sigal_link }}">sigal</a></p>
</footer>
</div>
{% if album.medias %}
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script>!window.jQuery && document.write(unescape('%3Cscript src="{{ theme.url }}/js/jquery-1.11.1.min.js"%3E%3C/script%3E'))</script>
<script src="{{ theme.url }}/js/galleria-1.3.5.min.js"></script>
<script src="{{ theme.url }}/js/galleria.classic.min.js"></script>
<script src="{{ theme.url }}/js/galleria.history.min.js"></script>
<script>
Galleria.configure({
imageCrop: false,
transition: "fade"
});
Galleria.run("#gallery");
Galleria.ready(function() {
this.attachKeyboard({
right: this.next,
left: this.prev
});
});
</script>
{% endif %}
{% include 'analytics.html' %}
</body>
</html>

75
utils.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2011-2014 - Simon Conseil
# 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.
import codecs
import os
import shutil
from markdown import Markdown
from subprocess import Popen, PIPE
from . import compat
#### TODO: split up/delete these functions.
# TODO: delete
def copy(src, dst, symlink=False):
"""Copy or symlink the file."""
func = os.symlink if symlink else shutil.copy2
if symlink and os.path.lexists(dst):
os.remove(dst)
func(src, dst)
def url_from_path(path):
"""Transform path to url, converting backslashes to slashes if needed."""
if os.sep == '/':
return path
else:
return '/'.join(path.split(os.sep))
# TODO: move to album.py
def read_markdown(filename):
# Use utf-8-sig codec to remove BOM if it is present
with codecs.open(filename, 'r', 'utf-8-sig') as f:
text = f.read()
md = Markdown(extensions=['meta'])
html = md.convert(text)
return {
'title': md.Meta.get('title', [''])[0],
'description': html,
'meta': md.Meta.copy()
}
# TODO: delete
def call_subprocess(cmd):
"""Wrapper to call ``subprocess.Popen`` and return stdout & stderr."""
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if not compat.PY2:
stderr = stderr.decode('utf8')
stdout = stdout.decode('utf8')
return p.returncode, stdout, stderr

178
video.py Normal file
View file

@ -0,0 +1,178 @@
# -*- coding:utf-8 -*-
# Copyright (c) 2013 - Christophe-Marie Duquesne
# Copyright (c) 2013-2014 - Simon Conseil
# 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 logging
import os
import re
import shutil
from os.path import splitext
from . import image
from .album import Video
from .utils import call_subprocess
# TODO: merge with image.py
def check_subprocess(cmd, source, outname):
"""Run the command to resize the video and remove the output file if the
processing fails.
"""
logger = logging.getLogger(__name__)
try:
returncode, stdout, stderr = call_subprocess(cmd)
except KeyboardInterrupt:
logger.debug('Process terminated, removing file %s', outname)
os.remove(outname)
raise
if returncode:
logger.error('Failed to process ' + source)
logger.debug('STDOUT:\n %s', stdout)
logger.debug('STDERR:\n %s', stderr)
logger.debug('Process failed, removing file %s', outname)
os.remove(outname)
def video_size(source):
"""Returns the dimensions of the video."""
ret, stdout, stderr = call_subprocess(['ffmpeg', '-i', source])
pattern = re.compile(r'Stream.*Video.* ([0-9]+)x([0-9]+)')
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=None):
"""Video processor.
:param source: path to a video
:param outname: path to the generated video
:param size: size of the resized video `(width, height)`
:param options: array of options passed to ffmpeg
"""
logger = logging.getLogger(__name__)
# Don't transcode if source is in the required format and
# has fitting datedimensions, copy instead.
w_src, h_src = video_size(source)
w_dst, h_dst = size
logger.debug('Video size: %i, %i -> %i, %i', w_src, h_src, w_dst, h_dst)
base, src_ext = splitext(source)
base, dst_ext = splitext(outname)
if dst_ext == src_ext and w_src <= w_dst and h_src <= h_dst:
logger.debug('Video is smaller than the max size, copying it instead')
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
cmd = ['ffmpeg', '-i', source, '-y'] # -y to overwrite output files
if options is not None:
cmd += options
cmd += resize_opt + [outname]
logger.debug('Processing video: %s', ' '.join(cmd))
check_subprocess(cmd, source, outname)
def generate_thumbnail(source, outname, box, fit=True, options=None):
"""Create a thumbnail image for the video source, based on ffmpeg."""
logger = logging.getLogger(__name__)
tmpfile = outname + ".tmp.jpg"
# dump an image of the video
cmd = ['ffmpeg', '-i', source, '-an', '-r', '1',
'-vframes', '1', '-y', tmpfile]
logger.debug('Create thumbnail for video: %s', ' '.join(cmd))
check_subprocess(cmd, source, outname)
# use the generate_thumbnail function from sigal.image
image.generate_thumbnail(tmpfile, outname, box, fit, options)
# remove the image
os.unlink(tmpfile)
def process_video(filepath, outpath, settings):
"""Process a video: resize, create thumbnail."""
filename = os.path.split(filepath)[1]
basename = splitext(filename)[0]
outname = os.path.join(outpath, basename + '.webm')
generate_video(filepath, outname, settings['video_size'],
options=settings['webm_options'])
if settings['make_thumbs']:
thumb_name = os.path.join(outpath, get_thumb(settings, filename))
generate_thumbnail(
outname, thumb_name, settings['thumb_size'],
fit=settings['thumb_fit'], options=settings['jpg_options'])
def get_thumb(settings, filename):
"""Return the path to the thumb.
examples:
>>> default_settings = create_settings()
>>> 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.lower() in Video.extensions:
ext = '.jpg'
return os.path.join(path, settings['thumb_dir'], settings['thumb_prefix'] +
name + settings['thumb_suffix'] + ext)

118
writer.py Normal file
View file

@ -0,0 +1,118 @@
# -*- coding:utf-8 -*-
# Copyright (c) 2009-2014 - 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
# 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.
# Copyright (c) 2014 - Scott Boone
# This is a copy of Sigal's Writer module with minor changes to the names of
# settings keys for siglican.
from __future__ import absolute_import
import codecs
import jinja2
import logging
import os
import sys
from distutils.dir_util import copy_tree
from jinja2 import Environment, FileSystemLoader, ChoiceLoader, PrefixLoader
from jinja2.exceptions import TemplateNotFound
from .pkgmeta import __url__ as sigal_link
from .utils import url_from_path
class Writer(object):
"""Generate html pages for each directory of images."""
def __init__(self, settings, theme=None, index_title=''):
self.settings = settings
self.output_dir = settings['SIGAL_DESTINATION']
self.index_title = index_title
self.logger = logging.getLogger(__name__)
# check for a custom theme in ./sigal/themes, if not found, look for a
# default in the sigal_theme/themes plugin directory
self.theme = settings['SIGAL_THEME']
default_themes = os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'themes'))
self.logger.debug("siglican: custom theme: %s", self.theme)
self.logger.debug("siglican: default themedir: %s", default_themes)
if not os.path.exists(self.theme):
self.theme = os.path.join(default_themes, os.path.basename(self.theme))
if not os.path.exists(self.theme):
raise Exception("siglican: unable to find theme: %s" %
os.path.basename(self.theme))
self.logger.info("siglican theme: %s", self.theme)
# pelican theme path merged with siglican theme path
theme_paths = [ os.path.join(self.theme, 'templates'),
os.path.join(self.settings['THEME'], 'templates') ]
# setup jinja env
env_options = {'trim_blocks': True}
try:
if tuple(int(x) for x in jinja2.__version__.split('.')) >= (2, 7):
env_options['lstrip_blocks'] = True
except ValueError:
pass
### note: removed the default loader since sigal default templates
### were only used for google analytics, which have been
### removed from the siglican plugin port
env = Environment(loader=FileSystemLoader(theme_paths),
**env_options)
try:
self.template = env.get_template('album.html')
except TemplateNotFound:
self.logger.error('siglican: template album.html not found')
sys.exit(1)
# 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 generate_context(self, album):
"""Generate the context dict for the given path."""
albumdict = {
'SIGAL_ALBUM': album,
'SIGAL_INDEX_TITLE': self.index_title,
'SIGAL_LINK': sigal_link,
'SIGAL_THEME_NAME': os.path.basename(self.theme),
'SIGAL_THEME_URL': url_from_path(
os.path.relpath(self.theme_path,
album.dst_path))
}
albumdict.update(self.settings)
return albumdict
def write(self, album):
"""Generate the HTML page and save it."""
page = self.template.render(**self.generate_context(album))
output_file = os.path.join(album.dst_path, album.output_file)
self.logger.debug("siglican: write output_file: %s",output_file)
with codecs.open(output_file, 'w', 'utf-8') as f:
f.write(page)