create_lightgallery.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #!/usr/bin/python3
  2. import datetime
  3. import os
  4. import pyexiv2
  5. import random
  6. import re
  7. import subprocess
  8. import sys
  9. import traceback
  10. from PIL import Image, ImageOps
  11. from configparser import ConfigParser
  12. from optparse import OptionParser
  13. from os.path import isdir, isfile, join, getmtime
  14. from pathlib import Path
  15. from string import Template
  16. __all__ = []
  17. __version__ = 0.1
  18. __date__ = '2021-12-27'
  19. __updated__ = '2021-12-27'
  20. DEBUG = 0
  21. TESTRUN = 0
  22. PROFILE = 0
  23. thumbsize = (202, 202)
  24. thumbsize = (250, 250)
  25. html = Template("""<html>
  26. <head>
  27. <title>$title</title>
  28. <link type="text/css" rel="stylesheet" href="/res/css/lightgallery-bundle.css" />
  29. <link type="text/css" rel="stylesheet" href="/res/css/lg-zoom.css" />
  30. <link type="text/css" rel="stylesheet" href="/res/css/lg-thumbnail.css" />
  31. <link type="text/css" rel="stylesheet" href="/res/css/bootstrap.min.css" />
  32. <link type="text/css" rel="stylesheet" href="/res/user.css" />
  33. <script src="/res/js/lightgallery.min.js"></script>
  34. <script src="/res/plugins/hash/lg-hash.min.js"></script>
  35. <script src="/res/plugins/fullscreen/lg-fullscreen.min.js"></script>
  36. <script src="/res/plugins/autoplay/lg-autoplay.min.js"></script>
  37. <script src="/res/plugins/thumbnail/lg-thumbnail.min.js"></script>
  38. <script src="/res/plugins/zoom/lg-zoom.min.js"></script>
  39. <script src="/res/js/zipfile_creation.js" defer></script>
  40. <style>
  41. #header {
  42. background-image: url("header/header.jpg");
  43. }
  44. </style>
  45. </head>
  46. <body>
  47. <div id="header">
  48. <h1>$title</h1>
  49. <div id="nav">$navigation</div>
  50. <div id="zip">
  51. <button id="startButton" style="width:100%" data-dir2zip="$dir_to_zip">Zipfile erstellen</button><br />
  52. <div id="zipstatus" style="width:100%; text-align:center;"></div>
  53. </div>
  54. </div>
  55. <div id="outer_lightgallery">
  56. <div id="lightgallery">
  57. $images
  58. </div>
  59. </div>
  60. <script type="text/javascript">
  61. lightGallery(document.getElementById('lightgallery'), {
  62. plugins: [lgZoom, lgThumbnail, lgAutoplay, lgFullscreen ],
  63. //speed: 500,
  64. //mode: 'fade',
  65. licenseKey: '0000-0000-000-0000'
  66. });
  67. </script>
  68. </body>
  69. </html>
  70. """)
  71. def find_file_upwards(filename, max_levels=4):
  72. current_dir = os.getcwd()
  73. for level in range(max_levels + 1): # +1 to include current directory
  74. file_path = os.path.join(current_dir, "res", "js", filename)
  75. if os.path.isfile(file_path):
  76. return current_dir, level
  77. parent_dir = os.path.dirname(current_dir)
  78. if parent_dir == current_dir: # Reached root directory
  79. break
  80. current_dir = parent_dir
  81. return None, None
  82. def get_updirs_to_webroot(filename):
  83. try:
  84. result_dir, levels = find_file_upwards(filename)
  85. path_components = []
  86. current = os.getcwd()
  87. for _ in range(levels):
  88. path_components.insert(0, os.path.basename(current))
  89. current = os.path.dirname(current)
  90. return "/"+"/".join(path_components)
  91. except:
  92. print(f"could not upwards find {filename}")
  93. def write_command_to_history(max_entries=20):
  94. # Define the history file path
  95. history_file = os.path.join(os.getcwd(), '.history')
  96. # Get the current timestamp
  97. timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  98. # Function to check if a string contains special characters
  99. def contains_special_chars(s):
  100. special_chars = r'[^a-zA-Z0-9_\-\.\/]'
  101. return bool(re.search(special_chars, s))
  102. # Quote each argument if it contains special characters
  103. quoted_args = []
  104. for arg in sys.argv[1:]:
  105. if contains_special_chars(arg):
  106. quoted_args.append(f'"{arg}"')
  107. else:
  108. quoted_args.append(arg)
  109. # Construct the full command with script name and quoted arguments
  110. full_command = f"{sys.argv[0]} {' '.join(quoted_args)}"
  111. # Read existing entries
  112. try:
  113. with open(history_file, 'r') as f:
  114. lines = f.readlines()
  115. except FileNotFoundError:
  116. lines = []
  117. # Append the new entry
  118. new_entry = f"{full_command} # {timestamp}\n"
  119. lines.append(new_entry)
  120. # Truncate to max_entries
  121. lines = lines[-max_entries:]
  122. # Write the updated entries back to the file
  123. with open(history_file, 'w') as f:
  124. f.writelines(lines)
  125. # Priorität der Auswertung
  126. # 1. Exifdaten
  127. def get_caption(jpg):
  128. metadata = pyexiv2.ImageMetadata(jpg)
  129. metadata.read()
  130. tags = ''
  131. indextags = ''
  132. year = ''
  133. try:
  134. year = re.sub(':\d\d:\d\d\s\d\d:\d\d:\d\d', '', metadata['Exif.Image.DateTimeOriginal'].raw_value)
  135. tags = str(year)
  136. indextags = 'year|' + year
  137. except:
  138. pass
  139. try:
  140. tags = tags + ', ' + metadata['Xmp.dc.title'].value["x-default"]
  141. indextags = indextags + ',' + 'title|' + metadata['Xmp.dc.title'].value["x-default"].replace(',','')
  142. except:
  143. pass
  144. try:
  145. tags = tags + ', ' + metadata['Xmp.dc.description'].value["x-default"]
  146. indextags = indextags + ',' + 'description|' + metadata['Xmp.dc.description'].value["x-default"].replace(',','')
  147. except:
  148. pass
  149. try:
  150. for l in metadata['Xmp.lr.hierarchicalSubject'].raw_value:
  151. tags = tags + ', ' + l.split('|')[-1] # last element of list
  152. indextags = indextags + ',' + l.split('|')[0] + '|' + l.split('|')[-1] # last element of list
  153. except:
  154. pass
  155. return(re.sub('^,\s', '', tags), re.sub('^,\s*', '', indextags), )
  156. def create_header(jpeglist):
  157. header = random.choice(jpeglist)
  158. if not os.path.exists('header'):
  159. os.makedirs('header')
  160. xsize = '2000'
  161. cmd = ['convert', '-quiet', header, '-thumbnail', xsize + 'x400^', '-gravity', 'center', '-extent', xsize + 'x400', "header/header.jpg"]
  162. subprocess.call(cmd, shell=False)
  163. def getimagesize(jpg):
  164. jpg = str(jpg)
  165. im = Image.open(jpg)
  166. width, height = im.size
  167. return(f"data-lg-size=\"{width}-{height}\"")
  168. def create_nav_link(link, text):
  169. if link == None:
  170. return('')
  171. link = str(link)
  172. if link.find(' ') > -1:
  173. link, text = link.split(' ', 1)
  174. return(f"<a href='{link}'>{text}</a>")
  175. def create_forward_link(link, text):
  176. if link == None:
  177. return('')
  178. link = str(link)
  179. if link.find(' ') > -1:
  180. link, text = link.split(' ', 1)
  181. return(f"<a href='{link}'>&gt;&gt;&nbsp;{text}</a>")
  182. def create_backward_link(link, text):
  183. if link == None:
  184. return('')
  185. link = str(link)
  186. if link.find(' ') > -1:
  187. link, text = link.split(' ', 1)
  188. return(f"<a href='{link}'>{text}&nbsp;&lt;&lt;</a>")
  189. def read_title_file():
  190. """ Wenn im aktuellen Verzeichnis eine Datei _title existiert, gib ihren Inhalt zurück"""
  191. try:
  192. with open('_title', 'r') as titlefile:
  193. return(titlefile.read())
  194. except:
  195. return None
  196. def makethumb(jpg):
  197. if not os.path.isdir('_thumbnails'):
  198. os.mkdir('_thumbnails');
  199. if not os.path.isfile(join('_thumbnails', jpg)):
  200. image = Image.open(jpg)
  201. # thumb = ImageOps.fit(image, thumbsize, Image.ANTIALIAS)
  202. # thumb.save(join('_thumbnails', jpg))
  203. image.thumbnail(thumbsize, Image.Resampling.LANCZOS)
  204. image.save(join('_thumbnails', jpg))
  205. else:
  206. if getmtime(jpg) > getmtime(join('_thumbnails', jpg)):
  207. image = Image.open(jpg)
  208. image.thumbnail(thumbsize, Image.Resampling.LANCZOS)
  209. image.save(join('_thumbnails', jpg))
  210. def read_config(opts):
  211. try:
  212. config_object = ConfigParser()
  213. config_object.read( "." + os.path.splitext(os.path.basename(__file__))[0] )
  214. ini = dict(config_object.items('default'))
  215. for key in ini:
  216. if not opts.__dict__[key]:
  217. opts.__dict__[key] = ini[key]
  218. except:
  219. pass
  220. def write_config(opts):
  221. ini = {}
  222. for key in opts.__dict__:
  223. value = opts.__dict__[key]
  224. if value is None:
  225. pass
  226. elif key == 'verbose':
  227. pass
  228. else:
  229. ini[key] = str(value)
  230. config_object = ConfigParser()
  231. config_object.read_dict({'default' : ini})
  232. with open("." + os.path.splitext(os.path.basename(__file__))[0], 'w') as conf:
  233. config_object.write(conf)
  234. def create_index_html(indexdir, opts):
  235. startdir = os.getcwd()
  236. os.chdir(indexdir)
  237. write_command_to_history()
  238. read_config(opts)
  239. jpegs = []
  240. jpeglist = []
  241. valid_images = [".jpg",".JPG"]
  242. dirlist = os.listdir('.')
  243. dirlist.sort()
  244. for jpg in dirlist:
  245. ext = os.path.splitext(jpg)[1]
  246. if ext.lower() not in valid_images:
  247. continue
  248. makethumb(jpg);
  249. jpg = str(jpg)
  250. jpeglist.append(jpg)
  251. thumb = jpg
  252. caption, indextags = get_caption(jpg)
  253. sizestring = getimagesize(jpg)
  254. jpegs.append(f""" <a href="{jpg}" {sizestring} data-sub-html="{caption}" data_index="{indextags}">
  255. <img src="_thumbnails/{thumb}" />
  256. </a>""")
  257. images = "\n".join(jpegs)
  258. create_header(jpeglist)
  259. forward = create_forward_link(opts.forward, 'weiter')
  260. backward = create_backward_link(opts.backward, 'zurück')
  261. up = create_nav_link('..', 'Index')
  262. title = read_title_file() or opts.title or os.path.basename(os.path.normpath(os.getcwd()))
  263. with open('index.html', 'w') as f:
  264. f.write(html.substitute(images=images, title=title, navigation='&nbsp;&nbsp;&nbsp;&nbsp;'.join([backward, up, forward]), dir_to_zip=get_updirs_to_webroot("lightgallery.min.js") ))
  265. write_config(opts)
  266. os.chdir(startdir)
  267. def main(argv=None):
  268. '''Command line options.'''
  269. program_name = os.path.basename(sys.argv[0])
  270. program_version = "v0.1"
  271. program_build_date = "%s" % __updated__
  272. program_version_string = '%%prog %s (%s)' % (program_version, program_build_date)
  273. program_longdesc = '''create index.html for lightgallery in a directory of jpegs'''
  274. program_license = "Copyright 2021 Markus Spring (markus-spring.info). \
  275. Licensed under the Apache License 2.0\nhttp://www.apache.org/licenses/LICENSE-2.0"
  276. if argv is None:
  277. argv = sys.argv[1:]
  278. try:
  279. # setup option parser
  280. parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
  281. # parser.add_option("-i", "--in", dest="infile", help="set input path [default: %default]", metavar="FILE")
  282. # parser.add_option("-o", "--out", dest="outfile", help="set output path [default: %default]", metavar="FILE")
  283. parser.add_option("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %default]")
  284. parser.add_option("-t", "--title", action="store", dest="title", help="set gallery title [default: %default]")
  285. parser.add_option("-f", "--forward", action="store", dest="forward", help="forward url")
  286. parser.add_option("-b", "--backward", action="store", dest="backward", help="backward url")
  287. parser.add_option("-u", "--up", action="store", dest="up", help="up url")
  288. parser.set_defaults( verbose=0 )
  289. (opts, args) = parser.parse_args(argv)
  290. if opts.verbose > 0:
  291. print(("verbosity level = %d" % opts.verbose))
  292. # if opts.title:
  293. # print(("title = %s" % opts.title))
  294. if len(args) < 1:
  295. parser.error("directory argument missing")
  296. # MAIN BODY #
  297. create_index_html(args[0], opts)
  298. except BaseException as ex:
  299. ex_type, ex_value, ex_traceback = sys.exc_info() # Get current system exception
  300. trace_back = traceback.extract_tb(ex_traceback) # Extract unformatter stack traces as tuples
  301. stack_trace = list() # Format stacktrace
  302. for trace in trace_back:
  303. stack_trace.append("File : %s , Line : %d, Func.Name : %s, Message : %s" % (trace[0], trace[1], trace[2], trace[3]))
  304. print("Exception type : %s " % ex_type.__name__)
  305. print("Exception message : %s" %ex_value)
  306. print("Stack trace : %s" %stack_trace)
  307. if __name__ == "__main__":
  308. if DEBUG:
  309. sys.argv.append("-h")
  310. if TESTRUN:
  311. import doctest
  312. doctest.testmod()
  313. if PROFILE:
  314. import cProfile
  315. import pstats
  316. profile_filename = 'config_reader.config_reader_profile.txt'
  317. cProfile.run('main()', profile_filename)
  318. statsfile = open("profile_stats.txt", "wb")
  319. p = pstats.Stats(profile_filename, stream=statsfile)
  320. stats = p.strip_dirs().sort_stats('cumulative')
  321. stats.print_stats()
  322. statsfile.close()
  323. sys.exit(0)
  324. sys.exit(main())
  325. # Local Variables:
  326. # compile-command: "python3 /home/springm/projekte/python/lightgallery/create_lightgallery.py -t '1958-1963' -f '../002_1960-1962 1960-1962' -u.. /media/vaters_dias/001_1958-1963"
  327. # End: