diff --git a/docs/index.rst b/docs/index.rst index 08abd78..75585ad 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,4 +11,5 @@ Documentation configuration album_information themes + plugins changelog diff --git a/docs/plugins.rst b/docs/plugins.rst new file mode 100644 index 0000000..95b7c12 --- /dev/null +++ b/docs/plugins.rst @@ -0,0 +1,86 @@ +========= + Plugins +========= + +How to use plugins +------------------ + +Plugins must be specified with the ``plugins`` setting: + +.. code-block:: python + + from sigal.plugins import copyright + plugins = ['sigal.plugins.adjust', copyright] + +You can either specify the name of the module which contains the plugin, or +import the plugin before adding it to the list. The ``plugin_paths`` setting +can be used to specify paths to search for plugins (if they are not in the +python path). + +Write a new plugin +------------------ + +Plugins are based on signals with the blinker_ library. A plugin must +subscribe to a signal (the list is given below). New signals can be added if +need. See an example with the copyright plugin: + +.. _blinker: http://pythonhosted.org/blinker/ + +.. literalinclude:: ../sigal/plugins/copyright.py + :language: python + +Signals +------- + +.. function:: sigal.signals.album_initialized(album) + :noindex: + + Called after the :class:`~sigal.gallery.Album` is initialized. + + :param album: the :class:`~sigal.gallery.Album` object. + +.. data:: sigal.signals.gallery_initialized(gallery) + :noindex: + + Called after the gallery is initialized. + + :param gallery: the :class:`Gallery` object. + +.. data:: sigal.signals.media_initialized(media) + :noindex: + + Called after the :class:`~sigal.gallery.Media` + (:class:`~sigal.gallery.Image` or :class:`~sigal.gallery.Video`) is + initialized. + + :param media: the media object. + +.. data:: sigal.signals.gallery_build(gallery) + :noindex: + + Called after the gallery is build (after media are resized and html files + are written). + + :param gallery: the :class:`Gallery` object. + +.. data:: sigal.signals.img_resized(img, settings=settings) + :noindex: + + Called after the image is resized. This signal work differently, the + modified image object must be returned by the registered funtion. + + :param img: the PIL image object. + :param settings: the settings dict. + +List of plugins +--------------- + +Adjust plugin +============= + +.. automodule:: sigal.plugins.adjust + +Copyright plugin +================ + +.. automodule:: sigal.plugins.copyright diff --git a/requirements.txt b/requirements.txt index adea3ff..67de39d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ argh +blinker Jinja2 Markdown Pillow diff --git a/setup.py b/setup.py index 664373f..cc3277d 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import os from setuptools import setup -requires = ['argh', 'jinja2', 'Markdown', 'Pillow', 'pilkit'] +requires = ['argh', 'blinker', 'jinja2', 'Markdown', 'Pillow', 'pilkit'] entry_points = { 'console_scripts': ['sigal = sigal:main'] diff --git a/sigal/__init__.py b/sigal/__init__.py index 70030fe..d94d0ea 100644 --- a/sigal/__init__.py +++ b/sigal/__init__.py @@ -28,10 +28,12 @@ sigal is yet another python script to prepare a static gallery of images: * resize images, create thumbnails with some options (squared thumbs, ...). * generate html pages. + """ from __future__ import absolute_import, print_function +import importlib import io import locale import logging @@ -41,6 +43,7 @@ import time from argh import ArghParser, arg +from .compat import server, socketserver, string_types from .gallery import Gallery from .log import init_logging from .pkgmeta import __version__ @@ -105,6 +108,8 @@ def build(source, destination, debug=False, verbose=False, force=False, sys.exit(1) locale.setlocale(locale.LC_ALL, settings['locale']) + init_plugins(settings) + gal = Gallery(settings, theme=theme, ncpu=ncpu) gal.build(force=force) @@ -120,14 +125,36 @@ def build(source, destination, debug=False, verbose=False, force=False, 'seconds.').format(duration=time.time() - start_time, **gal.stats)) +def init_plugins(settings): + """Load plugins and call register().""" + + logger = logging.getLogger(__name__) + logger.debug('Plugin paths: %s', settings['plugin_paths']) + + for path in settings['plugin_paths']: + sys.path.insert(0, path) + + for plugin in settings['plugins']: + try: + if isinstance(plugin, string_types): + mod = importlib.import_module(plugin) + mod.register(settings) + else: + plugin.register(settings) + logger.debug('Registered plugin %s', plugin) + except Exception as e: + logger.error('Failed to load plugin %s: %r', plugin, e) + + for path in settings['plugin_paths']: + sys.path.remove(path) + + @arg('path', nargs='?', default='_build', help='Directory to serve (default: _build/)') def serve(path): """Run a simple web server.""" if os.path.exists(path): - from .compat import server, socketserver - os.chdir(path) PORT = 8000 Handler = server.SimpleHTTPRequestHandler diff --git a/sigal/gallery.py b/sigal/gallery.py index a729642..6047a94 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -37,7 +37,7 @@ from datetime import datetime from os.path import isfile, join, splitext from PIL import Image as PILImage -from . import image, video +from . import image, video, signals from .compat import UnicodeMixin, strxfrm, url_quote from .image import process_image, get_exif_tags from .log import colored, BLUE @@ -80,6 +80,7 @@ class Media(UnicodeMixin): self.raw_exif = None self.exif = None self.date = None + signals.media_initialized.send(self) def __repr__(self): return "<%s>(%r)" % (self.__class__.__name__, str(self)) @@ -221,6 +222,8 @@ class Album(UnicodeMixin): medias.sort(key=key, reverse=settings['medias_sort_reverse']) + signals.album_initialized.send(self) + def __repr__(self): return "<%s>(path=%r, title=%r)" % (self.__class__.__name__, self.path, self.title) @@ -458,6 +461,7 @@ class Gallery(object): albums[relpath] = album self.logger.debug('Albums:\n%r', albums.values()) + signals.gallery_initialized.send(self) def init_pool(self, ncpu): try: @@ -527,6 +531,8 @@ class Gallery(object): for album in self.albums.values(): self.writer.write(album) + signals.gallery_build.send(self) + def process_dir(self, album, force=False): """Process a list of images in a directory.""" diff --git a/sigal/image.py b/sigal/image.py index 8404eb9..a5bf1cc 100644 --- a/sigal/image.py +++ b/sigal/image.py @@ -38,11 +38,11 @@ from copy import deepcopy from datetime import datetime from PIL.ExifTags import TAGS, GPSTAGS from PIL import Image as PILImage -from PIL import ImageDraw, ImageOps -from pilkit.processors import Transpose, Adjust +from PIL import ImageOps +from pilkit.processors import Transpose from pilkit.utils import save_image -from . import compat +from . import compat, signals from .settings import get_thumb @@ -97,11 +97,10 @@ def generate_image(source, outname, settings, options=None): processor = processor_cls(*settings['img_size'], upscale=False) img = processor.process(img) - # Adjust the image after resizing - img = Adjust(**settings['adjust_options']).process(img) - - if settings['copyright']: - add_copyright(img, settings['copyright']) + # 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)) @@ -152,13 +151,6 @@ def process_image(filepath, outpath, settings): fit=settings['thumb_fit'], options=options) -def add_copyright(img, text): - """Add a copyright to the image.""" - - draw = ImageDraw.Draw(img) - draw.text((5, img.size[1] - 15), '\xa9 ' + text) - - def _get_exif_data(filename): """Return a dict with EXIF data.""" diff --git a/sigal/plugins/__init__.py b/sigal/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sigal/plugins/adjust.py b/sigal/plugins/adjust.py new file mode 100644 index 0000000..eb6b0f5 --- /dev/null +++ b/sigal/plugins/adjust.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +"""Plugin which adjust the image after resizing. + +Based on pilkit's Adjust_ processor. + +.. _Adjust: https://github.com/matthewwithanm/pilkit/blob/master/pilkit/processors/base.py#L19 + +Settings:: + + adjust_options = {'color': 1.0, + 'brightness': 1.0, + 'contrast': 1.0, + 'sharpness': 1.0} + +""" + +import logging +from sigal import signals +from pilkit.processors import Adjust + +logger = logging.getLogger(__name__) + + +def adjust(img, settings=None): + logger.debug('Adjust image %r', img) + return Adjust(**settings['adjust_options']).process(img) + + +def register(settings): + if settings.get('adjust_options'): + signals.img_resized.connect(adjust) + else: + logger.warning('Adjust options are not set') diff --git a/sigal/plugins/copyright.py b/sigal/plugins/copyright.py new file mode 100644 index 0000000..5da08d0 --- /dev/null +++ b/sigal/plugins/copyright.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +"""Plugin which add a copyright to the image. + +Settings: + +- ``copyright``: the copyright text. + +TODO: Add more settings (font, size, ...) + +""" + +import logging +from PIL import ImageDraw +from sigal import signals + +logger = logging.getLogger(__name__) + + +def add_copyright(img, settings=None): + logger.debug('Adding copyright to %r', img) + draw = ImageDraw.Draw(img) + draw.text((5, img.size[1] - 15), settings['copyright']) + return img + + +def register(settings): + if settings.get('copyright'): + signals.img_resized.connect(add_copyright) + else: + logger.warning('Copyright text is not set') diff --git a/sigal/settings.py b/sigal/settings.py index f413d63..ea3cf09 100644 --- a/sigal/settings.py +++ b/sigal/settings.py @@ -31,13 +31,10 @@ from .compat import PY2 _DEFAULT_CONFIG = { - 'adjust_options': {'color': 1.0, 'brightness': 1.0, - 'contrast': 1.0, 'sharpness': 1.0}, 'albums_sort_reverse': False, 'autorotate_images': True, 'colorbox_column_size': 4, 'copy_exif_data': False, - 'copyright': '', 'destination': '_build', 'files_to_copy': (), 'google_analytics': '', @@ -55,6 +52,8 @@ _DEFAULT_CONFIG = { 'make_thumbs': True, 'orig_dir': 'original', 'orig_link': False, + 'plugins': [], + 'plugin_paths': [], 'source': '', 'theme': 'colorbox', 'thumb_dir': 'thumbnails', diff --git a/sigal/signals.py b/sigal/signals.py new file mode 100644 index 0000000..c175d33 --- /dev/null +++ b/sigal/signals.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from blinker import signal + +img_resized = signal('img_resized') + +album_initialized = signal('album_initialized') +gallery_initialized = signal('gallery_initialized') +gallery_build = signal('gallery_build') +media_initialized = signal('media_initialized') diff --git a/sigal/templates/sigal.conf.py b/sigal/templates/sigal.conf.py index 364a3ce..9bf8956 100644 --- a/sigal/templates/sigal.conf.py +++ b/sigal/templates/sigal.conf.py @@ -27,13 +27,6 @@ img_size = (800, 600) # - None: don't resize # img_processor = 'ResizeToFit' -# Adjust the image after resizing it. A default value of 1.0 leaves the images -# untouched. -# adjust_options = {'color': 1.0, -# 'brightness': 1.0, -# 'contrast': 1.0, -# 'sharpness': 1.0} - # Generate thumbnails # make_thumbs = True @@ -104,9 +97,6 @@ ignore_files = [] # links = [('Example link', 'http://example.org'), # ('Another link', 'http://example.org')] -# Add a copyright text on the image (default: '') -# copyright = "An example copyright message" - # Google Analytics tracking code (UA-xxxx-x) # google_analytics = '' @@ -138,3 +128,17 @@ ignore_files = [] # Then the image size must be adapted to fit the column size. # The default is 4 columns which gives 220px. 3 columns gives 160px. # colorbox_column_size = 4 + +# -------- +# Plugins +# -------- + +# Add a copyright text on the image (default: '') +# copyright = "© An example copyright message" + +# Adjust the image after resizing it. A default value of 1.0 leaves the images +# untouched. +# adjust_options = {'color': 1.0, +# 'brightness': 1.0, +# 'contrast': 1.0, +# 'sharpness': 1.0} diff --git a/tests/sample/sigal.conf.py b/tests/sample/sigal.conf.py index 348d0d5..ec6a44a 100644 --- a/tests/sample/sigal.conf.py +++ b/tests/sample/sigal.conf.py @@ -8,7 +8,11 @@ keep_orig = True links = [('Example link', 'http://example.org'), ('Another link', 'http://example.org')] -copyright = "An example copyright message" +from sigal.plugins import copyright +plugins = ['sigal.plugins.adjust', copyright] +copyright = u"© An example copyright message" +adjust_options = {'color': 0.0, 'brightness': 1.0, + 'contrast': 1.0, 'sharpness': 0.0} # theme = 'galleria' # thumb_size = (280, 210)