first version

This commit is contained in:
Ville Rantanen
2021-06-01 10:56:20 +03:00
commit 91355cf05b
11 changed files with 565 additions and 0 deletions

19
LICENSE.txt Normal file
View 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
View 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
View File

255
mirva/mirva.py Executable file
View 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;">&nbsp;</div>
</div>
<!-- end #content -->
<div style="clear: both;">&nbsp;</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;">&nbsp;</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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
mirva/resources/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

247
mirva/resources/style.css Normal file
View 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
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from mirva.mirva import Mirva
if __name__ == '__main__':
Mirva()

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[metadata]
description-file = README.md

18
setup.py Normal file
View 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',
)