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.
Files changed (64) hide show
  1. sigal/__init__.py +1 -1
  2. sigal/__main__.py +18 -10
  3. sigal/gallery.py +90 -88
  4. sigal/image.py +26 -7
  5. sigal/log.py +1 -1
  6. sigal/plugins/compress_assets.py +4 -3
  7. sigal/plugins/encrypt/encrypt.py +1 -1
  8. sigal/plugins/encrypt/endec.py +2 -2
  9. sigal/plugins/extended_caching.py +5 -1
  10. sigal/plugins/feeds.py +1 -0
  11. sigal/plugins/media_page.py +1 -1
  12. sigal/plugins/nomedia.py +1 -1
  13. sigal/plugins/nonmedia_files.py +26 -15
  14. sigal/plugins/titleregexp.py +2 -2
  15. sigal/plugins/zip_gallery.py +1 -1
  16. sigal/settings.py +35 -3
  17. sigal/templates/sigal.conf.py +9 -11
  18. sigal/themes/colorbox/templates/album.html +30 -28
  19. sigal/themes/default/templates/description.html +29 -0
  20. sigal/themes/default/templates/footer.html +3 -0
  21. sigal/themes/galleria/templates/album_items.html +3 -24
  22. sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.js +414 -0
  23. sigal/themes/photoswipe/static/photoswipe-dynamic-caption-plugin.esm.min.js +5 -0
  24. sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.js +129 -0
  25. sigal/themes/photoswipe/static/photoswipe-fullscreen.esm.min.js +8 -0
  26. sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js +1960 -0
  27. sigal/themes/photoswipe/static/photoswipe-lightbox.esm.js.map +1 -0
  28. sigal/themes/photoswipe/static/photoswipe-lightbox.esm.min.js +5 -0
  29. sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.js +257 -0
  30. sigal/themes/photoswipe/static/photoswipe-video-plugin.esm.min.js +1 -0
  31. sigal/themes/photoswipe/static/photoswipe.css +385 -140
  32. sigal/themes/photoswipe/static/photoswipe.esm.js +7081 -0
  33. sigal/themes/photoswipe/static/photoswipe.esm.js.map +1 -0
  34. sigal/themes/photoswipe/static/photoswipe.esm.min.js +5 -0
  35. sigal/themes/photoswipe/static/styles.css +57 -5
  36. sigal/themes/photoswipe/templates/album.html +2 -91
  37. sigal/themes/photoswipe/templates/album_items.html +31 -0
  38. sigal/themes/photoswipe/templates/album_list.html +5 -0
  39. sigal/themes/photoswipe/templates/base.html +49 -0
  40. sigal/utils.py +5 -2
  41. sigal/version.py +29 -3
  42. sigal/video.py +3 -3
  43. sigal/writer.py +11 -3
  44. {sigal-2.4.dist-info → sigal-2.6.dist-info}/METADATA +24 -28
  45. {sigal-2.4.dist-info → sigal-2.6.dist-info}/RECORD +49 -50
  46. {sigal-2.4.dist-info → sigal-2.6.dist-info}/WHEEL +1 -1
  47. {sigal-2.4.dist-info → sigal-2.6.dist-info/licenses}/LICENSE +1 -1
  48. sigal/plugins/upload_s3.py +0 -106
  49. sigal/themes/photoswipe/static/app.js +0 -214
  50. sigal/themes/photoswipe/static/default-skin/default-skin.css +0 -485
  51. sigal/themes/photoswipe/static/default-skin/default-skin.css.map +0 -10
  52. sigal/themes/photoswipe/static/default-skin/default-skin.png +0 -0
  53. sigal/themes/photoswipe/static/default-skin/default-skin.svg +0 -36
  54. sigal/themes/photoswipe/static/default-skin/preloader.gif +0 -0
  55. sigal/themes/photoswipe/static/echo/blank.gif +0 -0
  56. sigal/themes/photoswipe/static/echo/echo.js +0 -135
  57. sigal/themes/photoswipe/static/echo/echo.min.js +0 -2
  58. sigal/themes/photoswipe/static/photoswipe-ui-default.js +0 -871
  59. sigal/themes/photoswipe/static/photoswipe-ui-default.min.js +0 -1
  60. sigal/themes/photoswipe/static/photoswipe.css.map +0 -10
  61. sigal/themes/photoswipe/static/photoswipe.js +0 -3592
  62. sigal/themes/photoswipe/static/photoswipe.min.js +0 -1
  63. {sigal-2.4.dist-info → sigal-2.6.dist-info}/entry_points.txt +0 -0
  64. {sigal-2.4.dist-info → sigal-2.6.dist-info}/top_level.txt +0 -0
