photos.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import os
  4. import re
  5. import logging
  6. from pelican import signals
  7. from pelican.utils import pelican_open
  8. from PIL import Image, ExifTags
  9. from itertools import chain
  10. logger = logging.getLogger(__name__)
  11. queue_resize = dict()
  12. hrefs = None
  13. def initialized(pelican):
  14. p = os.path.expanduser('~/Pictures')
  15. from pelican.settings import DEFAULT_CONFIG
  16. DEFAULT_CONFIG.setdefault('PHOTO_LIBRARY', p)
  17. DEFAULT_CONFIG.setdefault('PHOTO_GALLERY', (1024, 768, 80))
  18. DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', ( 760, 506, 80))
  19. DEFAULT_CONFIG.setdefault('PHOTO_THUMB', ( 192, 144, 60))
  20. if pelican:
  21. pelican.settings.setdefault('PHOTO_LIBRARY', p)
  22. pelican.settings.setdefault('PHOTO_GALLERY', (1024, 768, 80))
  23. pelican.settings.setdefault('PHOTO_ARTICLE', ( 760, 506, 80))
  24. pelican.settings.setdefault('PHOTO_THUMB', ( 192, 144, 60))
  25. def read_notes(filename, msg=None):
  26. notes = {}
  27. try:
  28. with pelican_open(filename) as text:
  29. for line in text.splitlines():
  30. m = line.split(':', 1)
  31. if len(m) > 1:
  32. pic = m[0].strip()
  33. note = m[1].strip()
  34. if pic and note:
  35. notes[pic] = note
  36. except:
  37. if msg:
  38. logger.warning(msg, filename)
  39. return notes
  40. def enqueue_resize(orig, resized, spec=(640, 480, 80)):
  41. global queue_resize
  42. if resized not in queue_resize:
  43. queue_resize[resized] = (orig, spec)
  44. elif queue_resize[resized] != (orig, spec):
  45. logger.error('photos: resize conflict for {}, {}-{} is not {}-{}',
  46. resized,
  47. queue_resize[resized][0], queue_resize[resized][1],
  48. orig, spec)
  49. def resize_photos(generator, writer):
  50. print('photos: {} photo resizes to consider.'
  51. .format(len(queue_resize.items())))
  52. for resized, what in queue_resize.items():
  53. resized = os.path.join(generator.output_path, resized)
  54. orig, spec = what
  55. if (not os.path.isfile(resized) or
  56. os.path.getmtime(orig) > os.path.getmtime(resized)):
  57. logger.info('photos: make photo %s -> %s', orig, resized)
  58. im = Image.open(orig)
  59. try:
  60. exif = im._getexif()
  61. except Exception:
  62. exif = None
  63. try:
  64. icc_profile = im.info.get("icc_profile")
  65. except Exception:
  66. icc_profile = None
  67. if exif:
  68. for tag, value in exif.items():
  69. decoded = ExifTags.TAGS.get(tag, tag)
  70. if decoded == 'Orientation':
  71. if value == 3: im = im.rotate(180)
  72. elif value == 6: im = im.rotate(270)
  73. elif value == 8: im = im.rotate(90)
  74. break
  75. im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
  76. try:
  77. os.makedirs(os.path.split(resized)[0])
  78. except:
  79. pass
  80. im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile)
  81. def detect_content(content):
  82. def replacer(m):
  83. what = m.group('what')
  84. value = m.group('value')
  85. origin = m.group('path')
  86. if what == 'photo':
  87. if value.startswith('/'):
  88. value = value[1:]
  89. path = os.path.join(settings['PHOTO_LIBRARY'], value)
  90. if not os.path.isfile(path):
  91. logger.error('photos: No photo %s', path)
  92. else:
  93. photo = os.path.splitext(value)[0].lower() + 'a.jpg'
  94. origin = os.path.join('/photos', photo)
  95. enqueue_resize(
  96. path,
  97. os.path.join('photos', photo),
  98. settings['PHOTO_ARTICLE'])
  99. return ''.join((m.group('markup'), m.group('quote'), origin,
  100. m.group('quote')))
  101. global hrefs
  102. if hrefs is None:
  103. regex = r"""
  104. (?P<markup><\s*[^\>]* # match tag with src and href attr
  105. (?:href|src)\s*=)
  106. (?P<quote>["\']) # require value to be quoted
  107. (?P<path>{0}(?P<value>.*?)) # the url value
  108. \2""".format(content.settings['INTRASITE_LINK_REGEX'])
  109. hrefs = re.compile(regex, re.X)
  110. if content._content and '{photo}' in content._content:
  111. settings = content.settings
  112. content._content = hrefs.sub(replacer, content._content)
  113. def process_gallery_photo(generator, article, gallery):
  114. if gallery.startswith('/'):
  115. gallery = gallery[1:]
  116. dir_gallery = os.path.join(generator.settings['PHOTO_LIBRARY'], gallery)
  117. if os.path.isdir(dir_gallery):
  118. logger.info('photos: Gallery detected: %s', gallery)
  119. dir_photo = os.path.join('photos', gallery.lower())
  120. dir_thumb = os.path.join('photos', gallery.lower())
  121. exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
  122. msg='photos: No EXIF for gallery %s')
  123. captions = read_notes(os.path.join(dir_gallery, 'captions.txt'))
  124. article.photo_gallery = []
  125. for pic in os.listdir(dir_gallery):
  126. if pic.startswith('.'): continue
  127. if pic.endswith('.txt'): continue
  128. photo = os.path.splitext(pic)[0].lower() + '.jpg'
  129. thumb = os.path.splitext(pic)[0].lower() + 't.jpg'
  130. article.photo_gallery.append((
  131. pic,
  132. os.path.join(dir_photo, photo),
  133. os.path.join(dir_thumb, thumb),
  134. exifs.get(pic, ''),
  135. captions.get(pic, '')))
  136. enqueue_resize(
  137. os.path.join(dir_gallery, pic),
  138. os.path.join(dir_photo, photo),
  139. generator.settings['PHOTO_GALLERY'])
  140. enqueue_resize(
  141. os.path.join(dir_gallery, pic),
  142. os.path.join(dir_thumb, thumb),
  143. generator.settings['PHOTO_THUMB'])
  144. def process_gallery_filename(generator, article, gallery):
  145. if gallery.startswith('/'):
  146. gallery = gallery[1:]
  147. else:
  148. gallery = os.path.join(article.relative_dir, gallery)
  149. dir_gallery = os.path.join(generator.settings['PHOTO_LIBRARY'], gallery)
  150. if os.path.isdir(dir_gallery):
  151. logger.info('photos: Gallery detected: %s', gallery)
  152. dir_photo = gallery.lower()
  153. dir_thumb = os.path.join('photos', gallery.lower())
  154. exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
  155. msg='photos: No EXIF for gallery %s')
  156. captions = read_notes(os.path.join(dir_gallery, 'captions.txt'))
  157. article.photo_gallery = []
  158. for pic in os.listdir(dir_gallery):
  159. if pic.startswith('.'): continue
  160. if pic.endswith('.txt'): continue
  161. photo = pic.lower()
  162. thumb = os.path.splitext(pic)[0].lower() + 't.jpg'
  163. article.photo_gallery.append((
  164. pic,
  165. os.path.join(dir_photo, photo),
  166. os.path.join(dir_thumb, thumb),
  167. exifs.get(pic, ''),
  168. captions.get(pic, '')))
  169. enqueue_resize(
  170. os.path.join(dir_gallery, pic),
  171. os.path.join(dir_thumb, thumb),
  172. generator.settings['PHOTO_THUMB'])
  173. def detect_gallery(generator):
  174. for article in chain(generator.articles, generator.drafts):
  175. if 'gallery' in article.metadata:
  176. gallery = article.metadata.get('gallery')
  177. if gallery.startswith('{photo}'):
  178. process_gallery_photo(generator, article, gallery[7:])
  179. elif gallery.startswith('{filename}'):
  180. process_gallery_filename(generator, article, gallery[10:])
  181. elif gallery:
  182. logger.error('photos: Gallery tag not recognized: %s', gallery)
  183. def process_image_photo(generator, article, image):
  184. if image.startswith('/'):
  185. image = image[1:]
  186. path = os.path.join(generator.settings['PHOTO_LIBRARY'], image)
  187. if os.path.isfile(path):
  188. photo = os.path.splitext(image)[0].lower() + 'a.jpg'
  189. thumb = os.path.splitext(image)[0].lower() + 't.jpg'
  190. article.photo_image = (
  191. os.path.basename(image).lower(),
  192. os.path.join('photos', photo),
  193. os.path.join('photos', thumb))
  194. enqueue_resize(
  195. path,
  196. os.path.join('photos', photo),
  197. generator.settings['PHOTO_ARTICLE'])
  198. enqueue_resize(
  199. path,
  200. os.path.join('photos', thumb),
  201. generator.settings['PHOTO_THUMB'])
  202. else:
  203. logger.error('photo: No photo for %s at %s', article.source_path, path)
  204. def process_image_filename(generator, article, image):
  205. if image.startswith('/'):
  206. image = image[1:]
  207. else:
  208. image = os.path.join(article.relative_dir, image)
  209. path = os.path.join(generator.path, image)
  210. if os.path.isfile(path):
  211. small = os.path.splitext(image)[0].lower() + 't.jpg'
  212. article.photo_image = (
  213. os.path.basename(image),
  214. image.lower(),
  215. os.path.join('photos', small))
  216. enqueue_resize(
  217. path,
  218. os.path.join('photos', small),
  219. generator.settings['PHOTO_THUMB'])
  220. else:
  221. logger.error('photos: No photo at %s', path)
  222. def detect_image(generator):
  223. for article in chain(generator.articles, generator.drafts):
  224. image = article.metadata.get('image', None)
  225. if image:
  226. if image.startswith('{photo}'):
  227. process_image_photo(generator, article, image[7:])
  228. elif image.startswith('{filename}'):
  229. process_image_filename(generator, article, image[10:])
  230. else:
  231. logger.error('photos: Image tag not recognized: %s', image)
  232. def register():
  233. signals.initialized.connect(initialized)
  234. signals.content_object_init.connect(detect_content)
  235. signals.article_generator_finalized.connect(detect_gallery)
  236. signals.article_generator_finalized.connect(detect_image)
  237. signals.article_writer_finalized.connect(resize_photos)