diff --git a/src/stills/main.py b/src/stills/main.py index e4555e4..9174151 100644 --- a/src/stills/main.py +++ b/src/stills/main.py @@ -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'' + ) + 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' - f'{img.name}' + f' ' + f'{img.name}' f'' ) 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 = '
{}
'.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) diff --git a/src/stills/templates/base.html b/src/stills/templates/base.html index c00a03c..0145287 100644 --- a/src/stills/templates/base.html +++ b/src/stills/templates/base.html @@ -5,6 +5,7 @@ Portfolio {{ analytics }} +{{ style_overrides }}