sigal 2.4__py3-none-any.whl → 2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sigal/__init__.py +1 -1
- sigal/__main__.py +18 -10
- sigal/gallery.py +90 -88
- sigal/image.py +26 -7
- sigal/log.py +1 -1
- sigal/plugins/compress_assets.py +4 -3
- sigal/plugins/encrypt/encrypt.py +1 -1
- sigal/plugins/encrypt/endec.py +2 -2
- sigal/plugins/extended_caching.py +5 -1
- sigal/plugins/feeds.py +1 -0
- sigal/plugins/media_page.py +1 -1
- sigal/plugins/nomedia.py +1 -1
- sigal/plugins/nonmedia_files.py +26 -15
- sigal/plugins/titleregexp.py +2 -2
- sigal/plugins/zip_gallery.py +1 -1
- sigal/settings.py +35 -3
- sigal/templates/sigal.conf.py +9 -11
- sigal/themes/colorbox/templates/album.html +30 -28
- sigal/themes/default/templates/description.html +29 -0
- sigal/themes/default/templates/footer.html +3 -0
- sigal/themes/galleria/templates/album_items.html +3 -24
- sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.js +414 -0
- sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.min.js +5 -0
- sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.js +129 -0
- sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.min.js +8 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js +1960 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js.map +1 -0
- sigal/themes/photoswipe/static/photoswipe-lightbox.esm.min.js +5 -0
- sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.js +257 -0
- sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.min.js +1 -0
- sigal/themes/photoswipe/static/photoswipe.css +385 -140
- sigal/themes/photoswipe/static/photoswipe.esm.js +7081 -0
- sigal/themes/photoswipe/static/photoswipe.esm.js.map +1 -0
- sigal/themes/photoswipe/static/photoswipe.esm.min.js +5 -0
- sigal/themes/photoswipe/static/styles.css +57 -5
- sigal/themes/photoswipe/templates/album.html +2 -91
- sigal/themes/photoswipe/templates/album_items.html +31 -0
- sigal/themes/photoswipe/templates/album_list.html +5 -0
- sigal/themes/photoswipe/templates/base.html +49 -0
- sigal/utils.py +5 -2
- sigal/version.py +29 -3
- sigal/video.py +3 -3
- sigal/writer.py +11 -3
- {sigal-2.4.dist-info → sigal-2.6.dist-info}/METADATA +24 -28
- {sigal-2.4.dist-info → sigal-2.6.dist-info}/RECORD +49 -50
- {sigal-2.4.dist-info → sigal-2.6.dist-info}/WHEEL +1 -1
- {sigal-2.4.dist-info → sigal-2.6.dist-info/licenses}/LICENSE +1 -1
- sigal/plugins/upload_s3.py +0 -106
- sigal/themes/photoswipe/static/app.js +0 -214
- sigal/themes/photoswipe/static/default-skin/default-skin.css +0 -485
- sigal/themes/photoswipe/static/default-skin/default-skin.css.map +0 -10
- sigal/themes/photoswipe/static/default-skin/default-skin.png +0 -0
- sigal/themes/photoswipe/static/default-skin/default-skin.svg +0 -36
- sigal/themes/photoswipe/static/default-skin/preloader.gif +0 -0
- sigal/themes/photoswipe/static/echo/blank.gif +0 -0
- sigal/themes/photoswipe/static/echo/echo.js +0 -135
- sigal/themes/photoswipe/static/echo/echo.min.js +0 -2
- sigal/themes/photoswipe/static/photoswipe-ui-default.js +0 -871
- sigal/themes/photoswipe/static/photoswipe-ui-default.min.js +0 -1
- sigal/themes/photoswipe/static/photoswipe.css.map +0 -10
- sigal/themes/photoswipe/static/photoswipe.js +0 -3592
- sigal/themes/photoswipe/static/photoswipe.min.js +0 -1
- {sigal-2.4.dist-info → sigal-2.6.dist-info}/entry_points.txt +0 -0
- {sigal-2.4.dist-info → sigal-2.6.dist-info}/top_level.txt +0 -0
sigal/__init__.py
CHANGED
sigal/__main__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2009-
|
|
1
|
+
# Copyright (c) 2009-2026 - Simon Conseil
|
|
2
2
|
|
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
4
|
# of this software and associated documentation files (the "Software"), to
|
|
@@ -25,6 +25,7 @@ import pathlib
|
|
|
25
25
|
import socketserver
|
|
26
26
|
import sys
|
|
27
27
|
import time
|
|
28
|
+
import webbrowser
|
|
28
29
|
from http import server
|
|
29
30
|
|
|
30
31
|
import click
|
|
@@ -227,7 +228,8 @@ def build(
|
|
|
227
228
|
show_default=True,
|
|
228
229
|
help="Configuration file",
|
|
229
230
|
)
|
|
230
|
-
|
|
231
|
+
@option("-b", "--browser", is_flag=True, help="Open in your default browser")
|
|
232
|
+
def serve(destination, port, config, browser):
|
|
231
233
|
"""Run a simple web server."""
|
|
232
234
|
if os.path.exists(destination):
|
|
233
235
|
pass
|
|
@@ -235,15 +237,17 @@ def serve(destination, port, config):
|
|
|
235
237
|
settings = read_settings(config)
|
|
236
238
|
destination = settings.get("destination")
|
|
237
239
|
if not os.path.exists(destination):
|
|
238
|
-
|
|
240
|
+
click.echo(
|
|
239
241
|
f"The '{destination}' directory doesn't exist, maybe try building"
|
|
240
|
-
" first
|
|
242
|
+
" first?",
|
|
243
|
+
err=True,
|
|
241
244
|
)
|
|
242
245
|
sys.exit(1)
|
|
243
246
|
else:
|
|
244
|
-
|
|
247
|
+
click.echo(
|
|
245
248
|
f"The {destination} directory doesn't exist "
|
|
246
|
-
f"and the config file ({config}) could not be read
|
|
249
|
+
f"and the config file ({config}) could not be read.",
|
|
250
|
+
err=True,
|
|
247
251
|
)
|
|
248
252
|
sys.exit(2)
|
|
249
253
|
|
|
@@ -253,6 +257,9 @@ def serve(destination, port, config):
|
|
|
253
257
|
httpd = socketserver.TCPServer(("", port), Handler, False)
|
|
254
258
|
print(f" * Running on http://127.0.0.1:{port}/")
|
|
255
259
|
|
|
260
|
+
if browser:
|
|
261
|
+
webbrowser.open(f"http://127.0.0.1:{port}/")
|
|
262
|
+
|
|
256
263
|
try:
|
|
257
264
|
httpd.allow_reuse_address = True
|
|
258
265
|
httpd.server_bind()
|
|
@@ -279,10 +286,10 @@ def set_meta(target, keys, overwrite=False):
|
|
|
279
286
|
"""
|
|
280
287
|
|
|
281
288
|
if not os.path.exists(target):
|
|
282
|
-
|
|
289
|
+
click.echo(f"The target {target} does not exist.", err=True)
|
|
283
290
|
sys.exit(1)
|
|
284
291
|
if len(keys) < 2 or len(keys) % 2 > 0:
|
|
285
|
-
|
|
292
|
+
click.echo("Need an even number of arguments.", err=True)
|
|
286
293
|
sys.exit(1)
|
|
287
294
|
|
|
288
295
|
if os.path.isdir(target):
|
|
@@ -290,9 +297,10 @@ def set_meta(target, keys, overwrite=False):
|
|
|
290
297
|
else:
|
|
291
298
|
descfile = os.path.splitext(target)[0] + ".md"
|
|
292
299
|
if os.path.exists(descfile) and not overwrite:
|
|
293
|
-
|
|
300
|
+
click.echo(
|
|
294
301
|
f"Description file '{descfile}' already exists. "
|
|
295
|
-
"Use --overwrite to overwrite it
|
|
302
|
+
"Use --overwrite to overwrite it.",
|
|
303
|
+
err=True,
|
|
296
304
|
)
|
|
297
305
|
sys.exit(2)
|
|
298
306
|
|
sigal/gallery.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2009-
|
|
1
|
+
# Copyright (c) 2009-2026 - Simon Conseil
|
|
2
2
|
# Copyright (c) 2013 - Christophe-Marie Duquesne
|
|
3
3
|
# Copyright (c) 2014 - Jonas Kaufmann
|
|
4
4
|
# Copyright (c) 2015 - François D.
|
|
@@ -45,8 +45,14 @@ from natsort import natsort_keygen, ns
|
|
|
45
45
|
from PIL import Image as PILImage
|
|
46
46
|
|
|
47
47
|
from . import image, signals, video
|
|
48
|
-
from .image import
|
|
49
|
-
|
|
48
|
+
from .image import (
|
|
49
|
+
EXIF_EXTENSIONS,
|
|
50
|
+
get_exif_tags,
|
|
51
|
+
get_image_metadata,
|
|
52
|
+
get_size,
|
|
53
|
+
process_image,
|
|
54
|
+
)
|
|
55
|
+
from .settings import IMG_EXTENSIONS, Status, get_thumb
|
|
50
56
|
from .utils import (
|
|
51
57
|
Devnull,
|
|
52
58
|
check_or_create_dir,
|
|
@@ -240,15 +246,10 @@ class Image(Media):
|
|
|
240
246
|
super().__init__(filename, path, settings)
|
|
241
247
|
imgformat = settings.get("img_format")
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
PILImage.init()
|
|
245
|
-
|
|
246
|
-
if imgformat and PILImage.EXTENSION[self.src_ext] != imgformat.upper():
|
|
249
|
+
if imgformat and IMG_EXTENSIONS.ext2format[self.src_ext] != imgformat.upper():
|
|
247
250
|
# Find the extension that should match img_format
|
|
248
|
-
|
|
249
|
-
ext = extensions[imgformat.upper()]
|
|
251
|
+
ext = IMG_EXTENSIONS.format2ext[imgformat.upper()]
|
|
250
252
|
self.dst_filename = self.basename + ext
|
|
251
|
-
self.thumb_name = get_thumb(self.settings, self.dst_filename)
|
|
252
253
|
|
|
253
254
|
@cached_property
|
|
254
255
|
def date(self):
|
|
@@ -264,7 +265,7 @@ class Image(Media):
|
|
|
264
265
|
datetime_format = self.settings["datetime_format"]
|
|
265
266
|
return (
|
|
266
267
|
get_exif_tags(self.raw_exif, datetime_format=datetime_format)
|
|
267
|
-
if self.raw_exif and self.src_ext in
|
|
268
|
+
if self.raw_exif and self.src_ext in EXIF_EXTENSIONS
|
|
268
269
|
else None
|
|
269
270
|
)
|
|
270
271
|
|
|
@@ -289,7 +290,7 @@ class Image(Media):
|
|
|
289
290
|
@cached_property
|
|
290
291
|
def raw_exif(self):
|
|
291
292
|
"""If not `None`, contains the raw EXIF tags."""
|
|
292
|
-
if self.src_ext in
|
|
293
|
+
if self.src_ext in EXIF_EXTENSIONS:
|
|
293
294
|
return self.file_metadata["exif"]
|
|
294
295
|
|
|
295
296
|
@cached_property
|
|
@@ -486,6 +487,7 @@ class Album:
|
|
|
486
487
|
reverse = self.settings["albums_sort_reverse"]
|
|
487
488
|
|
|
488
489
|
if "sort" in self.meta:
|
|
490
|
+
# override default sort order from settings
|
|
489
491
|
albums_sort_attr = self.meta["sort"][0]
|
|
490
492
|
if albums_sort_attr[0] == "-":
|
|
491
493
|
albums_sort_attr = albums_sort_attr[1:]
|
|
@@ -515,7 +517,7 @@ class Album:
|
|
|
515
517
|
continue
|
|
516
518
|
return ""
|
|
517
519
|
|
|
518
|
-
key = natsort_keygen(key=sort_key, alg=ns.LOCALE)
|
|
520
|
+
key = natsort_keygen(key=sort_key, alg=ns.SIGNED | ns.LOCALE)
|
|
519
521
|
self.subdirs.sort(key=key, reverse=reverse)
|
|
520
522
|
|
|
521
523
|
signals.albums_sorted.send(self)
|
|
@@ -530,11 +532,13 @@ class Album:
|
|
|
530
532
|
elif medias_sort_attr.startswith("meta."):
|
|
531
533
|
meta_key = medias_sort_attr.split(".", 1)[1]
|
|
532
534
|
key = natsort_keygen(
|
|
533
|
-
key=lambda s: s.meta.get(meta_key, [""])[0],
|
|
535
|
+
key=lambda s: s.meta.get(meta_key, [""])[0],
|
|
536
|
+
alg=ns.SIGNED | ns.LOCALE,
|
|
534
537
|
)
|
|
535
538
|
else:
|
|
536
539
|
key = natsort_keygen(
|
|
537
|
-
key=lambda s: getattr(s, medias_sort_attr),
|
|
540
|
+
key=lambda s: getattr(s, medias_sort_attr),
|
|
541
|
+
alg=ns.SIGNED | ns.LOCALE,
|
|
538
542
|
)
|
|
539
543
|
|
|
540
544
|
self.medias.sort(key=key, reverse=self.settings["medias_sort_reverse"])
|
|
@@ -584,76 +588,76 @@ class Album:
|
|
|
584
588
|
# Test the thumbnail from the Markdown file.
|
|
585
589
|
thumbnail = self.meta.get("thumbnail", [""])[0]
|
|
586
590
|
|
|
587
|
-
if thumbnail
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
)
|
|
591
|
+
if thumbnail:
|
|
592
|
+
# if thumbnail is set in the markdown, it can be either the
|
|
593
|
+
# original filename or the generated name after format conversion
|
|
594
|
+
if isfile(join(self.src_path, thumbnail)):
|
|
595
|
+
thumbnail = get_thumb(self.settings, thumbnail)
|
|
596
|
+
self._thumbnail = url_from_path(join(self.name, thumbnail))
|
|
591
597
|
self.logger.debug("Thumbnail for %r : %s", self, self._thumbnail)
|
|
592
598
|
return self._thumbnail
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
599
|
+
|
|
600
|
+
# find and return the first landscape image
|
|
601
|
+
for f in self.medias:
|
|
602
|
+
ext = splitext(f.dst_filename)[1]
|
|
603
|
+
if ext.lower() not in self.settings["img_extensions"]:
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
# Use f.size if available as it is quicker (in cache), but
|
|
607
|
+
# fallback to the size of src_path if dst_path is missing
|
|
608
|
+
size = f.input_size
|
|
609
|
+
if size is None:
|
|
610
|
+
size = f.file_metadata["size"]
|
|
611
|
+
|
|
612
|
+
if size["width"] > size["height"]:
|
|
613
|
+
try:
|
|
614
|
+
self._thumbnail = url_quote(self.name) + "/" + f.thumbnail
|
|
615
|
+
except Exception as e:
|
|
616
|
+
self.logger.info(
|
|
617
|
+
"Failed to get thumbnail for %s: %s", f.dst_filename, e
|
|
618
|
+
)
|
|
619
|
+
else:
|
|
620
|
+
self.logger.debug(
|
|
621
|
+
"Use 1st landscape image as thumbnail for %r : %s",
|
|
622
|
+
self,
|
|
623
|
+
self._thumbnail,
|
|
624
|
+
)
|
|
625
|
+
return self._thumbnail
|
|
626
|
+
|
|
627
|
+
# else simply return the 1st media file
|
|
628
|
+
if not self._thumbnail and self.medias:
|
|
629
|
+
for media in self.medias:
|
|
630
|
+
if media.thumbnail is not None:
|
|
607
631
|
try:
|
|
608
|
-
self._thumbnail = url_quote(self.name) + "/" +
|
|
632
|
+
self._thumbnail = url_quote(self.name) + "/" + media.thumbnail
|
|
609
633
|
except Exception as e:
|
|
610
634
|
self.logger.info(
|
|
611
|
-
"Failed to get thumbnail for %s: %s",
|
|
635
|
+
"Failed to get thumbnail for %s: %s",
|
|
636
|
+
media.dst_filename,
|
|
637
|
+
e,
|
|
612
638
|
)
|
|
613
639
|
else:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
)
|
|
619
|
-
return self._thumbnail
|
|
620
|
-
|
|
621
|
-
# else simply return the 1st media file
|
|
622
|
-
if not self._thumbnail and self.medias:
|
|
623
|
-
for media in self.medias:
|
|
624
|
-
if media.thumbnail is not None:
|
|
625
|
-
try:
|
|
626
|
-
self._thumbnail = (
|
|
627
|
-
url_quote(self.name) + "/" + media.thumbnail
|
|
628
|
-
)
|
|
629
|
-
except Exception as e:
|
|
630
|
-
self.logger.info(
|
|
631
|
-
"Failed to get thumbnail for %s: %s",
|
|
632
|
-
media.dst_filename,
|
|
633
|
-
e,
|
|
634
|
-
)
|
|
635
|
-
else:
|
|
636
|
-
break
|
|
637
|
-
else:
|
|
638
|
-
self.logger.warning("No thumbnail found for %r", self)
|
|
639
|
-
return
|
|
640
|
+
break
|
|
641
|
+
else:
|
|
642
|
+
self.logger.warning("No thumbnail found for %r", self)
|
|
643
|
+
return
|
|
640
644
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
645
|
+
self.logger.debug(
|
|
646
|
+
"Use the 1st image as thumbnail for %r : %s", self, self._thumbnail
|
|
647
|
+
)
|
|
648
|
+
return self._thumbnail
|
|
649
|
+
|
|
650
|
+
# use the thumbnail of their sub-directories
|
|
651
|
+
if not self._thumbnail:
|
|
652
|
+
for path, album in self.gallery.get_albums(self.path):
|
|
653
|
+
if album.thumbnail:
|
|
654
|
+
self._thumbnail = url_quote(self.name) + "/" + album.thumbnail
|
|
655
|
+
self.logger.debug(
|
|
656
|
+
"Using thumbnail from sub-directory for %r : %s",
|
|
657
|
+
self,
|
|
658
|
+
self._thumbnail,
|
|
659
|
+
)
|
|
660
|
+
return self._thumbnail
|
|
657
661
|
|
|
658
662
|
self.logger.error("Thumbnail not found for %r", self)
|
|
659
663
|
|
|
@@ -734,6 +738,11 @@ class Gallery:
|
|
|
734
738
|
fnmatch.fnmatch(relpath, ignore) for ignore in ignore_dirs
|
|
735
739
|
):
|
|
736
740
|
self.logger.info("Ignoring %s", relpath)
|
|
741
|
+
# Remove sub-directories
|
|
742
|
+
for d in dirs[:]:
|
|
743
|
+
path = join(relpath, d) if relpath != "." else d
|
|
744
|
+
if path in albums.keys():
|
|
745
|
+
del albums[path]
|
|
737
746
|
continue
|
|
738
747
|
|
|
739
748
|
# Remove files that match the ignore_files settings
|
|
@@ -767,7 +776,7 @@ class Gallery:
|
|
|
767
776
|
|
|
768
777
|
with progressbar(
|
|
769
778
|
albums.values(),
|
|
770
|
-
label="
|
|
779
|
+
label="{:>16s}".format("Sorting albums"),
|
|
771
780
|
file=self.progressbar_target,
|
|
772
781
|
) as progress_albums:
|
|
773
782
|
for album in progress_albums:
|
|
@@ -775,7 +784,7 @@ class Gallery:
|
|
|
775
784
|
|
|
776
785
|
with progressbar(
|
|
777
786
|
albums.values(),
|
|
778
|
-
label="
|
|
787
|
+
label="{:>16s}".format("Sorting medias"),
|
|
779
788
|
file=self.progressbar_target,
|
|
780
789
|
) as progress_albums:
|
|
781
790
|
for album in progress_albums:
|
|
@@ -891,26 +900,19 @@ class Gallery:
|
|
|
891
900
|
|
|
892
901
|
if self.settings["write_html"]:
|
|
893
902
|
album_writer = AlbumPageWriter(self.settings, index_title=self.title)
|
|
903
|
+
album_writer.copy_theme_files()
|
|
894
904
|
album_list_writer = AlbumListPageWriter(
|
|
895
905
|
self.settings, index_title=self.title
|
|
896
906
|
)
|
|
897
907
|
with progressbar(
|
|
898
908
|
self.albums.values(),
|
|
899
|
-
label="
|
|
909
|
+
label="{:>16s}".format("Writing files"),
|
|
900
910
|
item_show_func=log_func,
|
|
901
911
|
show_eta=False,
|
|
902
912
|
file=self.progressbar_target,
|
|
903
913
|
) as albums:
|
|
904
914
|
for album in albums:
|
|
905
915
|
if album.albums:
|
|
906
|
-
if album.medias:
|
|
907
|
-
self.logger.warning(
|
|
908
|
-
"Album '%s' contains sub-albums and images. "
|
|
909
|
-
"Please move images to their own sub-album. "
|
|
910
|
-
"Images in album %s will not be visible.",
|
|
911
|
-
album.title,
|
|
912
|
-
album.title,
|
|
913
|
-
)
|
|
914
916
|
album_list_writer.write(album)
|
|
915
917
|
else:
|
|
916
918
|
album_writer.write(album)
|
sigal/image.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright (c) 2009-
|
|
1
|
+
# Copyright (c) 2009-2026 - Simon Conseil
|
|
2
2
|
# Copyright (c) 2015 - François D.
|
|
3
3
|
# Copyright (c) 2018 - Edwin Steele
|
|
4
4
|
|
|
@@ -44,11 +44,21 @@ from PIL.TiffImagePlugin import IFDRational
|
|
|
44
44
|
from pilkit.processors import Transpose
|
|
45
45
|
from pilkit.utils import save_image
|
|
46
46
|
|
|
47
|
+
try:
|
|
48
|
+
from pillow_heif import HeifImagePlugin # noqa: F401
|
|
49
|
+
|
|
50
|
+
HAS_HEIF = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
HAS_HEIF = False
|
|
53
|
+
|
|
47
54
|
from . import signals, utils
|
|
55
|
+
from .settings import Status
|
|
48
56
|
|
|
49
57
|
# Force loading of truncated files
|
|
50
58
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
51
59
|
|
|
60
|
+
EXIF_EXTENSIONS = (".jpg", ".jpeg", ".heic")
|
|
61
|
+
|
|
52
62
|
|
|
53
63
|
def _has_exif_tags(img):
|
|
54
64
|
return hasattr(img, "info") and "exif" in img.info
|
|
@@ -66,8 +76,7 @@ def _read_image(file_path):
|
|
|
66
76
|
|
|
67
77
|
for w in caught_warnings:
|
|
68
78
|
logger.warning(
|
|
69
|
-
f"
|
|
70
|
-
f"{w.category}: {w.message}"
|
|
79
|
+
f"Pillow reported a warning for file {file_path}\n{w.category}: {w.message}"
|
|
71
80
|
)
|
|
72
81
|
return im
|
|
73
82
|
|
|
@@ -90,6 +99,7 @@ def generate_image(source, outname, settings, options=None):
|
|
|
90
99
|
|
|
91
100
|
img = _read_image(source)
|
|
92
101
|
original_format = img.format
|
|
102
|
+
logger.debug("Read %s: %dx%d (%s)", source, *img.size, original_format)
|
|
93
103
|
|
|
94
104
|
if settings["copy_exif_data"] and settings["autorotate_images"]:
|
|
95
105
|
logger.warning(
|
|
@@ -140,7 +150,7 @@ def generate_image(source, outname, settings, options=None):
|
|
|
140
150
|
# format, or fall back to JPEG
|
|
141
151
|
outformat = settings.get("img_format") or img.format or original_format or "JPEG"
|
|
142
152
|
|
|
143
|
-
logger.debug("Save resized image
|
|
153
|
+
logger.debug("Save resized image: %s, %dx%d (%s)", outname, *img.size, outformat)
|
|
144
154
|
save_image(img, outname, outformat, options=options, autoconvert=True)
|
|
145
155
|
|
|
146
156
|
|
|
@@ -153,6 +163,7 @@ def generate_thumbnail(
|
|
|
153
163
|
img = _read_image(source)
|
|
154
164
|
img = Transpose().process(img)
|
|
155
165
|
original_format = img.format
|
|
166
|
+
logger.debug("Read %s: %dx%d (%s)", source, *img.size, original_format)
|
|
156
167
|
|
|
157
168
|
try:
|
|
158
169
|
method = PILImage.Resampling.LANCZOS
|
|
@@ -166,7 +177,7 @@ def generate_thumbnail(
|
|
|
166
177
|
img.thumbnail(box, method)
|
|
167
178
|
|
|
168
179
|
outformat = img.format or original_format or "JPEG"
|
|
169
|
-
logger.debug("Save
|
|
180
|
+
logger.debug("Save thumbnail image: %s, %dx%d (%s)", outname, *img.size, outformat)
|
|
170
181
|
save_image(img, outname, outformat, options=options, autoconvert=True)
|
|
171
182
|
|
|
172
183
|
|
|
@@ -180,6 +191,11 @@ def process_image(media):
|
|
|
180
191
|
options = media.settings["jpg_options"]
|
|
181
192
|
elif media.src_ext == ".png":
|
|
182
193
|
options = {"optimize": True}
|
|
194
|
+
elif media.src_ext == ".heic" and not HAS_HEIF:
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"cannot open {media.src_path}, pillow-heif is needed to open .heic files"
|
|
197
|
+
)
|
|
198
|
+
return Status.FAILURE
|
|
183
199
|
else:
|
|
184
200
|
options = {}
|
|
185
201
|
|
|
@@ -220,7 +236,10 @@ def get_exif_data(filename):
|
|
|
220
236
|
|
|
221
237
|
try:
|
|
222
238
|
with warnings.catch_warnings(record=True) as caught_warnings:
|
|
223
|
-
exif =
|
|
239
|
+
exif = {}
|
|
240
|
+
exifdata = img.getexif()
|
|
241
|
+
if exifdata:
|
|
242
|
+
exif = exifdata._get_merged_dict()
|
|
224
243
|
except ZeroDivisionError:
|
|
225
244
|
logger.warning("Failed to read EXIF data.")
|
|
226
245
|
return None
|
|
@@ -290,7 +309,7 @@ def get_image_metadata(filename):
|
|
|
290
309
|
logger.error("Could not open image %s metadata: %s", filename, e)
|
|
291
310
|
else:
|
|
292
311
|
try:
|
|
293
|
-
if os.path.splitext(filename)[1].lower() in
|
|
312
|
+
if os.path.splitext(filename)[1].lower() in EXIF_EXTENSIONS:
|
|
294
313
|
exif = get_exif_data(img)
|
|
295
314
|
except Exception as e:
|
|
296
315
|
logger.warning("Could not read EXIF data from %s: %s", filename, e)
|
sigal/log.py
CHANGED
sigal/plugins/compress_assets.py
CHANGED
|
@@ -106,9 +106,10 @@ class GZipCompressor(BaseCompressor):
|
|
|
106
106
|
suffix = "gz"
|
|
107
107
|
|
|
108
108
|
def do_compress(self, filename, compressed_filename):
|
|
109
|
-
with
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
with (
|
|
110
|
+
open(filename, "rb") as f_in,
|
|
111
|
+
gzip.open(compressed_filename, "wb") as f_out,
|
|
112
|
+
):
|
|
112
113
|
shutil.copyfileobj(f_in, f_out)
|
|
113
114
|
|
|
114
115
|
|
sigal/plugins/encrypt/encrypt.py
CHANGED
|
@@ -200,7 +200,7 @@ def encrypt_files(settings, config, cache, albums, progressbar_target):
|
|
|
200
200
|
medias = list(chain.from_iterable(albums.values()))
|
|
201
201
|
with progressbar(
|
|
202
202
|
medias,
|
|
203
|
-
label="
|
|
203
|
+
label="{:>16s}".format("Encrypting files"),
|
|
204
204
|
file=progressbar_target,
|
|
205
205
|
show_eta=True,
|
|
206
206
|
) as medias:
|
sigal/plugins/encrypt/endec.py
CHANGED
|
@@ -66,7 +66,7 @@ def dispatchargs(decorated):
|
|
|
66
66
|
|
|
67
67
|
def encrypt(key: bytes, infile: BinaryIO, outfile: BinaryIO, tag: bytes):
|
|
68
68
|
if len(key) != 128 / 8:
|
|
69
|
-
raise ValueError("Unsupported key length:
|
|
69
|
+
raise ValueError(f"Unsupported key length: {len(key)}")
|
|
70
70
|
aesgcm = AESGCM(key)
|
|
71
71
|
iv = os.urandom(12)
|
|
72
72
|
plaintext = infile
|
|
@@ -80,7 +80,7 @@ def encrypt(key: bytes, infile: BinaryIO, outfile: BinaryIO, tag: bytes):
|
|
|
80
80
|
|
|
81
81
|
def decrypt(key: bytes, infile: BinaryIO, outfile: BinaryIO, tag: bytes):
|
|
82
82
|
if len(key) != 128 / 8:
|
|
83
|
-
raise ValueError("Unsupported key length:
|
|
83
|
+
raise ValueError(f"Unsupported key length: {len(key)}")
|
|
84
84
|
aesgcm = AESGCM(key)
|
|
85
85
|
ciphertext = infile
|
|
86
86
|
plaintext = outfile
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
19
19
|
# IN THE SOFTWARE.
|
|
20
20
|
|
|
21
|
-
"""
|
|
21
|
+
"""Decreases the time needed to build large galleries (e.g.: 25k images in
|
|
22
22
|
2.5s instead of 30s)
|
|
23
23
|
|
|
24
24
|
This plugin allows extended caching, which is useful for large galleries. Once
|
|
@@ -78,6 +78,8 @@ def load_metadata(album):
|
|
|
78
78
|
media.file_metadata = data["file_metadata"]
|
|
79
79
|
if "exif" in data:
|
|
80
80
|
media.exif = data["exif"]
|
|
81
|
+
if "input_size" in data:
|
|
82
|
+
media.input_size = data["input_size"]
|
|
81
83
|
|
|
82
84
|
try:
|
|
83
85
|
mod_date = int(get_mod_date(media.markdown_metadata_filepath))
|
|
@@ -134,6 +136,8 @@ def save_cache(gallery):
|
|
|
134
136
|
data["file_metadata"] = media.file_metadata
|
|
135
137
|
if hasattr(media, "exif"):
|
|
136
138
|
data["exif"] = media.exif
|
|
139
|
+
if hasattr(media, "input_size"):
|
|
140
|
+
data["input_size"] = media.input_size
|
|
137
141
|
|
|
138
142
|
try:
|
|
139
143
|
meta_mod_date = int(get_mod_date(media.markdown_metadata_filepath))
|
sigal/plugins/feeds.py
CHANGED
|
@@ -70,6 +70,7 @@ def generate_feed(gallery, medias, feed_type=None, feed_url="", nb_items=0):
|
|
|
70
70
|
feed.add_item(
|
|
71
71
|
title=Markup.escape(item.title or item.url),
|
|
72
72
|
link=link,
|
|
73
|
+
content=None,
|
|
73
74
|
# unique_id='tag:%s,%s:%s' % (urlparse(link).netloc,
|
|
74
75
|
# item.date.date(),
|
|
75
76
|
# urlparse(link).path.lstrip('/')),
|
sigal/plugins/media_page.py
CHANGED
sigal/plugins/nomedia.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
19
19
|
# IN THE SOFTWARE.
|
|
20
20
|
|
|
21
|
-
"""
|
|
21
|
+
"""This plugin offers more fine-grained control over exluded images and
|
|
22
22
|
folders, similarly to how it's handled on Android.
|
|
23
23
|
|
|
24
24
|
To ignore a folder or image put a ``.nomedia`` file next to it in its parent
|
sigal/plugins/nonmedia_files.py
CHANGED
|
@@ -4,6 +4,9 @@ This plugin will copy the files into the build tree and generate generic
|
|
|
4
4
|
thumbnails for the files. In-browser previews will likely fail, and
|
|
5
5
|
it is up to the theme to provide correct support for downloads.
|
|
6
6
|
|
|
7
|
+
If the `pdf2image <https://pypi.org/project/pdf2image/>`_ optional dependency is installed,
|
|
8
|
+
it will be used to generate thumbnails for PDF files.
|
|
9
|
+
|
|
7
10
|
Settings available as dictionary in ``nonmedia_files_options``:
|
|
8
11
|
|
|
9
12
|
- ``ext_as_thumb``: Enable simple thumbnail showing ext. Default to ``True``
|
|
@@ -26,6 +29,10 @@ Settings available as dictionary in ``nonmedia_files_options``:
|
|
|
26
29
|
import logging
|
|
27
30
|
import os
|
|
28
31
|
|
|
32
|
+
try: # Optional dependency:
|
|
33
|
+
from pdf2image import convert_from_path as pdf2img
|
|
34
|
+
except ImportError:
|
|
35
|
+
pdf2img = None
|
|
29
36
|
from PIL import Image as PILImage
|
|
30
37
|
from PIL import ImageDraw, ImageFont
|
|
31
38
|
from pilkit.utils import save_image
|
|
@@ -102,9 +109,7 @@ def generate_thumbnail(
|
|
|
102
109
|
logger.info(f"kwargs: {kwargs}")
|
|
103
110
|
d.text(anchor, text, anchor="mm", **kwargs)
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
logger.info("Save thumnail image: %s (%s)", outname, outformat)
|
|
107
|
-
save_image(img, outname, outformat, options=options, autoconvert=True)
|
|
112
|
+
save_image(img, outname, "JPEG", options=options, autoconvert=True)
|
|
108
113
|
|
|
109
114
|
|
|
110
115
|
def process_thumb(media):
|
|
@@ -113,18 +118,24 @@ def process_thumb(media):
|
|
|
113
118
|
utils.copy(media.src_path, media.dst_path, symlink=settings["orig_link"])
|
|
114
119
|
|
|
115
120
|
if plugin_settings.get("ext_as_thumb", DEFAULT_CONFIG["ext_as_thumb"]):
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
if pdf2img and media.src_ext.lower() == ".pdf":
|
|
122
|
+
images = pdf2img(
|
|
123
|
+
media.src_path, single_file=True, size=settings["thumb_size"]
|
|
124
|
+
)
|
|
125
|
+
images[0].save(media.thumb_path)
|
|
126
|
+
else:
|
|
127
|
+
kwargs = {}
|
|
128
|
+
for key in ("bg_color", "font", "font_color", "font_size"):
|
|
129
|
+
if f"thumb_{key}" in plugin_settings:
|
|
130
|
+
kwargs[key] = plugin_settings[f"thumb_{key}"]
|
|
131
|
+
generate_thumbnail(
|
|
132
|
+
media.src_ext[1:].upper(),
|
|
133
|
+
media.thumb_path,
|
|
134
|
+
settings["thumb_size"],
|
|
135
|
+
options=settings["jpg_options"],
|
|
136
|
+
**kwargs,
|
|
137
|
+
)
|
|
138
|
+
logger.info("Saved thumbnail image: %s", media.thumb_path)
|
|
128
139
|
|
|
129
140
|
|
|
130
141
|
def process_nonmedia(media):
|
sigal/plugins/titleregexp.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
19
19
|
# IN THE SOFTWARE.
|
|
20
20
|
|
|
21
|
-
"""
|
|
21
|
+
"""This plugin modifies titles of galleries by using regular-expressions and
|
|
22
22
|
simple string or character replacements. It is acting in two phases: First, all
|
|
23
23
|
regular-expression-based modifications are carried out, second, string/character
|
|
24
24
|
replacements are done.
|
|
@@ -74,7 +74,7 @@ def titleregexp(album):
|
|
|
74
74
|
|
|
75
75
|
for r in cfg.get("regexp"):
|
|
76
76
|
album.title, n = re.subn(
|
|
77
|
-
r.get("search"), r.get("replace"), album.title, r.get("count", 0)
|
|
77
|
+
r.get("search"), r.get("replace"), album.title, count=r.get("count", 0)
|
|
78
78
|
)
|
|
79
79
|
total += n
|
|
80
80
|
|