Compare commits

...

10 commits

Author SHA1 Message Date
d297110d88 Add installation instructions to README 2024-08-11 17:32:02 -07:00
e2338df37c Release v0.1.1
- Add output_dir config option, defaults to "site" to keep original
  directory clean
- Add support for nested covers - if there are no images in a dir, pick
  the cover from the first child dir
- Show help text if no args are given
- Some HTML/CSS visual tweaks
- Start buildling standalone binaries using PEX
2024-08-11 17:26:31 -07:00
4d45aa5296 makefile tooling for building standalone binaries for various platforms and making a GH release 2024-08-11 17:25:54 -07:00
f7998cd708 Add output_dir config option, default to "site" to keep original directory clean 2024-08-11 12:13:27 -07:00
cd47a82ecf visual tweaks 2024-08-05 21:18:53 -07:00
6cd2e52408 Merge branch 'pex' 2024-08-05 20:00:28 -07:00
f60a14ae21 Add support for nested covers - if there are no images in a dir, pick the cover from the first child dir 2024-08-05 20:00:06 -07:00
c93f1997fc basic PEX tooling to build independent binaries 2024-08-05 19:29:40 -07:00
c08c5609ca Support running with no args - show help 2024-08-05 08:08:25 -07:00
acd736c970 Revert "mark py3.11 support"
This reverts commit 04586c8c45.

We actually need >= 3.12
2024-08-04 22:30:30 -07:00
11 changed files with 130 additions and 49 deletions

View file

@ -7,6 +7,9 @@ ci: init lint test
# Final pre-flight checks then deploy everywhere!
shipit: all build staging prod
version := $(shell yq -p toml .tool.poetry.version < pyproject.toml)
scie_platforms := linux-aarch64 linux-x86_64 macos-aarch64 macos-x86_64
init:
poetry install
@ -33,11 +36,17 @@ test-fast:
test-watch:
find . -name '*py' -or -name '*html' -or -name poetry.lock | entr -r -c make test-fast
run:
podman-compose up
clean:
rm -rv dist || true
build:
docker:
podman build -t nickpegg/photojawn . --build-arg GIT_COMMIT=$(shell git rev-parse --short HEAD)
clean:
podman-compose down --rmi all
dist:
poetry build
poetry run pex --project . -o dist/photojawn -c photojawn --scie eager $(foreach plat,$(scie_platforms), --scie-platform $(plat))
release: dist
git push --tags
gh release create --verify-tag v$(version)
gh release upload v$(version) dist/photojawn-$(version)-*whl $(foreach plat,$(scie_platforms),dist/photojawn-$(plat))

View file

@ -10,7 +10,16 @@ It's everything I need and nothing I don't.
## Getting Started
TODO: Installation instructions lol
### Installation
Photojawn requires at least Python 3.12. If you don't know what version you
have, you can check by running `python -V`.
1. Head on over to the [releases](https://github.com/nickpegg/photojawn/releases) page
2. If you have Python >=3.12, you can install using the wheel (`.whl` file)
a. To install, run `pip install photojawn-<version>-py3-none-any.whl`
3. If you don't have Python >=3.12, you can download one of the standalone binaries depending on your OS and architecture, e.g. `photojawn-linux-x86_64`.
### Initialization

View file

@ -1,4 +1,5 @@
import logging
import sys
from argparse import ArgumentParser, Namespace
from pathlib import Path
@ -15,16 +16,17 @@ def main() -> None:
setup_logging(args.logging)
# Load config from file if it exists
conf_path = Path(args.album_path) / Path(args.config)
if conf_path.exists():
logger.debug(f"Reading config from {conf_path}")
config = Config.from_yaml(conf_path.read_bytes())
elif args.action != "init":
logger.error(
f"No config file found at {conf_path}. If this is a new photo directory, "
"please run `photojawn init` in there first."
)
return
if hasattr(args, "album_path"):
conf_path = Path(args.album_path) / Path(args.config)
if conf_path.exists():
logger.debug(f"Reading config from {conf_path}")
config = Config.from_yaml(conf_path.read_bytes())
elif args.action != "init":
logger.error(
f"No config file found at {conf_path}. If this is a new photo directory, "
"please run `photojawn init` in there first."
)
return
# Call the subcommand function
match args.action:
@ -98,7 +100,12 @@ def parse_args() -> Namespace:
help="Path to the main photos directory",
)
return parser.parse_args()
args = parser.parse_args()
if not hasattr(args, "action"):
parser.print_help()
sys.exit(0)
return args
########################################

View file

