405 lines
12 KiB
Python
Executable File
405 lines
12 KiB
Python
Executable File
import configparser
|
|
import os
|
|
import sys
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from argparse import ArgumentParser
|
|
from mirva import get_version
|
|
|
|
|
|
class Mirva:
|
|
def __init__(self):
|
|
|
|
self.resource_src = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "resources"
|
|
)
|
|
self.resource_dir = ".mirva"
|
|
self.config_file = os.path.join(self.resource_dir, "config.cfg")
|
|
self.config_backup = os.path.join(self.resource_dir, "config.cfg.bkp")
|
|
self.image_match = re.compile(".*\.jpg$|.*\.jpeg$|.*\.png$|.*\.gif$|.*\.tif$", re.I)
|
|
self.video_match = re.compile(".*\.mp4$", re.I)
|
|
self.get_options()
|
|
os.chdir(self.options.folder)
|
|
self.write_resources()
|
|
self.file_list = self.get_files()
|
|
if self.options.config or not os.path.exists(self.config_file):
|
|
self.create_config()
|
|
print("Config created: Exiting without gallery creation. Check config first.")
|
|
return
|
|
self.get_config()
|
|
if self.options.exif:
|
|
self.append_exif()
|
|
self.create_posts()
|
|
self.write_index()
|
|
self.write_mediums()
|
|
print("Gallery written.")
|
|
|
|
def create_config(self):
|
|
|
|
self.config = configparser.ConfigParser()
|
|
self.config.read(self.config_file)
|
|
config_changed = False
|
|
if not "SITE" in self.config:
|
|
self.config["SITE"] = {
|
|
"title": "",
|
|
"sub_title": "",
|
|
"intro": "",
|
|
"image_size": 850,
|
|
}
|
|
config_changed = True
|
|
|
|
for f in self.file_list:
|
|
if not f in self.config:
|
|
title, _ = os.path.splitext(f)
|
|
title = title.replace("_", " ")
|
|
self.config[f] = {"title": title, "description": ""}
|
|
config_changed = True
|
|
|
|
if config_changed:
|
|
self.write_config()
|
|
|
|
def create_posts(self):
|
|
|
|
self.posts = []
|
|
for c in self.config:
|
|
if c in self.file_list:
|
|
post = self.get_post(
|
|
c, self.config[c]["title"], self.config[c]["description"]
|
|
)
|
|
self.posts.append(post)
|
|
|
|
def get_config(self):
|
|
self.config = configparser.ConfigParser()
|
|
self.config.read(self.config_file)
|
|
|
|
def write_config(self):
|
|
print(
|
|
"Modified config: {}".format(
|
|
os.path.join(self.options.folder, self.config_file)
|
|
)
|
|
)
|
|
if os.path.exists(self.config_file):
|
|
with open(self.config_file, "rt") as reader:
|
|
with open(self.config_backup, "wt") as writer:
|
|
writer.write(reader.read())
|
|
|
|
with open(self.config_file, "wt") as fp:
|
|
self.config.write(fp)
|
|
|
|
|
|
def get_files(self):
|
|
image_match = re.compile(".*\.jpg$|.*\.jpeg$|.*\.png$|.*\.gif$|.*\.tif$", re.I)
|
|
files = []
|
|
for f in sorted(os.listdir(".")):
|
|
if f.startswith("."):
|
|
continue
|
|
if not (self.image_match.match(f) or self.video_match.match(f)):
|
|
continue
|
|
files.append(f)
|
|
return files
|
|
|
|
def get_options(self):
|
|
parser = ArgumentParser(
|
|
prog="mirva",
|
|
epilog='Configuration note: item "image_size = [integer]" is the '
|
|
+ "middle sized image max width/height in pixels. It also accepts a special "
|
|
+ 'value "link" to make symbolic links.',
|
|
)
|
|
# parser.add_argument("-v", default=False, action="store_true")
|
|
parser.add_argument(
|
|
"--config", default=False, action="store_true", help="Write config and exit. Required if more images are added."
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
default=False,
|
|
action="store_true",
|
|
help="Force regeneration of middle sized images",
|
|
)
|
|
parser.add_argument(
|
|
"--exif",
|
|
default=False,
|
|
action="store_true",
|
|
help="Append EXIF information to image descriptions",
|
|
)
|
|
parser.add_argument(
|
|
"--version",
|
|
action="version",
|
|
version="%(prog)s {version}".format(version=get_version()),
|
|
)
|
|
parser.add_argument(
|
|
"folder",
|
|
type=str,
|
|
default=".",
|
|
nargs="?",
|
|
help="Folder for gallery",
|
|
)
|
|
self.options = parser.parse_args()
|
|
|
|
def get_index(self, page_title, sub_title, intro, posts):
|
|
|
|
return """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
<!--
|
|
Design by TEMPLATED
|
|
http://templated.co
|
|
Released for free under the Creative Commons Attribution License
|
|
|
|
Name : Green Forest
|
|
Description: A two-column, fixed-width design with dark color scheme.
|
|
Version : 1.0
|
|
Released : 20110306
|
|
|
|
-->
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<meta name="generator" content="Mirva" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
|
<link rel="shortcut icon" href="{resource}/mirva.ico"/>
|
|
<title>{page_title}</title>
|
|
<link href="{resource}/style.css" rel="stylesheet" type="text/css" media="screen" />
|
|
<script>
|
|
function r(f){{/in/.test(document.readyState)?setTimeout('r('+f+')',9):f()}}
|
|
r(function(){{
|
|
create_nav();
|
|
}});
|
|
function create_nav() {{
|
|
let navis = document.getElementsByClassName("navigation");
|
|
for (let i = 0; i<navis.length; i++) {{
|
|
if (i==navis.length-1) {{
|
|
navis[i].appendChild(create_button("up", navis[0]));
|
|
continue;
|
|
}}
|
|
if (navis[i-1]) {{
|
|
navis[i].appendChild(create_button("up", navis[i-1]));
|
|
}} else {{
|
|
navis[i].appendChild(create_button("up", document.body));
|
|
}}
|
|
if (navis[i+1]) {{
|
|
navis[i].appendChild(create_button("down", navis[i+1]));
|
|
}}
|
|
|
|
}}
|
|
}}
|
|
function create_button(direction, to) {{
|
|
let container = document.createElement('div');
|
|
container.classList.add("float_" + direction);
|
|
let button = document.createElement('img');
|
|
button.src = "{resource}/arrow_" + direction + ".png";
|
|
button.classList.add("navigation_button");
|
|
button.onclick = function(){{
|
|
to.parentElement.scrollIntoView({{behavior: "smooth"}});
|
|
}};
|
|
container.appendChild(button);
|
|
return container
|
|
}}
|
|
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="wrapper">
|
|
<div id="header">
|
|
<div id="logo">
|
|
<div id="page_title">
|
|
<h1>{page_title}</h1>
|
|
<p>{sub_title}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- end #header -->
|
|
<div id="page">
|
|
<div id="page-bgtop">
|
|
<div id="page-bgbtm">
|
|
<div id="content">
|
|
<div class="post">
|
|
<div class="entry">
|
|
{intro}
|
|
</div>
|
|
</div>
|
|
{posts}
|
|
|
|
<div style="clear: both;"> </div>
|
|
</div>
|
|
<!-- end #content -->
|
|
<div style="clear: both;"> </div>
|
|
</div>
|
|
</div>
|
|
<!-- end #page -->
|
|
</div>
|
|
</body>
|
|
</html>""".format(
|
|
page_title=page_title,
|
|
sub_title=sub_title,
|
|
intro=intro,
|
|
posts="\n".join(posts),
|
|
resource=self.resource_dir,
|
|
)
|
|
|
|
def get_post(self, image, title, content):
|
|
|
|
if self.video_match.match(image):
|
|
return """
|
|
<div class="post">
|
|
<div class="navigation"> </div>
|
|
<div class=center><a href="{image}">
|
|
<video class=post_image controls >
|
|
<source src="{image}" type="video/mp4" >
|
|
</video>
|
|
</a></div>
|
|
<div class="meta"><div class="name">{title}</div></div>
|
|
<div style="clear: both;"> </div>
|
|
<div class="entry">{content}</div>
|
|
</div>""".format(
|
|
image=image, title=title, content=content
|
|
)
|
|
|
|
return """
|
|
<div class="post">
|
|
<div class="navigation"> </div>
|
|
<div class=center><a href="{image}"><img loading=lazy class=post_image src=".med/{image}.jpg"></a></div>
|
|
<div class="meta"><div class="name">{title}</div></div>
|
|
<div style="clear: both;"> </div>
|
|
<div class="entry">{content}</div>
|
|
</div>""".format(
|
|
image=image, title=title, content=content
|
|
)
|
|
|
|
|
|
def is_created_with_mirva(self):
|
|
|
|
with open("index.html", "rt") as fp:
|
|
for line in fp.readlines():
|
|
if line.strip() == '<meta name="generator" content="Mirva" />':
|
|
return True
|
|
return False
|
|
|
|
def write_index(self):
|
|
|
|
if os.path.exists("index.html"):
|
|
if not self.is_created_with_mirva():
|
|
print("index.html exists, and it's not written with Mirva. Not overwriting.")
|
|
sys.exit(1)
|
|
|
|
with open("index.html", "wt") as fp:
|
|
fp.write(
|
|
self.get_index(
|
|
self.config["SITE"]["title"],
|
|
self.config["SITE"]["sub_title"],
|
|
self.config["SITE"]["intro"],
|
|
self.posts,
|
|
)
|
|
)
|
|
|
|
def write_resources(self):
|
|
|
|
try:
|
|
os.makedirs(self.resource_dir)
|
|
except Exception:
|
|
pass
|
|
|
|
for f in (
|
|
"style.css",
|
|
"arrow_up.png",
|
|
"arrow_down.png",
|
|
"banner.jpg",
|
|
"mirva.ico",
|
|
):
|
|
if os.path.exists(os.path.join(self.resource_dir, f)):
|
|
continue
|
|
shutil.copy(
|
|
os.path.join(self.resource_src, f), os.path.join(self.resource_dir, f)
|
|
)
|
|
|
|
def write_mediums(self):
|
|
|
|
try:
|
|
os.makedirs(".med")
|
|
except Exception:
|
|
pass
|
|
|
|
r = self.config["SITE"].get("image_size", 850)
|
|
link = str(r).lower() == "link"
|
|
if link:
|
|
r = 0
|
|
|
|
res = "{:d}x{:d}>".format(int(r), int(r))
|
|
force = self.options.force
|
|
for f in self.file_list:
|
|
if self.video_match.match(f):
|
|
continue
|
|
outfile = os.path.join(".med", "{}.jpg".format(f))
|
|
if force:
|
|
try:
|
|
os.remove(outfile)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
if not os.path.exists(outfile):
|
|
if link:
|
|
|
|
os.symlink("../{}".format(f), outfile)
|
|
continue
|
|
|
|
convargs = [
|
|
"convert",
|
|
"-define",
|
|
"jpeg:size={}x{}".format(r, r),
|
|
"{}[0]".format(f),
|
|
"-background",
|
|
"white",
|
|
"-flatten",
|
|
"-resize",
|
|
res,
|
|
"-quality",
|
|
"85",
|
|
outfile,
|
|
]
|
|
sys.stdout.write(".")
|
|
sys.stdout.flush()
|
|
subprocess.run(convargs)
|
|
sys.stdout.write("\n")
|
|
|
|
def append_exif(self):
|
|
exif_format = '''
|
|
<ul>
|
|
<li>Date: %[EXIF:DateTimeOriginal]
|
|
<li>Camera: %[EXIF:Make] %[EXIF:Model]
|
|
<li>Parameters: %[EXIF:ExposureTime]s / F%[EXIF:FNumber] / Focal %[EXIF:FocalLength]
|
|
<li>Size: %w x %h / {size}
|
|
</ul>
|
|
'''
|
|
|
|
for f in self.config:
|
|
if f in self.file_list:
|
|
sys.stdout.write(".")
|
|
sys.stdout.flush()
|
|
file_size = human_size(f)
|
|
p = subprocess.run(
|
|
['identify','-format',exif_format.format(size=file_size),"{}[0]".format(f)],
|
|
capture_output = True
|
|
)
|
|
|
|
self.config[f]['description'] += p.stdout.decode('utf-8')
|
|
sys.stdout.write("\n")
|
|
self.write_config()
|
|
|
|
|
|
|
|
def human_size(file_name, precision=1):
|
|
size = os.path.getsize(file_name)
|
|
|
|
if size == None:
|
|
return "nan"
|
|
sign = ""
|
|
if size < 0:
|
|
sign = "-"
|
|
size = -size
|
|
suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]
|
|
suffixIndex = 0
|
|
defPrecision = 0
|
|
while size > 1024:
|
|
suffixIndex += 1
|
|
size = size / 1024.0
|
|
defPrecision = precision
|
|
return "%s%.*f%s" % (sign, defPrecision, size, suffixes[suffixIndex])
|