Compare commits
No commits in common. "main" and "0.0.1" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
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,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…
|
|
||||||
|
|
177
emlg/__main__.py
177
emlg/__main__.py
|
@ -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__":
|
||||||
|
|
BIN
emlg/__pycache__/__main__.cpython-39.pyc
Normal file
BIN
emlg/__pycache__/__main__.cpython-39.pyc
Normal file
Binary file not shown.
105
index.html
105
index.html
|
@ -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>
|
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
Reference in a new issue