thumbnailer.py 7.4 KB

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