diff options
Diffstat (limited to 'nikola/plugins/task/galleries.py')
| -rw-r--r-- | nikola/plugins/task/galleries.py | 312 |
1 files changed, 157 insertions, 155 deletions
diff --git a/nikola/plugins/task/galleries.py b/nikola/plugins/task/galleries.py index f835444..e887f18 100644 --- a/nikola/plugins/task/galleries.py +++ b/nikola/plugins/task/galleries.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2014 Roberto Alsina and others. +# Copyright © 2012-2015 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -31,34 +31,30 @@ import glob import json import mimetypes import os +import sys try: from urlparse import urljoin except ImportError: from urllib.parse import urljoin # NOQA import natsort -Image = None try: - from PIL import Image, ExifTags # NOQA + from PIL import Image # NOQA except ImportError: - try: - import Image as _Image - import ExifTags - Image = _Image - except ImportError: - pass + import Image as _Image + Image = _Image import PyRSS2Gen as rss from nikola.plugin_categories import Task from nikola import utils +from nikola.image_processing import ImageProcessor from nikola.post import Post -from nikola.utils import req_missing _image_size_cache = {} -class Galleries(Task): +class Galleries(Task, ImageProcessor): """Render image galleries.""" name = 'render_galleries' @@ -66,47 +62,84 @@ class Galleries(Task): def set_site(self, site): site.register_path_handler('gallery', self.gallery_path) + site.register_path_handler('gallery_global', self.gallery_global_path) site.register_path_handler('gallery_rss', self.gallery_rss_path) + + self.logger = utils.get_logger('render_galleries', site.loghandlers) + + self.kw = { + 'thumbnail_size': site.config['THUMBNAIL_SIZE'], + 'max_image_size': site.config['MAX_IMAGE_SIZE'], + 'output_folder': site.config['OUTPUT_FOLDER'], + 'cache_folder': site.config['CACHE_FOLDER'], + 'default_lang': site.config['DEFAULT_LANG'], + 'use_filename_as_title': site.config['USE_FILENAME_AS_TITLE'], + 'gallery_folders': site.config['GALLERY_FOLDERS'], + 'sort_by_date': site.config['GALLERY_SORT_BY_DATE'], + 'filters': site.config['FILTERS'], + 'translations': site.config['TRANSLATIONS'], + 'global_context': site.GLOBAL_CONTEXT, + 'feed_length': site.config['FEED_LENGTH'], + 'tzinfo': site.tzinfo, + 'comments_in_galleries': site.config['COMMENTS_IN_GALLERIES'], + 'generate_rss': site.config['GENERATE_RSS'], + } + + # Verify that no folder in GALLERY_FOLDERS appears twice + appearing_paths = set() + for source, dest in self.kw['gallery_folders'].items(): + if source in appearing_paths or dest in appearing_paths: + problem = source if source in appearing_paths else dest + utils.LOGGER.error("The gallery input or output folder '{0}' appears in more than one entry in GALLERY_FOLDERS, exiting.".format(problem)) + sys.exit(1) + appearing_paths.add(source) + appearing_paths.add(dest) + + # Find all galleries we need to process + self.find_galleries() + # Create self.gallery_links + self.create_galleries_paths() + return super(Galleries, self).set_site(site) + def _find_gallery_path(self, name): + # The system using self.proper_gallery_links and self.improper_gallery_links + # is similar as in listings.py. + if name in self.proper_gallery_links: + return self.proper_gallery_links[name] + elif name in self.improper_gallery_links: + candidates = self.improper_gallery_links[name] + if len(candidates) == 1: + return candidates[0] + self.logger.error("Gallery name '{0}' is not unique! Possible output paths: {1}".format(name, candidates)) + else: + self.logger.error("Unknown gallery '{0}'!".format(name)) + self.logger.info("Known galleries: " + str(list(self.proper_gallery_links.keys()))) + sys.exit(1) + 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] + gallery_path = self._find_gallery_path(name) + return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + + gallery_path.split(os.sep) + + [self.site.config['INDEX_FILE']] if _f] + + def gallery_global_path(self, name, lang): + gallery_path = self._find_gallery_path(name) + return [_f for _f in gallery_path.split(os.sep) + + [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] + gallery_path = self._find_gallery_path(name) + return [_f for _f in [self.site.config['TRANSLATIONS'][lang]] + + gallery_path.split(os.sep) + + ['rss.xml'] if _f] def gen_tasks(self): """Render image galleries.""" - if Image is None: - req_missing(['pillow'], 'render 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 = self.image_ext_list_builtin 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'], - 'tzinfo': self.site.tzinfo, - 'comments_in_galleries': self.site.config['COMMENTS_IN_GALLERIES'], - 'generate_rss': self.site.config['GENERATE_RSS'], - } - for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items(): self.kw['||template_hooks|{0}||'.format(k)] = v._items @@ -114,22 +147,19 @@ class Galleries(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: + for gallery, input_folder, output_folder in self.gallery_list: # Create subfolder list folder_list = [(x, 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) + post = self.parse_index(gallery, input_folder, output_folder) # Create image list, filter exclusions image_list = self.get_image_list(gallery) @@ -143,12 +173,12 @@ class Galleries(Task): # Create thumbnails and large images in destination for image in image_list: - for task in self.create_target_images(image): + for task in self.create_target_images(image, input_folder): yield task # Remove excluded images for image in self.get_excluded_images(gallery): - for task in self.remove_excluded_image(image): + for task in self.remove_excluded_image(image, input_folder): yield task crumbs = utils.get_crumbs(gallery, index_folder=self) @@ -160,9 +190,7 @@ class Galleries(Task): dst = os.path.join( self.kw['output_folder'], - self.site.path( - "gallery", - os.path.relpath(gallery, self.kw['gallery_path']), lang)) + self.site.path("gallery", gallery, lang)) dst = os.path.normpath(dst) for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE: @@ -187,25 +215,27 @@ class Galleries(Task): 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] - dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in image_list] + thumbs = [os.path.join(self.kw['output_folder'], output_folder, os.path.relpath(t, input_folder)) for t in thumbs] + dst_img_list = [os.path.join(output_folder, os.path.relpath(t, input_folder)) for t in image_list] + dest_img_list = [os.path.join(self.kw['output_folder'], t) for t in dst_img_list] folders = [] # Generate friendly gallery names for path, folder in folder_list: - fpost = self.parse_index(path) + fpost = self.parse_index(path, input_folder, output_folder) if fpost: ft = fpost.title(lang) or folder else: ft = folder + if not folder.endswith('/'): + folder += '/' folders.append((folder, ft)) - context["folders"] = natsort.natsorted(folders) + context["folders"] = natsort.natsorted( + folders, alg=natsort.ns.F | natsort.ns.IC) context["crumbs"] = crumbs - context["permalink"] = self.site.link( - "gallery", os.path.basename( - os.path.relpath(gallery, self.kw['gallery_path'])), lang) + context["permalink"] = self.site.link("gallery", gallery, lang) context["enable_comments"] = self.kw['comments_in_galleries'] context["thumbnail_size"] = self.kw["thumbnail_size"] @@ -216,15 +246,18 @@ class Galleries(Task): 'targets': [post.translated_base_path(lang)], 'file_dep': post.fragment_deps(lang), 'actions': [(post.compile, [lang])], - 'uptodate': [utils.config_changed(self.kw)] + 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:post')] + post.fragment_deps_uptodate(lang) } context['post'] = post else: context['post'] = None file_dep = self.site.template_system.template_deps( template_name) + image_list + thumbs + file_dep_dest = self.site.template_system.template_deps( + template_name) + dest_img_list + thumbs if post: file_dep += [post.translated_base_path(l) for l in self.kw['translations']] + file_dep_dest += [post.translated_base_path(l) for l in self.kw['translations']] yield utils.apply_filters({ 'basename': self.name, @@ -244,58 +277,87 @@ class Galleries(Task): 'uptodate': [utils.config_changed({ 1: self.kw, 2: self.site.config["COMMENTS_IN_GALLERIES"], - 3: context, - })], + 3: context.copy(), + }, 'nikola.plugins.task.galleries:gallery')], }, self.kw['filters']) # RSS for the gallery if self.kw["generate_rss"]: rss_dst = os.path.join( self.kw['output_folder'], - self.site.path( - "gallery_rss", - os.path.relpath(gallery, self.kw['gallery_path']), lang)) + self.site.path("gallery_rss", gallery, lang)) rss_dst = os.path.normpath(rss_dst) yield utils.apply_filters({ 'basename': self.name, 'name': rss_dst, - 'file_dep': file_dep, + 'file_dep': file_dep_dest, 'targets': [rss_dst], 'actions': [ (self.gallery_rss, ( image_list, + dst_img_list, img_titles, lang, - self.site.link( - "gallery_rss", os.path.basename(gallery), lang), + self.site.link("gallery_rss", gallery, lang), rss_dst, context['title'] ))], 'clean': True, 'uptodate': [utils.config_changed({ 1: self.kw, - })], + }, 'nikola.plugins.task.galleries:rss')], }, 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'], followlinks=True): - self.gallery_list.append(root) + for input_folder, output_folder in self.kw['gallery_folders'].items(): + for root, dirs, files in os.walk(input_folder, followlinks=True): + self.gallery_list.append((root, input_folder, output_folder)) + + def create_galleries_paths(self): + """Given a list of galleries, puts their paths into self.gallery_links.""" + + # gallery_path is "gallery/foo/name" + self.proper_gallery_links = dict() + self.improper_gallery_links = dict() + for gallery_path, input_folder, output_folder in self.gallery_list: + if gallery_path == input_folder: + gallery_name = '' + # special case, because relpath will return '.' in this case + else: + gallery_name = os.path.relpath(gallery_path, input_folder) + + output_path = os.path.join(output_folder, gallery_name) + self.proper_gallery_links[gallery_path] = output_path + self.proper_gallery_links[output_path] = output_path + + # If the input and output names differ, the gallery is accessible + # only by `input` and `output/`. + output_path_noslash = output_path[:-1] + if output_path_noslash not in self.proper_gallery_links: + self.proper_gallery_links[output_path_noslash] = output_path + + gallery_path_slash = gallery_path + '/' + if gallery_path_slash not in self.proper_gallery_links: + self.proper_gallery_links[gallery_path_slash] = output_path + + if gallery_name not in self.improper_gallery_links: + self.improper_gallery_links[gallery_name] = list() + self.improper_gallery_links[gallery_name].append(output_path) 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']) + for gallery_path, input_folder, _ in self.gallery_list: # 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))) + self.site.path("gallery", gallery_path))) output_gallery = os.path.normpath(output_gallery) # Task to create gallery in output/ yield { @@ -304,16 +366,16 @@ class Galleries(Task): 'actions': [(utils.makedirs, (output_gallery,))], 'targets': [output_gallery], 'clean': True, - 'uptodate': [utils.config_changed(self.kw)], + 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:mkdir')], } - def parse_index(self, gallery): + def parse_index(self, gallery, input_folder, output_folder): """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) + self.kw["output_folder"], output_folder, + os.path.relpath(gallery, input_folder)) if os.path.isfile(index_path): post = Post( index_path, @@ -361,12 +423,12 @@ class Galleries(Task): 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']) + def create_target_images(self, img, input_path): + gallery_name = os.path.dirname(img) output_gallery = os.path.dirname( os.path.join( self.kw["output_folder"], - self.site.path("gallery", gallery_name))) + self.site.path("gallery_global", gallery_name))) # Do thumbnails and copy originals # img is "galleries/name/image_name.jpg" # img_name is "image_name.jpg" @@ -392,7 +454,7 @@ class Galleries(Task): 'clean': True, 'uptodate': [utils.config_changed({ 1: self.kw['thumbnail_size'] - })], + }, 'nikola.plugins.task.galleries:resize_thumb')], }, self.kw['filters']) yield utils.apply_filters({ @@ -407,19 +469,19 @@ class Galleries(Task): 'clean': True, 'uptodate': [utils.config_changed({ 1: self.kw['max_image_size'] - })], + }, 'nikola.plugins.task.galleries:resize_max')], }, self.kw['filters']) - def remove_excluded_image(self, img): + def remove_excluded_image(self, img, input_folder): # Remove excluded images - # img is something like galleries/demo/tesla2_lg.jpg so it's the *source* path + # img is something like input_folder/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)))) + self.site.path("gallery_global", os.path.dirname(img)))) + img = os.path.relpath(img, input_folder) img_path = os.path.join(output_folder, os.path.basename(img)) fname, ext = os.path.splitext(img_path) thumb_path = fname + '.thumbnail' + ext @@ -431,7 +493,7 @@ class Galleries(Task): (utils.remove_file, (thumb_path,)) ], 'clean': True, - 'uptodate': [utils.config_changed(self.kw)], + 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_thumb')], }, self.kw['filters']) yield utils.apply_filters({ @@ -441,7 +503,7 @@ class Galleries(Task): (utils.remove_file, (img_path,)) ], 'clean': True, - 'uptodate': [utils.config_changed(self.kw)], + 'uptodate': [utils.config_changed(self.kw, 'nikola.plugins.task.galleries:clean_file')], }, self.kw['filters']) def render_gallery_index( @@ -484,7 +546,7 @@ class Galleries(Task): context['photo_array_json'] = json.dumps(photo_array) self.site.render_template(template_name, output_name, context) - def gallery_rss(self, img_list, img_titles, lang, permalink, output_path, title): + def gallery_rss(self, img_list, dest_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 @@ -492,10 +554,10 @@ class Galleries(Task): """ def make_url(url): - return urljoin(self.site.config['BASE_URL'], url) + return urljoin(self.site.config['BASE_URL'], url.lstrip('/')) items = [] - for img, title in list(zip(img_list, img_titles))[:self.kw["feed_length"]]: + for img, srcimg, title in list(zip(dest_img_list, img_list, img_titles))[:self.kw["feed_length"]]: img_size = os.stat( os.path.join( self.site.config['OUTPUT_FOLDER'], img)).st_size @@ -503,7 +565,7 @@ class Galleries(Task): 'title': title, 'link': make_url(img), 'guid': rss.Guid(img, False), - 'pubDate': self.image_date(img), + 'pubDate': self.image_date(srcimg), 'enclosure': rss.Enclosure( make_url(img), img_size, @@ -515,12 +577,15 @@ class Galleries(Task): title=title, link=make_url(permalink), description='', - lastBuildDate=datetime.datetime.now(), + lastBuildDate=datetime.datetime.utcnow(), items=items, generator='http://getnikola.com/', language=lang ) + rss_obj.rss_attrs["xmlns:dc"] = "http://purl.org/dc/elements/1.1/" + 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 io.open(output_path, "w+", encoding="utf-8") as rss_file: @@ -528,66 +593,3 @@ class Galleries(Task): 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 as e: - self.logger.warn("Can't thumbnail {0}, using original " - "image as thumbnail ({1})".format(src, e)) - 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 in ('DateTimeOriginal', 'DateTimeDigitized'): - 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] |
