This is a gentle fork from https://framagit.org/marienfressinaud/photos.marienfressinaud.fr with a responsive and optimized mindset. https://media.larlet.fr/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

boop.py 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. #!/usr/bin/env python3
  2. import os
  3. import shutil
  4. import re
  5. import yaml
  6. from operator import itemgetter
  7. from jinja2 import Environment, PackageLoader, select_autoescape
  8. from PIL import Image, ExifTags
  9. from dotenv import load_dotenv
  10. from configuration import SITE_TITLE, SITE_AUTHOR, SITE_AUTHOR_WEBSITE, THEME
  11. # Load env var from .env file
  12. load_dotenv(dotenv_path=".env")
  13. PICTURES_DIR_NAME = "photos"
  14. OUTPUT_DIR_NAME = "output"
  15. # Configure YAML to accept environment variables in metadata files
  16. # Based on https://stackoverflow.com/a/27232341 solution
  17. pattern = re.compile(r"^ENV\[\'(.*)\'\]$")
  18. yaml.add_implicit_resolver("!pathex", pattern, Loader=yaml.SafeLoader)
  19. def pathex_constructor(loader, node):
  20. value = loader.construct_scalar(node)
  21. env_var, = pattern.match(value).groups()
  22. return os.getenv(env_var)
  23. yaml.add_constructor("!pathex", pathex_constructor, yaml.SafeLoader)
  24. def list_galleries_in(path):
  25. gallery_dirs = [f for f in os.scandir(path) if f.is_dir()]
  26. for gallery_dir in gallery_dirs:
  27. metadata = {}
  28. metadata_path = os.path.join(gallery_dir.path, "metadata.yml")
  29. if os.path.exists(metadata_path):
  30. with open(metadata_path, "r") as metadata_file:
  31. metadata = yaml.safe_load(metadata_file)
  32. private = False
  33. url = f"{gallery_dir.name}.html"
  34. output_path = gallery_dir.name
  35. password = metadata.get("password", None)
  36. if password:
  37. private = True
  38. url = f"{gallery_dir.name}-{password}.html"
  39. output_path = f"{output_path}-{password}"
  40. photos = list(list_photos_in(gallery_dir, output_path))
  41. if len(photos) == 0:
  42. continue
  43. photos.sort(key=itemgetter("name"))
  44. # Try to get cover from metadata, if it doesn't exist, take one by
  45. # default.
  46. cover_photo = None
  47. cover_name = metadata.get("cover")
  48. if cover_name:
  49. cover_photo = find_photo(photos, cover_name)
  50. if cover_photo is None:
  51. cover_index = (len(gallery_dir.name) + 42) % len(photos)
  52. cover_photo = photos[cover_index]
  53. gallery = {
  54. "name": metadata.get("name", gallery_dir.name),
  55. "path": gallery_dir.path,
  56. "output_path": output_path,
  57. "url": url,
  58. "num_photos": len(photos),
  59. "photos": photos,
  60. "cover_photo": cover_photo,
  61. "private": private,
  62. }
  63. yield gallery
  64. def find_photo(photos, photo_name):
  65. for photo in photos:
  66. if photo["name"] == photo_name:
  67. return photo
  68. return None
  69. def list_photos_in(gallery_dir, gallery_output_path):
  70. photo_files = [
  71. f for f in os.scandir(gallery_dir) if re.match(".+\.jpg", f.name, re.I)
  72. ]
  73. for photo_file in photo_files:
  74. url = os.path.join(gallery_output_path, photo_file.name)
  75. thumb_url = os.path.join(gallery_output_path, f"thumb_{photo_file.name}")
  76. photo = {
  77. "name": photo_file.name,
  78. "path": photo_file.path,
  79. "url": url,
  80. "thumb_url": thumb_url,
  81. }
  82. yield photo
  83. def generate_output_dir():
  84. output_path = os.path.join(os.curdir, OUTPUT_DIR_NAME)
  85. if not os.path.isdir(output_path):
  86. os.mkdir(output_path)
  87. return output_path
  88. def generate_style(output_path):
  89. style_path = os.path.join(os.curdir, THEME, "style")
  90. style_output_path = os.path.join(output_path, "style")
  91. if os.path.isdir(style_output_path):
  92. shutil.rmtree(style_output_path)
  93. shutil.copytree(style_path, style_output_path)
  94. def generate_scripts(output_path):
  95. scripts_path = os.path.join(os.curdir, THEME, "scripts")
  96. scripts_output_path = os.path.join(output_path, "scripts")
  97. if os.path.isdir(scripts_output_path):
  98. shutil.rmtree(scripts_output_path)
  99. shutil.copytree(scripts_path, scripts_output_path)
  100. def generate_index(output_path, galleries):
  101. index_path = os.path.join(output_path, "index.html")
  102. theme_path = os.path.join(os.curdir, THEME)
  103. jinja_env = Environment(
  104. loader=PackageLoader("boop", theme_path), autoescape=select_autoescape(["html"])
  105. )
  106. index_template = jinja_env.get_template("index.html.j2")
  107. with open(index_path, "w") as index_file:
  108. index_file.write(
  109. index_template.render(
  110. galleries=galleries,
  111. site_title=SITE_TITLE,
  112. site_author=SITE_AUTHOR,
  113. site_author_website=SITE_AUTHOR_WEBSITE,
  114. )
  115. )
  116. def generate_gallery(output_path, gallery):
  117. generate_gallery_index(output_path, gallery)
  118. generate_gallery_photos(output_path, gallery)
  119. def generate_gallery_index(output_path, gallery):
  120. gallery_index_path = os.path.join(output_path, gallery["url"])
  121. theme_path = os.path.join(os.curdir, THEME)
  122. jinja_env = Environment(
  123. loader=PackageLoader("boop", theme_path), autoescape=select_autoescape(["html"])
  124. )
  125. gallery_template = jinja_env.get_template("gallery.html.j2")
  126. with open(gallery_index_path, "w") as gallery_file:
  127. gallery_file.write(
  128. gallery_template.render(
  129. gallery=gallery,
  130. site_title=SITE_TITLE,
  131. site_author=SITE_AUTHOR,
  132. site_author_website=SITE_AUTHOR_WEBSITE,
  133. )
  134. )
  135. def generate_gallery_photos(output_path, gallery):
  136. gallery_output_path = os.path.join(output_path, gallery["output_path"])
  137. if not os.path.isdir(gallery_output_path):
  138. os.mkdir(gallery_output_path)
  139. for photo in gallery["photos"]:
  140. photo_output_path = os.path.join(output_path, photo["url"])
  141. if not os.path.exists(photo_output_path):
  142. shutil.copyfile(photo["path"], photo_output_path)
  143. thumb_output_path = os.path.join(output_path, photo["thumb_url"])
  144. if not os.path.exists(thumb_output_path):
  145. generate_thumb_file(thumb_output_path, photo)
  146. def generate_thumb_file(output_path, photo):
  147. orientation_key = get_orientation_exif_key()
  148. size = (440, 264)
  149. with Image.open(photo["path"]) as image:
  150. # First, make sure image is correctly oriented
  151. exif = image._getexif()
  152. if exif[orientation_key] == 3:
  153. image = image.rotate(180, expand=True)
  154. elif exif[orientation_key] == 6:
  155. image = image.rotate(270, expand=True)
  156. elif exif[orientation_key] == 8:
  157. image = image.rotate(90, expand=True)
  158. w, h = image.size
  159. if w > size[0] and h > size[1]:
  160. # If the original file is larger in width AND height, we resize
  161. # first the image to the lowest size accepted (both width and
  162. # height stays greater or equal to requested size).
  163. # E.g. 1200x900 is resized to 440x330
  164. # 1200x600 is resized to 528x264
  165. if size[0] / size[1] <= w / h:
  166. w = int(max(size[1] * w / h, 1))
  167. h = 264
  168. else:
  169. h = int(max(size[0] * h / w, 1))
  170. w = 440
  171. new_size = (w, h)
  172. image.draft(None, new_size)
  173. image = image.resize(new_size, Image.BICUBIC)
  174. # We now have an image with at least w = 440 OR h = 264 (unless one of
  175. # the size is smaller). But the image can still be larger than
  176. # requested size, so we have to crop the image in the middle.
  177. crop_box = None
  178. if w > size[0]:
  179. left = (w - size[0]) / 2
  180. right = left + size[0]
  181. crop_box = (left, 0, right, h)
  182. elif h > size[1]:
  183. upper = (h - size[1]) / 2
  184. lower = upper + size[1]
  185. crop_box = (0, upper, w, lower)
  186. if crop_box is not None:
  187. image = image.crop(crop_box)
  188. # And we save the final image.
  189. image.save(output_path)
  190. def get_orientation_exif_key():
  191. for (key, tag) in ExifTags.TAGS.items():
  192. if tag == "Orientation":
  193. return key
  194. def main():
  195. print("Loading galleries... ", end="")
  196. pictures_folder = os.path.join(os.curdir, PICTURES_DIR_NAME)
  197. galleries = list(list_galleries_in(pictures_folder))
  198. if len(galleries) == 0:
  199. print(f"No galleries found in {pictures_folder}")
  200. return
  201. galleries.sort(key=itemgetter("name"))
  202. print(f"{len(galleries)} galleries found.")
  203. print("Generating output folder... ", end="")
  204. output_path = generate_output_dir()
  205. print("✔️")
  206. print("Generating style folder... ", end="")
  207. generate_style(output_path)
  208. print("✔️")
  209. print("Generating scripts folder... ", end="")
  210. generate_scripts(output_path)
  211. print("✔️")
  212. print("Generating index file... ", end="")
  213. generate_index(output_path, galleries)
  214. print("✔️")
  215. for gallery in galleries:
  216. print(f"Generating {gallery['name']} gallery ({gallery['url']})... ", end="")
  217. generate_gallery(output_path, gallery)
  218. print("✔️")
  219. print("Galleries generated 🎉")
  220. if __name__ == "__main__":
  221. main()