sigal/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2009-2023 - Simon Conseil
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
sigal/__main__.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2009-2023 - Simon Conseil
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
- def serve(destination, port, config):
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
- sys.stderr.write(
240
+ click.echo(
239
241
  f"The '{destination}' directory doesn't exist, maybe try building"
240
- " first?\n"
242
+ " first?",
243
+ err=True,
241
244
  )
242
245
  sys.exit(1)
243
246
  else:
244
- sys.stderr.write(
247
+ click.echo(
245
248
  f"The {destination} directory doesn't exist "
246
- f"and the config file ({config}) could not be read.\n"
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
- sys.stderr.write(f"The target {target} does not exist.\n")
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
- sys.stderr.write("Need an even number of arguments.\n")
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
- sys.stderr.write(
300
+ click.echo(
294
301
  f"Description file '{descfile}' already exists. "
295
- "Use --overwrite to overwrite it.\n"
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-2023 - Simon Conseil
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 get_exif_tags, get_image_metadata, get_size, process_image
49
- from .settings import Status, get_thumb
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
- # Register all formats
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
- extensions = {v: k for k, v in PILImage.EXTENSION.items()}
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 (".jpg", ".jpeg")
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 (".jpg", ".jpeg"):
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], alg=ns.LOCALE
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), alg=ns.LOCALE
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 and isfile(join(self.src_path, thumbnail)):
588
- self._thumbnail = url_from_path(
589
- join(self.name, get_thumb(self.settings, thumbnail))
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
- else:
594
- # find and return the first landscape image
595
- for f in self.medias:
596
- ext = splitext(f.dst_filename)[1]
597
- if ext.lower() not in self.settings["img_extensions"]:
598
- continue
599
-
600
- # Use f.size if available as it is quicker (in cache), but
601
- # fallback to the size of src_path if dst_path is missing
602
- size = f.input_size
603
- if size is None:
604
- size = f.file_metadata["size"]
605
-
606
- if size["width"] > size["height"]:
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) + "/" + f.thumbnail
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", f.dst_filename, e
635
+ "Failed to get thumbnail for %s: %s",
636
+ media.dst_filename,
637
+ e,
612
638
  )
613
639
  else:
614
- self.logger.debug(
615
- "Use 1st landscape image as thumbnail for %r : %s",
616
- self,
617
- self._thumbnail,
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
- self.logger.debug(
642
- "Use the 1st image as thumbnail for %r : %s", self, self._thumbnail
643
- )
644
- return self._thumbnail
645
-
646
- # use the thumbnail of their sub-directories
647
- if not self._thumbnail:
648
- for path, album in self.gallery.get_albums(self.path):
649
- if album.thumbnail:
650
- self._thumbnail = url_quote(self.name) + "/" + album.thumbnail
651
- self.logger.debug(
652
- "Using thumbnail from sub-directory for %r : %s",
653
- self,
654
- self._thumbnail,
655
- )
656
- return self._thumbnail
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="%16s" % "Sorting albums",
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="%16s" % "Sorting media",
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="%16s" % "Writing files",
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-2023 - Simon Conseil
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"PILImage reported a warning for file {file_path}\n"
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 to %s (%s)", outname, outformat)
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 thumnail image: %s (%s)", outname, outformat)
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 = img._getexif() or {}
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 (".jpg", ".jpeg"):
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
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2013-2023 - Simon Conseil
1
+ # Copyright (c) 2013-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
@@ -106,9 +106,10 @@ class GZipCompressor(BaseCompressor):
106
106
  suffix = "gz"
107
107
 
108
108
  def do_compress(self, filename, compressed_filename):
109
- with open(filename, "rb") as f_in, gzip.open(
110
- compressed_filename, "wb"
111
- ) as f_out:
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
 
@@ -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="%16s" % "Encrypting files",
203
+ label="{:>16s}".format("Encrypting files"),
204
204
  file=progressbar_target,
205
205
  show_eta=True,
206
206
  ) as medias:
@@ -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: %d" % len(key))
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: %d" % len(key))
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
- """ Decreases the time needed to build large galleries (e.g.: 25k images in
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('/')),
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2009-2023 - Simon Conseil
1
+ # Copyright (c) 2009-2026 - Simon Conseil
2
2
  # Copyright (c) 2014 - Jamie Starke
3
3
 
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
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
- """ This plugin offers more fine-grained control over exluded images and
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
@@ -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
- outformat = "JPEG"
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
- logger.info("plugin_settings: %r", plugin_settings)
117
- kwargs = {}
118
- for key in ("bg_color", "font", "font_color", "font_size"):
119
- if f"thumb_{key}" in plugin_settings:
120
- kwargs[key] = plugin_settings[f"thumb_{key}"]
121
- generate_thumbnail(
122
- media.src_ext[1:].upper(),
123
- media.thumb_path,
124
- settings["thumb_size"],
125
- options=settings["jpg_options"],
126
- **kwargs,
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):
@@ -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
- """ This plugin modifies titles of galleries by using regular-expressions and
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