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 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. #!/usr/bin/env python3
  2. import os
  3. import shutil
  4. import re
  5. import yaml
  6. from operator import attrgetter, itemgetter
  7. from pathlib import Path
  8. from jinja2 import Environment, PackageLoader, select_autoescape
  9. from PIL import Image
  10. from configuration import OUTPUT_DIR, PICTURES_DIR, SIZES, THEME_DIR
  11. jinja_env = Environment(
  12. loader=PackageLoader("revelateur", THEME_DIR),
  13. autoescape=select_autoescape(["html"]),
  14. )
  15. def neighborhood(iterable, first=None, last=None):
  16. """
  17. Yield the (previous, current, next) items given an iterable.
  18. You can specify a `first` and/or `last` item for bounds.
  19. """
  20. iterator = iter(iterable)
  21. previous = first
  22. current = next(iterator) # Throws StopIteration if empty.
  23. for next_ in iterator:
  24. yield (previous, current, next_)
  25. previous = current
  26. current = next_
  27. yield (previous, current, last)
  28. def list_galleries_in(path):
  29. gallery_dirs = [f for f in os.scandir(path) if f.is_dir()]
  30. for gallery_dir in gallery_dirs:
  31. gallery_path = Path(gallery_dir.path)
  32. metadata = {}
  33. metadata_path = gallery_path / "metadata.yml"
  34. if metadata_path.exists():
  35. metadata = yaml.safe_load(metadata_path.read_text())
  36. private = False
  37. slug = gallery_dir.name
  38. password = metadata.get("password", None)
  39. if password:
  40. private = True
  41. slug = f"{slug}-{password}"
  42. photos = list(list_photos_in(gallery_dir, metadata, slug))
  43. if not photos:
  44. continue
  45. photos.sort(key=itemgetter("name"))
  46. cover_photo = find_photo(photos, metadata.get("cover", "01"))
  47. gallery = {
  48. "name": metadata.get("name", gallery_dir.name),
  49. "type": metadata.get("type", "photo"),
  50. "path": gallery_path,
  51. "slug": slug,
  52. "photos": photos,
  53. "cover_photo": cover_photo,
  54. "private": private,
  55. }
  56. yield gallery
  57. def find_photo(photos, photo_name):
  58. for photo in photos:
  59. if photo["name"] == photo_name:
  60. return photo
  61. return None
  62. def list_photos_in(gallery_dir, gallery_metadata, gallery_slug):
  63. photo_files = [
  64. f for f in os.scandir(gallery_dir) if re.match(".+\.jpg", f.name, re.I)
  65. ]
  66. for photo_file in sorted(photo_files, key=attrgetter("name")):
  67. with Image.open(photo_file.path) as image:
  68. width, height = image.size
  69. name = Path(photo_file.name).stem
  70. metadata = gallery_metadata.get(name, {})
  71. page_url = f"{gallery_slug}-{name}.html"
  72. photo = {
  73. "gallery_slug": gallery_slug,
  74. "name": name,
  75. "path": photo_file.path,
  76. "url": Path(gallery_slug) / photo_file.name,
  77. "page_url": page_url,
  78. "width": width,
  79. "height": height,
  80. "narrow": height > width,
  81. "wide": height * 2 < width,
  82. "alt": metadata.get("alt", ""),
  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. image = Image.open(jpg_output_path)
  132. image.save(output_path, format="webp", icc_profile=image.info.get("icc_profile"))
  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. image = Image.open(photo["path"])
  138. image.thumbnail((width, height), resample=Image.LANCZOS)
  139. image.save(output_path, icc_profile=image.info.get("icc_profile"))
  140. return output_path
  141. def main():
  142. print("Loading galleries... ", end="")
  143. galleries = list(list_galleries_in(PICTURES_DIR))
  144. if not galleries:
  145. print(f"No galleries found in {PICTURES_DIR}")
  146. return
  147. galleries.sort(key=itemgetter("path"), reverse=True)
  148. print(f"{len(galleries)} galleries found.")
  149. print("Creating output folder... ", end="")
  150. create_output_dir()
  151. print("✔️")
  152. print("Copying style folder... ", end="")
  153. copy_theme_folder("style")
  154. print("✔️")
  155. print("Copying scripts folder... ", end="")
  156. copy_theme_folder("scripts")
  157. print("✔️")
  158. print("Copying root (favicon, etc) folder... ", end="")
  159. copy_theme_folder("root", "")
  160. print("✔️")
  161. print("Generating index file... ", end="")
  162. generate_index(OUTPUT_DIR, galleries)
  163. print("✔️")
  164. for gallery in galleries:
  165. print(
  166. (
  167. f"Generating {gallery['name']} gallery "
  168. f"(http://localhost:8080/{gallery['cover_photo']['page_url']})... "
  169. ),
  170. end="",
  171. )
  172. generate_gallery(OUTPUT_DIR, gallery)
  173. print("✔️")
  174. print("Galleries generated 🎉")
  175. if __name__ == "__main__":
  176. main()