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.
This commit is contained in:
parent
da48104702
commit
cbe0446382
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
__pycache__/
|
||||||
|
|
48
README.md
48
README.md
|
@ -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
|
|
||||||
|
|
||||||
optional arguments:
|
Host the HTML file somewhere and link it with the URL to your data.json as the
|
||||||
-h, --help show this help message and exit
|
fragment part of the URL (anything after the #).
|
||||||
--title TITLE page title
|
|
||||||
--jquery JQUERY Web path to JQuery library
|
Example: `http://gallery.dece.space/#http://unrelated.host/`
|
||||||
--masonry MASONRY Web path to Masonry library
|
|
||||||
--imagesloaded IMAGESLOADED
|
This method lets you host only one copy of the gallery page and provide
|
||||||
Web path to ImagesLoaded library
|
different links for each gallery. One drawback is that the server hosting your
|
||||||
--lightbox LIGHTBOX Web path to Lightbox2 library
|
images must have its CORS policy configured to let your browser load the photos.
|
||||||
--lightbox-css LIGHTBOX_CSS
|
|
||||||
Web path to Lightbox2 CSS
|
### Second method: embed data.json into the page
|
||||||
```
|
|
||||||
|
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…
|
||||||
|
|
177
emlg/__main__.py
177
emlg/__main__.py
|
@ -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
|
continue
|
||||||
image = Image.open(os.path.join(dirpath, filename))
|
if ext[1:].lower() not in SUPPORTED_TYPES:
|
||||||
|
continue
|
||||||
|
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 gen_thumbnail(image: Image.Image, file_name: str, dir_path: str) -> str:
|
||||||
def main():
|
"""Generate a thumbnail from this image, return its name."""
|
||||||
ap = argparse.ArgumentParser()
|
thumbnail = image.copy()
|
||||||
ap.add_argument("dir", help="directory with the photos")
|
thumbnail.thumbnail((360, 720), resample=Image.Resampling.LANCZOS)
|
||||||
ap.add_argument("--title", default="EmlGallery", help="page title")
|
name, ext = os.path.splitext(file_name)
|
||||||
ap.add_argument("--jquery", help="Web path to JQuery library")
|
thumbnail_file_name = name + "_t" + ext
|
||||||
ap.add_argument("--masonry", help="Web path to Masonry library")
|
thumbnail_path = os.path.join(dir_path, thumbnail_file_name)
|
||||||
ap.add_argument("--imagesloaded", help="Web path to ImagesLoaded library")
|
thumbnail.save(thumbnail_path)
|
||||||
ap.add_argument("--lightbox", help="Web path to Lightbox2 library")
|
return thumbnail_file_name
|
||||||
ap.add_argument("--lightbox-css", help="Web path to Lightbox2 CSS")
|
|
||||||
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__":
|
||||||
|
|
Binary file not shown.
105
index.html
Normal file
105
index.html
Normal file
|
@ -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 =
|
||||||
|
|
Reference in a new issue