This is a gentle fork from https://framagit.org/marienfressinaud/photos.marienfressinaud.fr with a responsive and optimized mindset. https://media.larlet.fr/
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

revelateur.py 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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, metadata, 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_metadata, 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. metadata = gallery_metadata.get(name, {})
  73. page_url = f"{gallery_slug}-{name}.html"
  74. photo = {
  75. "gallery_slug": gallery_slug,
  76. "name": name,
  77. "path": photo_file.path,
  78. "url": Path(gallery_slug) / photo_file.name,
  79. "page_url": page_url,
  80. "width": width,
  81. "height": height,
  82. "narrow": height > width,
  83. "wide": height * 2 < width,
  84. "alt": metadata.get("alt", ""),
  85. }
  86. yield photo
  87. def create_output_dir():
  88. if not OUTPUT_DIR.exists():
  89. os.mkdir(OUTPUT_DIR)
  90. def copy_theme_folder(name, target=None):
  91. local_path = THEME_DIR / name
  92. output_path = OUTPUT_DIR / (target if target is not None else name)
  93. if output_path.exists() and target is None:
  94. shutil.rmtree(output_path)
  95. shutil.copytree(local_path, output_path, dirs_exist_ok=True)
  96. def generate_index(output_path, galleries):
  97. index_template = jinja_env.get_template("index.html.j2")
  98. content = index_template.render(galleries=galleries, sizes=SIZES)
  99. (OUTPUT_DIR / "index.html").write_text(content)
  100. def generate_gallery(output_path, gallery):
  101. generate_gallery_photos(output_path, gallery)
  102. generate_gallery_pages(output_path, gallery)
  103. def generate_gallery_pages(output_path, gallery):
  104. photo_template = jinja_env.get_template("media.html.j2")
  105. for previous, photo, next_ in neighborhood(gallery["photos"]):
  106. content = photo_template.render(
  107. photo=photo, previous=previous, next=next_, gallery=gallery, sizes=SIZES
  108. )
  109. (OUTPUT_DIR / photo["page_url"]).write_text(content)
  110. def generate_gallery_photos(output_path, gallery):
  111. gallery_output_path_jpg = output_path / gallery["slug"] / "jpg"
  112. gallery_output_path_webp = output_path / gallery["slug"] / "webp"
  113. os.makedirs(gallery_output_path_jpg, exist_ok=True)
  114. os.makedirs(gallery_output_path_webp, exist_ok=True)
  115. for photo in gallery["photos"]:
  116. # Let's not share the original file for now.
  117. # photo_output_path = output_path / photo["url"]
  118. # if not photo_output_path.exists():
  119. # shutil.copyfile(photo["path"], photo_output_path)
  120. for width, height in SIZES:
  121. jpg_output_path = generate_jpg_photo_for_size(
  122. gallery_output_path_jpg, photo, width, height
  123. )
  124. generate_webp_photo_for_size(
  125. gallery_output_path_webp, jpg_output_path, photo, width, height
  126. )
  127. def generate_webp_photo_for_size(
  128. gallery_output_path, jpg_output_path, photo, width, height
  129. ):
  130. output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.webp"
  131. if output_path.exists():
  132. return
  133. command = ["cwebp", "-q", "80", jpg_output_path, "-o", output_path]
  134. subprocess.Popen(command, stdout=FNULL, stderr=subprocess.STDOUT).communicate()
  135. def generate_jpg_photo_for_size(gallery_output_path, photo, width, height):
  136. output_path = gallery_output_path / f"{photo['name']}_{width}x{height}.jpg"
  137. if output_path.exists():
  138. return output_path
  139. with Image.open(photo["path"]) as image:
  140. w, h = image.size
  141. if w > width and h > height:
  142. # If the original file is larger in width AND height, we resize
  143. # first the image to the lowest size accepted (both width and
  144. # height stays greater or equal to requested size).
  145. # E.g. in case of (440, 264):
  146. # 1200x900 is resized to 440x330
  147. # 1200x600 is resized to 528x264
  148. if width / height <= w / h:
  149. w = int(max(height * w / h, 1))
  150. h = height
  151. else:
  152. h = int(max(width * h / w, 1))
  153. w = width
  154. new_size = (w, h)
  155. image.draft(None, new_size)
  156. image = image.resize(new_size, Image.BICUBIC)
  157. image.save(output_path)
  158. return output_path
  159. def main():
  160. print("Loading galleries... ", end="")
  161. galleries = list(list_galleries_in(PICTURES_DIR))
  162. if not galleries:
  163. print(f"No galleries found in {PICTURES_DIR}")
  164. return
  165. galleries.sort(key=itemgetter("path"), reverse=True)
  166. print(f"{len(galleries)} galleries found.")
  167. print("Creating output folder... ", end="")
  168. create_output_dir()
  169. print("✔️")
  170. print("Copying style folder... ", end="")
  171. copy_theme_folder("style")
  172. print("✔️")
  173. print("Copying scripts folder... ", end="")
  174. copy_theme_folder("scripts")
  175. print("✔️")
  176. print("Copying root (favicon, etc) folder... ", end="")
  177. copy_theme_folder("root", "")
  178. print("✔️")
  179. print("Generating index file... ", end="")
  180. generate_index(OUTPUT_DIR, galleries)
  181. print("✔️")
  182. for gallery in galleries:
  183. print(
  184. (
  185. f"Generating {gallery['name']} gallery "
  186. f"(http://localhost:8080/{gallery['cover_photo']['page_url']})... "
  187. ),
  188. end="",
  189. )
  190. generate_gallery(OUTPUT_DIR, gallery)
  191. print("✔️")
  192. print("Galleries generated 🎉")
  193. if __name__ == "__main__":
  194. main()