4 from pelican import signals
7 logger = logging.getLogger(__name__)
10 from PIL import Image, ImageOps
13 logger.warning("Unable to load PIL, disabling thumbnailer")
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',
23 DEFAULT_TEMPLATE = """<a href="{url}" rel="shadowbox" title="{filename}"><img src="{thumbnail}" alt="{filename}"></a>"""
24 DEFAULT_GALLERY_THUMB = "thumbnail_square"
26 class Resizer(object):
27 """ Resizes based on a text specification, see readme """
29 REGEX = re.compile(r'(\d+|\?)x(\d+|\?)')
31 def __init__(self, name, spec, root):
34 # The location of input images from _image_path.
37 def _null_resize(self, w, h, image):
40 def _exact_resize(self, w, h, image):
41 retval = ImageOps.fit(image, (w,h), Image.BICUBIC)
44 def _aspect_resize(self, w, h, image):
46 retval.thumbnail((w, h), Image.ANTIALIAS)
50 def resize(self, image):
51 resizer = self._null_resize
53 # Square resize and crop
54 if 'x' not in self._spec:
55 resizer = self._exact_resize
56 targetw = int(self._spec)
59 matches = self.REGEX.search(self._spec)
60 tmpw = matches.group(1)
61 tmph = matches.group(2)
64 if tmpw == '?' and tmph == '?':
65 targetw = image.size[0]
66 targeth = image.size[1]
67 resizer = self._null_resize
71 targetw = image.size[0]
73 resizer = self._aspect_resize
78 targeth = image.size[1]
79 resizer = self._aspect_resize
85 resizer = self._exact_resize
87 logger.debug("Using resizer {0}".format(resizer.__name__))
88 return resizer(targetw, targeth, image)
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:]
97 # Generate the new filename.
98 (basename, ext) = path.splitext(new_filename)
99 return "{0}_{1}{2}".format(basename, self._name, ext)
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
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
109 filename = path.join(out_path, path.basename(in_path))
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):
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)))
122 logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename)))
125 def resize_thumbnails(pelican):
126 """ Resize a directory tree full of images into thumbnails
128 :param pelican: The pelican instance
135 in_path = _image_path(pelican)
137 include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX')
139 pattern = re.compile(include_regex)
140 is_included = lambda name: pattern.match(name)
142 is_included = lambda name: not name.startswith('.')
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(
155 out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME'))
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)))
166 return path.join(base_out_path, name)
171 def _image_path(pelican):
172 return path.join(pelican.settings['PATH'],
173 pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/')
176 def expand_gallery(generator, metadata):
177 """ Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH
179 :param pelican: The pelican instance
182 if "gallery" not in metadata or metadata['gallery'] is None:
183 return # If no gallery specified, we do nothing
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(
205 metadata['gallery_content'] = "\n".join(lines)
209 signals.finalized.connect(resize_thumbnails)
210 signals.article_generator_context.connect(expand_gallery)