Improve gallery page

This commit is contained in:
Alexander Wainwright
2025-04-16 22:09:10 +10:00
parent 89b4e902f6
commit 786c633a7b
3 changed files with 68 additions and 27 deletions

View File

@@ -19,12 +19,14 @@ def parse_args():
parser = argparse.ArgumentParser(
description='Generate a static HTML gallery from a folder of images.'
)
parser.add_argument('--config', help='Path to a config file (TOML or JSON)', default=None)
parser.add_argument('--config', help='Path to a config file (TOML)', default=None)
parser.add_argument('--source', required=True, help='Source directory with images')
parser.add_argument('--output', required=True, help='Output directory for HTML and images')
parser.add_argument('--copyright', help='Copyright owner name')
parser.add_argument('--analytics-id', help='ID for analytics tracker')
parser.add_argument('--analytics-host', help='Analytics host')
parser.add_argument('--thumb-width', type=int, help='Thumbnail width in pixels')
parser.add_argument('--thumb-height', type=int, help='Thumbnail height in pixels')
return parser.parse_args()
def collect_images(source_dir: Path):
@@ -42,7 +44,7 @@ def extract_metadata(image_path: Path):
make = meta.get('Exif.Image.Make', None)
camera = meta.get('Exif.Image.Model', None)
if make not in camera:
if make and camera and make not in camera:
camera = make + ' ' + camera
try:
film = xmp['Xmp.dc.description']['lang="x-default"']
@@ -57,14 +59,45 @@ def load_image_template() -> str:
with importlib.resources.files('stills.templates').joinpath('image.html').open('r', encoding='utf-8') as f:
return f.read()
def generate_thumbnail(image_path: Path, thumb_path: Path, size=(300, 300)):
with Image.open(image_path) as img:
img = img.convert('RGB')
img_ratio = img.width / img.height
thumb_ratio = size[0] / size[1]
if img_ratio > thumb_ratio:
new_height = size[1]
new_width = int(new_height * img_ratio)
else:
new_width = size[0]
new_height = int(new_width / img_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
left = (new_width - size[0]) // 2
top = (new_height - size[1]) // 2
img = img.crop((left, top, left + size[0], top + size[1]))
thumb_path.parent.mkdir(parents=True, exist_ok=True)
img.save(thumb_path, format='JPEG', quality=85)
def generate_html(images, output_dir: Path, author: str | None = None,
analytics_id=None, analytics_host=None):
analytics_id=None, analytics_host=None,
thumb_size=(300, 300)):
template = load_template()
image_tags = []
thumb_css = (
f'<style>:root {{ '
f'--thumb-width: {thumb_size[0]}px; '
f'--thumb-height: {thumb_size[1]}px; '
f'}}</style>'
)
image_template = load_image_template()
photos_dir = output_dir / 'photos'
thumbs_dir = output_dir / 'thumbs'
photos_dir.mkdir(parents=True, exist_ok=True)
thumbs_dir.mkdir(parents=True, exist_ok=True)
analytics_html = ''
if analytics_host and analytics_id:
@@ -80,20 +113,18 @@ def generate_html(images, output_dir: Path, author: str | None = None,
)
for img in reversed(images):
relative_image_path = f'photos/{img.name}'
with Image.open(img) as im:
width, height = im.size
# Generate thumbnail HTML tag linking to individual page
html_page = img.with_suffix('.html').name
thumb_path = thumbs_dir / img.with_suffix('.jpg').name
relative_thumb_path = f'thumbs/{thumb_path.name}'
generate_thumbnail(img, thumb_path, thumb_size)
image_tags.append(
f'\t<a href="photos/{html_page}">'
f'<img src="{relative_image_path}" alt="{img.name}" loading="lazy" width="{width}" height="{height}">'
f' <a href="photos/{html_page}">'
f'<img src="{relative_thumb_path}" alt="{img.name}" loading="lazy">'
f'</a>'
)
camera_model, film = extract_metadata(img)
metadata_html = ''
if camera_model or film:
parts = []
@@ -103,18 +134,21 @@ def generate_html(images, output_dir: Path, author: str | None = None,
parts.append(film)
metadata_html = '<div class="metadata">{}</div>'.format(', '.join(parts))
page_content = image_template.replace('{{ filename }}', img.name)\
.replace('{{ analytics }}', analytics_html)\
.replace('{{ image_src }}', f'../photos/{img.name}')\
.replace('{{ metadata }}', metadata_html)\
.replace('{{ footer }}', footer_html)
page_content = image_template.replace('{{ filename }}', img.name)
page_content = page_content.replace('{{ analytics }}', analytics_html)
page_content = page_content.replace('{{ image_src }}', f'../photos/{img.name}')
page_content = page_content.replace('{{ metadata }}', metadata_html)
page_content = page_content.replace('{{ footer }}', footer_html)
(photos_dir / html_page).write_text(page_content, encoding='utf-8')
images_html = '\n'.join(image_tags)
return template.replace('{{ images }}', images_html)\
.replace('{{ analytics }}', analytics_html)\
.replace('{{ footer }}', footer_html)
.replace('{{ analytics }}', analytics_html)\
.replace('{{ footer }}', footer_html)\
.replace('{{ style_overrides }}', thumb_css)
def copy_images(images, dest_dir: Path):
dest_dir.mkdir(parents=True, exist_ok=True)
@@ -132,7 +166,6 @@ def copy_images(images, dest_dir: Path):
def main():
args = parse_args()
# Load config file if provided
config = {}
if args.config:
config_path = Path(args.config)
@@ -140,13 +173,16 @@ def main():
raise ValueError(f'Config file "{config_path}" not found.')
config = toml.load(config_path)
# Merge config values with command-line args (CLI wins if both provided)
args.source = args.source or config.get('source')
args.output = args.output or config.get('output')
args.copyright = args.copyright or config.get('copyright')
args.analytics_id = args.analytics_id or config.get('analytics_id')
args.analytics_host = args.analytics_host or config.get('analytics_host')
thumb_width = args.thumb_width or config.get('thumb_width', 300)
thumb_height = args.thumb_height or config.get('thumb_height', 300)
thumb_size = (thumb_width, thumb_height)
source_dir = Path(args.source)
output_dir = Path(args.output)
photos_dir = output_dir / 'photos'
@@ -161,7 +197,8 @@ def main():
output_dir,
args.copyright,
args.analytics_id,
args.analytics_host
args.analytics_host,
thumb_size
)
copy_images(images, photos_dir)

View File

@@ -5,6 +5,7 @@
<title>Portfolio</title>
<link rel="stylesheet" href="style.css">
{{ analytics }}
{{ style_overrides }}
</head>
<body>
<div class="gallery">

View File

@@ -40,11 +40,9 @@ body {
}
}
.gallery img,
.content img {
width: 700px;
max-width: 90vw;
height: auto;
.gallery img {
width: var(--thumb-width, 300px);
height: var(--thumb-height, 300px);
object-fit: cover;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
opacity: 0;
@@ -60,6 +58,12 @@ body {
object-fit: contain;
height: auto;
width: auto;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
opacity: 0;
cursor: zoom-in;
background: none;
padding: 0;
animation: fadeInScale 1s ease forwards;
}
.gallery a,
@@ -67,7 +71,6 @@ body {
font-size: 0;
}
.metadata {
margin-top: 10px;
font-size: 0.8rem;