first version of the new website
[markerbeacon.git] / plugins / thumbnailer / thumbnailer.py
1 import os
2 import os.path as path
3 import re
4 from pelican import signals
5
6 import logging
7 logger = logging.getLogger(__name__)
8
9 try:
10     from PIL import Image, ImageOps
11     enabled = True
12 except ImportError:
13     logger.warning("Unable to load PIL, disabling thumbnailer")
14     enabled = False
15
16 DEFAULT_IMAGE_DIR = "pictures"
17 DEFAULT_THUMBNAIL_DIR = "thumbnails"
18 DEFAULT_THUMBNAIL_SIZES = {
19     'thumbnail_square': '150',
20     'thumbnail_wide': '150x?',
21     'thumbnail_tall': '?x150',
22 }
23 DEFAULT_TEMPLATE = """<a href="{url}" rel="shadowbox" title="{filename}"><img src="{thumbnail}" alt="{filename}"></a>"""
24 DEFAULT_GALLERY_THUMB = "thumbnail_square"
25
26 class Resizer(object):
27     """ Resizes based on a text specification, see readme """
28
29     REGEX = re.compile(r'(\d+|\?)x(\d+|\?)')
30
31     def __init__(self, name, spec, root):
32         self._name = name
33         self._spec = spec
34         # The location of input images from _image_path.
35         self._root = root
36
37     def _null_resize(self, w, h, image):
38         return image
39
40     def _exact_resize(self, w, h, image):
41         retval = ImageOps.fit(image, (w,h), Image.BICUBIC)
42         return retval
43
44     def _aspect_resize(self, w, h, image):
45         retval = image.copy()
46         retval.thumbnail((w, h), Image.ANTIALIAS)
47
48         return retval
49
50     def resize(self, image):
51         resizer = self._null_resize
52
53         # Square resize and crop
54         if 'x' not in self._spec:
55             resizer = self._exact_resize
56             targetw = int(self._spec)
57             targeth = targetw
58         else:
59             matches = self.REGEX.search(self._spec)
60             tmpw = matches.group(1)
61             tmph = matches.group(2)
62
63             # Full Size
64             if tmpw == '?' and tmph == '?':
65                 targetw = image.size[0]
66                 targeth = image.size[1]
67                 resizer = self._null_resize
68
69             # Set Height Size
70             if tmpw == '?':
71                 targetw = image.size[0]
72                 targeth = int(tmph)
73                 resizer = self._aspect_resize
74
75             # Set Width Size
76             elif tmph == '?':
77                 targetw = int(tmpw)
78                 targeth = image.size[1]
79                 resizer = self._aspect_resize
80
81             # Scale and Crop
82             else:
83                 targetw = int(tmpw)
84                 targeth = int(tmph)
85                 resizer = self._exact_resize
86
87         logger.debug("Using resizer {0}".format(resizer.__name__))
88         return resizer(targetw, targeth, image)
89
90     def get_thumbnail_name(self, in_path):
91         # Find the partial path + filename beyond the input image directory.
92         prefix = path.commonprefix([in_path, self._root])
93         new_filename = in_path[len(prefix):]
94         if new_filename.startswith('/'):
95             new_filename = new_filename[1:]
96
97         # Generate the new filename.
98         (basename, ext) = path.splitext(new_filename)
99         return "{0}_{1}{2}".format(basename, self._name, ext)
100
101     def resize_file_to(self, in_path, out_path, keep_filename=False):
102         """ Given a filename, resize and save the image per the specification into out_path
103
104         :param in_path: path to image file to save.  Must be supported by PIL
105         :param out_path: path to the directory root for the outputted thumbnails to be stored
106         :return: None
107         """
108         if keep_filename:
109             filename = path.join(out_path, path.basename(in_path))
110         else:
111             filename = path.join(out_path, self.get_thumbnail_name(in_path))
112         out_path = path.dirname(filename)
113         if not path.exists(out_path):
114             os.makedirs(out_path)
115         if not path.exists(filename):
116             try:
117                 image = Image.open(in_path)
118                 thumbnail = self.resize(image)
119                 thumbnail.save(filename)
120                 logger.info("Generated Thumbnail {0}".format(path.basename(filename)))
121             except IOError:
122                 logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename)))
123
124
125 def resize_thumbnails(pelican):
126     """ Resize a directory tree full of images into thumbnails
127
128     :param pelican: The pelican instance
129     :return: None
130     """
131     global enabled
132     if not enabled:
133         return
134
135     in_path = _image_path(pelican)
136
137     include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX')
138     if include_regex:
139         pattern = re.compile(include_regex)
140         is_included = lambda name: pattern.match(name)
141     else:
142         is_included = lambda name: not name.startswith('.')
143
144     sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES)
145     resizers = dict((k, Resizer(k, v, in_path)) for k,v in sizes.items())
146     logger.debug("Thumbnailer Started")
147     for dirpath, _, filenames in os.walk(in_path):
148         for filename in filenames:
149             if is_included(filename):
150                 for name, resizer in resizers.items():
151                     in_filename = path.join(dirpath, filename)
152                     out_path = get_out_path(pelican, in_path, in_filename, name)
153                     resizer.resize_file_to(
154                         in_filename,
155                         out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME'))
156
157
158 def get_out_path(pelican, in_path, in_filename, name):
159     base_out_path = path.join(pelican.settings['OUTPUT_PATH'],
160                          pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR))
161     logger.debug("Processing thumbnail {0}=>{1}".format(in_filename, name))
162     if pelican.settings.get('THUMBNAIL_KEEP_NAME', False):
163         if pelican.settings.get('THUMBNAIL_KEEP_TREE', False):
164             return path.join(base_out_path, name, path.dirname(path.relpath(in_filename, in_path)))
165         else:
166             return path.join(base_out_path, name)
167     else:
168         return base_out_path
169
170
171 def _image_path(pelican):
172     return path.join(pelican.settings['PATH'],
173         pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/')
174
175
176 def expand_gallery(generator, metadata):
177     """ Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH
178
179     :param pelican: The pelican instance
180     :return: None
181     """
182     if "gallery" not in metadata or metadata['gallery'] is None:
183         return  # If no gallery specified, we do nothing
184
185     lines = [ ]
186     base_path = _image_path(generator)
187     in_path = path.join(base_path, metadata['gallery'])
188     template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE)
189     thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB)
190     thumbnail_prefix = generator.settings.get("")
191     resizer = Resizer(thumbnail_name, '?x?', base_path)
192     for dirpath, _, filenames in os.walk(in_path):
193         for filename in filenames:
194             if not filename.startswith('.'):
195                 url = path.join(dirpath, filename).replace(base_path, "")[1:]
196                 url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/')
197                 logger.debug("GALLERY: {0}".format(url))
198                 thumbnail = resizer.get_thumbnail_name(filename)
199                 thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/')
200                 lines.append(template.format(
201                     filename=filename,
202                     url=url,
203                     thumbnail=thumbnail,
204                 ))
205     metadata['gallery_content'] = "\n".join(lines)
206
207
208 def register():
209     signals.finalized.connect(resize_thumbnails)
210     signals.article_generator_context.connect(expand_gallery)