Browse Source

Merge branch 'master' into additional_video_options

pull/420/head
ksfeldman 5 years ago committed by GitHub
parent
commit
1e65fc5cd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 132
      sigal/gallery.py
  2. 35
      sigal/image.py
  3. 86
      sigal/plugins/encrypt/encrypt.py
  4. 4
      sigal/plugins/extended_caching.py
  5. 2
      sigal/plugins/media_page.py
  6. 2
      sigal/settings.py
  7. 26
      sigal/video.py
  8. BIN
      tests/sample/pictures/dir1/test2/21.jpg
  9. BIN
      tests/sample/pictures/dir1/test2/21.tiff
  10. 2
      tests/sample/pictures/dir1/test2/CMB_Timeline300_no_WMAP.md
  11. 1
      tests/sample/sigal.conf.py
  12. 63
      tests/test_gallery.py
  13. 41
      tests/test_image.py
  14. 17
      tests/test_video.py

132
sigal/gallery.py

@ -38,6 +38,7 @@ from urllib.parse import quote as url_quote
from click import get_terminal_size, progressbar
from natsort import natsort_keygen, ns
from PIL import Image as PILImage
from . import image, signals, video
from .image import get_exif_tags, get_image_metadata, get_size, process_image
@ -55,7 +56,7 @@ class Media:
Attributes:
:var Media.type: ``"image"`` or ``"video"``.
:var Media.filename: Filename of the resized image.
:var Media.dst_filename: Filename of the resized image.
:var Media.thumbnail: Location of the corresponding thumbnail image.
:var Media.big: If not None, location of the unmodified image.
:var Media.big_url: If not None, url of the unmodified image.
@ -66,21 +67,23 @@ class Media:
"""Type of media, e.g. ``"image"`` or ``"video"``."""
def __init__(self, filename, path, settings):
self.filename = filename
self.path = path
self.settings = settings
self.basename = os.path.splitext(filename)[0]
self.dst_filename = filename
"""Filename of the resized image."""
self.src_filename = filename
"""Filename of the resized image."""
"""Filename of the input image."""
self.path = path
self.settings = settings
self.ext = os.path.splitext(filename)[1].lower()
self.src_ext = os.path.splitext(filename)[1].lower()
"""Input extension."""
self.src_path = join(settings['source'], path, filename)
self.dst_path = join(settings['destination'], path, filename)
self.src_path = join(settings['source'], path, self.src_filename)
self.thumb_name = get_thumb(self.settings, self.filename)
self.thumb_path = join(settings['destination'], path, self.thumb_name)
self.thumb_name = get_thumb(self.settings, self.dst_filename)
self.logger = logging.getLogger(__name__)
@ -89,19 +92,38 @@ class Media:
# default: title is the filename
if not self.title:
self.title = self.filename
self.title = self.basename
signals.media_initialized.send(self)
def __repr__(self):
return "<{}>({!r})".format(self.__class__.__name__, str(self))
def __str__(self):
return join(self.path, self.filename)
return join(self.path, self.src_filename)
def __getstate__(self):
state = self.__dict__.copy()
# remove un-pickable objects
state['logger'] = None
return state
def __setstate__(self, state):
for slot, value in state.items():
setattr(self, slot, value)
self.logger = logging.getLogger(__name__)
@property
def dst_path(self):
return join(self.settings['destination'], self.path, self.dst_filename)
@property
def thumb_path(self):
return join(self.settings['destination'], self.path, self.thumb_name)
@property
def url(self):
"""URL of the media."""
return url_from_path(self.filename)
return url_from_path(self.dst_filename)
@property
def big(self):
@ -112,7 +134,7 @@ class Media:
s = self.settings
if s['use_orig']:
# The image *is* the original, just use it
return self.filename
return self.src_filename
orig_path = join(s['destination'], self.path, s['orig_dir'])
check_or_create_dir(orig_path)
big_path = join(orig_path, self.src_filename)
@ -180,6 +202,17 @@ class Image(Media):
type = 'image'
def __init__(self, filename, path, settings):
super().__init__(filename, path, settings)
imgformat = settings.get('img_format')
if imgformat and PILImage.EXTENSION[self.src_ext] != imgformat.upper():
# Find the extension that should match img_format
extensions = {v: k for k, v in PILImage.EXTENSION.items()}
ext = extensions[imgformat.upper()]
self.dst_filename = self.basename + ext
self.thumb_name = get_thumb(self.settings, self.dst_filename)
@cached_property
def date(self):
"""The date from the EXIF DateTimeOriginal metadata if available, or
@ -194,7 +227,7 @@ class Image(Media):
"""
datetime_format = self.settings['datetime_format']
return (get_exif_tags(self.raw_exif, datetime_format=datetime_format)
if self.raw_exif and self.ext in ('.jpg', '.jpeg') else None)
if self.raw_exif and self.src_ext in ('.jpg', '.jpeg') else None)
def _get_metadata(self):
super()._get_metadata()
@ -215,7 +248,7 @@ class Image(Media):
@cached_property
def raw_exif(self):
"""If not `None`, contains the raw EXIF tags."""
if self.ext in ('.jpg', '.jpeg'):
if self.src_ext in ('.jpg', '.jpeg'):
return self.file_metadata['exif']
@cached_property
@ -223,6 +256,11 @@ class Image(Media):
"""The dimensions of the resized image."""
return get_size(self.dst_path)
@cached_property
def input_size(self):
"""The dimensions of the input image."""
return get_size(self.src_path)
@cached_property
def thumb_size(self):
"""The dimensions of the thumbnail image."""
@ -240,17 +278,15 @@ class Video(Media):
def __init__(self, filename, path, settings):
super().__init__(filename, path, settings)
base, ext = splitext(filename)
self.src_filename = filename
self.date = self._get_file_date()
if not settings['use_orig'] or not is_valid_html5_video(ext):
if not settings['use_orig'] or not is_valid_html5_video(self.src_ext):
video_format = settings['video_format']
ext = '.' + video_format
self.filename = base + ext
self.dst_filename = self.basename + ext
self.mime = get_mime(ext)
self.dst_path = join(settings['destination'], path, base + ext)
else:
self.mime = get_mime(ext)
self.mime = get_mime(self.src_ext)
class Album:
@ -393,6 +429,9 @@ class Album:
def sort_medias(self, medias_sort_attr):
if self.medias:
if medias_sort_attr == 'filename':
medias_sort_attr = 'dst_filename'
if medias_sort_attr == 'date':
key = lambda s: s.date or datetime.now()
elif medias_sort_attr.startswith('meta.'):
@ -458,13 +497,13 @@ class Album:
else:
# find and return the first landscape image
for f in self.medias:
ext = splitext(f.filename)[1]
ext = splitext(f.dst_filename)[1]
if ext.lower() not in self.settings['img_extensions']:
continue
# Use f.size if available as it is quicker (in cache), but
# fallback to the size of src_path if dst_path is missing
size = f.size
size = f.input_size
if size is None:
size = f.file_metadata['size']
@ -474,7 +513,7 @@ class Album:
f.thumbnail)
except Exception as e:
self.logger.info("Failed to get thumbnail for %s: %s",
f.filename, e)
f.dst_filename, e)
else:
self.logger.debug(
"Use 1st landscape image as thumbnail for %r : %s",
@ -491,7 +530,7 @@ class Album:
except Exception as e:
self.logger.info(
"Failed to get thumbnail for %s: %s",
media.filename, e
media.dst_filename, e
)
else:
break
@ -698,14 +737,13 @@ class Gallery:
bar_opt = {'label': "Processing files",
'show_pos': True,
'file': self.progressbar_target}
failed_files = []
if self.pool:
result = []
try:
with progressbar(length=len(media_list), **bar_opt) as bar:
for res in self.pool.imap_unordered(worker, media_list):
if res:
failed_files.append(res)
for status in self.pool.imap_unordered(worker, media_list):
result.append(status)
bar.update(1)
self.pool.close()
self.pool.join()
@ -721,12 +759,11 @@ class Gallery:
sys.exit('Abort')
else:
with progressbar(media_list, **bar_opt) as medias:
for media_item in medias:
res = process_file(media_item)
if res:
failed_files.append(res)
result = [process_file(media_item) for media_item in medias]
if failed_files:
if any(result):
failed_files = [media for status, media in zip(result, media_list)
if status != 0]
self.remove_files(failed_files)
if self.settings['write_html']:
@ -754,13 +791,13 @@ class Gallery:
signals.gallery_build.send(self)
def remove_files(self, files):
def remove_files(self, medias):
self.logger.error('Some files have failed to be processed:')
for path, filename in files:
self.logger.error(' - %s/%s', path, filename)
album = self.albums[path]
for media in medias:
self.logger.error(' - %s', media.dst_filename)
album = self.albums[media.path]
for f in album.medias:
if f.filename == filename:
if f.dst_filename == media.dst_filename:
self.stats[f.type + '_failed'] += 1
album.medias.remove(f)
break
@ -771,21 +808,16 @@ class Gallery:
"""Process a list of images in a directory."""
for f in album:
if isfile(f.dst_path) and not force:
self.logger.info("%s exists - skipping", f.filename)
self.logger.info("%s exists - skipping", f.dst_filename)
self.stats[f.type + '_skipped'] += 1
else:
self.stats[f.type] += 1
yield (f.type, f.path, f.filename, f.src_path, album.dst_path,
self.settings)
yield f
def process_file(args):
# args => ftype, path, filename, src_path, dst_path, settings
processor = process_image if args[0] == 'image' else process_video
ret = processor(*args[3:])
# If the processor return an error (ret != 0), then we return the path and
# filename of the failed file to the parent process.
return args[1:3] if ret else None
def process_file(media):
processor = process_image if media.type == 'image' else process_video
return processor(media)
def worker(args):

35
sigal/image.py

@ -143,7 +143,9 @@ def generate_image(source, outname, settings, options=None):
# first, use hard-coded output format, or PIL format, or original image
# format, or fall back to JPEG
outformat = settings.get('img_format') or img.format or original_format or 'JPEG'
outformat = (settings.get('img_format') or img.format or
original_format or 'JPEG')
logger.debug('Save resized image to %s (%s)', outname, outformat)
save_image(img, outname, outformat, options=options, autoconvert=True)
@ -168,31 +170,32 @@ def generate_thumbnail(source, outname, box, fit=True, options=None,
save_image(img, outname, outformat, options=options, autoconvert=True)
def process_image(filepath, outpath, settings):
def process_image(media):
"""Process one image: resize, create thumbnail."""
logger = logging.getLogger(__name__)
logger.info('Processing %s', filepath)
filename = os.path.split(filepath)[1]
outname = os.path.join(outpath, filename)
ext = os.path.splitext(filename)[1]
if ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
options = settings['jpg_options']
elif ext == '.png':
logger.info('Processing %s', media.src_path)
if media.src_ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
options = media.settings['jpg_options']
elif media.src_ext == '.png':
options = {'optimize': True}
else:
options = {}
try:
generate_image(filepath, outname, settings, options=options)
generate_image(media.src_path, media.dst_path, media.settings,
options=options)
if settings['make_thumbs']:
thumb_name = os.path.join(outpath, get_thumb(settings, filename))
if media.settings['make_thumbs']:
generate_thumbnail(
outname, thumb_name, settings['thumb_size'],
fit=settings['thumb_fit'], options=options,
thumb_fit_centering=settings["thumb_fit_centering"])
media.dst_path,
media.thumb_path,
media.settings['thumb_size'],
fit=media.settings['thumb_fit'],
options=options,
thumb_fit_centering=media.settings["thumb_fit_centering"]
)
except Exception as e:
logger.info('Failed to process: %r', e)
if logger.getEffectiveLevel() == logging.DEBUG:

86
sigal/plugins/encrypt/encrypt.py

@ -30,20 +30,25 @@ from click import progressbar
from sigal import signals
from sigal.settings import get_thumb
from sigal.utils import copy, url_from_path
from sigal.utils import copy
from .endec import encrypt, kdf_gen_key
logger = logging.getLogger(__name__)
ASSETS_PATH = os.path.normpath(os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'static'))
ASSETS_PATH = os.path.normpath(
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static'))
class Abort(Exception):
pass
def gen_rand_string(length=16):
return "".join(random.SystemRandom().choices(string.ascii_letters + string.digits, k=length))
return "".join(random.SystemRandom().choices(string.ascii_letters +
string.digits,
k=length))
def get_options(settings, cache):
if "encrypt_options" not in settings:
@ -58,7 +63,7 @@ def get_options(settings, cache):
table = str.maketrans({'"': r'\"', '\\': r'\\'})
if "password" not in settings["encrypt_options"] \
or len(settings["encrypt_options"]["password"]) == 0:
or len(settings["encrypt_options"]["password"]) == 0:
logger.error("Encrypt: no password provided")
raise ValueError("no password provided")
else:
@ -66,8 +71,10 @@ def get_options(settings, cache):
options["escaped_password"] = options["password"].translate(table)
if "ask_password" not in options:
options["ask_password"] = settings["encrypt_options"].get("ask_password", False)
options["filtered_password"] = "" if options["ask_password"] else options["escaped_password"]
options["ask_password"] = settings["encrypt_options"].get(
"ask_password", False)
options["filtered_password"] = "" if options["ask_password"] else options[
"escaped_password"]
if "gcm_tag" not in options:
options["gcm_tag"] = gen_rand_string()
@ -83,7 +90,8 @@ def get_options(settings, cache):
if "kdf_iters" not in options:
options["kdf_iters"] = 10000
# in case any of the credentials are newly generated, write them back to cache
# in case any of the credentials are newly generated, write them back
# to cache
cache["credentials"] = {
"gcm_tag": options["gcm_tag"],
"kdf_salt": options["kdf_salt"],
@ -93,8 +101,10 @@ def get_options(settings, cache):
return options
def cache_key(media):
return os.path.join(media.path, media.filename)
return os.path.join(media.path, media.dst_filename)
def save_property(cache, media):
key = cache_key(media)
@ -104,16 +114,20 @@ def save_property(cache, media):
cache[key]["thumb_size"] = media.thumb_size
cache[key]["encrypted"] = set()
def get_encrypt_list(settings, media):
to_encrypt = []
to_encrypt.append(media.filename) #resized image or in case of "use_orig", the original
# resized image or in case of "use_orig", the original
to_encrypt.append(media.dst_filename)
if settings["make_thumbs"]:
to_encrypt.append(get_thumb(settings, media.filename)) #thumbnail
to_encrypt.append(get_thumb(settings, media.dst_filename)) # thumbnail
if media.big is not None and not settings["use_orig"]:
to_encrypt.append(media.big) #original image
to_encrypt = list(map(lambda path: os.path.join(media.path, path), to_encrypt))
to_encrypt.append(media.big) # original image
to_encrypt = list(
map(lambda path: os.path.join(media.path, path), to_encrypt))
return to_encrypt
def load_property(album):
gallery = album.gallery
cache = load_cache(gallery.settings)
@ -125,21 +139,25 @@ def load_property(album):
media.size = cache[key]["size"]
media.thumb_size = cache[key]["thumb_size"]
def load_cache(settings):
cachePath = os.path.join(settings["destination"], ".encryptCache")
try:
with open(cachePath, "rb") as cacheFile:
encryptCache = pickle.load(cacheFile)
logger.debug("Loaded encryption cache with %d entries", len(encryptCache))
logger.debug("Loaded encryption cache with %d entries",
len(encryptCache))
return encryptCache
except FileNotFoundError:
encryptCache = {}
return encryptCache
except Exception as e:
logger.error("Could not load encryption cache: %s", e)
logger.error("Giving up encryption. You may have to delete and rebuild the entire gallery.")
logger.error("Giving up encryption. You may have to delete and "
"rebuild the entire gallery.")
raise Abort
def save_cache(settings, cache):
cachePath = os.path.join(settings["destination"], ".encryptCache")
try:
@ -150,6 +168,7 @@ def save_cache(settings, cache):
logger.warning("Could not store encryption cache: %s", e)
logger.warning("Next build of the gallery is likely to fail!")
def encrypt_gallery(gallery):
albums = gallery.albums
settings = gallery.settings
@ -163,19 +182,26 @@ def encrypt_gallery(gallery):
encrypt_files(settings, config, cache, albums, gallery.progressbar_target)
save_cache(settings, cache)
def encrypt_files(settings, config, cache, albums, progressbar_target):
if settings["keep_orig"] and settings["orig_link"]:
logger.warning("Original images are symlinked! Encryption is aborted. Please set \"orig_link\" to False and restart gallery build.")
logger.warning(
"Original images are symlinked! Encryption is aborted. "
"Please set 'orig_link' to False and restart gallery build.")
raise Abort
key = kdf_gen_key(config["password"], config["kdf_salt"], config["kdf_iters"])
key = kdf_gen_key(config["password"], config["kdf_salt"],
config["kdf_iters"])
gcm_tag = config["gcm_tag"].encode("utf-8")
medias = list(chain.from_iterable(albums.values()))
with progressbar(medias, label="%16s" % "Encrypting files", file=progressbar_target, show_eta=True) as medias:
with progressbar(medias,
label="%16s" % "Encrypting files",
file=progressbar_target,
show_eta=True) as medias:
for media in medias:
if media.type != "image":
logger.info("Skipping non-image file %s", media.filename)
logger.info("Skipping non-image file %s", media.src_filename)
continue
save_property(cache, media)
@ -196,9 +222,11 @@ def encrypt_files(settings, config, cache, albums, progressbar_target):
save_cache(settings, cache)
raise Abort
key_check_path = os.path.join(settings["destination"], 'static', 'keycheck.txt')
key_check_path = os.path.join(settings["destination"], 'static',
'keycheck.txt')
encrypt_file("keycheck.txt", key_check_path, key, gcm_tag)
def encrypt_file(filename, full_path, key, gcm_tag):
with BytesIO() as outBuffer:
try:
@ -217,16 +245,28 @@ def encrypt_file(filename, full_path, key, gcm_tag):
return False
return True
def copy_assets(settings):
theme_path = os.path.join(settings["destination"], 'static')
copy(os.path.join(ASSETS_PATH, "decrypt.js"), theme_path, symlink=False, rellink=False)
copy(os.path.join(ASSETS_PATH, "keycheck.txt"), theme_path, symlink=False, rellink=False)
copy(os.path.join(ASSETS_PATH, "sw.js"), settings["destination"], symlink=False, rellink=False)
copy(os.path.join(ASSETS_PATH, "decrypt.js"),
theme_path,
symlink=False,
rellink=False)
copy(os.path.join(ASSETS_PATH, "keycheck.txt"),
theme_path,
symlink=False,
rellink=False)
copy(os.path.join(ASSETS_PATH, "sw.js"),
settings["destination"],
symlink=False,
rellink=False)
def inject_scripts(context):
cache = load_cache(context['settings'])
context["encrypt_options"] = get_options(context['settings'], cache)
def register(settings):
signals.gallery_build.connect(encrypt_gallery)
signals.album_initialized.connect(load_property)

4
sigal/plugins/extended_caching.py

@ -46,7 +46,7 @@ def load_exif(album):
for media in album.medias:
if media.type == "image":
key = os.path.join(media.path, media.filename)
key = os.path.join(media.path, media.dst_filename)
if key in cache:
media.exif = cache[key]
@ -76,7 +76,7 @@ def save_cache(gallery):
for album in gallery.albums.values():
for image in album.images:
cache[os.path.join(image.path, image.filename)] = image.exif
cache[os.path.join(image.path, image.dst_filename)] = image.exif
cachePath = os.path.join(gallery.settings["destination"], ".exif_cache")

2
sigal/plugins/media_page.py

@ -45,7 +45,7 @@ class PageWriter(AbstractWriter):
''' Generate the media page and save it '''
from sigal import __url__ as sigal_link
file_path = os.path.join(album.dst_path, media_group[0].filename)
file_path = os.path.join(album.dst_path, media_group[0].dst_filename)
page = self.template.render({
'album': album,

2
sigal/settings.py

@ -40,7 +40,7 @@ _DEFAULT_CONFIG = {
'google_tag_manager': '',
'ignore_directories': [],
'ignore_files': [],
'img_extensions': ['.jpg', '.jpeg', '.png', '.gif'],
'img_extensions': ['.jpg', '.jpeg', '.png', '.gif', '.tif', '.tiff'],
'img_processor': 'ResizeToFit',
'img_size': (640, 480),
'img_format': None,

26
sigal/video.py

@ -199,16 +199,15 @@ def generate_thumbnail(source, outname, box, delay, fit=True, options=None,
os.unlink(tmpfile)
def process_video(filepath, outpath, settings):
def process_video(media):
"""Process a video: resize, create thumbnail."""
logger = logging.getLogger(__name__)
filename = os.path.split(filepath)[1]
basename, ext = splitext(filename)
settings = media.settings
try:
if settings['use_orig'] and is_valid_html5_video(ext):
outname = os.path.join(outpath, filename)
utils.copy(filepath, outname, symlink=settings['orig_link'])
if settings['use_orig'] and is_valid_html5_video(media.src_ext):
utils.copy(media.src_path, media.dst_path,
symlink=settings['orig_link'])
else:
valid_formats = ['mp4', 'webm']
video_format = settings['video_format']
@ -217,9 +216,7 @@ def process_video(filepath, outpath, settings):
logger.error('Invalid video_format. Please choose one of: %s',
valid_formats)
raise ValueError
outname = os.path.join(outpath, basename + '.' + video_format)
generate_video(filepath, outname, settings)
generate_video(media.src_path, media.dst_path, settings)
except Exception:
if logger.getEffectiveLevel() == logging.DEBUG:
raise
@ -227,13 +224,16 @@ def process_video(filepath, outpath, settings):
return Status.FAILURE
if settings['make_thumbs']:
thumb_name = os.path.join(outpath, get_thumb(settings, filename))
try:
generate_thumbnail(
outname, thumb_name, settings['thumb_size'],
settings['thumb_video_delay'], fit=settings['thumb_fit'],
media.dst_path,
media.thumb_path,
settings['thumb_size'],
settings['thumb_video_delay'],
fit=settings['thumb_fit'],
options=settings['jpg_options'],
converter=settings['video_converter'])
converter=settings['video_converter']
)
except Exception:
if logger.getEffectiveLevel() == logging.DEBUG:
raise

BIN
tests/sample/pictures/dir1/test2/21.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

BIN
tests/sample/pictures/dir1/test2/21.tiff

Binary file not shown.

2
tests/sample/pictures/dir1/test2/CMB_Timeline300_no_WMAP.md

@ -0,0 +1,2 @@
Order: 03

1
tests/sample/sigal.conf.py

@ -4,6 +4,7 @@ source = 'pictures'
thumb_suffix = '.tn'
keep_orig = True
thumb_video_delay = 5
# img_format = 'jpeg'
links = [('Example link', 'http://example.org'),
('Another link', 'http://example.org')]

63
tests/test_gallery.py

@ -2,7 +2,6 @@ import datetime
import logging
import os
from os.path import join
from urllib.parse import quote
import pytest
@ -31,9 +30,9 @@ REF = {
'dir1/test2': {
'title': 'test2',
'name': 'test2',
'thumbnail': 'test2/thumbnails/21.tn.jpg',
'thumbnail': 'test2/thumbnails/21.tn.tiff',
'subdirs': [],
'medias': ['21.jpg', '22.jpg', 'CMB_Timeline300_no_WMAP.jpg'],
'medias': ['21.tiff', '22.jpg', 'CMB_Timeline300_no_WMAP.jpg'],
},
'dir1/test3': {
'title': '01 First title alphabetically',
@ -76,7 +75,7 @@ def test_media(settings):
file_path = join(path, '11.jpg')
thumb = join('thumbnails', '11.tn.jpg')
assert m.filename == '11.jpg'
assert m.dst_filename == '11.jpg'
assert m.src_path == join(settings['source'], file_path)
assert m.dst_path == join(settings['destination'], file_path)
assert m.thumb_name == thumb
@ -100,7 +99,7 @@ def test_media_orig(settings, tmpdir):
assert m.big == 'original/11.jpg'
m = Video('example video.ogv', 'video', settings)
assert m.filename == 'example video.webm'
assert m.dst_filename == 'example video.webm'
assert m.big_url == 'original/example%20video.ogv'
assert os.path.isfile(join(settings['destination'], m.path, m.big))
@ -116,14 +115,34 @@ def test_media_iptc_override(settings):
# Markdown parsing adds formatting. Let's just focus on content
assert "Markdown description beats iptc" in img_with_md.description
img_no_md = Image('1.jpg', 'iptcTest', settings)
assert img_no_md.title == 'Haemostratulus clouds over Canberra - ' + \
'2005-12-28 at 03-25-07'
assert img_no_md.description == \
'"Haemo" because they look like haemoglobin ' + \
'cells and "stratulus" because I can\'t work out whether ' + \
'they\'re Stratus or Cumulus clouds.\nWe\'re driving down ' + \
'the main drag in Canberra so it\'s Parliament House that ' + \
'you can see at the end of the road.'
assert img_no_md.title == ('Haemostratulus clouds over Canberra - '
'2005-12-28 at 03-25-07')
assert img_no_md.description == (
'"Haemo" because they look like haemoglobin '
'cells and "stratulus" because I can\'t work out whether '
'they\'re Stratus or Cumulus clouds.\nWe\'re driving down '
'the main drag in Canberra so it\'s Parliament House that '
'you can see at the end of the road.'
)
def test_media_img_format(settings):
settings['img_format'] = 'jpeg'
m = Image('11.tiff', 'dir1/test1', settings)
path = join('dir1', 'test1')
thumb = join('thumbnails', '11.tn.jpeg')
assert m.dst_filename == '11.jpeg'
assert m.src_path == join(settings['source'], path, '11.tiff')
assert m.dst_path == join(settings['destination'], path, '11.jpeg')
assert m.thumb_name == thumb
assert m.thumb_path == join(settings['destination'], path, thumb)
assert m.title == "Foo Bar"
assert m.description == "<p>This is a funny description of this image</p>"
file_path = join(path, '11.tiff')
assert repr(m) == f"<Image>('{file_path}')"
assert str(m) == file_path
def test_image(settings, tmpdir):
@ -141,8 +160,11 @@ def test_image(settings, tmpdir):
def test_video(settings, tmpdir):
settings['destination'] = str(tmpdir)
m = Video('example video.ogv', 'video', settings)
src_path = join('video', 'example video.ogv')
assert str(m) == src_path
file_path = join('video', 'example video.webm')
assert str(m) == file_path
assert m.dst_path == join(settings['destination'], file_path)
os.makedirs(join(settings['destination'], 'video', 'thumbnails'))
@ -161,11 +183,11 @@ def test_album(path, album, settings, tmpdir):
assert a.thumbnail == album['thumbnail']
if path == 'video':
assert list(a.images) == []
assert [m.filename for m in a.medias] == \
assert [m.dst_filename for m in a.medias] == \
[album['medias'][0].replace('.ogv', '.webm')]
else:
assert list(a.videos) == []
assert [m.filename for m in a.medias] == album['medias']
assert [m.dst_filename for m in a.medias] == album['medias']
assert len(a) == len(album['medias'])
@ -216,21 +238,20 @@ def test_medias_sort(settings):
settings['medias_sort_reverse'] = True
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
assert [im.filename for im in a.images] == list(reversed(album['medias']))
assert [im.dst_filename for im in a.images] == list(reversed(album['medias']))
settings['medias_sort_attr'] = 'date'
settings['medias_sort_reverse'] = False
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
assert [im.filename for im in a.images] == ['22.jpg', '21.jpg',
'CMB_Timeline300_no_WMAP.jpg']
assert a.medias[0].src_filename == '22.jpg'
settings['medias_sort_attr'] = 'meta.order'
settings['medias_sort_reverse'] = False
a = Album('dir1/test2', settings, album['subdirs'], album['medias'], gal)
a.sort_medias(settings['medias_sort_attr'])
assert [im.filename for im in a.images] == [
'CMB_Timeline300_no_WMAP.jpg', '21.jpg', '22.jpg']
assert [im.dst_filename for im in a.images] == [
'21.tiff', '22.jpg', 'CMB_Timeline300_no_WMAP.jpg']
def test_gallery(settings, tmpdir):

41
tests/test_image.py

@ -2,32 +2,37 @@ import os
from unittest.mock import patch
import pytest
from PIL import Image
from PIL import Image as PILImage
from sigal import init_logging
from sigal.image import (generate_image, generate_thumbnail, get_exif_data,
get_exif_tags, get_iptc_data, get_size, process_image)
from sigal.gallery import Image
from sigal.settings import Status, create_settings
CURRENT_DIR = os.path.dirname(__file__)
SRCDIR = os.path.join(CURRENT_DIR, 'sample', 'pictures')
TEST_IMAGE = 'KeckObservatory20071020.jpg'
SRCFILE = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'dir2', TEST_IMAGE)
SRCFILE = os.path.join(SRCDIR, 'dir2', TEST_IMAGE)
TEST_GIF_IMAGE = 'example.gif'
SRC_GIF_FILE = os.path.join(CURRENT_DIR, 'sample', 'pictures',
'dir1', 'test1', TEST_GIF_IMAGE)
SRC_GIF_FILE = os.path.join(SRCDIR, 'dir1', 'test1', TEST_GIF_IMAGE)
def test_process_image(tmpdir):
"Test the process_image function."
status = process_image('foo.txt', 'none.txt', {})
status = process_image(Image('foo.txt', 'bar', create_settings()))
assert status == Status.FAILURE
settings = create_settings(img_processor='ResizeToFill', make_thumbs=False)
status = process_image(SRCFILE, str(tmpdir), settings)
settings = create_settings(img_processor='ResizeToFill',
make_thumbs=False,
source=os.path.join(SRCDIR, 'dir2'),
destination=str(tmpdir))
image = Image(TEST_IMAGE, '.', settings)
status = process_image(image)
assert status == Status.SUCCESS
im = Image.open(os.path.join(str(tmpdir), TEST_IMAGE))
im = PILImage.open(os.path.join(str(tmpdir), TEST_IMAGE))
assert im.size == settings['img_size']
@ -40,21 +45,25 @@ def test_generate_image(tmpdir):
copy_exif_data=True)
options = None if i == 0 else {'quality': 85}
generate_image(SRCFILE, dstfile, settings, options=options)
im = Image.open(dstfile)
im = PILImage.open(dstfile)
assert im.size == size
def test_generate_image_imgformat(tmpdir):
"Test the effects of the img_format setting on generate_image."
dstfile = str(tmpdir.join(TEST_IMAGE))
for i, outfmt in enumerate(["JPEG", "PNG", "TIFF"]):
settings = create_settings(img_size=(300,300), img_processor='ResizeToFill',
copy_exif_data=True, img_format=outfmt)
settings = create_settings(img_size=(300, 300),
img_processor='ResizeToFill',
copy_exif_data=True,
img_format=outfmt)
options = {'quality': 85}
generate_image(SRCFILE, dstfile, settings, options=options)
im = Image.open(dstfile)
im = PILImage.open(dstfile)
assert im.format == outfmt
def test_resize_image_portrait(tmpdir):
"""Test that the area is the same regardless of aspect ratio."""
size = (300, 200)
@ -65,7 +74,7 @@ def test_resize_image_portrait(tmpdir):
portrait_dst = str(tmpdir.join(portrait_image))
generate_image(portrait_src, portrait_dst, settings)
im = Image.open(portrait_dst)
im = PILImage.open(portrait_dst)
# In the default mode, PILKit resizes in a way to never make an image
# smaller than either of the lengths, the other is scaled accordingly.
@ -77,7 +86,7 @@ def test_resize_image_portrait(tmpdir):
landscape_dst = str(tmpdir.join(landscape_image))
generate_image(landscape_src, landscape_dst, settings)
im = Image.open(landscape_dst)
im = PILImage.open(landscape_dst)
assert im.size[1] == 200
@ -129,13 +138,13 @@ def test_generate_thumbnail(tmpdir, image, path, wide_size, high_size):
dstfile = str(tmpdir.join(image))
for size in [(200, 150), (150, 200)]:
generate_thumbnail(path, dstfile, size)
im = Image.open(dstfile)
im = PILImage.open(dstfile)
assert im.size == size
for size, thumb_size in [((200, 150), wide_size),
((150, 200), high_size)]:
generate_thumbnail(path, dstfile, size, fit=False)
im = Image.open(dstfile)
im = PILImage.open(dstfile)
assert im.size == thumb_size

17
tests/test_video.py

@ -2,14 +2,16 @@ import os
import pytest
from sigal.gallery import Video
from sigal.settings import Status, create_settings
from sigal.video import (generate_thumbnail, generate_video, process_video,
video_size, get_resize_options, generate_video_pass)
from unittest.mock import patch
CURRENT_DIR = os.path.dirname(__file__)
SRCDIR = os.path.join(CURRENT_DIR, 'sample', 'pictures')
TEST_VIDEO = 'example video.ogv'
SRCFILE = os.path.join(CURRENT_DIR, 'sample', 'pictures', 'video', TEST_VIDEO)
SRCFILE = os.path.join(SRCDIR, 'video', TEST_VIDEO)
def test_video_size():
@ -28,17 +30,20 @@ def test_generate_thumbnail(tmpdir):
def test_process_video(tmpdir):
base, ext = os.path.splitext(TEST_VIDEO)
settings = create_settings(video_format='ogv', use_orig=True,
orig_link=True)
process_video(SRCFILE, str(tmpdir), settings)
settings = create_settings(video_format='ogv',
use_orig=True, orig_link=True,
source=os.path.join(SRCDIR, 'video'),
destination=str(tmpdir))
video = Video(TEST_VIDEO, '.', settings)
process_video(video)
dstfile = str(tmpdir.join(base + '.ogv'))
assert os.path.realpath(dstfile) == SRCFILE
settings = create_settings(video_format='mjpg')
assert process_video(SRCFILE, str(tmpdir), settings) == Status.FAILURE
assert process_video(video) == Status.FAILURE
settings = create_settings(thumb_video_delay=-1)
assert process_video(SRCFILE, str(tmpdir), settings) == Status.FAILURE
assert process_video(video) == Status.FAILURE
@pytest.mark.parametrize("fmt", ['webm', 'mp4'])

Loading…
Cancel
Save