Compare commits

..

No commits in common. "main" and "v0.0.2b" have entirely different histories.

9 changed files with 136 additions and 178 deletions

15
LICENSE
View file

@ -1,5 +1,3 @@
SOFTWARE
The MIT License (MIT)
Copyright (c) 2014 - Scott Boone
@ -20,16 +18,3 @@ 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.
===
IMAGES
All example images copyright (c) 2014 - Scott Boone
They are licensed under Creative Commons Attribution 4.0 International:
http://creativecommons.org/licenses/by/4.0/legalcode
This license allows others to distribute, remix, tweak, and build upon this
work, even commercially, as long as they provide credit for the original
creation.

View file

@ -7,8 +7,7 @@ Colorbox/Galleria static site generator.
##How To
1. Add this package to your Pelican plugins directory.
2. Add 'siglican' to PLUGINS in pelicanconf.py. Add SIGLICAN_ settings to
pelicanconf.py as desired (see "Pelican Configuration Settings" below for
a complete list).
pelicanconf.py as desired.
3. Create a *siglican* directory in your base directory, at the same level as
*content*.
4. Drag gallery.md from examples to your pelican *pages* directory and edit it.
@ -22,9 +21,7 @@ Colorbox/Galleria static site generator.
That will give your siglican theme a way to inject gallery-specific css and
javascript into your gallery pages.
8. Create an 'images' folder under 'siglican'. Add album folders along with
images and metadata. See *examples/images* for simple examples or the
[Sigal documentation](http://sigal.readthedocs.org/en/latest/) for more
details.
images and metadata.
###Example directory excerpt:
```

View file

@ -33,11 +33,9 @@ import os
import logging
from collections import defaultdict
from PIL import Image as PILImage
from .compat import strxfrm, UnicodeMixin, url_quote
from .utils import read_markdown, url_from_path
from .image import process_image, get_exif_tags
class Media(UnicodeMixin):
"""Base Class for media files.
@ -84,7 +82,22 @@ class Media(UnicodeMixin):
@property
def thumbnail(self):
"""Path to the thumbnail image (relative to the album directory)."""
# cleanup: make this deal better with SIGLICAN_MAKE_THUMBS: False
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['SIGLICAN_THUMB_SIZE'],
fit=self.settings['SIGLICAN_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):
@ -397,3 +410,81 @@ def get_thumb(settings, filename):
ext = '.jpg'
return os.path.join(path, settings['SIGLICAN_THUMB_DIR'], settings['SIGLICAN_THUMB_PREFIX'] +
name + settings['SIGLICAN_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)

View file

@ -4,7 +4,7 @@
{% block content %}
<h2>albums</h2>
<ul>
{% for aname,album in ALBUMS.items() %}
{% for aname,album in ALBUMS.iteritems() %}
<li>{{ aname }}<a href="{{ SITEURL }}/{{ SIGLICAN_DESTINATION }}/{{ album.url }}"><img src="{{ SITEURL }}/{{ SIGLICAN_DESTINATION }}/{{ album.thumbnail }}"></a>
{% endfor %}
</ul>

View file

@ -1,3 +0,0 @@
To make this play nicely with limited modification, move siglican_gallery.html
to your Pelican template. This will provide an entry point to colorbox which is
driven by album.html here.

View file

@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h2>albums</h2>
<hr>
<div class="row">
{% for aname,album in ROOT_ALBUMS.iteritems() %}
{% if loop.last %}
<div class="four columns end" style="text-align:right;">
{% else %}
<div class="four columns" style="text-align:right;">
{% endif %}
<a href="{{ SITEURL }}/{{ SIGLICAN_DESTINATION }}/{{ album.url }}"><img src="{{ SITEURL }}/{{ SIGLICAN_DESTINATION }}/{{ album.thu
mbnail }}"><br>{{ aname }}</a><p>
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -48,6 +48,7 @@ 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
@ -176,84 +177,6 @@ def dms_to_degrees(v):
s = float(v[2][0]) / float(v[2][1])
return d + (m / 60.0) + (s / 3600.0)
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)
def get_thumb(settings, filename):
"""Return the path to the thumb.
@ -272,8 +195,7 @@ def get_thumb(settings, filename):
path, filen = os.path.split(filename)
name, ext = os.path.splitext(filen)
# TODO: replace this list with Video.extensions github #16
if ext.lower() in ('.mov', '.avi', '.mp4', '.webm', '.ogv'):
if ext.lower() in Video.extensions:
ext = '.jpg'
return os.path.join(path, settings['SIGLICAN_THUMB_DIR'], settings['SIGLICAN_THUMB_PREFIX'] +
name + settings['SIGLICAN_THUMB_SUFFIX'] + ext)

View file

@ -1,7 +1,7 @@
# -*- 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
@ -11,7 +11,7 @@
#
# 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
@ -20,8 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import print_function
import os
import sys
import locale
@ -37,7 +35,7 @@ from .writer import Writer
logger = logging.getLogger(__name__)
# Default config from Sigal's settings module. These have been changed to
# Default config from Sigal's settings module. These have been changed to
# upper case because Pelican does not recognize lower case configuration names.
# note: if a default is changed, please also update README.md
_DEFAULT_SIGLICAN_SETTINGS = {
@ -83,9 +81,9 @@ 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,
# 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."""
@ -94,27 +92,27 @@ class SigalGalleryGenerator(Generator):
# 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 list(_DEFAULT_SIGLICAN_SETTINGS.keys()):
for k in _DEFAULT_SIGLICAN_SETTINGS.keys()[:]:
self.settings[k] = self.settings.get(k, _DEFAULT_SIGLICAN_SETTINGS[k])
#logger.debug("sigal.pelican: setting %s: %s",k,self.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['SIGLICAN_SOURCE']
self.settings['SIGLICAN_SOURCE'] = os.path.normpath(self.settings['PATH'] +
self.settings['SIGLICAN_SOURCE'] = os.path.normpath(self.settings['PATH'] +
"/../" + self.settings['SIGLICAN_SOURCE'] + '/images')
self.settings['SIGLICAN_THEME'] = os.path.normpath(self.settings['PATH'] +
"/../" + init_source + "/" + self.settings['SIGLICAN_THEME'])
self.settings['SIGLICAN_DESTINATION'] = os.path.normpath(
self.settings['OUTPUT_PATH'] + "/" + self.settings['SIGLICAN_DESTINATION'])
enc = locale.getpreferredencoding() if PY2 else None
# test for existence of source directories
pathkeys = ['SIGLICAN_SOURCE', 'SIGLICAN_THEME']
for k in pathkeys:
@ -127,7 +125,7 @@ class SigalGalleryGenerator(Generator):
logger.error("siglican: missing source directory %s: %s",
k,self.settings[k])
sys.exit(1)
# normalize sizes as e landscape
for key in ('SIGLICAN_IMG_SIZE', 'SIGLICAN_THUMB_SIZE', 'SIGLICAN_VIDEO_SIZE'):
w, h = self.settings[key]
@ -135,11 +133,11 @@ class SigalGalleryGenerator(Generator):
self.settings[key] = (h, w)
logger.warning("siglican: The %s setting should be specified "
"with the largest value first.", key)
if not self.settings['SIGLICAN_IMG_PROCESSOR']:
logger.info('No Processor, images will not be resized')
# based on Sigal's Gallery.__init__() method:
# based on Sigal's Gallery.__init__() method:
def generate_context(self):
""""Update the global Pelican context that's shared between generators."""
@ -174,66 +172,55 @@ class SigalGalleryGenerator(Generator):
for d in dirs[:]:
path = os.path.join(relpath, d) if relpath != '.' else d
if path not in self.albums.keys():
dirs.remove(d)
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())
# update the jinja context so that templates can access it:
#self._update_context(('albums', )) # unnecessary? **
self.context['ALBUMS'] = self.albums # ** change to SIGLICAN_ALBUMS?
root_albums = {}
for k,v in self.albums.items():
if os.sep not in v.path:
root_albums[k] = v
self.context['ROOT_ALBUMS'] = root_albums
# update the jinja context with the default sigal settings:
for k,v in _DEFAULT_SIGLICAN_SETTINGS.items():
for k,v in _DEFAULT_SIGLICAN_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."""
# note: ignore the writer sent by Pelican because it's not certain
# which Writer it will send. if another active plugin also implements
# Writer, Pelican may send that instead of one of its core Writers.
# I logged a feature request here:
# https://github.com/getpelican/pelican/issues/1459
# create destination directory
if not os.path.isdir(self.settings['SIGLICAN_DESTINATION']):
os.makedirs(self.settings['SIGLICAN_DESTINATION'])
# github7 ** improve exception catching
# github7 ** improve exception catching
# github8 ** re-integrate multiprocessing logic from Sigal
# generate thumbnails, process images, and move them to the destination
if logger.getEffectiveLevel() > logging.INFO:
print('siglican is processing media: ', end='')
sys.stdout.flush()
albums = self.albums
for a in albums:
logger.info("siglican: processing album: %s",a)
logger.debug("siglican: creating directory for %s",a)
albums[a].create_output_directories()
for media in albums[a].medias:
if logger.getEffectiveLevel() > logging.INFO:
print('.', end='')
sys.stdout.flush()
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:
logger.info("siglican: processing %r , source: %s, dst: %s",
media,media.src_path,media.dst_path)
self.stats[media.type] += 1
logger.debug("MEDIA TYPE: %s",media.type)
# create/move resized images and thumbnails to output dirs:
@ -243,9 +230,7 @@ class SigalGalleryGenerator(Generator):
elif media.type == 'video':
process_video(media.src_path,os.path.dirname(
media.dst_path),self.settings)
if logger.getEffectiveLevel() > logging.INFO:
print('')
# generate the index.html files for the albums
if self.settings['SIGLICAN_WRITE_HTML']: # defaults to True
# locate the theme; check for a custom theme in ./sigal/themes, if not
@ -260,21 +245,19 @@ class SigalGalleryGenerator(Generator):
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)
self.writer = Writer(self.context, self.theme, 'album')
for album in self.albums.values():
self.writer.write(album)
## possible cleanup:
## - bring back Writer options that Sigal had?
## - make sure thumbnails don't break in some cases [fixed?]
def get_generators(generators):
return SigalGalleryGenerator
def register():
signals.get_generators.connect(get_generators)
signals.get_generators.connect(get_generators)

View file

@ -30,6 +30,7 @@ import shutil
from os.path import splitext
from . import image
from .album import Video
from .utils import call_subprocess
# TODO: merge with image.py
@ -169,8 +170,7 @@ def get_thumb(settings, filename):
path, filen = os.path.split(filename)
name, ext = os.path.splitext(filen)
# TODO: replace this list with Video.extensions github #16
if ext.lower() in ('.mov', '.avi', '.mp4', '.webm', '.ogv'):
if ext.lower() in Video.extensions:
ext = '.jpg'
return os.path.join(path, settings['thumb_dir'], settings['thumb_prefix'] +
name + settings['thumb_suffix'] + ext)