focus on JSON generation

EMLG does not generate the HTML file anymore, users are invited to copy
the provided index.html and use it as they see fit.
main
dece 2 years ago
parent da48104702
commit cbe0446382

1
.gitignore vendored

@ -1,3 +1,4 @@
build/ build/
dist/ dist/
*.egg-info/ *.egg-info/
__pycache__/

@ -1,7 +1,7 @@
EmlGallery EmlGallery
========== ==========
EmlGallery (Elementary Masonry & Lightbox Gallery) is a simple generator for Web EmlGallery (elementary Masonry & Lightbox Gallery) is a simple generator for Web
galleries, based on Masonry, a library that beautifully tiles images on a page, galleries, based on Masonry, a library that beautifully tiles images on a page,
and Lightbox, a library for presenting individual images nicely. and Lightbox, a library for presenting individual images nicely.
@ -16,31 +16,33 @@ Usage
----- -----
Point the script to a folder with photos and it generates a JSON file with Point the script to a folder with photos and it generates a JSON file with
metadata and an index.html file to visit, push everything to a Web host and you metadata, push it alongside your photos to a Web host and you are ready to go.
are ready to go.
```bash ```bash
emlg "/home/dece/Photos/2022 dubious trip to antartica" $ emlg "/home/dece/Photos/2022 dubious trip to antartica"
ls $! # "Data JSON saved."
# → data.json index.html IMG1.jpg IMG2.jpg IMG3.jpg … $ ls $!
# → data.json IMG1.jpg IMG2.jpg IMG3.jpg …
``` ```
As it relies on external libraries to work, you have to provide your own path to The `index.html` file at the root of this repository is able to load the JSON
the different libraries, either a relative path on your computer or server, or data and build the gallery when someone visits your page. There are two ways to
an URL to some CDN. See the optional arguments below for the argument names. provide the JSON data to the gallery, explained below but also at the bottom of
`index.html` itself.
``` ### First method: provide data.json as an URL
positional arguments:
dir directory with the photos Host the HTML file somewhere and link it with the URL to your data.json as the
fragment part of the URL (anything after the #).
optional arguments:
-h, --help show this help message and exit Example: `http://gallery.dece.space/#http://unrelated.host/`
--title TITLE page title
--jquery JQUERY Web path to JQuery library This method lets you host only one copy of the gallery page and provide
--masonry MASONRY Web path to Masonry library different links for each gallery. One drawback is that the server hosting your
--imagesloaded IMAGESLOADED images must have its CORS policy configured to let your browser load the photos.
Web path to ImagesLoaded library
--lightbox LIGHTBOX Web path to Lightbox2 library ### Second method: embed data.json into the page
--lightbox-css LIGHTBOX_CSS
Web path to Lightbox2 CSS Override some variables as explained in `index.html` and you should be good to
``` go. This method avoids the second request, but as you need to fetch the
thumbnails anyway…

@ -1,171 +1,62 @@
"""An elementary Web gallery generator using Masonry and Lightbox.""" """An elementary Web gallery generator using Masonry and Lightbox."""
import argparse
import json import json
import os import os
from argparse import ArgumentParser
from PIL import Image from PIL import Image
SUPPORTED_TYPES = ("jpg", "jpeg", "png", "gif", "webp") SUPPORTED_TYPES = ("jpg", "jpeg", "png", "gif", "webp")
JQUERY_PATH = "/libs/jquery/jquery.min.js"
MASONRY_PATH = "/libs/masonry/masonry.pkgd.min.js"
IMAGESLOADED_PATH = "/libs/masonry/imagesloaded.pkgd.min.js"
LIGHTBOX_PATH = "/libs/lightbox2/js/lightbox.min.js"
LIGHTBOX_CSS_PATH = "/libs/lightbox2/css/lightbox.min.css"
HTML = """\ def main():
<!DOCTYPE html> ap = ArgumentParser(description="Generate JSON metadata from images.")
<html> ap.add_argument("dir", help="directory with the images to use")
<head> ap.add_argument("--no-thumbnails", action="store_true", help="disable thumbnail generation")
<meta charset="utf-8"> args = ap.parse_args()
<title>{title}</title> gen_gallery(args.dir, not bool(args.no_thumbnails))
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
{css}
</style>
<!-- jQuery -->
<script src="{jquery}"></script>
<!-- Masonry & ImagesLoaded -->
<script src="{masonry}"></script>
<script src="{imagesloaded}"></script>
<!-- Lightbox2 -->
<link href="{lightbox_css}" rel="stylesheet">
<script src="{lightbox}"></script>
</head>
<body>
<div class="masonry">
</div>
<script>
{js}
</script>
</body>
</html>
"""
CSS = """\
body {
background: black;
}
/* 4 columns by default */
.masonry {
margin: auto;
width: 1470px;
}
.masonry-item img {
margin-bottom: 10px;
width: 360px;
}
/* 3 columns on small desktop screens */
@media only screen and (max-width: 1500px) {
.masonry { width: 1100px; }
}
/* 2 columns on medium-size screens */
@media only screen and (max-width: 1200px) {
.masonry { width: 730px; }
}
/* 1 column on mobile */
@media only screen and (max-width: 768px) {
.masonry { width: 100%; }
.masonry-item { width: 100%; }
.masonry-item img { width: 100%; }
}
"""
JS = """\
var dirname = path => path.replace(/\\\\/g,'/').replace(/\\/[^\\/]*$/, '');
function initMasonry(json) {
var $grid = $('.masonry');
for (var i = 0; i < json.length; i++) {
var entry = json[i];
var $img = $(document.createElement('img'));
$img.attr('src', entry.src);
var $link = $(document.createElement('a'));
$link.attr({
'href': entry.src,
'data-lightbox': 'gallery',
'data-title': entry.title,
});
$link.append($img);
var $container = $(document.createElement('div'));
$container.addClass('masonry-item');
$container.append($link);
$grid.append($container);
}
$grid.masonry({
itemSelector: '.masonry-item',
columnWidth: 360,
gutter: 10,
});
$grid.imagesLoaded().progress(() => $grid.masonry('layout'));
}
var loc = dirname(location.pathname) + '/data.json';
$(document).ready(() => {
fetch(loc)
.then(response => response.json())
.then(json => initMasonry(json));
});
"""
def gen_gallery(dirpath: str, title: str, libs: dict): def gen_gallery(dir_path: str, generate_thumbnails: bool = True):
"""Generate the JSON metadata for the images found at dirpath."""
entries = [] entries = []
for filename in os.listdir(dirpath): for file_name in sorted(os.listdir(dir_path)):
ext = os.path.splitext(filename)[1][1:].lower() name, ext = os.path.splitext(file_name)
if ext not in SUPPORTED_TYPES: if name.endswith("_t"): # do not add already generated thumbnails
continue
if ext[1:].lower() not in SUPPORTED_TYPES:
continue continue
image = Image.open(os.path.join(dirpath, filename)) image = Image.open(os.path.join(dir_path, file_name))
dimensions = image.size dimensions = image.size
entries.append({ entry = {
"src": filename, "src": file_name,
"w": dimensions[0], "w": dimensions[0],
"h": dimensions[1], "h": dimensions[1],
"title": "", "title": "",
}) }
if generate_thumbnails:
try:
entry["thumb"] = gen_thumbnail(image, file_name, dir_path)
except OSError as exc:
exit(f"Can't create thumbnail: {exc}")
entries.append(entry)
try: try:
with open(os.path.join(dirpath, "data.json"), "wt") as data_file: with open(os.path.join(dir_path, "data.json"), "wt") as data_file:
json.dump(entries, data_file) json.dump(entries, data_file)
except OSError as exc: except OSError as exc:
exit(f"Can't write data.json file: {exc}") exit(f"Can't write data.json file: {exc}")
print("Data JSON saved.") print("Data JSON saved.")
try:
with open(os.path.join(dirpath, "index.html"), "wt") as index_file:
index_file.write(HTML.format(title=title, css=CSS, js=JS, **libs))
except OSError as exc:
exit(f"Can't write index.html file: {exc}")
print("Web page saved.")
def main(): def gen_thumbnail(image: Image.Image, file_name: str, dir_path: str) -> str:
ap = argparse.ArgumentParser() """Generate a thumbnail from this image, return its name."""
ap.add_argument("dir", help="directory with the photos") thumbnail = image.copy()
ap.add_argument("--title", default="EmlGallery", help="page title") thumbnail.thumbnail((360, 720), resample=Image.Resampling.LANCZOS)
ap.add_argument("--jquery", help="Web path to JQuery library") name, ext = os.path.splitext(file_name)
ap.add_argument("--masonry", help="Web path to Masonry library") thumbnail_file_name = name + "_t" + ext
ap.add_argument("--imagesloaded", help="Web path to ImagesLoaded library") thumbnail_path = os.path.join(dir_path, thumbnail_file_name)
ap.add_argument("--lightbox", help="Web path to Lightbox2 library") thumbnail.save(thumbnail_path)
ap.add_argument("--lightbox-css", help="Web path to Lightbox2 CSS") return thumbnail_file_name
args = ap.parse_args()
libs = {
"jquery": args.jquery or JQUERY_PATH,
"masonry": args.masonry or MASONRY_PATH,
"imagesloaded": args.imagesloaded or IMAGESLOADED_PATH,
"lightbox": args.lightbox or LIGHTBOX_PATH,
"lightbox_css": args.lightbox_css or LIGHTBOX_CSS_PATH,
}
gen_gallery(args.dir, args.title, libs)
if __name__ == "__main__": if __name__ == "__main__":

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<style>
body { background: black; }
/* 4 columns by default */
.masonry { margin: auto; width: 1470px; }
.masonry-item img { margin-bottom: 10px; width: 360px; }
/* 3 columns on small desktop screens */
@media only screen and (max-width: 1500px) { .masonry { width: 1100px; } }
/* 2 columns on medium-size screens */
@media only screen and (max-width: 1200px) { .masonry { width: 730px; } }
/* 1 column on mobile */
@media only screen and (max-width: 768px) {
.masonry { width: 100%; }
.masonry-item { width: 100%; }
.masonry-item img { width: 100%; }
}
</style>
<!-- Replace the following link with your own if you do not wish to use CDNs. -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/css/lightbox.min.css"
integrity="sha512-ZKX+BvQihRJPA8CROKBhDNvoc2aDMOdAlcm7TUQY+35XYtrd3yh95QOOhsPDQY9QnKE0Wqag9y38OIgEvb88cA=="
crossorigin="anonymous"
referrerpolicy="no-referrer" />
</head>
<body>
<div class="masonry"></div>
<!-- Replace the following links with your own if you do not wish to use CDNs. -->
<!-- jQuery -->
<script
src="https://code.jquery.com/jquery-3.6.1.min.js"
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ="
crossorigin="anonymous"></script>
<!-- Masonry & ImagesLoaded -->
<script src="https://unpkg.com/masonry-layout@4.2.2/dist/masonry.pkgd.min.js"></script>
<script src="https://unpkg.com/imagesloaded@5/imagesloaded.pkgd.min.js"></script>
<!-- Lightbox2 -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.11.3/js/lightbox.min.js"
integrity="sha512-k2GFCTbp9rQU412BStrcD/rlwv1PYec9SNrkbQlo6RZCf75l6KcC3UwDY8H5n5hl4v77IDtIPwOk9Dqjs/mMBQ=="
crossorigin="anonymous"
referrerpolicy="no-referrer"></script>
<script>
function initMasonry(json, baseUrl) {
var $grid = $('.masonry');
for (var i = 0; i < json.length; i++) {
var entry = json[i];
var $img = $(document.createElement('img'));
var imageSrc = baseUrl + entry.src;
var thumbSrc = entry.thumb ? baseUrl + entry.thumb : imageSrc;
$img.attr('src', thumbSrc);
var $link = $(document.createElement('a'));
$link.attr({
'href': imageSrc,
'data-lightbox': 'gallery',
'data-title': entry.title,
});
$link.append($img);
var $container = $(document.createElement('div'));
$container.addClass('masonry-item');
$container.append($link);
$grid.append($container);
}
$grid.masonry({
itemSelector: '.masonry-item',
columnWidth: 360,
gutter: 10,
});
$grid.imagesLoaded().progress(() => $grid.masonry('layout'));
}
$(document).ready(() => {
// Uncomment this block to load image data remotely, using the URL
// in the current URL fragment.
/*/
var dataUrl = location.hash.substring(1);
var dirUrl = dataUrl.substring(0, dataUrl.lastIndexOf("/") + 1);
fetch(location.hash.substring(1))
.then(response => response.json())
.then(json => initMasonry(json, dirUrl));
//*/
// Uncomment this block to use embed data.
/*/
var data = {}; // set this to the content of data.json.
var dirUrl = ""; // set this to the directory where images are stored.
initMasonry(data, dirUrl);
//*/
});
</script>
</body>
</html>

@ -1,6 +1,6 @@
[metadata] [metadata]
name = emlg name = emlg
version = 0.0.1 version = 0.0.2
description = Minimal HTML/JS photo gallery description = Minimal HTML/JS photo gallery
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown
@ -16,6 +16,8 @@ classifiers =
packages = emlg packages = emlg
python_requires = >= 3.7 python_requires = >= 3.7
setup_requires = setuptools >= 38.3.0 setup_requires = setuptools >= 38.3.0
install_requires =
Pillow
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

Loading…
Cancel
Save