@ -16,6 +16,9 @@ class Config:
# Size of the image when looking at the standalone image page
view_size: tuple[int, int] = (1920, 1080)
# Directory inside the photo directory to output the site to
output_dir: str = "site"
# Quick mode:
# - Don't regenerate thumbnails if they already exist
quick: bool = False
@ -29,6 +32,8 @@ class Config:
for key, val in data.items():
match key:
case "output_dir":
conf.output_dir = val
case "thumnail_size":
conf.thumbnail_size = tuple(val)
case "view_size":

View file

@ -1,7 +1,8 @@
import logging
import shutil
import os
from dataclasses import dataclass
from pathlib import Path
from pprint import pformat
from typing import Iterator, Optional
from jinja2 import Environment, FileSystemLoader, select_autoescape
@ -17,7 +18,6 @@ logger = logging.getLogger(__name__)
@dataclass
class ImageDirectory:
path: Path
rel_path: Path # Path relative to the root dir
children: list["ImageDirectory"]
images: list["ImagePath"]
is_root: bool = False
@ -75,13 +75,22 @@ def generate(config: Config, album_path: Path) -> None:
"""
Main generation function
"""
root_dir = find_images(album_path)
logger.debug(pformat(root_dir))
generate_thumbnails(config, root_dir)
# Change the working directory to the album_path so that all paths are relative to
# it when we find images. We need to do this because all the paths in HTML need to
# be relative to it and we don't want to have to do a bunch of path gymnastics to
# re-relative all those paths.
orig_wd = Path.cwd()
os.chdir(album_path)
root_dir = find_images(config, Path("."))
generate_images(config, root_dir)
generate_html(config, root_dir)
shutil.copytree("static", Path(config.output_dir) / "static")
os.chdir(orig_wd)
def find_images(root_path: Path) -> ImageDirectory:
def find_images(config: Config, root_path: Path) -> ImageDirectory:
"""
Build up an ImageDirectory to track all of the directories and their images.
@ -91,20 +100,17 @@ def find_images(root_path: Path) -> ImageDirectory:
# image_dirs keeps track of all directories we find with images in them, so we can
# attach them as children to parent directories
image_dirs: dict[Path, ImageDirectory] = {
root_path: ImageDirectory(
path=root_path, rel_path=Path("."), children=[], images=[], is_root=True
)
root_path: ImageDirectory(path=root_path, children=[], images=[], is_root=True)
}
for dirpath, dirnames, filenames in root_path.walk(top_down=False):
if dirpath.name in {"slides", "_templates", "static"}:
if dirpath.name in {"slides", "_templates", "static", config.output_dir}:
continue
image_dir = image_dirs.get(
dirpath,
ImageDirectory(
path=dirpath,
rel_path=dirpath.relative_to(root_path),
children=[],
images=[],
),
@ -141,8 +147,14 @@ def find_images(root_path: Path) -> ImageDirectory:
image_dir.images.append(ip)
if image_dir.cover_path is None and len(image_dir.images) > 0:
image_dir.cover_path = image_dir.images[0]
if image_dir.cover_path is None:
if len(image_dir.images) > 0:
image_dir.cover_path = image_dir.images[0]
elif len(image_dir.children) > 0:
cover = image_dir.children[0].cover_path
logger.debug(f"nested cover path for {image_dir.path.name}: {cover}")
image_dir.cover_path = cover
image_dirs[image_dir.path] = image_dir
return image_dirs[root_path]
@ -159,20 +171,26 @@ def is_image(path: Path) -> bool:
return False
def generate_thumbnails(config: Config, root_dir: ImageDirectory) -> None:
def generate_images(config: Config, root_dir: ImageDirectory) -> None:
"""
Find all of the images and generate thumbnails and on-screen versions
Find all of the images and generate various image sizes
"""
# Include cover images here because we want thumbnails for all of them
all_images = root_dir.image_paths() + root_dir.cover_image_paths()
for image_path in track(all_images, description="Making thumbnails..."):
for image_path in track(all_images, description="Making smaller images..."):
orig_img = Image.open(image_path.path)
slides_path = image_path.path.parent / "slides"
slides_path.mkdir(exist_ok=True)
slides_path = config.output_dir / image_path.path.parent / "slides"
slides_path.mkdir(exist_ok=True, parents=True)
thumb_path = image_path.thumbnail_path()
# Copy the original image in
orig_new_path = config.output_dir / image_path.path
if not orig_new_path.exists() or not config.quick:
logger.info(f"Copying original image to {orig_new_path}")
orig_img.save(orig_new_path)
thumb_path = config.output_dir / image_path.thumbnail_path()
if not thumb_path.exists() or not config.quick:
thumb_img = orig_img.copy()
thumb_img.thumbnail(config.thumbnail_size)
@ -181,7 +199,7 @@ def generate_thumbnails(config: Config, root_dir: ImageDirectory) -> None:
f'Generated thumbnail size "{image_path.path}" -> "{thumb_path}"'
)
screen_path = image_path.display_path()
screen_path = config.output_dir / image_path.display_path()
if not screen_path.exists() or not config.quick:
screen_img = orig_img.copy()
screen_img.thumbnail(config.view_size)
@ -207,8 +225,8 @@ def generate_html(config: Config, root_dir: ImageDirectory) -> None:
for album_dir in root_dir.walk():
html_path = album_dir.path / "index.html"
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
html_path = config.output_dir / html_path
# TODO build breadcrumbs here, (href, name)
breadcrumbs = []
if not album_dir.is_root:
crumb_pos = album_dir.path.parent
@ -240,6 +258,7 @@ def generate_html(config: Config, root_dir: ImageDirectory) -> None:
html_path = image_path.html_path()
root_path = root_dir.path.relative_to(html_path.parent, walk_up=True)
html_path = config.output_dir / html_path
html_path.parent.mkdir(exist_ok=True)
prev_image = None

View file

@ -9,20 +9,25 @@
{% endfor %}
/ {{album_dir.path.name}}
</h1>
<hr>
{% endif %}
{% if album_dir.description %}
<div class="caption">
{{ album_dir.description | safe }}
</div>
{% endif %}
{% if album_dir.children %}
<h2>Albums</h2>
<div id="album-children">
{% for child in album_dir.children %}
<div class="album">
<a href="{{child.path.name}}/">
<div>
{% if child.cover_path %}
<img src="{{child.path.name}}/slides/{{child.cover_path.thumbnail_filename()}}" />
<img
src="{{child.cover_path.path.parent.relative_to(album_dir.path)}}/slides/{{child.cover_path.thumbnail_filename()}}" />
{% endif %}
</div>
<div>

View file

@ -42,7 +42,7 @@
</div>
</div>
<div id="photo-description">
<div id="photo-description" class="caption">
{% if image_path.description %}
{{ image_path.description | safe }}
{% endif %}

View file

@ -3,3 +3,7 @@ thumbnail_size: [256, 256]
# Max size of images when viewing a single one on screen
view_size: [1024, 768]
# Directory where the generated site will be created in. All original images will be
# copied in to here with the same directory structure.
output_dir: "site"

View file

@ -22,15 +22,14 @@ a {
#content {
text-align: center;
max-width: 1200px;
margin: 0.5em;
margin-left: auto;
margin-right: auto;
}
#content > * {
width: fit-content;
max-width: 1500px;
margin-top: 1em;
margin-left: auto;
margin-right: auto;
}
ul {
@ -47,6 +46,9 @@ ul {
margin: 1em;
padding: 0.75em;
background-color: lightgrey;
height: min-content;
border: thin solid black;
box-shadow: 0.25em 0.25em #ccc;
}
#album-photos {
@ -81,6 +83,12 @@ ul {
height: auto;
}
.caption {
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
.arrow {
border: solid black;
border-width: 0 3px 3px 0;

18
poetry.lock generated
View file

@ -290,6 +290,20 @@ files = [
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pex"
version = "2.14.1"
description = "The PEX packaging toolchain."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,<3.14,>=2.7"
files = [
{file = "pex-2.14.1-py2.py3-none-any.whl", hash = "sha256:43210f64e5461d91ad6c99d80724e83f5010bfa82a1664bf44ffdb14e2defb58"},
{file = "pex-2.14.1.tar.gz", hash = "sha256:e71296873101732deffa5341d7dc9a9d8ed9e58413acc07579f7b39998c273a2"},
]
[package.extras]
subprocess = ["subprocess32 (>=3.2.7)"]
[[package]]
name = "pillow"
version = "10.4.0"
@ -590,5 +604,5 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "3f22f947559bd9d54a838d51953828baa88e2485fb90c33931359511805cacc9"
python-versions = "^3.12,<3.14"
content-hash = "9ec27256e1d8988f06634a217388d357a211f6120d6ade104fdd87182b18279c"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "photojawn"
version = "0.1.0"
version = "0.1.1"
description = "A simple photo album static site generator"
authors = ["Nick Pegg <nick@nickpegg.com>"]
repository = "https://github.com/nickpegg/photojawn"
@ -12,7 +12,7 @@ photojawn = 'photojawn.cli:main'
[tool.poetry.dependencies]
# TODO: make sure we support >=3.10 via tests
python = "<4.0,>=3.11"
python = "^3.12,<3.14"
jinja2 = "^3.1.4"
pillow = "^10.4.0"
rich = "^13.7.1"
@ -26,6 +26,7 @@ mypy = "*"
pytest-testmon = "*"
types-pyyaml = "^6.0.12.20240724"
types-markdown = "^3.6.0.20240316"
pex = "^2.14.1"
[tool.mypy]