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