Improve gallery page
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<title>Portfolio</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
{{ analytics }}
|
||||
{{ style_overrides }}
|
||||
</head>
|
||||
<body>
|
||||
<div class="gallery">
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user