first version
This commit is contained in:
19
LICENSE.txt
Normal file
19
LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2016 Ville Rantanen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Mirva
|
||||||
|
|
||||||
|
A tool to create a web gallery from a single folder of images.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Usage requires ImageMagick binaries! Make sure you have `convert` in PATH.
|
||||||
|
|
||||||
|
Use pip or similar to install from source.
|
||||||
|
|
||||||
|
ex.
|
||||||
|
|
||||||
|
`pip3 install --user --upgrade https://bitbucket.org/MoonQ/mirva/get/tip.tar.gz`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use the command line tool `mirva` to render web sites.
|
||||||
|
|
||||||
0
mirva/__init__.py
Normal file
0
mirva/__init__.py
Normal file
255
mirva/mirva.py
Executable file
255
mirva/mirva.py
Executable file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
|
||||||
|
class Mirva:
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.resource_src = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "resources"
|
||||||
|
)
|
||||||
|
self.resource_dir = ".template"
|
||||||
|
self.config_file = os.path.join(self.resource_dir, "config")
|
||||||
|
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("Exiting. Check config.")
|
||||||
|
return
|
||||||
|
self.get_config()
|
||||||
|
self.create_posts()
|
||||||
|
self.write_index()
|
||||||
|
self.write_mediums()
|
||||||
|
|
||||||
|
def create_config(self):
|
||||||
|
print("Creating config: {}".format(os.path.join(self.options.folder,self.config_file)))
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(self.config_file)
|
||||||
|
if not "SITE" in config:
|
||||||
|
config["SITE"] = {
|
||||||
|
"title": "",
|
||||||
|
"sub_title": "",
|
||||||
|
"intro": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
for f in self.file_list:
|
||||||
|
if not f in config:
|
||||||
|
config[f] = {"title": f, "description": ""}
|
||||||
|
|
||||||
|
with open(self.config_file, "wt") as fp:
|
||||||
|
config.write(fp)
|
||||||
|
|
||||||
|
def create_posts(self):
|
||||||
|
|
||||||
|
self.posts = []
|
||||||
|
for f in self.file_list:
|
||||||
|
if not f in self.config:
|
||||||
|
post = self.get_post(f, f, "")
|
||||||
|
else:
|
||||||
|
post = self.get_post(
|
||||||
|
f, self.config[f]["title"], self.config[f]["description"]
|
||||||
|
)
|
||||||
|
self.posts.append(post)
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
self.config = configparser.ConfigParser()
|
||||||
|
self.config.read(self.config_file)
|
||||||
|
|
||||||
|
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 image_match.match(f):
|
||||||
|
continue
|
||||||
|
files.append(f)
|
||||||
|
return files
|
||||||
|
|
||||||
|
def get_options(self):
|
||||||
|
parser = ArgumentParser()
|
||||||
|
#parser.add_argument("-v", default=False, action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--config", default=False, action="store_true", help="Write config and exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
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="keywords" content="" />
|
||||||
|
<meta name="description" content="" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<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 (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">
|
||||||
|
<h1 id="page_title">{page_title}</h1>
|
||||||
|
<p>{sub_title}</a></p>
|
||||||
|
</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):
|
||||||
|
|
||||||
|
return """
|
||||||
|
<div class="post">
|
||||||
|
<div class="navigation"></div>
|
||||||
|
<div class=center><a href="{image}"><img class=post_image src=".med/{image}.jpg"></a></div>
|
||||||
|
<p class="meta"><span class="date">{title}</span></p>
|
||||||
|
<div style="clear: both;"> </div>
|
||||||
|
<div class="entry">{content}</div>
|
||||||
|
</div>""".format(
|
||||||
|
image=image, title=title, content=content
|
||||||
|
)
|
||||||
|
|
||||||
|
def write_index(self):
|
||||||
|
|
||||||
|
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"):
|
||||||
|
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
|
||||||
|
|
||||||
|
# TODO: config image res
|
||||||
|
# TODO opts: force recreation
|
||||||
|
r = 850
|
||||||
|
force = False
|
||||||
|
for f in self.file_list:
|
||||||
|
res = "{:d}x{:d}>".format(r, r)
|
||||||
|
outfile = os.path.join(".med", "{}.jpg".format(f))
|
||||||
|
if not os.path.exists(outfile) or force:
|
||||||
|
convargs = [
|
||||||
|
"convert",
|
||||||
|
"-define",
|
||||||
|
"jpeg:size={}x{}".format(r, r),
|
||||||
|
"{}[0]".format(f),
|
||||||
|
"-background",
|
||||||
|
"white",
|
||||||
|
"-flatten",
|
||||||
|
"-resize",
|
||||||
|
res,
|
||||||
|
"-quality",
|
||||||
|
"85",
|
||||||
|
outfile,
|
||||||
|
]
|
||||||
|
subprocess.call(convargs)
|
||||||
|
|
||||||
|
|
||||||
BIN
mirva/resources/arrow_down.png
Normal file
BIN
mirva/resources/arrow_down.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
mirva/resources/arrow_up.png
Normal file
BIN
mirva/resources/arrow_up.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
mirva/resources/banner.jpg
Normal file
BIN
mirva/resources/banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
247
mirva/resources/style.css
Normal file
247
mirva/resources/style.css
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
Design by TEMPLATED
|
||||||
|
http://templated.co
|
||||||
|
Released for free under the Creative Commons Attribution License
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #383838;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ul, ol {
|
||||||
|
margin-top: 0;
|
||||||
|
line-height: 180%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #7EAD01;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapper {
|
||||||
|
width: 100vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
|
||||||
|
#header {
|
||||||
|
clear: both;
|
||||||
|
width: 100vw;
|
||||||
|
height: 330px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0px;
|
||||||
|
background: url(banner.jpg) no-repeat left top;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px 0px 0px 60px;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo h1, #logo p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo h1 {
|
||||||
|
padding-top: 160px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
font-size: 3.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0px 0 0 10px;
|
||||||
|
font: normal 14px Georgia, "Times New Roman", Times, serif;
|
||||||
|
font-style: italic;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo a {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Page */
|
||||||
|
|
||||||
|
#page {
|
||||||
|
width: 90vw;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0px 0px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-bgtop {
|
||||||
|
padding: 20px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-bgbtm {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content */
|
||||||
|
|
||||||
|
#content {
|
||||||
|
width: 90vw;
|
||||||
|
padding: 30px 0px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-top: lightgray dashed;
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-bgtop {
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-bgbtm {
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .title {
|
||||||
|
height: 38px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px 0 0 0px;
|
||||||
|
letter-spacing: -.5px;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .title a {
|
||||||
|
color: #000000;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .meta {
|
||||||
|
/* margin-bottom: 30px;*/
|
||||||
|
padding: 5px 0px 15px 0px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .meta .date {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .meta .posted {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .meta a {
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .entry {
|
||||||
|
padding: 0px 0px 20px 0px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
padding-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_image {
|
||||||
|
max-height: 95vh;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0px 0 15px 0;
|
||||||
|
background: #ECECEC;
|
||||||
|
border-top: 1px solid #DEDEDE;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer p {
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 20px;
|
||||||
|
line-height: normal;
|
||||||
|
font-size: 9px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
color: #A0A0A0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer a {
|
||||||
|
color: #8A8A8A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* navi */
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation_button {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 4px 4px 5px #888888;
|
||||||
|
}
|
||||||
|
.navigation_button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 2px 2px 2px #888888;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.navigation_button {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.float_up {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float_down {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
6
scripts/mirva
Executable file
6
scripts/mirva
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from mirva.mirva import Mirva
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
Mirva()
|
||||||
18
setup.py
Normal file
18
setup.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from distutils.core import setup
|
||||||
|
setup(
|
||||||
|
name = 'mirva',
|
||||||
|
packages = ['mirva'],
|
||||||
|
scripts = ['scripts/mirva',
|
||||||
|
],
|
||||||
|
package_data={'':['resources/*']},
|
||||||
|
include_package_data=True,
|
||||||
|
version = '20210601',
|
||||||
|
description = 'A tool to create a web gallery from a folder of images.',
|
||||||
|
author = 'Ville Rantanen',
|
||||||
|
author_email = 'ville.q.rantanen@gmail.com',
|
||||||
|
url = 'https://bitbucket.org/MoonQ/mirva',
|
||||||
|
download_url = 'https://bitbucket.org/MoonQ/mirva/get/tip.tar.gz',
|
||||||
|
keywords = ['album', 'generator', 'javascript'],
|
||||||
|
classifiers = [],
|
||||||
|
license = 'MIT',
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user