photos.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import datetime
  4. import itertools
  5. import json
  6. import logging
  7. import multiprocessing
  8. import os
  9. import pprint
  10. import re
  11. import sys
  12. from pelican.generators import ArticlesGenerator
  13. from pelican.generators import PagesGenerator
  14. from pelican.settings import DEFAULT_CONFIG
  15. from pelican import signals
  16. from pelican.utils import pelican_open
  17. logger = logging.getLogger(__name__)
  18. try:
  19. from PIL import Image
  20. from PIL import ImageDraw
  21. from PIL import ImageEnhance
  22. from PIL import ImageFont
  23. from PIL import ImageOps
  24. except ImportError:
  25. logger.error('PIL/Pillow not found')
  26. try:
  27. import piexif
  28. except ImportError:
  29. ispiexif = False
  30. logger.warning('piexif not found! Cannot use exif manipulation features')
  31. else:
  32. ispiexif = True
  33. logger.debug('piexif found.')
  34. def initialized(pelican):
  35. p = os.path.expanduser('~/Pictures')
  36. DEFAULT_CONFIG.setdefault('PHOTO_LIBRARY', p)
  37. DEFAULT_CONFIG.setdefault('PHOTO_GALLERY', (1024, 768, 80))
  38. DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
  39. DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
  40. DEFAULT_CONFIG.setdefault('PHOTO_SQUARE_THUMB', False)
  41. DEFAULT_CONFIG.setdefault('PHOTO_GALLERY_TITLE', '')
  42. DEFAULT_CONFIG.setdefault('PHOTO_ALPHA_BACKGROUND_COLOR', (255, 255, 255))
  43. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK', False)
  44. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_THUMB', False)
  45. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_TEXT', DEFAULT_CONFIG['SITENAME'])
  46. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_TEXT_COLOR', (255, 255, 255))
  47. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG', '')
  48. DEFAULT_CONFIG.setdefault('PHOTO_WATERMARK_IMG_SIZE', False)
  49. DEFAULT_CONFIG.setdefault('PHOTO_RESIZE_JOBS', 1)
  50. DEFAULT_CONFIG.setdefault('PHOTO_EXIF_KEEP', False)
  51. DEFAULT_CONFIG.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
  52. DEFAULT_CONFIG.setdefault('PHOTO_EXIF_AUTOROTATE', True)
  53. DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT', False)
  54. DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', DEFAULT_CONFIG['SITENAME'])
  55. DEFAULT_CONFIG.setdefault('PHOTO_LIGHTBOX_GALLERY_ATTR', 'data-lightbox')
  56. DEFAULT_CONFIG.setdefault('PHOTO_LIGHTBOX_CAPTION_ATTR', 'data-title')
  57. DEFAULT_CONFIG['queue_resize'] = {}
  58. DEFAULT_CONFIG['created_galleries'] = {}
  59. DEFAULT_CONFIG['plugin_dir'] = os.path.dirname(os.path.realpath(__file__))
  60. if pelican:
  61. pelican.settings.setdefault('PHOTO_LIBRARY', p)
  62. pelican.settings.setdefault('PHOTO_GALLERY', (1024, 768, 80))
  63. pelican.settings.setdefault('PHOTO_ARTICLE', (760, 506, 80))
  64. pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
  65. pelican.settings.setdefault('PHOTO_SQUARE_THUMB', False)
  66. pelican.settings.setdefault('PHOTO_GALLERY_TITLE', '')
  67. pelican.settings.setdefault('PHOTO_ALPHA_BACKGROUND_COLOR', (255, 255, 255))
  68. pelican.settings.setdefault('PHOTO_WATERMARK', False)
  69. pelican.settings.setdefault('PHOTO_WATERMARK_THUMB', False)
  70. pelican.settings.setdefault('PHOTO_WATERMARK_TEXT', pelican.settings['SITENAME'])
  71. pelican.settings.setdefault('PHOTO_WATERMARK_TEXT_COLOR', (255, 255, 255))
  72. pelican.settings.setdefault('PHOTO_WATERMARK_IMG', '')
  73. pelican.settings.setdefault('PHOTO_WATERMARK_IMG_SIZE', False)
  74. pelican.settings.setdefault('PHOTO_RESIZE_JOBS', 1)
  75. pelican.settings.setdefault('PHOTO_EXIF_KEEP', False)
  76. pelican.settings.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
  77. pelican.settings.setdefault('PHOTO_EXIF_AUTOROTATE', True)
  78. pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT', False)
  79. pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', pelican.settings['AUTHOR'])
  80. pelican.settings.setdefault('PHOTO_LIGHTBOX_GALLERY_ATTR', 'data-lightbox')
  81. pelican.settings.setdefault('PHOTO_LIGHTBOX_CAPTION_ATTR', 'data-title')
  82. def read_notes(filename, msg=None):
  83. notes = {}
  84. try:
  85. with pelican_open(filename) as text:
  86. for line in text.splitlines():
  87. if line.startswith('#'):
  88. continue
  89. m = line.split(':', 1)
  90. if len(m) > 1:
  91. pic = m[0].strip()
  92. note = m[1].strip()
  93. if pic and note:
  94. notes[pic] = note
  95. else:
  96. notes[line] = ''
  97. except Exception as e:
  98. if msg:
  99. logger.info('{} at file {}'.format(msg, filename))
  100. logger.debug('read_notes issue: {} at file {}. Debug message:{}'.format(msg, filename, e))
  101. return notes
  102. def enqueue_resize(orig, resized, spec=(640, 480, 80)):
  103. if resized not in DEFAULT_CONFIG['queue_resize']:
  104. DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
  105. elif DEFAULT_CONFIG['queue_resize'][resized] != (orig, spec):
  106. logger.error('photos: resize conflict for {}, {}-{} is not {}-{}'.format(resized, DEFAULT_CONFIG['queue_resize'][resized][0], DEFAULT_CONFIG['queue_resize'][resized][1], orig, spec))
  107. def isalpha(img):
  108. return True if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info) else False
  109. def remove_alpha(img, bg_color):
  110. background = Image.new("RGB", img.size, bg_color)
  111. background.paste(img, mask=img.split()[3]) # 3 is the alpha channel
  112. return background
  113. def ReduceOpacity(im, opacity):
  114. """Reduces Opacity.
  115. Returns an image with reduced opacity.
  116. Taken from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/362879
  117. """
  118. assert opacity >= 0 and opacity <= 1
  119. if isalpha(im):
  120. im = im.copy()
  121. else:
  122. im = im.convert('RGBA')
  123. alpha = im.split()[3]
  124. alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
  125. im.putalpha(alpha)
  126. return im
  127. def watermark_photo(image, settings):
  128. margin = [10, 10]
  129. opacity = 0.6
  130. watermark_layer = Image.new("RGBA", image.size, (0, 0, 0, 0))
  131. draw_watermark = ImageDraw.Draw(watermark_layer)
  132. text_reducer = 32
  133. image_reducer = 8
  134. text_size = [0, 0]
  135. mark_size = [0, 0]
  136. text_position = [0, 0]
  137. if settings['PHOTO_WATERMARK_TEXT']:
  138. font_name = 'SourceCodePro-Bold.otf'
  139. default_font = os.path.join(DEFAULT_CONFIG['plugin_dir'], font_name)
  140. font = ImageFont.FreeTypeFont(default_font, watermark_layer.size[0] // text_reducer)
  141. text_size = draw_watermark.textsize(settings['PHOTO_WATERMARK_TEXT'], font)
  142. text_position = [image.size[i] - text_size[i] - margin[i] for i in [0, 1]]
  143. draw_watermark.text(text_position, settings['PHOTO_WATERMARK_TEXT'], settings['PHOTO_WATERMARK_TEXT_COLOR'], font=font)
  144. if settings['PHOTO_WATERMARK_IMG']:
  145. mark_image = Image.open(settings['PHOTO_WATERMARK_IMG'])
  146. mark_image_size = [watermark_layer.size[0] // image_reducer for size in mark_size]
  147. mark_image_size = settings['PHOTO_WATERMARK_IMG_SIZE'] if settings['PHOTO_WATERMARK_IMG_SIZE'] else mark_image_size
  148. mark_image.thumbnail(mark_image_size, Image.ANTIALIAS)
  149. mark_position = [watermark_layer.size[i] - mark_image.size[i] - margin[i] for i in [0, 1]]
  150. mark_position = tuple([mark_position[0] - (text_size[0] // 2) + (mark_image_size[0] // 2), mark_position[1] - text_size[1]])
  151. if not isalpha(mark_image):
  152. mark_image = mark_image.convert('RGBA')
  153. watermark_layer.paste(mark_image, mark_position, mark_image)
  154. watermark_layer = ReduceOpacity(watermark_layer, opacity)
  155. image.paste(watermark_layer, (0, 0), watermark_layer)
  156. return image
  157. def rotate_image(img, exif_dict):
  158. if "exif" in img.info and piexif.ImageIFD.Orientation in exif_dict["0th"]:
  159. orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
  160. if orientation == 2:
  161. img = img.transpose(Image.FLIP_LEFT_RIGHT)
  162. elif orientation == 3:
  163. img = img.rotate(180)
  164. elif orientation == 4:
  165. img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
  166. elif orientation == 5:
  167. img = img.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT)
  168. elif orientation == 6:
  169. img = img.rotate(-90, expand=True)
  170. elif orientation == 7:
  171. img = img.rotate(90).transpose(Image.FLIP_LEFT_RIGHT)
  172. elif orientation == 8:
  173. img = img.rotate(90)
  174. return (img, exif_dict)
  175. def build_license(license, author):
  176. year = datetime.datetime.now().year
  177. license_file = os.path.join(DEFAULT_CONFIG['plugin_dir'], 'licenses.json')
  178. with open(license_file) as data_file:
  179. licenses = json.load(data_file)
  180. if any(license in k for k in licenses):
  181. return licenses[license]['Text'].format(Author=author, Year=year, URL=licenses[license]['URL'])
  182. else:
  183. return 'Copyright {Year} {Author}, All Rights Reserved'.format(Author=author, Year=year)
  184. def manipulate_exif(img, settings):
  185. try:
  186. exif = piexif.load(img.info['exif'])
  187. except Exception:
  188. logger.debug('EXIF information not found')
  189. exif = {}
  190. if settings['PHOTO_EXIF_AUTOROTATE']:
  191. img, exif = rotate_image(img, exif)
  192. if settings['PHOTO_EXIF_REMOVE_GPS']:
  193. exif.pop('GPS')
  194. if settings['PHOTO_EXIF_COPYRIGHT']:
  195. # We want to be minimally destructive to any preset exif author or copyright information.
  196. # If there is copyright or author information prefer that over everything else.
  197. if not exif['0th'].get(piexif.ImageIFD.Artist):
  198. exif['0th'][piexif.ImageIFD.Artist] = settings['PHOTO_EXIF_COPYRIGHT_AUTHOR']
  199. author = settings['PHOTO_EXIF_COPYRIGHT_AUTHOR']
  200. if not exif['0th'].get(piexif.ImageIFD.Copyright):
  201. license = build_license(settings['PHOTO_EXIF_COPYRIGHT'], author)
  202. exif['0th'][piexif.ImageIFD.Copyright] = license
  203. return (img, piexif.dump(exif))
  204. def resize_worker(orig, resized, spec, settings):
  205. logger.info('photos: make photo {} -> {}'.format(orig, resized))
  206. im = Image.open(orig)
  207. if ispiexif and settings['PHOTO_EXIF_KEEP'] and im.format == 'JPEG': # Only works with JPEG exif for sure.
  208. try:
  209. im, exif_copy = manipulate_exif(im, settings)
  210. except:
  211. logger.info('photos: no EXIF or EXIF error in {}'.format(orig))
  212. exif_copy = b''
  213. else:
  214. exif_copy = b''
  215. icc_profile = im.info.get("icc_profile", None)
  216. if settings['PHOTO_SQUARE_THUMB'] and spec == settings['PHOTO_THUMB']:
  217. im = ImageOps.fit(im, (spec[0], spec[1]), Image.ANTIALIAS)
  218. im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
  219. directory = os.path.split(resized)[0]
  220. if isalpha(im):
  221. im = remove_alpha(im, settings['PHOTO_ALPHA_BACKGROUND_COLOR'])
  222. if not os.path.exists(directory):
  223. try:
  224. os.makedirs(directory)
  225. except Exception:
  226. logger.exception('Could not create {}'.format(directory))
  227. else:
  228. logger.debug('Directory already exists at {}'.format(os.path.split(resized)[0]))
  229. if settings['PHOTO_WATERMARK']:
  230. isthumb = True if spec == settings['PHOTO_THUMB'] else False
  231. if not isthumb or (isthumb and settings['PHOTO_WATERMARK_THUMB']):
  232. im = watermark_photo(im, settings)
  233. im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile, exif=exif_copy)
  234. def resize_photos(generator, writer):
  235. if generator.settings['PHOTO_RESIZE_JOBS'] == -1:
  236. debug = True
  237. generator.settings['PHOTO_RESIZE_JOBS'] = 1
  238. else:
  239. debug = False
  240. pool = multiprocessing.Pool(generator.settings['PHOTO_RESIZE_JOBS'])
  241. logger.debug('Debug Status: {}'.format(debug))
  242. for resized, what in DEFAULT_CONFIG['queue_resize'].items():
  243. resized = os.path.join(generator.output_path, resized)
  244. orig, spec = what
  245. if (not os.path.isfile(resized) or os.path.getmtime(orig) > os.path.getmtime(resized)):
  246. if debug:
  247. resize_worker(orig, resized, spec, generator.settings)
  248. else:
  249. pool.apply_async(resize_worker, (orig, resized, spec, generator.settings))
  250. pool.close()
  251. pool.join()
  252. def detect_content(content):
  253. hrefs = None
  254. def replacer(m):
  255. what = m.group('what')
  256. value = m.group('value')
  257. tag = m.group('tag')
  258. output = m.group(0)
  259. if what in ('photo', 'lightbox'):
  260. if value.startswith('/'):
  261. value = value[1:]
  262. path = os.path.join(
  263. os.path.expanduser(settings['PHOTO_LIBRARY']),
  264. value
  265. )
  266. if os.path.isfile(path):
  267. photo_prefix = os.path.splitext(value)[0].lower()
  268. if what == 'photo':
  269. photo_article = photo_prefix + 'a.jpg'
  270. enqueue_resize(
  271. path,
  272. os.path.join('photos', photo_article),
  273. settings['PHOTO_ARTICLE']
  274. )
  275. output = ''.join((
  276. '<',
  277. m.group('tag'),
  278. m.group('attrs_before'),
  279. m.group('src'),
  280. '=',
  281. m.group('quote'),
  282. os.path.join(settings['SITEURL'], 'photos', photo_article),
  283. m.group('quote'),
  284. m.group('attrs_after'),
  285. ))
  286. elif what == 'lightbox' and tag == 'img':
  287. photo_gallery = photo_prefix + '.jpg'
  288. enqueue_resize(
  289. path,
  290. os.path.join('photos', photo_gallery),
  291. settings['PHOTO_GALLERY']
  292. )
  293. photo_thumb = photo_prefix + 't.jpg'
  294. enqueue_resize(
  295. path,
  296. os.path.join('photos', photo_thumb),
  297. settings['PHOTO_THUMB']
  298. )
  299. lightbox_attr_list = ['']
  300. gallery_name = value.split('/')[0]
  301. lightbox_attr_list.append('{}="{}"'.format(
  302. settings['PHOTO_LIGHTBOX_GALLERY_ATTR'],
  303. gallery_name
  304. ))
  305. captions = read_notes(
  306. os.path.join(os.path.dirname(path), 'captions.txt'),
  307. msg = 'photos: No captions for gallery'
  308. )
  309. caption = captions.get(os.path.basename(path)) if captions else None
  310. if caption:
  311. lightbox_attr_list.append('{}="{}"'.format(
  312. settings['PHOTO_LIGHTBOX_CAPTION_ATTR'],
  313. caption
  314. ))
  315. lightbox_attrs = ' '.join(lightbox_attr_list)
  316. output = ''.join((
  317. '<a href=',
  318. m.group('quote'),
  319. os.path.join(settings['SITEURL'], 'photos', photo_gallery),
  320. m.group('quote'),
  321. lightbox_attrs,
  322. '><img',
  323. m.group('attrs_before'),
  324. 'src=',
  325. m.group('quote'),
  326. os.path.join(settings['SITEURL'], 'photos', photo_thumb),
  327. m.group('quote'),
  328. m.group('attrs_after'),
  329. '</a>'
  330. ))
  331. else:
  332. logger.error('photos: No photo %s', path)
  333. return output
  334. if hrefs is None:
  335. regex = r"""
  336. <\s*
  337. (?P<tag>[^\s\>]+) # detect the tag
  338. (?P<attrs_before>[^\>]*)
  339. (?P<src>href|src) # match tag with src and href attr
  340. \s*=
  341. (?P<quote>["\']) # require value to be quoted
  342. (?P<path>{0}(?P<value>.*?)) # the url value
  343. (?P=quote)
  344. (?P<attrs_after>[^\>]*>)
  345. """.format(
  346. content.settings['INTRASITE_LINK_REGEX']
  347. )
  348. hrefs = re.compile(regex, re.X)
  349. if content._content and ('{photo}' in content._content or '{lightbox}' in content._content):
  350. settings = content.settings
  351. content._content = hrefs.sub(replacer, content._content)
  352. def galleries_string_decompose(gallery_string):
  353. splitter_regex = re.compile(r'[\s,]*?({photo}|{filename})')
  354. title_regex = re.compile(r'{(.+)}')
  355. galleries = map(unicode.strip if sys.version_info.major == 2 else str.strip, filter(None, splitter_regex.split(gallery_string)))
  356. galleries = [gallery[1:] if gallery.startswith('/') else gallery for gallery in galleries]
  357. if len(galleries) % 2 == 0 and ' ' not in galleries:
  358. galleries = zip(zip(['type'] * len(galleries[0::2]), galleries[0::2]), zip(['location'] * len(galleries[0::2]), galleries[1::2]))
  359. galleries = [dict(gallery) for gallery in galleries]
  360. for gallery in galleries:
  361. title = re.search(title_regex, gallery['location'])
  362. if title:
  363. gallery['title'] = title.group(1)
  364. gallery['location'] = re.sub(title_regex, '', gallery['location']).strip()
  365. else:
  366. gallery['title'] = DEFAULT_CONFIG['PHOTO_GALLERY_TITLE']
  367. return galleries
  368. else:
  369. logger.error('Unexpected gallery location format! \n{}'.format(pprint.pformat(galleries)))
  370. def process_gallery(generator, content, location):
  371. content.photo_gallery = []
  372. galleries = galleries_string_decompose(location)
  373. for gallery in galleries:
  374. if gallery['location'] in DEFAULT_CONFIG['created_galleries']:
  375. content.photo_gallery.append((gallery['location'], DEFAULT_CONFIG['created_galleries'][gallery]))
  376. continue
  377. if gallery['type'] == '{photo}':
  378. dir_gallery = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), gallery['location'])
  379. rel_gallery = gallery['location']
  380. elif gallery['type'] == '{filename}':
  381. base_path = os.path.join(generator.path, content.relative_dir)
  382. dir_gallery = os.path.join(base_path, gallery['location'])
  383. rel_gallery = os.path.join(content.relative_dir, gallery['location'])
  384. if os.path.isdir(dir_gallery):
  385. logger.info('photos: Gallery detected: {}'.format(rel_gallery))
  386. dir_photo = os.path.join('photos', rel_gallery.lower())
  387. dir_thumb = os.path.join('photos', rel_gallery.lower())
  388. exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
  389. msg='photos: No EXIF for gallery')
  390. captions = read_notes(os.path.join(dir_gallery, 'captions.txt'), msg='photos: No captions for gallery')
  391. blacklist = read_notes(os.path.join(dir_gallery, 'blacklist.txt'), msg='photos: No blacklist for gallery')
  392. content_gallery = []
  393. title = gallery['title']
  394. for pic in sorted(os.listdir(dir_gallery)):
  395. if pic.startswith('.'):
  396. continue
  397. if pic.endswith('.txt'):
  398. continue
  399. if pic in blacklist:
  400. continue
  401. photo = os.path.splitext(pic)[0].lower() + '.jpg'
  402. thumb = os.path.splitext(pic)[0].lower() + 't.jpg'
  403. content_gallery.append((
  404. pic,
  405. os.path.join(dir_photo, photo),
  406. os.path.join(dir_thumb, thumb),
  407. exifs.get(pic, ''),
  408. captions.get(pic, '')))
  409. enqueue_resize(
  410. os.path.join(dir_gallery, pic),
  411. os.path.join(dir_photo, photo),
  412. generator.settings['PHOTO_GALLERY'])
  413. enqueue_resize(
  414. os.path.join(dir_gallery, pic),
  415. os.path.join(dir_thumb, thumb),
  416. generator.settings['PHOTO_THUMB'])
  417. content.photo_gallery.append((title, content_gallery))
  418. logger.debug('Gallery Data: '.format(pprint.pformat(content.photo_gallery)))
  419. DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
  420. else:
  421. logger.error('photos: Gallery does not exist: {} at {}'.format(gallery['location'], dir_gallery))
  422. def detect_gallery(generator, content):
  423. if 'gallery' in content.metadata:
  424. gallery = content.metadata.get('gallery')
  425. if gallery.startswith('{photo}') or gallery.startswith('{filename}'):
  426. process_gallery(generator, content, gallery)
  427. elif gallery:
  428. logger.error('photos: Gallery tag not recognized: {}'.format(gallery))
  429. def image_clipper(x):
  430. return x[8:] if x[8] == '/' else x[7:]
  431. def file_clipper(x):
  432. return x[11:] if x[10] == '/' else x[10:]
  433. def process_image(generator, content, image):
  434. if image.startswith('{photo}'):
  435. path = os.path.join(os.path.expanduser(generator.settings['PHOTO_LIBRARY']), image_clipper(image))
  436. image = image_clipper(image)
  437. elif image.startswith('{filename}'):
  438. path = os.path.join(content.relative_dir, file_clipper(image))
  439. image = file_clipper(image)
  440. if os.path.isfile(path):
  441. photo = os.path.splitext(image)[0].lower() + 'a.jpg'
  442. thumb = os.path.splitext(image)[0].lower() + 't.jpg'
  443. content.photo_image = (
  444. os.path.basename(image).lower(),
  445. os.path.join('photos', photo),
  446. os.path.join('photos', thumb))
  447. enqueue_resize(
  448. path,
  449. os.path.join('photos', photo),
  450. generator.settings['PHOTO_ARTICLE'])
  451. enqueue_resize(
  452. path,
  453. os.path.join('photos', thumb),
  454. generator.settings['PHOTO_THUMB'])
  455. else:
  456. logger.error('photo: No photo for {} at {}'.format(content.source_path, path))
  457. def detect_image(generator, content):
  458. image = content.metadata.get('image', None)
  459. if image:
  460. if image.startswith('{photo}') or image.startswith('{filename}'):
  461. process_image(generator, content, image)
  462. else:
  463. logger.error('photos: Image tag not recognized: {}'.format(image))
  464. def detect_images_and_galleries(generators):
  465. """Runs generator on both pages and articles."""
  466. for generator in generators:
  467. if isinstance(generator, ArticlesGenerator):
  468. for article in itertools.chain(generator.articles, generator.translations, generator.drafts):
  469. detect_image(generator, article)
  470. detect_gallery(generator, article)
  471. elif isinstance(generator, PagesGenerator):
  472. for page in itertools.chain(generator.pages, generator.translations, generator.hidden_pages):
  473. detect_image(generator, page)
  474. detect_gallery(generator, page)
  475. def register():
  476. """Uses the new style of registration based on GitHub Pelican issue #314."""
  477. signals.initialized.connect(initialized)
  478. try:
  479. signals.content_object_init.connect(detect_content)
  480. signals.all_generators_finalized.connect(detect_images_and_galleries)
  481. signals.article_writer_finalized.connect(resize_photos)
  482. except Exception as e:
  483. logger.exception('Plugin failed to execute: {}'.format(pprint.pformat(e)))