Initial commit
commit
0f164913d0
|
@ -0,0 +1,5 @@
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
*.py[co]
|
|
@ -0,0 +1,2 @@
|
||||||
|
graft src/gallerygen/resources
|
||||||
|
recursive-include src/gallerygen/templates *.j2
|
|
@ -0,0 +1,30 @@
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='gallerygen',
|
||||||
|
version='1',
|
||||||
|
description='Static photo gallery website generator',
|
||||||
|
author='Dustin C. Hatch',
|
||||||
|
author_email='dustin@hatch.name',
|
||||||
|
url='http://dustin.hatch.name/projects/gallerygen',
|
||||||
|
license='GPL-3',
|
||||||
|
package_dir={'': 'src'},
|
||||||
|
packages=find_packages('src'),
|
||||||
|
package_data={
|
||||||
|
'gallerygen': [
|
||||||
|
'resources/*',
|
||||||
|
'resources/css/*',
|
||||||
|
'templates/*.j2',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
install_requires=[
|
||||||
|
'Jinja2',
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'gallerygen=gallerygen:main',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
|
@ -0,0 +1,271 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from PIL import Image
|
||||||
|
import argparse
|
||||||
|
import functools
|
||||||
|
import jinja2
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pkg_resources
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('gallerygen')
|
||||||
|
|
||||||
|
|
||||||
|
class StaticMeta(type):
|
||||||
|
|
||||||
|
def __call__(cls):
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class Gallerygen(metaclass=StaticMeta):
|
||||||
|
dist = pkg_resources.get_distribution('Gallerygen')
|
||||||
|
version = dist.version
|
||||||
|
|
||||||
|
|
||||||
|
class Album:
|
||||||
|
|
||||||
|
SPEC_SECTION_PREFIX = 'album '
|
||||||
|
|
||||||
|
def __init__(self, name, title=None, basedir=None):
|
||||||
|
self.name = name
|
||||||
|
if title is None:
|
||||||
|
title = name
|
||||||
|
self.title = title
|
||||||
|
if basedir is None:
|
||||||
|
basedir = os.getcwd()
|
||||||
|
self.path = os.path.join(basedir, name)
|
||||||
|
self.description = ''
|
||||||
|
self.cover = None
|
||||||
|
self.captions = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_spec(cls, spec, captions=None, basedir=None):
|
||||||
|
self = cls(spec['name'], spec.get('title'))
|
||||||
|
self.description = spec.get('description', '')
|
||||||
|
self.cover = spec.get('cover')
|
||||||
|
if captions:
|
||||||
|
self.captions.update(captions)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def photos(self):
|
||||||
|
for filename in sorted(os.listdir(self.path)):
|
||||||
|
path = os.path.join(self.path, filename)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
yield filename
|
||||||
|
|
||||||
|
|
||||||
|
class Gallery:
|
||||||
|
|
||||||
|
DEFAULT_LOADER = jinja2.PackageLoader(__name__)
|
||||||
|
DEFAULT_SPEC = {
|
||||||
|
'gallery': {
|
||||||
|
'title': 'Photo Gallery',
|
||||||
|
},
|
||||||
|
'albums': [],
|
||||||
|
'captions': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, title, basedir=None):
|
||||||
|
self.title = title
|
||||||
|
if basedir is None:
|
||||||
|
basedir = os.getcwd()
|
||||||
|
self.basedir = basedir
|
||||||
|
self.templates = None
|
||||||
|
self.extra_resources = None
|
||||||
|
self.albums = []
|
||||||
|
self.cover_size = (300, 300) # TODO: configurable
|
||||||
|
self.thumbnail_size = (200, 200) # TODO: configurable
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_spec_fp(cls, fp, basedir=None):
|
||||||
|
spec = cls.DEFAULT_SPEC.copy()
|
||||||
|
spec.update(yaml.safe_load(fp))
|
||||||
|
self = cls(spec['gallery']['title'], basedir)
|
||||||
|
self.templates = spec['gallery'].get('templates')
|
||||||
|
self.extra_resources = spec['gallery'].get('extra_resources')
|
||||||
|
for item in spec['albums']:
|
||||||
|
captions = spec['captions'].get(item['name'])
|
||||||
|
self.albums.append(Album.from_spec(item, captions, self.basedir))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_template_loader(self):
|
||||||
|
if self.templates:
|
||||||
|
tmpl_path = self.templates
|
||||||
|
if not os.path.isabs(tmpl_path):
|
||||||
|
tmpl_path = os.path.join(self.basedir, tmpl_path)
|
||||||
|
return jinja2.ChoiceLoader([
|
||||||
|
jinja2.FileSystemLoader(tmpl_path),
|
||||||
|
self.DEFAULT_LOADER,
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
return self.DEFAULT_LOADER
|
||||||
|
|
||||||
|
def generate(self, outdir=None):
|
||||||
|
if not outdir:
|
||||||
|
outdir = os.getcwd()
|
||||||
|
mkdir_p(outdir)
|
||||||
|
resdir = os.path.join(outdir, 'res')
|
||||||
|
env = jinja2.Environment(loader=self.get_template_loader())
|
||||||
|
env.globals.update(
|
||||||
|
gallery=self,
|
||||||
|
gallerygen=Gallerygen(),
|
||||||
|
)
|
||||||
|
self.render_index(outdir, env)
|
||||||
|
for album in self.albums:
|
||||||
|
self.render_album(album, outdir, env)
|
||||||
|
self.copy_resources(resdir)
|
||||||
|
|
||||||
|
def render_index(self, outdir, env):
|
||||||
|
context = {
|
||||||
|
'page_title': self.title,
|
||||||
|
'res_path': 'res',
|
||||||
|
'cover': functools.partial(self.mkcover, outdir),
|
||||||
|
}
|
||||||
|
tmpl = env.get_template('index.html.j2')
|
||||||
|
self.render(os.path.join(outdir, 'index.html'), tmpl, context)
|
||||||
|
|
||||||
|
def render_album(self, album, outdir, env):
|
||||||
|
context = {
|
||||||
|
'page_title': album.title,
|
||||||
|
'res_path': '../res',
|
||||||
|
'album': album,
|
||||||
|
'thumbnail': functools.partial(self.mkthumbnail, outdir, album),
|
||||||
|
}
|
||||||
|
tmpl = env.get_template('album.html.j2')
|
||||||
|
subdir = os.path.join(outdir, album.name)
|
||||||
|
mkdir_p(subdir)
|
||||||
|
self.render(os.path.join(subdir, 'index.html'), tmpl, context)
|
||||||
|
for photo in album.photos:
|
||||||
|
self.render_photo(album, photo, subdir, env)
|
||||||
|
|
||||||
|
def render_photo(self, album, photo, subdir, env):
|
||||||
|
context = {
|
||||||
|
'page_title': photo,
|
||||||
|
'res_path': '../res',
|
||||||
|
'album': album,
|
||||||
|
'photo': photo,
|
||||||
|
'caption': album.captions.get(photo, ''),
|
||||||
|
}
|
||||||
|
tmpl = env.get_template('photo.html.j2')
|
||||||
|
self.render(os.path.join(subdir, photo + '.html'), tmpl, context)
|
||||||
|
srcname = os.path.join(self.basedir, album.name, photo)
|
||||||
|
destname = os.path.join(subdir, photo)
|
||||||
|
cp_u(srcname, destname)
|
||||||
|
|
||||||
|
def render(self, filename, tmpl, context):
|
||||||
|
log.info('Rendering %s', filename)
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(tmpl.render(**context))
|
||||||
|
|
||||||
|
def copy_resources(self, resdir):
|
||||||
|
mkdir_p(resdir)
|
||||||
|
self.copydir(resdir, pkg_resources.resource_filename(
|
||||||
|
Gallerygen.dist.key,
|
||||||
|
'resources',
|
||||||
|
))
|
||||||
|
extra_resources = self.extra_resources
|
||||||
|
if extra_resources:
|
||||||
|
if not os.path.isabs(extra_resources):
|
||||||
|
extra_resources = os.path.join(self.basedir, extra_resources)
|
||||||
|
self.copydir(resdir, extra_resources)
|
||||||
|
|
||||||
|
def copydir(self, resdir, src):
|
||||||
|
for dirpath, dirnames, filenames in os.walk(src):
|
||||||
|
subdir = os.path.relpath(dirpath, src)
|
||||||
|
for dirname in dirnames:
|
||||||
|
destname = os.path.join(resdir, subdir, dirname)
|
||||||
|
mkdir_p(destname)
|
||||||
|
for filename in filenames:
|
||||||
|
destname = os.path.normpath(
|
||||||
|
os.path.join(resdir, subdir, filename)
|
||||||
|
)
|
||||||
|
srcname = os.path.join(dirpath, filename)
|
||||||
|
cp_u(srcname, destname)
|
||||||
|
|
||||||
|
def mkcover(self, outdir, album):
|
||||||
|
cover = album.cover or next(album.photos)
|
||||||
|
log.info('Creating cover for album %s: %s', album.name, cover)
|
||||||
|
return self.gen_thumbnail(
|
||||||
|
outdir,
|
||||||
|
'covers',
|
||||||
|
album,
|
||||||
|
cover,
|
||||||
|
self.cover_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def mkthumbnail(self, outdir, album, photo):
|
||||||
|
log.info('Creating thubnail: %s (in %s)', photo, album.name)
|
||||||
|
return self.gen_thumbnail(
|
||||||
|
outdir,
|
||||||
|
'thumbnails',
|
||||||
|
album,
|
||||||
|
photo,
|
||||||
|
self.thumbnail_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
def gen_thumbnail(self, outdir, prefix, album, photo, size):
|
||||||
|
subdir = os.path.join(prefix, album.name)
|
||||||
|
destdir = os.path.join(outdir, subdir)
|
||||||
|
img_path = os.path.join(self.basedir, album.name, photo)
|
||||||
|
try:
|
||||||
|
mkdir_p(destdir)
|
||||||
|
with open(img_path, 'rb') as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
im.thumbnail(size)
|
||||||
|
im.save(os.path.join(destdir, photo))
|
||||||
|
except:
|
||||||
|
log.exception('Failed to make thumbnail image:')
|
||||||
|
return os.path.join(subdir, photo)
|
||||||
|
|
||||||
|
|
||||||
|
def cp_u(srcname, destname):
|
||||||
|
if os.path.exists(destname):
|
||||||
|
s_mtime = os.path.getmtime(srcname)
|
||||||
|
d_mtime = os.path.getmtime(destname)
|
||||||
|
if s_mtime <= d_mtime:
|
||||||
|
return
|
||||||
|
log.info('Copying %s %s', srcname, destname)
|
||||||
|
shutil.copy2(srcname, destname)
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir_p(dirname):
|
||||||
|
dirname = os.path.normpath(dirname)
|
||||||
|
if not os.path.isdir(dirname):
|
||||||
|
log.debug('Creating directory: %s', dirname)
|
||||||
|
os.makedirs(dirname)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'spec',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help='Path to specification file',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'output',
|
||||||
|
nargs='?',
|
||||||
|
help='Path to output directory',
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
with args.spec:
|
||||||
|
basedir = os.path.dirname(args.spec.name)
|
||||||
|
gallery = Gallery.from_spec_fp(args.spec, basedir)
|
||||||
|
|
||||||
|
gallery.generate(args.output)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,9 @@
|
||||||
|
.photo figcaption {
|
||||||
|
margin: .5em .25em;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .attribution {
|
||||||
|
margin-top: 2em;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "default-album.html.j2" %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "default-base.html.j2" %}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
|
||||||
|
{%- block title %}
|
||||||
|
<title>{{ album.title }} :: {{ gallery.title }}</title>
|
||||||
|
{%- endblock title %}
|
||||||
|
|
||||||
|
{%- block content %}
|
||||||
|
<main class="album">
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="row columns">
|
||||||
|
<p class="album-description">{{ album.description }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="thumbnails">
|
||||||
|
<div class="row columns">
|
||||||
|
{%- for photo in album.photos %}
|
||||||
|
<a href="{{ photo }}.html"><img src="../{{ thumbnail(photo) }}"
|
||||||
|
{%- if album.captions[photo] %} title="{{ album.captions[photo] }}"{% endif %}
|
||||||
|
class="thumbnail"></a>
|
||||||
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{%- endblock content %}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="no-js" lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{%- block head %}
|
||||||
|
{%- block title %}
|
||||||
|
<title>{{ page_title }}</title>
|
||||||
|
{%- endblock title %}
|
||||||
|
{%- block meta %}
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
{%- endblock meta %}
|
||||||
|
{%- block head_links %}
|
||||||
|
<link href="{{ res_path }}/css/foundation.min.css" rel="stylesheet" />
|
||||||
|
<link href="{{ res_path }}/css/gallerygen.css" rel="stylesheet" />
|
||||||
|
{%- endblock head_links %}
|
||||||
|
{%- endblock head %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{%- block body %}
|
||||||
|
{%- block header %}
|
||||||
|
<header class="row column">
|
||||||
|
<h1 class="title">{{ page_title }}</h1>
|
||||||
|
</header>
|
||||||
|
{%- endblock header %}
|
||||||
|
{%- block content %}
|
||||||
|
{%- endblock content %}
|
||||||
|
{%- block footer %}
|
||||||
|
<footer class="row column">
|
||||||
|
<div class="attribution">
|
||||||
|
Created with Gallerygen {{ gallerygen.version }}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{%- endblock footer %}
|
||||||
|
{%- block script %}
|
||||||
|
{%- endblock script %}
|
||||||
|
{%- endblock body %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
|
||||||
|
{%- macro album_section(album) %}
|
||||||
|
<div class="album large-6 columns">
|
||||||
|
<div class="row">
|
||||||
|
<div class="large-6 columns">
|
||||||
|
<a href="{{ album.name }}/index.html"><img class="thumbnail"
|
||||||
|
src="{{ cover(album) }}" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="large-6 columns">
|
||||||
|
<h3><a href="{{ album.name }}/index.html">{{ album.title }}</a></h3>
|
||||||
|
<p class="album-description">
|
||||||
|
{{ album.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{%- block content %}
|
||||||
|
<main class="index">
|
||||||
|
|
||||||
|
{%- block albumlist %}
|
||||||
|
<div class="row columns">
|
||||||
|
<section class="album-list">
|
||||||
|
<h2>Photo Albums</h2>
|
||||||
|
<div class="row">
|
||||||
|
{%- for album in gallery.albums %}
|
||||||
|
{{ album_section(album) }}
|
||||||
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{%- endblock %}
|
||||||
|
|
||||||
|
</main>
|
||||||
|
{%- endblock content %}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
|
||||||
|
{%- block title %}
|
||||||
|
<title>{{ photo }} :: {{ album.title }} :: {{ gallery.title }}</title>
|
||||||
|
{%- endblock title %}
|
||||||
|
|
||||||
|
{%- block content %}
|
||||||
|
<main class="photo">
|
||||||
|
<section class="photo">
|
||||||
|
<div class="row columns">
|
||||||
|
<a href="{{ photo }}"><img src="{{ photo }}" style="width: 100%;" /></a>
|
||||||
|
{%- if caption %}
|
||||||
|
<figcaption>{{ caption }}</figcaption>
|
||||||
|
{%- endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{%- endblock content %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "default-index.html.j2" %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "default-photo.html.j2" %}
|
Loading…
Reference in New Issue