Procházet zdrojové kódy

Fixed issue with images that had no alpha channel. New feature that dynamically sizes image as part of watermark.

Refactored photos. Added watermark and blacklist. Fixed some bugs. Updated documentation

Updated documentation to include pillow
Brian Levin před 7 roky
rodič
revize
5cb810ebbd
5 změnil soubory, kde provedl 406 přidání a 212 odebrání
  1. 169 70
      photos/README.md
  2. binární
      photos/SourceCodePro-Bold.otf
  3. binární
      photos/SourceCodePro-Regular.otf
  4. 236 141
      photos/photos.py
  5. 1 1
      photos/requirements.txt

+ 169 - 70
photos/README.md

@@ -6,36 +6,45 @@ Use Photos to add a photo or a gallery of photos to an article, or to include ph
 
 Maintain an organized library of high resolution photos somewhere on disk, using folders to group related images. The default path `~/Pictures` is convenient for Mac OS X users.
 
-* To create a gallery of photos, add the metadata field `gallery: {photo}folder` to an article. To simplify the transition from the plug-in Gallery, the syntax `gallery: {filename}folder` is also accepted, but images are not resized.
+* To create a gallery of photos, add the metadata field `gallery: {photo}folder` to an article. To simplify the transition from the plug-in Gallery, the syntax `gallery: {filename}folder` is also accepted.
+* You can now have multiple galleries. The galleries need to be seperated by a comma in the metadata field. The syntax is gallery: `{photo}folder, {photo}folder2`. You can also add titles to your galleries. The syntax is: `{photo}folder, {photo}folder2{This is a title}`. Using the following example the first gallery would have the title of the folder location and the second would have the title `This is a tile.`
 * To use an image in the body of the text, just use the syntax `{photo}folder/image.jpg` instead of the usual `{filename}/images/image.jpg`.
-* To associate an image with an article, add the metadata field `image: {photo}folder/image.jpg` to an article. Use associated images to improve navigation. For compatibility, the syntax `image: {filename}/images/image.jpg` is also accepted, but the image is not resized.
+* To associate an image with an article, add the metadata field `image: {photo}folder/image.jpg` to an article. Use associated images to improve navigation. For compatibility, the syntax `image: {filename}/images/image.jpg` is also accepted.
 
