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.

revelateur.py 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. #!/usr/bin/env python3
  2. import os
  3. import shutil
  4. import re
  5. import subprocess
  6. import yaml
  7. from operator import attrgetter, itemgetter
  8. from pathlib import Path
  9. from jinja2 import Environment, PackageLoader, select_autoescape
  10. from PIL import Image
  11. from configuration import OUTPUT_DIR, PICTURES_DIR, SIZES, THEME_DIR
  12. FNULL = open(os.devnull, "w")
  13. jinja_env = Environment(
  14. loader=PackageLoader("revelateur", THEME_DIR),
  15. autoescape=select_autoescape(["html"]),
  16. )
  17. def neighborhood(iterable, first=None, last=None):
  18. """
  19. Yield the (previous, current, next) items given an iterable.
  20. You can specify a `first` and/or `last` item for bounds.
  21. """
  22. iterator = iter(iterable)
  23. previous = first
  24. current = next(iterator) # Throws StopIteration if empty.
  25. for next_ in iterator:
  26. yield (previous, current, next_)
  27. previous = current
  28. current = next_
  29. yield (previous, current, last)
  30. def list_galleries_in(path):
  31. gallery_dirs = [f for f in os.scandir(path) if f.is_dir()]
  32. for gallery_dir in gallery_dirs:
  33. gallery_path = Path(gallery_dir.path)
  34. metadata = {}
  35. metadata_path = gallery_path / "metadata.yml"
  36. if metadata_path.exists():
  37. metadata = yaml.safe_load(metadata_path.read_text())
  38. private = False
  39. slug = gallery_dir.name
  40. password = metadata.get("password", None)
  41. if password:
  42. private = True
  43. slug = f"{slug}-{password}"
  44. photos = list(list_photos_in(gallery_dir, slug))
  45. if not photos:
  46. continue
  47. photos.sort(key=itemgetter("name"))
  48. cover_photo = find_photo(photos, metadata.get("cover", "01"))
  49. gallery = {
  50. "name": metadata.get("name", gallery_dir.name),
  51. "type": metadata.get("type", "photo"),
  52. "path": gallery_path,
  53. "slug": slug,
  54. "photos": photos,
  55. "cover_photo": cover_photo,
  56. "private": private,
  57. }
  58. yield gallery
  59. def find_photo(photos, photo_name):
  60. for photo in photos:
  61. if photo["name"] == photo_name:
  62. return photo
  63. return None
  64. def list_photos_in(gallery_dir, gallery_slug):
  65. photo_files = [
  66. f for f in os.scandir(gallery_dir) if re.match(".+\.jpg", f.name, re.I)
  67. ]
  68. for photo_file in sorted(photo_files, key=attrgetter("name")):
  69. with Image.open(photo_file.path) as image:
  70. width, height = image.size
  71. name = Path(photo_file.name).stem
  72. page_url = f"{gallery_slug}-{name}.html"
  73. photo = {
  74. "gallery_slug": gallery_slug,
  75. "name": name,
  76. "path": photo_file.path,
  77. "url": Path(gallery_slug) / photo_file.name,
  78. "page_url": page_url,
  79. "width": width,
  80. "height": height,
  81. "narrow": height > width,
  82. "wide": height * 2 < width,
  83. }
  84. yield photo
  85. def create_output_dir():
  86. if not OUTPUT_DIR.exists():
  87. os.mkdir(OUTPUT_DIR)
  88. def copy_theme_folder(name, target=None):
  89. local_path = THEME_DIR / name
  90. output_path = OUTPUT_DIR / (target if target is not None else name)
  91. if output_path.exists() and target is None:
  92. shutil.rmtree(output_path)
  93. shutil.copytree(local_path, output_path, dirs_exist_ok=True)
  94. def generate_index(output_path, galleries):
  95. index_template = jinja_env.get_template("index.html.j2")
  96. content = index_template.render(galleries=galleries, sizes=SIZES)
  97. (OUTPUT_DIR / "index.html").write_text(content)
  98. def generate_gallery(output_path, gallery):
  99. generate_gallery_photos(output_path, gallery)
  100. generate_gallery_pages(output_path, gallery)
  101. def generate_gallery_pages(output_path, gallery):
  102. photo_template = jinja_env.get_template("media.html.j2")
  103. for previous, photo, next_ in neighborhood(gallery["photos"]):
  104. content = photo_template.render(
  105. photo=photo, previous=previous, next=next_, gallery=gallery, sizes=SIZES
  106. )
  107. (OUTPUT_DIR / photo["page_url"]).write_text(content)
  108. def generate_gallery_photos(output_path, gallery):
  109. gallery_output_path_jpg = output_path / gallery["slug"] / "jpg"
  110. gallery_output_path_webp = output_path / gallery["slug"] / "webp"
  111. os.makedirs(gallery_output_path_jpg, exist_ok=True)
  112. os.makedirs(gallery_output_path_webp, exist_ok=True)
  113. for photo in gallery["photos"]:
  114. # Let's not share the original file for now.
  115. # photo_output_path = output_path / photo["url"]
  116. # if not photo_output_path.exists():
  117. # shutil.copyfile(photo["path"], photo_output_path)
  118. for width, height in SIZES:
  119. jpg_output_path = generate_jpg_photo_for_size(
  120. gallery_output_path_jpg, photo, width, height
  121. )
  122. generate_webp_photo_for_size(
  123. gallery_output_path_webp, jpg_output_path, photo, width, height
  124. )
  125. def generate_webp_photo_for_size(
  126. gallery_output_path, jpg_output_path, photo, width, height
  127. ):
  128. output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.webp"
  129. if output_path.exists():
  130. return
  131. command = ["cwebp", "-q", "80", jpg_output_path, "-o", output_path]
  132. subprocess.Popen(command, stdout=FNULL, stderr=subprocess.STDOUT).communicate()
  133. def generate_jpg_photo_for_size(gallery_output_path, photo, width, height):
  134. output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.jpg"
  135. if output_path.exists():
  136. return output_path
  137. with Image.open(photo["path"]) as image:
  138. w, h = image.size
  139. if w > width and h > height:
  140. # If the original file is larger in width AND height, we resize
  141. # first the image to the lowest size accepted (both width and
  142. # height stays greater or equal to requested size).
  143. # E.g. in case of (440, 264):
  144. # 1200x900 is resized to 440x330
  145. # 1200x600 is resized to 528x264
  146. if width / height <= w / h:
  147. w = int(max(height * w / h, 1))
  148. h = height
  149. else:
  150. h = int(max(width * h / w, 1))
  151. w = width
  152. new_size = (w, h)
  153. image.draft(None, new_size)
  154. image = image.resize(new_size, Image.BICUBIC)
  155. image.save(output_path)
  156. return output_path
  157. def main():
  158. print("Loading galleries... ", end="")
  159. galleries = list(list_galleries_in(PICTURES_DIR))
  160. if not galleries:
  161. print(f"No galleries found in {PICTURES_DIR}")
  162. return
  163. galleries.sort(key=itemgetter("path"), reverse=True)
  164. print(f"{len(galleries)} galleries found.")
  165. print("Creating output folder... ", end="")
  166. create_output_dir()
  167. print("✔️")
  168. print("Copying style folder... ", end="")
  169. copy_theme_folder("style")
  170. print("✔️")
  171. print("Copying scripts folder... ", end="")
  172. copy_theme_folder("scripts")
  173. print("✔️")
  174. print("Copying root (favicon, etc) folder... ", end="")
  175. copy_theme_folder("root", "")
  176. print("✔️")
  177. print("Generating index file... ", end="")
  178. generate_index(OUTPUT_DIR, galleries)
  179. print("✔️")
  180. for gallery in galleries:
  181. print(
  182. f"Generating {gallery['name']} gallery ({gallery['cover_photo']['page_url']})... ",
  183. end="",
  184. )
  185. generate_gallery(OUTPUT_DIR, gallery)
  186. print("✔️")
  187. print("Galleries generated 🎉")
  188. if __name__ == "__main__":
  189. main()