Merge branch 'master' into additional_video_options
This commit is contained in:
132
sigal/gallery.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):
|
||||
|
||||
@@ -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]
|
||||
logger.info('Processing %s', media.src_path)
|
||||
|
||||
if ext in ('.jpg', '.jpeg', '.JPG', '.JPEG'):
|
||||
options = settings['jpg_options']
|
||||
elif ext == '.png':
|
||||
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
BIN
tests/sample/pictures/dir1/test2/21.tiff
Normal file
BIN
tests/sample/pictures/dir1/test2/21.tiff
Normal file
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
Order: 03
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user