Skip to content
Snippets Groups Projects
Commit ad2e77eb authored by Anton Sarukhanov's avatar Anton Sarukhanov
Browse files

WIP: Photo Gallery

parent cf048d51
No related branches found
No related tags found
No related merge requests found
Title: Ukraine
Description: Kyiv, Turbiv, Vinnitsa
gallery: {photo}/ukraine-2019
test
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import itertools
import logging
import multiprocessing
import os
import pprint
import re
import shutil
from pelican.generators import ArticlesGenerator
from pelican.generators import PagesGenerator
from pelican.settings import DEFAULT_CONFIG
from pelican import signals
from pelican.utils import pelican_open
logger = logging.getLogger(__name__)
try:
from PIL import Image
from PIL import ImageOps
except ImportError:
logger.error('PIL/Pillow not found')
try:
import piexif
except ImportError:
ispiexif = False
logger.warning('piexif not found! Cannot use exif manipulation features')
else:
ispiexif = True
logger.debug('piexif found.')
def initialized(pelican):
"""Initialize the plugin.
This should be triggered on Pelican's `initialized` signal."""
p = os.path.expanduser('~/Pictures')
DEFAULT_CONFIG.setdefault('PHOTO_LIBRARY', p)
DEFAULT_CONFIG.setdefault('PHOTO_GALLERY', (1024, 768, 80))
DEFAULT_CONFIG.setdefault('PHOTO_ARTICLE', (760, 506, 80))
DEFAULT_CONFIG.setdefault('PHOTO_THUMB', (192, 144, 60))
DEFAULT_CONFIG.setdefault('PHOTO_SQUARE_THUMB', False)
DEFAULT_CONFIG.setdefault('PHOTO_GALLERY_TITLE', '')
DEFAULT_CONFIG.setdefault('PHOTO_RESIZE_JOBS', 1)
DEFAULT_CONFIG.setdefault('VIDEO_COPY_JOBS', 1)
DEFAULT_CONFIG.setdefault('PHOTO_EXIF_KEEP', False)
DEFAULT_CONFIG.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
DEFAULT_CONFIG.setdefault('PHOTO_EXIF_AUTOROTATE', True)
DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT', False)
DEFAULT_CONFIG.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', DEFAULT_CONFIG['SITENAME'])
DEFAULT_CONFIG.setdefault('PHOTO_LIGHTBOX_GALLERY_ATTR', 'data-lightbox')
DEFAULT_CONFIG.setdefault('PHOTO_LIGHTBOX_CAPTION_ATTR', 'data-title')
DEFAULT_CONFIG['queue_resize'] = {}
DEFAULT_CONFIG['queue_copy'] = {}
DEFAULT_CONFIG['created_galleries'] = {}
DEFAULT_CONFIG['plugin_dir'] = os.path.dirname(os.path.realpath(__file__))
if pelican:
pelican.settings.setdefault('PHOTO_LIBRARY', p)
pelican.settings.setdefault('PHOTO_GALLERY', (1024, 768, 80))
pelican.settings.setdefault('PHOTO_ARTICLE', (760, 506, 80))
pelican.settings.setdefault('PHOTO_THUMB', (192, 144, 60))
pelican.settings.setdefault('PHOTO_SQUARE_THUMB', False)
pelican.settings.setdefault('PHOTO_GALLERY_TITLE', '')
pelican.settings.setdefault('PHOTO_RESIZE_JOBS', 1)
pelican.settings.setdefault('VIDEO_COPY_JOBS', 1)
pelican.settings.setdefault('PHOTO_EXIF_KEEP', False)
pelican.settings.setdefault('PHOTO_EXIF_REMOVE_GPS', False)
pelican.settings.setdefault('PHOTO_EXIF_AUTOROTATE', True)
pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT', False)
pelican.settings.setdefault('PHOTO_EXIF_COPYRIGHT_AUTHOR', pelican.settings['AUTHOR'])
pelican.settings.setdefault('PHOTO_LIGHTBOX_GALLERY_ATTR', 'data-lightbox')
pelican.settings.setdefault('PHOTO_LIGHTBOX_CAPTION_ATTR', 'data-title')
def read_notes(filename, msg=None):
notes = {}
try:
with pelican_open(filename) as text:
for line in text.splitlines():
if line.startswith('#'):
continue
m = line.split(':', 1)
if len(m) > 1:
item = m[0].strip()
note = m[1].strip()
if item and note:
notes[item] = note
else:
notes[line] = ''
except Exception as e:
if msg:
logger.info('{} at file {}'.format(msg, filename))
logger.debug('read_notes issue: {} at file {}. Debug message:{}'.format(msg, filename, e))
return notes
def enqueue_resize(orig, resized, spec=(640, 480, 80)):
logger.debug('photos: enqueue_resize({}, {}, {}'
.format(orig, resized, spec))
if resized not in DEFAULT_CONFIG['queue_resize']:
DEFAULT_CONFIG['queue_resize'][resized] = (orig, spec)
elif DEFAULT_CONFIG['queue_resize'][resized] != (orig, spec):
logger.error('photos: resize conflict for {}, {}-{} is not {}-{}'
.format(resized,
DEFAULT_CONFIG['queue_resize'][resized][0],
DEFAULT_CONFIG['queue_resize'][resized][1],
orig,
spec))
def enqueue_copy(orig, copied):
logger.debug('photos: enqueue_copy({}, {}'.format(orig, copied))
if copied not in DEFAULT_CONFIG['queue_copy']:
DEFAULT_CONFIG['queue_copy'][copied] = orig
elif DEFAULT_CONFIG['queue_copy'][copied] != orig:
logger.error('photos: copy conflict for {}, {} is not {}'
.format(copied, DEFAULT_CONFIG['queue_copy'][copied],
orig))
def rotate_image(img, exif_dict):
if "exif" in img.info and piexif.ImageIFD.Orientation in exif_dict["0th"]:
orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
if orientation == 2:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 3:
img = img.rotate(180)
elif orientation == 4:
img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 5:
img = img.rotate(-90).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 6:
img = img.rotate(-90, expand=True)
elif orientation == 7:
img = img.rotate(90).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 8:
img = img.rotate(90)
return (img, exif_dict)
def manipulate_exif(img, settings):
try:
exif = piexif.load(img.info['exif'])
except Exception:
logger.debug('EXIF information not found')
exif = {}
if settings['PHOTO_EXIF_AUTOROTATE']:
img, exif = rotate_image(img, exif)
if settings['PHOTO_EXIF_REMOVE_GPS']:
exif.pop('GPS')
return (img, piexif.dump(exif))
def resize_worker(orig, resized, spec, settings):
logger.info('photos: make photo {} -> {}'.format(orig, resized))
im = Image.open(orig)
if ispiexif and settings['PHOTO_EXIF_KEEP'] and im.format == 'JPEG':
# Only works with JPEG exif for sure.
try:
im, exif_copy = manipulate_exif(im, settings)
except Exception:
logger.info('photos: no EXIF or EXIF error in {}'.format(orig))
exif_copy = b''
else:
exif_copy = b''
icc_profile = im.info.get("icc_profile", None)
if settings['PHOTO_SQUARE_THUMB'] and spec == settings['PHOTO_THUMB']:
im = ImageOps.fit(im, (spec[0], spec[1]), Image.ANTIALIAS)
im.thumbnail((spec[0], spec[1]), Image.ANTIALIAS)
directory = os.path.split(resized)[0]
if not os.path.exists(directory):
try:
os.makedirs(directory)
except Exception:
logger.exception('Could not create {}'.format(directory))
else:
logger.debug('Directory already exists at {}'.format(os.path.split(resized)[0]))
im.save(resized, 'JPEG', quality=spec[2], icc_profile=icc_profile, exif=exif_copy)
def copy_worker(orig, resized, settings):
logger.info('photos: copy video {} -> {}'.format(orig, resized))
directory = os.path.split(resized)[0]
if not os.path.exists(directory):
try:
os.makedirs(directory)
except Exception:
logger.exception('Could not create {}'.format(directory))
else:
logger.debug('Directory already exists at {}'.format(os.path.split(resized)[0]))
shutil.copyfile(orig, resized)
def resize_photos(generator, writer):
if generator.settings['PHOTO_RESIZE_JOBS'] == -1:
debug = True
generator.settings['PHOTO_RESIZE_JOBS'] = 1
else:
debug = False
pool = multiprocessing.Pool(generator.settings['PHOTO_RESIZE_JOBS'])
logger.debug('Debug Status: {}'.format(debug))
for resized, what in DEFAULT_CONFIG['queue_resize'].items():
resized = os.path.join(generator.output_path, resized)
orig, spec = what
if (not os.path.isfile(resized) or os.path.getmtime(orig) > os.path.getmtime(resized)):
if debug:
resize_worker(orig, resized, spec, generator.settings)
else:
pool.apply_async(resize_worker, (orig, resized, spec, generator.settings))
pool.close()
pool.join()
def copy_videos(generator, writer):
if generator.settings['VIDEO_COPY_JOBS'] == -1:
debug = True
generator.settings['VIDEO_COPY_JOBS'] = 1
else:
debug = False
pool = multiprocessing.Pool(generator.settings['VIDEO_COPY_JOBS'])
logger.debug('Debug Status: {}'.format(debug))
for copied, orig in DEFAULT_CONFIG['queue_copy'].items():
copied = os.path.join(generator.output_path, copied)
if (not os.path.isfile(copied) or os.path.getmtime(orig) > os.path.getmtime(copied)):
if debug:
copy_worker(orig, copied, generator.settings)
else:
pool.apply_async(resize_worker, (orig, copied, generator.settings))
pool.close()
pool.join()
def detect_content(content):
"""
Triggered by Pelican's `content_object_init` signal."""
hrefs = None
def replacer(m):
what = m.group('what')
value = m.group('value')
tag = m.group('tag')
output = m.group(0)
if what in ('photo', 'lightbox'):
if value.startswith('/'):
value = value[1:]
path = os.path.join(
os.path.expanduser(settings['PHOTO_LIBRARY']),
value
)
if os.path.isfile(path):
photo_prefix = os.path.splitext(value)[0].lower()
if what == 'photo':
photo_article = photo_prefix + 'a.jpg'
enqueue_resize(
path,
os.path.join('photos', photo_article),
settings['PHOTO_ARTICLE']
)
output = ''.join((
'<',
m.group('tag'),
m.group('attrs_before'),
m.group('src'),
'=',
m.group('quote'),
os.path.join(settings['SITEURL'], 'photos', photo_article),
m.group('quote'),
m.group('attrs_after'),
))
elif what == 'lightbox' and tag == 'img':
photo_gallery = photo_prefix + '.jpg'
enqueue_resize(
path,
os.path.join('photos', photo_gallery),
settings['PHOTO_GALLERY']
)
photo_thumb = photo_prefix + 't.jpg'
enqueue_resize(
path,
os.path.join('photos', photo_thumb),
settings['PHOTO_THUMB']
)
lightbox_attr_list = ['']
gallery_name = value.split('/')[0]
lightbox_attr_list.append('{}="{}"'.format(
settings['PHOTO_LIGHTBOX_GALLERY_ATTR'],
gallery_name
))
captions = read_notes(
os.path.join(os.path.dirname(path), 'captions.txt'),
msg='photos: No captions for gallery'
)
caption = captions.get(os.path.basename(path)) if captions else None
if caption:
lightbox_attr_list.append('{}="{}"'.format(
settings['PHOTO_LIGHTBOX_CAPTION_ATTR'],
caption
))
lightbox_attrs = ' '.join(lightbox_attr_list)
output = ''.join((
'<a href=',
m.group('quote'),
os.path.join(settings['SITEURL'], 'photos', photo_gallery),
m.group('quote'),
lightbox_attrs,
'><img',
m.group('attrs_before'),
'src=',
m.group('quote'),
os.path.join(settings['SITEURL'], 'photos', photo_thumb),
m.group('quote'),
m.group('attrs_after'),
'</a>'
))
else:
logger.error('photos: No photo %s', path)
return output
if hrefs is None:
regex = r"""
<\s*
(?P<tag>[^\s\>]+) # detect the tag
(?P<attrs_before>[^\>]*)
(?P<src>href|src) # match tag with src and href attr
\s*=
(?P<quote>["\']) # require value to be quoted
(?P<path>{0}(?P<value>.*?)) # the url value
(?P=quote)
(?P<attrs_after>[^\>]*>)
""".format(
content.settings['INTRASITE_LINK_REGEX']
)
hrefs = re.compile(regex, re.X)
if content._content and ('{photo}' in content._content or '{lightbox}' in content._content):
settings = content.settings
content._content = hrefs.sub(replacer, content._content)
def galleries_string_decompose(gallery_string):
splitter_regex = re.compile(r'[\s,]*?({photo}|{filename})')
title_regex = re.compile(r'{(.+)}')
galleries = map(str.strip, filter(None, splitter_regex.split(gallery_string)))
galleries = [gallery[1:] if gallery.startswith('/') else gallery for gallery in galleries]
if len(galleries) % 2 == 0 and ' ' not in galleries:
galleries = zip(zip(['type'] * len(galleries[0::2]), galleries[0::2]), zip(['location'] * len(galleries[0::2]), galleries[1::2]))
galleries = [dict(gallery) for gallery in galleries]
for gallery in galleries:
title = re.search(title_regex, gallery['location'])
if title:
gallery['title'] = title.group(1)
gallery['location'] = re.sub(title_regex, '', gallery['location']).strip()
else:
gallery['title'] = DEFAULT_CONFIG['PHOTO_GALLERY_TITLE']
return galleries
else:
logger.error('Unexpected gallery location format! \n{}'.format(pprint.pformat(galleries)))
def process_gallery(generator, content, location):
content.photo_gallery = []
galleries = galleries_string_decompose(location)
for gallery in galleries:
if gallery['location'] in DEFAULT_CONFIG['created_galleries']:
content.photo_gallery.append(
(gallery['location'],
DEFAULT_CONFIG['created_galleries'][gallery]))
continue
if gallery['type'] == '{photo}':
dir_gallery = os.path.join(
os.path.expanduser(generator.settings['PHOTO_LIBRARY']),
gallery['location'])
rel_gallery = gallery['location']
elif gallery['type'] == '{filename}':
base_path = os.path.join(generator.path, content.relative_dir)
dir_gallery = os.path.join(base_path, gallery['location'])
rel_gallery = os.path.join(content.relative_dir,
gallery['location'])
if os.path.isdir(dir_gallery):
logger.info('photos: Gallery detected: {}'.format(rel_gallery))
dir_photo = os.path.join('photos', rel_gallery.lower())
dir_thumb = os.path.join('photos', rel_gallery.lower())
exifs = read_notes(os.path.join(dir_gallery, 'exif.txt'),
msg='photos: No EXIF for gallery')
captions = read_notes(os.path.join(dir_gallery, 'captions.txt'),
msg='photos: No captions for gallery')
blacklist = read_notes(os.path.join(dir_gallery, 'blacklist.txt'),
msg='photos: No blacklist for gallery')
content_gallery = []
title = gallery['title']
for item in sorted(os.listdir(dir_gallery)):
if item.startswith('.'):
continue
if item.endswith('.txt'):
continue
if item in blacklist:
continue
if item.endswith('.mp4.jpg'):
if os.path.isfile(
os.path.join(dir_gallery, os.path.splitext(item)[0])):
continue
thumb = os.path.splitext(item)[0] + 't.jpg'
if item.endswith('.jpg'):
enqueue_resize(
os.path.join(dir_gallery, item),
os.path.join(dir_photo, item),
generator.settings['PHOTO_GALLERY'])
enqueue_resize(
os.path.join(dir_gallery, item),
os.path.join(dir_thumb, thumb),
generator.settings['PHOTO_THUMB'])
elif item.endswith('.mp4'):
enqueue_copy(
os.path.join(dir_gallery, item),
os.path.join(generator.output_path, dir_photo, item),
)
still = item + '.jpg'
if os.path.isfile(os.path.join(dir_gallery, still)):
still = os.path.join(dir_gallery, still)
else:
here = os.path.dirname(__file__)
still = os.path.join(here, 'images/video-icon.png')
enqueue_resize(
still,
os.path.join(dir_thumb, thumb),
generator.settings['PHOTO_THUMB'])
content_gallery.append((
item,
os.path.join(dir_photo, item),
os.path.join(dir_thumb, thumb),
exifs.get(item, ''),
captions.get(item, '')))
# DEBUG TODO
# for c in content_gallery:
# raise(Exception(c))
content.photo_gallery.append((title, content_gallery))
logger.debug('Gallery Data: '
.format(pprint.pformat(content.photo_gallery)))
DEFAULT_CONFIG['created_galleries']['gallery'] = content_gallery
else:
logger.error('photos: Gallery does not exist: {} at {}'
.format(gallery['location'], dir_gallery))
def detect_gallery(generator, content):
if 'gallery' in content.metadata:
gallery = content.metadata.get('gallery')
if gallery.startswith('{photo}') or gallery.startswith('{filename}'):
process_gallery(generator, content, gallery)
elif gallery:
logger.error('photos: Gallery tag not recognized: {}'
.format(gallery))
def image_clipper(x):
return x[8:] if x[8] == '/' else x[7:]
def file_clipper(x):
return x[11:] if x[10] == '/' else x[10:]
def process_image(generator, content, image):
if image.startswith('{photo}'):
path = os.path.join(
os.path.expanduser(generator.settings['PHOTO_LIBRARY']),
image_clipper(image))
image = image_clipper(image)
elif image.startswith('{filename}'):
path = os.path.join(content.relative_dir, file_clipper(image))
image = file_clipper(image)
if os.path.isfile(path):
photo = os.path.splitext(image)[0].lower() + 'a.jpg'
thumb = os.path.splitext(image)[0].lower() + 't.jpg'
content.photo_image = (
os.path.basename(image).lower(),
os.path.join('photos', photo),
os.path.join('photos', thumb))
enqueue_resize(
path,
os.path.join('photos', photo),
generator.settings['PHOTO_ARTICLE'])
enqueue_resize(
path,
os.path.join('photos', thumb),
generator.settings['PHOTO_THUMB'])
else:
logger.error('photo: No photo for {} at {}'
.format(content.source_path, path))
def detect_image(generator, content):
image = content.metadata.get('image', None)
if image:
if image.startswith('{photo}') or image.startswith('{filename}'):
process_image(generator, content, image)
else:
logger.error('photos: Image tag not recognized: {}'.format(image))
def detect_images_and_galleries(generators):
"""Runs generator on both pages and articles."""
for generator in generators:
if isinstance(generator, ArticlesGenerator):
for article in itertools.chain(generator.articles,
generator.translations,
generator.drafts):
detect_image(generator, article)
detect_gallery(generator, article)
elif isinstance(generator, PagesGenerator):
for page in itertools.chain(generator.pages,
generator.translations,
generator.hidden_pages):
detect_image(generator, page)
detect_gallery(generator, page)
def register():
"""Map signals to plugin logic.
Required by Pelican.
Uses the new style of registration based on GitHub Pelican issue #314."""
signals.initialized.connect(initialized)
try:
signals.content_object_init.connect(detect_content)
signals.all_generators_finalized.connect(detect_images_and_galleries)
signals.article_writer_finalized.connect(resize_photos)
signals.article_writer_finalized.connect(copy_videos)
except Exception as e:
logger.exception('Plugin failed to execute: {}'
.format(pprint.pformat(e)))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment