Compare commits

..

No commits in common. "main" and "0.0.1" have entirely different histories.
main ... 0.0.1

6 changed files with 167 additions and 168 deletions

1
.gitignore vendored
View file

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

View file

@ -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,33 +16,31 @@ 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, push it alongside your photos to a Web host and you are ready to go. metadata and an index.html file to visit, push everything to a Web host and you
are ready to go.
```bash ```bash
$ emlg "/home/dece/Photos/2022 dubious trip to antartica" emlg "/home/dece/Photos/2022 dubious trip to antartica"
# "Data JSON saved." ls $!
$ ls $! # → data.json index.html IMG1.jpg IMG2.jpg IMG3.jpg …
# → data.json IMG1.jpg IMG2.jpg IMG3.jpg …
``` ```
The `index.html` file at the root of this repository is able to load the JSON As it relies on external libraries to work, you have to provide your own path to
data and build the gallery when someone visits your page. There are two ways to the different libraries, either a relative path on your computer or server, or
provide the JSON data to the gallery, explained below but also at the bottom of an URL to some CDN. See the optional arguments below for the argument names.
`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 optional arguments:
fragment part of the URL (anything after the #). -h, --help show this help message and exit
--title TITLE page title
Example: `http://gallery.dece.space/#http://unrelated.host/` --jquery JQUERY Web path to JQuery library
--masonry MASONRY Web path to Masonry library
This method lets you host only one copy of the gallery page and provide --imagesloaded IMAGESLOADED
different links for each gallery. One drawback is that the server hosting your Web path to ImagesLoaded library
images must have its CORS policy configured to let your browser load the photos. --lightbox LIGHTBOX Web path to Lightbox2 library
--lightbox-css LIGHTBOX_CSS
### Second method: embed data.json into the page 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…

View file

@ -1,62 +1,171 @@
"""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"
def main(): HTML = """\
ap = ArgumentParser(description="Generate JSON metadata from images.") <!DOCTYPE html>
ap.add_argument("dir", help="directory with the images to use") <html>
ap.add_argument("--no-thumbnails", action="store_true", help="disable thumbnail generation") <head>
args = ap.parse_args() <meta charset="utf-8">
gen_gallery(args.dir, not bool(args.no_thumbnails)) <title>{title}</title>
<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(dir_path: str, generate_thumbnails: bool = True): def gen_gallery(dirpath: str, title: str, libs: dict):
"""Generate the JSON metadata for the images found at dirpath."""
entries = [] entries = []
for file_name in sorted(os.listdir(dir_path)): for filename in os.listdir(dirpath):
name, ext = os.path.splitext(file_name) ext = os.path.splitext(filename)[1][1:].lower()
if name.endswith("_t"): # do not add already generated thumbnails if ext not in SUPPORTED_TYPES:
continue continue
if ext[1:].lower() not in SUPPORTED_TYPES: image = Image.open(os.path.join(dirpath, filename))
continue
image = Image.open(os.path.join(dir_path, file_name))
dimensions = image.size dimensions = image.size
entry = { entries.append({
"src": file_name, "src": filename,
"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(dir_path, "data.json"), "wt") as data_file: with open(os.path.join(dirpath, "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:
"""Generate a thumbnail from this image, return its name.""" def main():
thumbnail = image.copy() ap = argparse.ArgumentParser()
thumbnail.thumbnail((360, 720), resample=Image.Resampling.LANCZOS) ap.add_argument("dir", help="directory with the photos")
name, ext = os.path.splitext(file_name) ap.add_argument("--title", default="EmlGallery", help="page title")
thumbnail_file_name = name + "_t" + ext ap.add_argument("--jquery", help="Web path to JQuery library")
thumbnail_path = os.path.join(dir_path, thumbnail_file_name) ap.add_argument("--masonry", help="Web path to Masonry library")
thumbnail.save(thumbnail_path) ap.add_argument("--imagesloaded", help="Web path to ImagesLoaded library")
return thumbnail_file_name ap.add_argument("--lightbox", help="Web path to Lightbox2 library")
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.

View file

@ -1,105 +0,0 @@
<!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>

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = emlg name = emlg
version = 0.0.2 version = 0.0.1
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,8 +16,6 @@ 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 =