-Folders of photos may optionally have two text files, where each line describes one photo. Generating these optional files is left as an exercise for the reader (but consider using Phil Harvey's [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/)).
+Folders of photos may optionally have three text files, where each line describes one photo. You can use the `#` to comment out a line. Generating these optional files is left as an exercise for the reader (but consider using Phil Harvey's [exiftool](http://www.sno.phy.queensu.ca/~phil/exiftool/)). See below for one method of extracting exif data.
 
 `exif.txt`
 :	Associates compact technical information with photos, typically the camera settings. For example:
 
-		best:jpg: Canon EOS 5D Mark II - 20mm f/8 1/250s ISO 100
-		night.jpg: Canon EOS 5D Mark II - 47mm f/8 5s ISO 100
+	best:jpg: Canon EOS 5D Mark II - 20mm f/8 1/250s ISO 100
+	night.jpg: Canon EOS 5D Mark II - 47mm f/8 5s ISO 100
+	# new.jpg: Canon EOS 5D Mark II - 47mm f/8 5s ISO 100
 
 `captions.txt`
 :	Associates comments with photos. For example:
 
-		best.jpg: My best photo ever! How lucky of me!
-		night.jpg: Twilight over the dam.
+	best.jpg: My best photo ever! How lucky of me!
+	night.jpg: Twilight over the dam.
+	# new.jpg: My new photo blog entry is not quite ready.
+
+`blacklist.txt`
+: Skips photos the user does not want to include. For example:
+
+	# best.jpg
+	new.jpg
 
 Here is an example Markdown article that shows the three use cases:
 
-		title: My Article
-		gallery: {photo}favorite
-		image: {photo}favorite/best.jpg
+	title: My Article
+	gallery: {photo}favorite
+	image: {photo}favorite/best.jpg
 
-		Here are my best photos, taken with my favorite camera:
-		![]({photo}mybag/camera.jpg).
+	Here are my best photos, taken with my favorite camera:
+	![]({photo}mybag/camera.jpg).
 
 ## How to install and configure
 
-The plug-in requires PIL, the Python Imaging Library, whose installation is outside the scope of this document.
+The plug-in requires Pillow, the Python Imaging Library, whose installation is outside the scope of this document.
 
 The plug-in resizes the referred photos, and generates thumbnails for galleries and associated photos, based on the following configuration and default values:
 
@@ -51,55 +60,74 @@ The plug-in resizes the referred photos, and generates thumbnails for galleries
 `PHOTO_THUMB = (192, 144, 60)`
 :	For thumbnails, maximum width, height, and quality.
 
+`PHOTO_WATERMARK = True`
+: Adds a watermark to all photos in articles and pages. Defaults to using your site name.
+
+`PHOTO_WATERMARK_TEXT' = SITENAME`
+: Allow the user to change the watermark text or remove it completely. By default it uses [SourceCodePro-Bold](http://www.adobe.com/products/type/font-information/source-code-pro-readme.html) as the font.
+
+`PHOTO_WATERMARK_IMG = ''`
+: Allows the user to add an image in addition to or as the only watermark. Set the variable to the location.
+
 The plug-in automatically resizes the photos and publishes them to the following output folder:
 
     ./output/photos
 
-__WARNING:__ The plug-in can take hours to resize 40,000 photos, therefore, photos and thumbnails are only generated once. Clean the output folders to regenerate the resized photos again.
+**WARNING:** The plug-in can take hours to resize 40,000 photos, therefore, photos and thumbnails are only generated once. Clean the output folders to regenerate the resized photos again.
 
 ## How to change the Jinja templates
 
 The plugin provides the following variables to your templates:
 
-`article.photo_gallery`
-:	For articles with a gallery, a list of the photos in the gallery. Each item in the list is a tuple with five elements:
-
-	* The filename of the original photo.
-	* The output path to the generated photo.
-	* The output path to the generated thumbnail.
-	* The EXIF information of the photo, as read from the file `exif.txt`.
-	* The caption of the photo, as read from `captions.txt`.
-
 `article.photo_image`
 :	For articles with an associated photo, a tuple with the following information:
 
-	* The filename of the original photo.
-	* The output path to the generated photo.
-	* The output path to the generated thumbnail.
+* The filename of the original photo.
+* The output path to the generated photo.
+* The output path to the generated thumbnail.
 
 For example, modify the template `article.html` as shown below to display the associated image before the article content:
 
-		<div class="entry-content">
-			{% if article.photo_image %}<img src="{{ SITEURL }}/{{ article.photo_image[1] }}" />{% endif %}
-			{% include 'article_infos.html' %}
-			{{ article.content }}
-		</div><!-- /.entry-content -->
+```html
+<div class="entry-content">
+	{% if article.photo_image %}<img src="{{ SITEURL }}/{{ article.photo_image[1] }}" />{% endif %}
+	{% include 'article_infos.html' %}
+	{{ article.content }}
+</div><!-- /.entry-content -->
+```
+
+`article.photo_gallery`
+:	For articles with a gallery, a list of the photos in the gallery. Each item in the list is a tuple with five elements:
+
+* The title of the gallery
+* The filename of the original photo.
+* The output path to the generated photo.
+* The output path to the generated thumbnail.
+* The EXIF information of the photo, as read from the file `exif.txt`.
+* The caption of the photo, as read from `captions.txt`.
 
 For example, add the following to the template `article.html` to add the gallery as the end of the article:
 
-		{% if article.photo_gallery %}
-		<div class="gallery">
-			{% for name, photo, thumb, exif, caption in article.photo_gallery %}
-			<a href="{{ SITEURL }}/{{ photo }}" title="{{ name }}" exif="{{ exif }}" caption="{{ caption }}"><img src="{{ SITEURL }}/{{ thumb }}"></a>
-			{% endfor %}
-		</div>
-		{% endif %}
+```html
+{% if article.photo_gallery %}
+<div class="gallery">
+		{% for title, gallery in article.photo_gallery %}
+			<h1>{{ title }}</h1>
+				{% for name, photo, thumb, exif, caption in gallery %}
+						<a href="{{ SITEURL }}/{{ photo }}" title="{{ name }}" exif="{{ exif }}" caption="{{ caption }}"><img src="{{ SITEURL }}/{{ thumb }}"></a>
+				{% endfor %}
+		{% endfor %}
+</div>
+{% endif %}
+```
 
 For example, add the following to the template `index.html`, inside the `entry-content`, to display the thumbnail with a link to the article:
 
-		{% if article.photo_image %}<a href="{{ SITEURL }}/{{ article.url }}"><img src="{{ SITEURL }}/{{ article.photo_image[2] }}"
-			style="display: inline; float: right; margin: 2px 0 2ex 4ex;" /></a>
-		{% endif %}
+```html
+{% if article.photo_image %}<a href="{{ SITEURL }}/{{ article.url }}"><img src="{{ SITEURL }}/{{ article.photo_image[2] }}"
+	style="display: inline; float: right; margin: 2px 0 2ex 4ex;" /></a>
+{% endif %}
+```
 
 ## How to make the gallery lightbox
 
@@ -109,38 +137,109 @@ Copy the files `magnific-popup.css` and `magnific-popup.js` to the root of your
 
 Add the following to the template `base.html`, inside the HTML `head` tags:
 
-		{% if (article and article.photo_gallery) or (articles_page and articles_page.object_list[0].photo_gallery) %}
-		<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/magnific-popup.css">
-		{% endif %}
+```html
+{% if (article and article.photo_gallery) or (articles_page and articles_page.object_list[0].photo_gallery) %}
+	<link rel="stylesheet" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/magnific-popup.css">
+{% endif %}
+```
 
 Add the following to the template `base.html`, before the closing HTML `</body>` tag:
 
-		{% if (article and article.photo_gallery) or (articles_page and articles_page.object_list[0].photo_gallery) %}
-		<!-- jQuery 1.7.2+ or Zepto.js 1.0+ -->
-		<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
-
-		<!-- Magnific Popup core JS file -->
-		<script src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/magnific-popup.js"></script>
-		<script>
-		$('.gallery').magnificPopup({
-			delegate: 'a',
-			type: 'image',
-			gallery: {
-				enabled: true,
-				navigateByImgClick: true,
-				preload: [1,2]
-			},
-			image: {
-				titleSrc: function(item) {
-					if (item.el.attr('caption') && item.el.attr('exif')) {
-						return (item.el.attr('caption').replace(/\\n/g, '<br />') +
-							'<small>' + item.el.attr('title') + ' - ' + item.el.attr('exif') + '</small>');
-					}
-				return item.el.attr('title') + '<small>' + item.el.attr('exif') + '</small>';
-			} }
-		});
-		</script>
-		{% endif %}
+```JavaScript
+{% if (article and article.photo_gallery) or (articles_page and articles_page.object_list[0].photo_gallery) %}
+<!-- jQuery 1.7.2+ or Zepto.js 1.0+ -->
+<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
+
+<!-- Magnific Popup core JS file -->
+<script src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/magnific-popup.js"></script>
+<script>
+$('.gallery').magnificPopup({
+	delegate: 'a',
+	type: 'image',
+	gallery: {
+		enabled: true,
+		navigateByImgClick: true,
+		preload: [1,2]
+	},
+	image: {
+		titleSrc: function(item) {
+			if (item.el.attr('caption') && item.el.attr('exif')) {
+				return (item.el.attr('caption').replace(/\\n/g, '<br />') +
+					'<small>' + item.el.attr('title') + ' - ' + item.el.attr('exif') + '</small>');
+			}
+		return item.el.attr('title') + '<small>' + item.el.attr('exif') + '</small>';
+	} }
+});
+</script>
+{% endif %}
+```
+
+## How to make a Bootstrap Carousel
+
+If you are using bootstrap, the following code is an example of how one could create a carousel.
+
+```html
+{% if article.photo_gallery %}
+  {% for title, gallery in article.photo_gallery %}
+    <h1>{{ title }}</h1>
+    <div id="carousel-{{ loop.index }}" class="carousel slide">
+      <ol class="carousel-indicators">
+          {% for i in range(0, gallery|length) %}
+          <li data-target="#carousel-{{ loop.index }}" data-slide-to="{{ i }}" {% if i==0 %} class="active" {% endif %}></li>
+          {% endfor %}
+      </ol>
+      <div class="carousel-inner">
+        {% for name, photo, thumb, exif, caption in gallery %}
+          {% if loop.first %}
+            <div class="item active">
+          {% else %}
+            <div class="item">
+          {% endif %}
+          <img src="{{ SITEURL }}/{{ photo }}" exif="{{ exif }}" alt="{{ caption }}">
+          <div class="carousel-caption">
+              <h5>{{ caption }}</h5>
+          </div> <!-- carousel-caption -->
+        </div> <!-- item -->
+        {% endfor %}
+      </div> <!-- carousel-inner -->
+      <a class="left carousel-control" href="#carousel-{{ loop.index }}" data-slide="prev">
+        <span class="glyphicon glyphicon-chevron-left"></span>
+      </a>
+      <a class="right carousel-control" href="#carousel-{{ loop.index }}" data-slide="next">
+        <span class="glyphicon glyphicon-chevron-right"></span>
+      </a>
+    </div> <!-- closes carousel-{{ loop.index }} -->
+    {% endfor %}
+{% endif %}
+```
+
+## Exiftool example
+
+You can add the following stanza to your fab file if you are using fabric to generate the appropriate text files for your galleries. You need to set the location of `Exiftool` control files.
+
+```Python
+def photo_gallery_gen(location):
+    """Create gallery metadata files."""
+    local_path = os.getcwd() + 'LOCATION OF YOUR EXIF CONTROL FILES'
+    with lcd(location):
+        local("exiftool -p {fmt_path}/exif.fmt . > exif.txt".format(
+            fmt_path=local_path))
+        local("exiftool -p {fmt_path}/captions.fmt . > captions.txt".format(
+            fmt_path=local_path))
+
+```
+
+`captions.fmt` example file
+
+```
+$FileName: $Description
+```
+
+`exif.fmt` example file
+
+```
+$FileName: $CreateDate - $Make $Model Stats:(f/$Aperture, ${ShutterSpeed}s, ISO $ISO Flash: $Flash) GPS:($GPSPosition $GPSAltitude)
+```
 
 ## Known use cases
 

binární
photos/SourceCodePro-Bold.otf


binární
photos/SourceCodePro-Regular.otf


+ 236 - 141
photos/photos.py

@@ -1,31 +1,58 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
+
+import itertools
+import logging
 import os
+import pprint
 import re
-import logging
+
+from pelican.generators import ArticlesGenerator
+from pelican.generators import PagesGenerator
+from pelican.settings import DEFAULT_CONFIG
 from pelican import signals
 from pelican.utils import pelican_open
-from PIL import Image, ExifTags
-from itertools import chain
 
 logger = logging.getLogger(__name__)
-queue_resize = dict()
-hrefs = None
 
-created_galleries = {}
+
+try:
+    from PIL import ExifTags
+    from PIL import Image
+    from PIL import ImageDraw
+    from PIL import ImageEnhance
+    from PIL import ImageFont
+except ImportError:
+    raise Exception('PIL/Pillow not found')
+
 
 def initialized(pelican):
+
     p = os.path.expanduser('~/Pictures')
-    from pelican.settings import DEFAULT_CONFIG
+
     DEFAULT_CONFIG.setdefault('PHOTO_LIBRARY', p)
     DEFAULT_CONFIG.setdefault('PHOTO_GALLERY', (1024, 768, 80))
-    DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', ( 760, 506, 80))
-    DEFAULT_CONFIG.setdefault('PHOTO_THUMB',   ( 192, 144, 60))
+    DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
+    DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
+    DEFAULT_CONFIG.setdefault('PHOTO_GALLERY_TITLE', '')
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK', 'False')
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_TEXT', DEFAULT_CONFIG['SITENAME'])
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG', '')
+    DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG_SIZE', (64, 64))
+
+    DEFAULT_CONFIG['queue_resize'] = {}
+    DEFAULT_CONFIG['created_galleries'] = {}
+
     if pelican:
         pelican.settings.setdefault('PHOTO_LIBRARY', p)
         pelican.settings.setdefault('PHOTO_GALLERY', (1024, 768, 80))
-        pelican.settings.setdefault('PHOTO_ARTICLE', ( 760, 506, 80))
-        pelican.settings.setdefault('PHOTO_THUMB',   ( 192, 144, 60))
+        pelican.settings.setdefault('PHOTO_ARTICLE', (760, 506, 80))
+        pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
+        pelican.settings.setdefault('PHOTO_GALLERY_TITLE', '')
+        pelican.settings.setdefault('PHOTO_WATERMARK', 'False')
+        pelican.settings.setdefault('PHOTO_WATERMARK_TEXT', pelican.settings['SITENAME'])
+        pelican.settings.setdefault('PHOTO_WATERMARK_IMG', '')
+        pelican.settings.setdefault('PHOTO_WATERMARK_IMG_SIZE', (64, 64))
 
 
 def read_notes(filename, msg=None):
@@ -33,38 +60,105 @@ def read_notes(filename, msg=None):
     try:
         with pelican_open(filename) as text:
             for line in text.splitlines():
+                if line.startswith('#'):
+                    continue
+
                 m = line.split(':', 1)
                 if len(m) > 1:
                     pic = m[0].strip()
                     note = m[1].strip()
                     if pic and note:
                         notes[pic] = note
-    except:
+                else:
+                    notes[line] = ''
+    except Exception as e:
         if msg:
-            logger.warning(msg, filename)
+            logger.warning('{} at file {}'.format(msg, filename))
+        logger.debug('read_notes issue: {} at file {}. Debug message:{}'.format(msg, filename, e))
     return notes
 
 
 def enqueue_resize(orig, resized, spec=(640, 480, 80)):
-    global queue_resize
-    if resized not in queue_resize:
-        queue_resize[resized] = (orig, spec)
-    elif queue_resize[resized] != (orig, spec):
-        logger.error('photos: resize conflict for {}, {}-{} is not {}-{}',
-                     resized,
-                     queue_resize[resized][0], queue_resize[resized][1],
-                     orig, spec)
+
+    if resized not in DEFAULT_CONFIG['queue_resize']:
+        DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
+    elif DEFAULT_CONFIG['queue_resize'][resized] != (orig, spec):
+        logger.error('photos: resize conflict for {}, {}-{} is not {}-{}'.format(resized, DEFAULT_CONFIG['queue_resize'][resized][0], DEFAULT_CONFIG['queue_resize'][resized][1], orig, spec))
+
+
+def isalpha(img):
+    return True if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) else False
+
+
+def ReduceOpacity(im, opacity):
+    """Reduces Opacity
+
+    Returns an image with reduced opacity.
+    Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
+    """
+
+    assert opacity >= 0 and opacity <= 1
+    if isalpha(im):
+        im = im.copy()
+    else:
+        im = im.convert('RGBA')
+
+    alpha = im.split()[3]
+    alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
+    im.putalpha(alpha)
+    return im
+
+
+def watermark_photo(image, watermark_text, watermark_image, watermark_image_size):
+
+    margin = [10, 10]
+    opacity = 0.6
+
+    watermark_layer = Image.new("RGBA", image.size, (0, 0, 0, 0))
+    draw_watermark = ImageDraw.Draw(watermark_layer)
+    text_reducer = 32
+    image_reducer = 8
+    text_size = [0, 0]
+    mark_size = [0, 0]
+    font_height = 0
+    text_position = [0, 0]
+
+    if watermark_text:
+        font_name = 'SourceCodePro-Bold.otf'
+
+        plugin_dir = os.path.dirname(os.path.realpath(__file__))
+        default_font = os.path.join(plugin_dir, font_name)
+        font = ImageFont.FreeTypeFont(default_font, watermark_layer.size[0] // text_reducer)
+        text_size = draw_watermark.textsize(watermark_text, font)
+        text_position = [image.size[i] - text_size[i] - margin[i] for i in [0, 1]]
+        draw_watermark.text(text_position, watermark_text, (255, 255, 255), font=font)
+
+    if watermark_image:
+        mark_image = Image.open(watermark_image)
+        mark_image_size = [ watermark_layer.size[0] // image_reducer for size in mark_size]
+        mark_image.thumbnail(mark_image_size, Image.ANTIALIAS)
+        mark_position = [watermark_layer.size[i] - mark_image.size[i] - margin[i] for i in [0, 1]]
+        mark_position = tuple([mark_position[0] - (text_size[0] // 2) + (mark_image_size[0] // 2), mark_position[1] - text_size[1]])
+
+        if not isalpha(mark_image):
+            mark_image = mark_image.convert('RGBA')
+
+        watermark_layer.paste(mark_image, mark_position, mark_image)
+
+    watermark_layer = ReduceOpacity(watermark_layer, opacity)
+    image.paste(watermark_layer, (0, 0), watermark_layer)
+
+    return image
 
 
 def resize_photos(generator, writer):
-    print('photos: {} photo resizes to consider.'
-          .format(len(queue_resize.items())))
-    for resized, what in queue_resize.items():
+    logger.info('photos: {} photo resizes to consider.'.format(len(DEFAULT_CONFIG['queue_resize'].items())))
+    for resized, what in DEFAULT_CONFIG['queue_resize'].items():
         resized = os.path.join(generator.output_path, resized)
         orig, spec = what
         if (not os.path.isfile(resized) or
                 os.path.getmtime(orig) > os.path.getmtime(resized)):
-            logger.info('photos: make photo %s -> %s', orig, resized)
+            logger.info('photos: make photo {} -> {}'.format(orig, resized))
             im = Image.open(orig)
             try:
                 exif = im._getexif()
@@ -78,19 +172,28 @@ def resize_photos(generator, writer):
                 for tag, value in 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)
+                        if value == 3:
+                            im = im.rotate(180)
+                        elif value == 6:
+                            im = im.rotate(270)
+                        elif value == 8:
+                            im = im.rotate(90)
                         break
             im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
             try:
                 os.makedirs(os.path.split(resized)[0])
-            except:
+            except Exception:
                 pass
+
+            if generator.settings['PHOTO_WATERMARK']:
+                im = watermark_photo(im, generator.settings['PHOTO_WATERMARK_TEXT'], generator.settings['PHOTO_WATERMARK_IMG'], generator.settings['PHOTO_WATERMARK_IMG_SIZE'])
             im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile)
 
+
 def detect_content(content):
 
+    hrefs = None
+
     def replacer(m):
         what = m.group('what')
         value = m.group('value')
@@ -100,8 +203,8 @@ def detect_content(content):
             if value.startswith('/'):
                 value = value[1:]
             path = os.path.join(
-                        os.path.expanduser(settings['PHOTO_LIBRARY']),
-                        value)
+                os.path.expanduser(settings['PHOTO_LIBRARY']),
+                value)
             if not os.path.isfile(path):
                 logger.error('photos: No photo %s', path)
             else:
@@ -114,7 +217,6 @@ def detect_content(content):
         return ''.join((m.group('markup'), m.group('quote'), origin,
                         m.group('quote')))
 
-    global hrefs
     if hrefs is None:
         regex = r"""
             (?P<markup><\s*[^\>]*  # match tag with src and href attr
@@ -130,40 +232,68 @@ def detect_content(content):
         content._content = hrefs.sub(replacer, content._content)
 
 
-def process_gallery_photo(generator, article, gallery):
-    if gallery.startswith('/'):
-        gallery = gallery[1:]
+def galleries_string_decompose(gallery_string):
+    splitter_regex = re.compile(r'[\s,]*?({photo}|{filename})')
+    title_regex = re.compile(r'{(.+)}')
+    galleries = map(unicode.strip, filter(None, splitter_regex.split(gallery_string)))
+    galleries = [gallery[1:] if gallery.startswith('/') else gallery for gallery in galleries]
+    if len(galleries) % 2 == 0 and u' ' not in galleries:
+        galleries = zip(zip(['type'] * len(galleries[0::2]), galleries[0::2]), zip(['location'] * len(galleries[0::2]), galleries[1::2]))
+        galleries = [dict(gallery) for gallery in galleries]
+        for gallery in galleries:
+            title = re.search(title_regex, gallery['location'])
+            if title:
+                gallery['title'] = title.group(1)
+                gallery['location'] = re.sub(title_regex, '', gallery['location']).strip()
+            else:
+                gallery['title'] = DEFAULT_CONFIG['PHOTO_GALLERY_TITLE']
+        return galleries
+    else:
+        logger.critical('Unexpected gallery location format! \n{}'.format(pprint.pformat(galleries)))
+        raise Exception
 
-    galleries = gallery.split('{photo}')
-    article.photo_gallery = []
+
+def process_gallery(generator, content, location):
+
+    content.photo_gallery = []
+
+    galleries = galleries_string_decompose(location)
 
     for gallery in galleries:
-        # strip whitespaces
-        gallery = gallery.strip()
 
-        if gallery in created_galleries:
-            article.photo_gallery.append((gallery, created_galleries[gallery]))
+        if gallery['location'] in DEFAULT_CONFIG['created_galleries']:
+            content.photo_gallery.append((gallery['location'], DEFAULT_CONFIG['created_galleries'][gallery]))
             continue
 
-        dir_gallery = os.path.join(
-                        os.path.expanduser(generator.settings['PHOTO_LIBRARY']),
-                        gallery)
+        if gallery['type'] == '{photo}':
+            dir_gallery = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), gallery['location'])
+            rel_gallery = gallery['location']
+        elif gallery['type'] == '{filename}':
+            base_path = os.path.join(generator.path, content.relative_dir)
+            dir_gallery = os.path.join(base_path, gallery['location'])
+            rel_gallery = os.path.join(content.relative_dir, gallery['location'])
 
         if os.path.isdir(dir_gallery):
-            logger.info('photos: Gallery detected: %s', gallery)
-            dir_photo = os.path.join('photos', gallery.lower())
-            dir_thumb = os.path.join('photos', gallery.lower())
+            logger.info('photos: Gallery detected: {}'.format(rel_gallery))
+            dir_photo = os.path.join('photos', rel_gallery.lower())
+            dir_thumb = os.path.join('photos', rel_gallery.lower())
             exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
-                               msg='photos: No EXIF for gallery %s')
-            captions = read_notes(os.path.join(dir_gallery, 'captions.txt'))
-            articleGallery = []
+                               msg='photos: No EXIF for gallery')
+            captions = read_notes(os.path.join(dir_gallery, 'captions.txt'), msg='photos: No captions for gallery')
+            blacklist = read_notes(os.path.join(dir_gallery, 'blacklist.txt'), msg='photos: No blacklist for gallery')
+            content_gallery = []
 
+            title = gallery['title']
             for pic in sorted(os.listdir(dir_gallery)):
-                if pic.startswith('.'): continue
-                if pic.endswith('.txt'): continue
+                if pic.startswith('.'):
+                    continue
+                if pic.endswith('.txt'):
+                    continue
+                if pic in blacklist:
+                    continue
                 photo = os.path.splitext(pic)[0].lower() + '.jpg'
                 thumb = os.path.splitext(pic)[0].lower() + 't.jpg'
-                articleGallery.append((
+                content_gallery.append((
                     pic,
                     os.path.join(dir_photo, photo),
                     os.path.join(dir_thumb, thumb),
@@ -179,67 +309,39 @@ def process_gallery_photo(generator, article, gallery):
                     os.path.join(dir_thumb, thumb),
                     generator.settings['PHOTO_THUMB'])
 
-            article.photo_gallery.append((gallery, articleGallery))
-            created_galleries[gallery] = articleGallery
+            content.photo_gallery.append((title, content_gallery))
+            logger.debug('Gallery Data: '.format(pprint.pformat(content.photo_gallery)))
+            DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
         else:
-            logger.error('photos: Gallery does not exist: %s at %s', gallery, dir_gallery)
+            logger.critical('photos: Gallery does not exist: {} at {}'.format(gallery['location'], dir_gallery))
+            raise Exception
 
 
-def process_gallery_filename(generator, article, gallery):
-    if gallery.startswith('/'):
-        gallery = gallery[1:]
-    else:
-        gallery = os.path.join(article.relative_dir, gallery)
-    dir_gallery = os.path.join(generator.path, gallery)
-    if os.path.isdir(dir_gallery):
-        logger.info('photos: Gallery detected: %s', gallery)
-        dir_photo = gallery.lower()
-        dir_thumb = os.path.join('photos', gallery.lower())
-        exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
-                           msg='photos: No EXIF for gallery %s')
-        captions = read_notes(os.path.join(dir_gallery, 'captions.txt'))
-        article.photo_gallery = []
-        for pic in sorted(os.listdir(dir_gallery)):
-            if pic.startswith('.'): continue
-            if pic.endswith('.txt'): continue
-            photo = pic.lower()
-            thumb = os.path.splitext(pic)[0].lower() + 't.jpg'
-            article.photo_gallery.append((
-                pic,
-                os.path.join(dir_photo, photo),
-                os.path.join(dir_thumb, thumb),
-                exifs.get(pic, ''),
-                captions.get(pic, '')))
-            enqueue_resize(
-                os.path.join(dir_gallery, pic),
-                os.path.join(dir_thumb, thumb),
-                generator.settings['PHOTO_THUMB'])
-    else:
-        logger.error('photos: Gallery does not exist: %s at %s', gallery, dir_gallery)
-
-
-def detect_gallery(generator):
-    for article in chain(generator.articles, generator.drafts):
-        if 'gallery' in article.metadata:
-            gallery = article.metadata.get('gallery')
-            if gallery.startswith('{photo}'):
-                process_gallery_photo(generator, article, gallery[7:])
-            elif gallery.startswith('{filename}'):
-                process_gallery_filename(generator, article, gallery[10:])
-            elif gallery:
-                logger.error('photos: Gallery tag not recognized: %s', gallery)
-
-
-def process_image_photo(generator, article, image):
-    if image.startswith('/'):
-        image = image[1:]
-    path = os.path.join(
-                os.path.expanduser(generator.settings['PHOTO_LIBRARY']),
-                image)
+def detect_gallery(generator, content):
+    if 'gallery' in content.metadata:
+        gallery = content.metadata.get('gallery')
+        if gallery.startswith('{photo}') or gallery.startswith('{filename}'):
+            process_gallery(generator, content, gallery)
+        elif gallery:
+            logger.error('photos: Gallery tag not recognized: {}'.format(gallery))
+
+
+def process_image(generator, content, image):
+
+    image_clipper = lambda x: x[8:] if x[8] == '/' else x[7:]
+    file_clipper = lambda x: x[11:] if x[10] == '/' else x[10:]
+
+    if image.startswith('{photo}'):
+        path = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), image_clipper(image))
+        image = image_clipper(image)
+    elif image.startswith('{filename}'):
+        path = os.path.join(content.relative_dir, file_clipper(image))
+        image = file_clipper(image)
+
     if os.path.isfile(path):
         photo = os.path.splitext(image)[0].lower() + 'a.jpg'
         thumb = os.path.splitext(image)[0].lower() + 't.jpg'
-        article.photo_image = (
+        content.photo_image = (
             os.path.basename(image).lower(),
             os.path.join('photos', photo),
             os.path.join('photos', thumb))
@@ -252,44 +354,37 @@ def process_image_photo(generator, article, image):
             os.path.join('photos', thumb),
             generator.settings['PHOTO_THUMB'])
     else:
-        logger.error('photo: No photo for %s at %s', article.source_path, path)
-
-
-def process_image_filename(generator, article, image):
-    if image.startswith('/'):
-        image = image[1:]
-    else:
-        image = os.path.join(article.relative_dir, image)
-    path = os.path.join(generator.path, image)
-    if os.path.isfile(path):
-        small = os.path.splitext(image)[0].lower() + 't.jpg'
-        article.photo_image = (
-            os.path.basename(image),
-            image.lower(),
-            os.path.join('photos', small))
-        enqueue_resize(
-            path,
-            os.path.join('photos', small),
-            generator.settings['PHOTO_THUMB'])
-    else:
-        logger.error('photos: No photo at %s', path)
+        logger.error('photo: No photo for {} at {}'.format(content.source_path, path))
 
 
-def detect_image(generator):
-    for article in chain(generator.articles, generator.drafts):
-        image = article.metadata.get('image', None)
+def detect_image(generator, content):
+        image = content.metadata.get('image', None)
         if image:
-            if image.startswith('{photo}'):
-                process_image_photo(generator, article, image[7:])
-            elif image.startswith('{filename}'):
-                process_image_filename(generator, article, image[10:])
+            if image.startswith('{photo}') or image.startswith('{filename}'):
+                process_image(generator, content, image)
             else:
-                logger.error('photos: Image tag not recognized: %s', image)
+                logger.error('photos: Image tag not recognized: {}'.format(image))
+
+
+def detect_images_and_galleries(generators):
+    """Runs generator on both pages and articles. """
+    for generator in generators:
+        if isinstance(generator, ArticlesGenerator):
+            for article in itertools.chain(generator.articles, generator.drafts):
+                detect_image(generator, article)
+                detect_gallery(generator, article)
+        elif isinstance(generator, PagesGenerator):
+            for page in itertools.chain(generator.pages, generator.hidden_pages):
+                detect_image(generator, page)
+                detect_gallery(generator, page)
 
 
 def register():
+    """Uses the new style of registration based on GitHub Pelican issue #314. """
     signals.initialized.connect(initialized)
-    signals.content_object_init.connect(detect_content)
-    signals.article_generator_finalized.connect(detect_gallery)
-    signals.article_generator_finalized.connect(detect_image)
-    signals.article_writer_finalized.connect(resize_photos)
+    try:
+        signals.content_object_init.connect(detect_content)
+        signals.all_generators_finalized.connect(detect_images_and_galleries)
+        signals.article_writer_finalized.connect(resize_photos)
+    except Exception as e:
+        logger.exception('Plugin failed to execute: {}'.format(pprint.pformat(e)))

+ 1 - 1
photos/requirements.txt

@@ -1 +1 @@
-PIL
+Pillow