summaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task/galleries.py
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-11-20 16:58:50 -0300
commitca94afc07df55cb7fc6fe3b4f3011877b7881195 (patch)
treed81e1f275aa77545f33740723f307a83dde2e0b4 /nikola/plugins/task/galleries.py
parentf794eee787e9cde54e6b8f53e45d69c9ddc9936a (diff)
Imported Upstream version 6.2.1upstream/6.2.1
Diffstat (limited to 'nikola/plugins/task/galleries.py')
-rw-r--r--nikola/plugins/task/galleries.py553
1 files changed, 553 insertions, 0 deletions
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py
new file mode 100644
index 0000000..cf670e0
--- /dev/null
+++ b/nikola/plugins/task/galleries.py
@@ -0,0 +1,553 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# 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 unicode_literals
+import codecs
+import datetime
+import glob
+import json
+import mimetypes
+import os
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
+
+Image = None
+try:
+ from PIL import Image, ExifTags # NOQA
+except ImportError:
+ try:
+ import Image as _Image
+ import ExifTags
+ Image = _Image
+ except ImportError:
+ pass
+import PyRSS2Gen as rss
+
+from nikola.plugin_categories import Task
+from nikola import utils
+from nikola.post import Post
+
+
+class Galleries(Task):
+ """Render image galleries."""
+
+ name = 'render_galleries'
+ dates = {}
+
+ def set_site(self, site):
+ site.register_path_handler('gallery', self.gallery_path)
+ site.register_path_handler('gallery_rss', self.gallery_rss_path)
+ return super(Galleries, self).set_site(site)
+
+ def gallery_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ self.site.config['INDEX_FILE']] if _f]
+
+ def gallery_rss_path(self, name, lang):
+ return [_f for _f in [self.site.config['TRANSLATIONS'][lang],
+ self.site.config['GALLERY_PATH'], name,
+ 'rss.xml'] if _f]
+
+ def gen_tasks(self):
+ """Render image galleries."""
+
+ self.logger = utils.get_logger('render_galleries', self.site.loghandlers)
+ self.image_ext_list = ['.jpg', '.png', '.jpeg', '.gif', '.svg', '.bmp', '.tiff']
+ self.image_ext_list.extend(self.site.config.get('EXTRA_IMAGE_EXTENSIONS', []))
+
+ self.kw = {
+ 'thumbnail_size': self.site.config['THUMBNAIL_SIZE'],
+ 'max_image_size': self.site.config['MAX_IMAGE_SIZE'],
+ 'output_folder': self.site.config['OUTPUT_FOLDER'],
+ 'cache_folder': self.site.config['CACHE_FOLDER'],
+ 'default_lang': self.site.config['DEFAULT_LANG'],
+ 'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_path': self.site.config['GALLERY_PATH'],
+ 'sort_by_date': self.site.config['GALLERY_SORT_BY_DATE'],
+ 'filters': self.site.config['FILTERS'],
+ 'translations': self.site.config['TRANSLATIONS'],
+ 'global_context': self.site.GLOBAL_CONTEXT,
+ "feed_length": self.site.config['FEED_LENGTH'],
+ }
+
+ yield self.group_task()
+
+ template_name = "gallery.tmpl"
+
+ # Find all galleries we need to process
+ self.find_galleries()
+
+ # Create all output folders
+ for task in self.create_galleries():
+ yield task
+
+ # For each gallery:
+ for gallery in self.gallery_list:
+
+ # Create subfolder list
+ folder_list = [x.split(os.sep)[-2] for x in
+ glob.glob(os.path.join(gallery, '*') + os.sep)]
+
+ # Parse index into a post (with translations)
+ post = self.parse_index(gallery)
+
+ # Create image list, filter exclusions
+ image_list = self.get_image_list(gallery)
+
+ # Sort as needed
+ # Sort by date
+ if self.kw['sort_by_date']:
+ image_list.sort(key=lambda a: self.image_date(a))
+ else: # Sort by name
+ image_list.sort()
+
+ # Create thumbnails and large images in destination
+ for image in image_list:
+ for task in self.create_target_images(image):
+ yield task
+
+ # Remove excluded images
+ for image in self.get_excluded_images(gallery):
+ for task in self.remove_excluded_image(image):
+ yield task
+
+ crumbs = utils.get_crumbs(gallery)
+
+ # Create index.html for each language
+ for lang in self.kw['translations']:
+ dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ dst = os.path.normpath(dst)
+
+ context = {}
+ context["lang"] = lang
+ if post:
+ context["title"] = post.title(lang)
+ else:
+ context["title"] = os.path.basename(gallery)
+ context["description"] = None
+
+ image_name_list = [os.path.basename(p) for p in image_list]
+
+ if self.kw['use_filename_as_title']:
+ img_titles = []
+ for fn in image_name_list:
+ name_without_ext = os.path.splitext(fn)[0]
+ img_titles.append(
+ 'id="{0}" alt="{1}" title="{2}"'.format(
+ name_without_ext,
+ name_without_ext,
+ utils.unslugify(name_without_ext)))
+ else:
+ img_titles = [''] * len(image_name_list)
+
+ thumbs = ['.thumbnail'.join(os.path.splitext(p)) for p in image_list]
+ thumbs = [os.path.join(self.kw['output_folder'], t) for t in thumbs]
+
+ ## TODO: in v7 remove images from context, use photo_array
+ context["images"] = list(zip(image_name_list, thumbs, img_titles))
+ context["folders"] = folder_list
+ context["crumbs"] = crumbs
+ context["permalink"] = self.site.link(
+ "gallery", os.path.basename(gallery), lang)
+ # FIXME: use kw
+ context["enable_comments"] = (
+ self.site.config["COMMENTS_IN_GALLERIES"])
+ context["thumbnail_size"] = self.kw["thumbnail_size"]
+
+ # FIXME: render post in a task
+ if post:
+ post.compile(lang)
+ context['text'] = post.text(lang)
+ else:
+ context['text'] = ''
+
+ file_dep = self.site.template_system.template_deps(
+ template_name) + image_list + thumbs
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': dst,
+ 'file_dep': file_dep,
+ 'targets': [dst],
+ 'actions': [
+ (self.render_gallery_index, (
+ template_name,
+ dst,
+ context,
+ image_list,
+ thumbs,
+ file_dep))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ 2: self.site.config["COMMENTS_IN_GALLERIES"],
+ 3: context,
+ })],
+ }, self.kw['filters'])
+
+ # RSS for the gallery
+ rss_dst = os.path.join(
+ self.kw['output_folder'],
+ self.site.path(
+ "gallery_rss",
+ os.path.relpath(gallery, self.kw['gallery_path']), lang))
+ rss_dst = os.path.normpath(rss_dst)
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': rss_dst,
+ 'file_dep': file_dep,
+ 'targets': [rss_dst],
+ 'actions': [
+ (self.gallery_rss, (
+ image_list,
+ img_titles,
+ lang,
+ self.site.link(
+ "gallery_rss", os.path.basename(gallery), lang),
+ rss_dst,
+ context['title']
+ ))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw,
+ })],
+ }, self.kw['filters'])
+
+ def find_galleries(self):
+ """Find all galleries to be processed according to conf.py"""
+
+ self.gallery_list = []
+ for root, dirs, files in os.walk(self.kw['gallery_path']):
+ self.gallery_list.append(root)
+
+ def create_galleries(self):
+ """Given a list of galleries, create the output folders."""
+
+ # gallery_path is "gallery/foo/name"
+ for gallery_path in self.gallery_list:
+ gallery_name = os.path.relpath(gallery_path, self.kw['gallery_path'])
+ # have to use dirname because site.path returns .../index.html
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ output_gallery = os.path.normpath(output_gallery)
+ # Task to create gallery in output/
+ yield {
+ 'basename': self.name,
+ 'name': output_gallery,
+ 'actions': [(utils.makedirs, (output_gallery,))],
+ 'targets': [output_gallery],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }
+
+ def parse_index(self, gallery):
+ """Returns a Post object if there is an index.txt."""
+
+ index_path = os.path.join(gallery, "index.txt")
+ destination = os.path.join(
+ self.kw["output_folder"],
+ gallery)
+ if os.path.isfile(index_path):
+ post = Post(
+ index_path,
+ self.site.config,
+ destination,
+ False,
+ self.site.MESSAGES,
+ 'story.tmpl',
+ self.site.get_compiler(index_path).compile_html
+ )
+ else:
+ post = None
+ return post
+
+ def get_excluded_images(self, gallery_path):
+ exclude_path = os.path.join(gallery_path, "exclude.meta")
+
+ try:
+ f = open(exclude_path, 'r')
+ excluded_image_name_list = f.read().split()
+ except IOError:
+ excluded_image_name_list = []
+
+ excluded_image_list = ["{0}/{1}".format(gallery_path, i) for i in excluded_image_name_list]
+ return excluded_image_list
+
+ def get_image_list(self, gallery_path):
+
+ # Gather image_list contains "gallery/name/image_name.jpg"
+ image_list = []
+
+ for ext in self.image_ext_list:
+ image_list += glob.glob(gallery_path + '/*' + ext.lower()) +\
+ glob.glob(gallery_path + '/*' + ext.upper())
+
+ # Filter ignored images
+ excluded_image_list = self.get_excluded_images(gallery_path)
+ image_set = set(image_list) - set(excluded_image_list)
+ image_list = list(image_set)
+ return image_list
+
+ def create_target_images(self, img):
+ gallery_name = os.path.relpath(os.path.dirname(img), self.kw['gallery_path'])
+ output_gallery = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", gallery_name)))
+ # Do thumbnails and copy originals
+ # img is "galleries/name/image_name.jpg"
+ # img_name is "image_name.jpg"
+ # fname, ext are "image_name", ".jpg"
+ # thumb_path is
+ # "output/GALLERY_PATH/name/image_name.thumbnail.jpg"
+ img_name = os.path.basename(img)
+ fname, ext = os.path.splitext(img_name)
+ thumb_path = os.path.join(
+ output_gallery,
+ ".thumbnail".join([fname, ext]))
+ # thumb_path is "output/GALLERY_PATH/name/image_name.jpg"
+ orig_dest_path = os.path.join(output_gallery, img_name)
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': thumb_path,
+ 'file_dep': [img],
+ 'targets': [thumb_path],
+ 'actions': [
+ (self.resize_image,
+ (img, thumb_path, self.kw['thumbnail_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['thumbnail_size']
+ })],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': orig_dest_path,
+ 'file_dep': [img],
+ 'targets': [orig_dest_path],
+ 'actions': [
+ (self.resize_image,
+ (img, orig_dest_path, self.kw['max_image_size']))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed({
+ 1: self.kw['max_image_size']
+ })],
+ }, self.kw['filters'])
+
+ def remove_excluded_image(self, img):
+ # Remove excluded images
+ # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path
+ # and we should remove both the large and thumbnail *destination* paths
+
+ img = os.path.relpath(img, self.kw['gallery_path'])
+ output_folder = os.path.dirname(
+ os.path.join(
+ self.kw["output_folder"],
+ self.site.path("gallery", os.path.dirname(img))))
+ img_path = os.path.join(output_folder, os.path.basename(img))
+ fname, ext = os.path.splitext(img_path)
+ thumb_path = fname + '.thumbnail' + ext
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': thumb_path,
+ 'actions': [
+ (utils.remove_file, (thumb_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ yield utils.apply_filters({
+ 'basename': '_render_galleries_clean',
+ 'name': img_path,
+ 'actions': [
+ (utils.remove_file, (img_path,))
+ ],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(self.kw)],
+ }, self.kw['filters'])
+
+ def render_gallery_index(
+ self,
+ template_name,
+ output_name,
+ context,
+ img_list,
+ thumbs,
+ file_dep):
+ """Build the gallery index."""
+
+ # The photo array needs to be created here, because
+ # it relies on thumbnails already being created on
+ # output
+
+ def url_from_path(p):
+ url = '/'.join(os.path.relpath(p, os.path.dirname(output_name) + os.sep).split(os.sep))
+ return url
+
+ photo_array = []
+ for img, thumb in zip(img_list, thumbs):
+ im = Image.open(thumb)
+ w, h = im.size
+ title = ''
+ if self.kw['use_filename_as_title']:
+ title = utils.unslugify(os.path.splitext(img)[0])
+ # Thumbs are files in output, we need URLs
+ photo_array.append({
+ 'url': url_from_path(img),
+ 'url_thumb': url_from_path(thumb),
+ 'title': title,
+ 'size': {
+ 'w': w,
+ 'h': h
+ },
+ })
+ context['photo_array_json'] = json.dumps(photo_array)
+ context['photo_array'] = photo_array
+
+ self.site.render_template(template_name, output_name, context)
+
+ def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title):
+ """Create a RSS showing the latest images in the gallery.
+
+ This doesn't use generic_rss_renderer because it
+ doesn't involve Post objects.
+ """
+
+ def make_url(url):
+ return urljoin(self.site.config['BASE_URL'], url)
+
+ items = []
+ for img, full_title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]:
+ img_size = os.stat(
+ os.path.join(
+ self.site.config['OUTPUT_FOLDER'], img)).st_size
+ args = {
+ 'title': full_title.split('"')[-2],
+ 'link': make_url(img),
+ 'guid': rss.Guid(img, False),
+ 'pubDate': self.image_date(img),
+ 'enclosure': rss.Enclosure(
+ make_url(img),
+ img_size,
+ mimetypes.guess_type(img)[0]
+ ),
+ }
+ items.append(rss.RSSItem(**args))
+ rss_obj = utils.ExtendedRSS2(
+ title=title,
+ link=make_url(permalink),
+ description='',
+ lastBuildDate=datetime.datetime.now(),
+ items=items,
+ generator='nikola',
+ language=lang
+ )
+ rss_obj.self_url = make_url(permalink)
+ rss_obj.rss_attrs["xmlns:atom"] = "http://www.w3.org/2005/Atom"
+ dst_dir = os.path.dirname(output_path)
+ utils.makedirs(dst_dir)
+ with codecs.open(output_path, "wb+", "utf-8") as rss_file:
+ data = rss_obj.to_xml(encoding='utf-8')
+ if isinstance(data, utils.bytes_str):
+ data = data.decode('utf-8')
+ rss_file.write(data)
+
+ def resize_image(self, src, dst, max_size):
+ """Make a copy of the image in the requested size."""
+ if not Image:
+ utils.copy_file(src, dst)
+ return
+ im = Image.open(src)
+ w, h = im.size
+ if w > max_size or h > max_size:
+ size = max_size, max_size
+
+ # Panoramas get larger thumbnails because they look *awful*
+ if w > 2 * h:
+ size = min(w, max_size * 4), min(w, max_size * 4)
+
+ try:
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+
+ if decoded == 'Orientation':
+ if value == 3:
+ im = im.rotate(180)
+ elif value == 6:
+ im = im.rotate(270)
+ elif value == 8:
+ im = im.rotate(90)
+ break
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ im.save(dst)
+ except Exception:
+ self.logger.warn("Can't thumbnail {0}, using original image as thumbnail".format(src))
+ utils.copy_file(src, dst)
+ else: # Image is small
+ utils.copy_file(src, dst)
+
+ def image_date(self, src):
+ """Try to figure out the date of the image."""
+ if src not in self.dates:
+ try:
+ im = Image.open(src)
+ exif = im._getexif()
+ except Exception:
+ exif = None
+ if exif is not None:
+ for tag, value in list(exif.items()):
+ decoded = ExifTags.TAGS.get(tag, tag)
+ if decoded == 'DateTimeOriginal':
+ try:
+ self.dates[src] = datetime.datetime.strptime(
+ value, r'%Y:%m:%d %H:%M:%S')
+ break
+ except ValueError: # Invalid EXIF date.
+ pass
+ if src not in self.dates:
+ self.dates[src] = datetime.datetime.fromtimestamp(
+ os.stat(src).st_mtime)
+ return self.dates[src]