From fe59a701586972793ea110b465f65d47b825fb8e Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 16 Jun 2026 18:19:51 +0300 Subject: in0 split album.source.sh into cohesive modules Refactor the ~1442-line album.source.sh ([SRP] violation) into three cohesive modules, leaving album.source.sh a thin coordinator. Pure code move, no behavior change -- whole functions and their doc comments moved verbatim; the assembled bin/shuriken contains the same function set. - image-pipeline.source.sh: ImageMagick derivative generation (create_photo_derivatives, create_all_photo_derivatives) and photo asset prep (prepare_generation_photo_assets). - album-metadata.source.sh: EXIF identify caching, exif tooltip/details helpers, file counts, shuriken.json metadata, EXIF cache clearing and the dry-run plan. - album-render.source.sh: preview/view/details/splash/redirect page rendering, the parallel render-job orchestration and the ALBUM_VIEW_PAGE_BY_PHOTO global. - album.source.sh: thin coordinator (generate, refresh_splash, generate_stats_pages, site-asset/archive/html-clean helpers). Add the three modules to LIB_SOURCES (Justfile) and to the SHURIKEN_LIB_SOURCES markers in src/shuriken.sh, ordered after random and before album.source.sh / stats.source.sh. just build, just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 --- src/lib/album-metadata.source.sh | 493 ++++++++++++++ src/lib/album-render.source.sh | 739 +++++++++++++++++++++ src/lib/album.source.sh | 1324 +------------------------------------- src/lib/image-pipeline.source.sh | 84 +++ 4 files changed, 1322 insertions(+), 1318 deletions(-) create mode 100644 src/lib/album-metadata.source.sh create mode 100644 src/lib/album-render.source.sh create mode 100644 src/lib/image-pipeline.source.sh (limited to 'src/lib') diff --git a/src/lib/album-metadata.source.sh b/src/lib/album-metadata.source.sh new file mode 100644 index 0000000..9953fe3 --- /dev/null +++ b/src/lib/album-metadata.source.sh @@ -0,0 +1,493 @@ +photo_cache_signature() { + local -r photo="$1"; shift + local -r photo_path="$1"; shift + local stat_output + + stat_output=$(stat -c '%s:%Y' "$photo_path") + printf '%s:%s\n' "$photo" "$stat_output" +} + +print_cached_photo_identify_output() { + local -r cache_file="$1"; shift + local line + local skipped_signature=no + + while IFS= read -r line || [ -n "$line" ]; do + if [ "$skipped_signature" = no ]; then + skipped_signature=yes + continue + fi + printf '%s\n' "$line" + done < "$cache_file" +} + +cached_photo_identify_output() { + local -r photo="$1"; shift + local -r photo_path="$1"; shift + local cache_dir + local cache_file + local cached_signature='' + local current_signature + + # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist + # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is + # the working dir in both staging and direct contexts). Keeping it outside + # dist means it survives a fresh/cleared dist and is never deployed, so an + # unchanged photo skips the slow `identify -verbose` on every regenerate. + cache_dir="$(dirname "$DIST_DIR")/cache/exif" + cache_file="$cache_dir/$photo.txt" + current_signature=$(photo_cache_signature "$photo" "$photo_path") + + # Reuse the cache when its signature still matches the source file. --force + # is handled once up front by clear_exif_cache (which empties this directory), + # so the first call per photo then rebuilds it and the rest of the run reuses + # it -- exactly one identify per photo even under force. + if [ -f "$cache_file" ]; then + IFS= read -r cached_signature < "$cache_file" || true + if [ "$cached_signature" = "$current_signature" ]; then + print_cached_photo_identify_output "$cache_file" + return + fi + fi + + mkdir -p "$cache_dir" + printf '%s\n' "$current_signature" > "$cache_file" + imagemagick_identify -verbose "$photo_path" >> "$cache_file" 2>/dev/null \ + || true + print_cached_photo_identify_output "$cache_file" +} + +photo_exif_details_html() { + local -r photo="$1"; shift + local -r photo_path="$1"; shift + local key + local key_html + local line + local value + local value_html + local -i exif_count=0 + + while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then + key="exif:${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + key_html=$(_html_escape "$key") + value_html=$(_html_escape "$value") + + if (( exif_count == 0 )); then + printf '\n' + printf '\n' + fi + + printf '\n' \ + "$key_html" \ + "$value_html" + (( ++exif_count )) + fi + done < <(cached_photo_identify_output "$photo" "$photo_path") + + if (( exif_count == 0 )); then + printf '

No EXIF details available.

\n' + return + fi + + printf '\n' + printf '
%s%s
\n' +} + +_first_exif_value_to() { + local -n output_ref="$1"; shift + local -n exif_ref="$1"; shift + local key + + output_ref='' + for key in "$@"; do + if [ -n "${exif_ref[$key]:-}" ]; then + # shellcheck disable=SC2034 + output_ref="${exif_ref[$key]}" + return + fi + done +} + +_photo_exif_values_to() { + local -n output_ref="$1"; shift + local -r photo="$1"; shift + local -r photo_path="$1"; shift + local line + + output_ref=() + while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then + # output_ref writes to the caller-provided associative array. + # shellcheck disable=SC2034 + output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" + fi + done < <(cached_photo_identify_output "$photo" "$photo_path") +} + +_photo_exif_tooltip_text_from_values() { + local -r exif_name="$1"; shift + local -n values_ref="$exif_name" + local aperture + local camera + local date_time + local iso + local key + local make + local model + local separator='' + local shutter_speed + local -a tooltip_parts=() + + make="${values_ref[Make]:-}" + model="${values_ref[Model]:-}" + camera="$make" + if [ -n "$model" ]; then + if [ -n "$make" ]; then + case "$model" in + "$make"|"$make "*) + camera="$model" + ;; + *) + camera="$make $model" + ;; + esac + else + camera="$model" + fi + fi + + _first_exif_value_to aperture "$exif_name" FNumber ApertureValue + _first_exif_value_to iso "$exif_name" \ + ISOSpeedRatings PhotographicSensitivity ISO + _first_exif_value_to shutter_speed "$exif_name" \ + ExposureTime ShutterSpeedValue + _first_exif_value_to date_time "$exif_name" \ + DateTimeOriginal DateTimeDigitized DateTime + + if [ -n "$camera" ]; then + tooltip_parts+=("Camera: $camera") + fi + if [ -n "$aperture" ]; then + tooltip_parts+=("Aperture: $aperture") + fi + if [ -n "$iso" ]; then + tooltip_parts+=("ISO: $iso") + fi + if [ -n "$shutter_speed" ]; then + tooltip_parts+=("Shutter speed: $shutter_speed") + fi + if [ -n "$date_time" ]; then + tooltip_parts+=("Taken: $date_time") + fi + + for key in "${tooltip_parts[@]}"; do + printf '%s%s' "$separator" "$key" + separator='; ' + done + if (( ${#tooltip_parts[@]} > 0 )); then + printf '\n' + fi +} + +photo_exif_tooltip_text() { + local -r photo="$1"; shift + local -r photo_path="$1"; shift + # exif_values is populated and read through nameref helpers. + # shellcheck disable=SC2034 + local -A exif_values=() + + _photo_exif_values_to exif_values "$photo" "$photo_path" + _photo_exif_tooltip_text_from_values exif_values +} + +count_files() { + local -r dir="$1"; shift + local -r name="${1:-}"; shift || true + + if [ ! -d "$dir" ]; then + printf '0\n' + return + fi + + if [ -n "$name" ]; then + find "$dir" -maxdepth 1 -type f -name "$name" | wc -l + else + find "$dir" -maxdepth 1 -type f | wc -l + fi +} + +count_incoming_images() { + incoming_image_files | wc -l +} + +tarball_name_plan() { + local base + + base=$(basename "$INCOMING_DIR") + printf '%s-%s\n' "$base" "$TARBALL_SUFFIX" +} + +generated_tarball_name() { + local base + local timestamp + + base=$(basename "$INCOMING_DIR") + timestamp=$(current_timestamp_slug) + printf '%s-%s%s\n' "$base" "$timestamp" "$TARBALL_SUFFIX" +} + +count_tree_files() { + local -r dir="$1"; shift + local -r name="$1"; shift + + if [ ! -d "$dir" ]; then + printf '0\n' + return + fi + + find "$dir" -type f -name "$name" | wc -l +} + +_collect_generation_metadata() { + local -r tarball_file="$1"; shift + + declare -gA _GENERATION_METADATA=() + _GENERATION_METADATA["generator_name"]='shuriken' + _GENERATION_METADATA["generator_version"]="$VERSION" + _GENERATION_METADATA["generated_at"]=$(current_timestamp_iso) + _GENERATION_METADATA["config_source"]="$SHURIKEN_CONFIG_SOURCE" + _GENERATION_METADATA["template_name"]=$(basename "$TEMPLATE_DIR") + _GENERATION_METADATA["template_directory"]="$TEMPLATE_DIR" + _GENERATION_METADATA["source_incoming_dir"]="$INCOMING_DIR" + _GENERATION_METADATA["source_image_count"]=$(count_incoming_images) + _GENERATION_METADATA["generated_photo_count"]=$(count_files "$DIST_DIR/photos") + _GENERATION_METADATA["generated_thumb_count"]=$(count_files "$DIST_DIR/thumbs") + _GENERATION_METADATA["generated_html_count"]=$( + count_tree_files "$DIST_DIR" '*.html' + ) + _GENERATION_METADATA["tarball_included"]="$TARBALL_INCLUDE" + _GENERATION_METADATA["tarball_file"]="$tarball_file" + _GENERATION_METADATA["settings_title"]="$TITLE" + _GENERATION_METADATA["settings_height"]="$HEIGHT" + _GENERATION_METADATA["settings_thumbheight"]="$THUMBHEIGHT" + _GENERATION_METADATA["settings_maxpreviews"]="$MAXPREVIEWS" + _GENERATION_METADATA["settings_image_jobs"]="$IMAGE_JOBS" + _GENERATION_METADATA["settings_random_seed"]="$RANDOM_SEED" + _GENERATION_METADATA["settings_shuffle"]="$SHUFFLE" + _GENERATION_METADATA["settings_splash_page"]="$SPLASH_PAGE" + _GENERATION_METADATA["settings_stats_page"]="$STATS_PAGE" + _GENERATION_METADATA["settings_original_basepath"]="$ORIGINAL_BASEPATH" +} + +_generation_metadata_json() { + printf '{\n' + printf ' "generator": {\n' + printf ' "name": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["generator_name"]}")" + printf ' "version": %s\n' \ + "$(_json_string "${_GENERATION_METADATA["generator_version"]}")" + printf ' },\n' + printf ' "generated_at": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["generated_at"]}")" + printf ' "config_source": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["config_source"]}")" + printf ' "template": {\n' + printf ' "name": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["template_name"]}")" + printf ' "directory": %s\n' \ + "$(_json_string "${_GENERATION_METADATA["template_directory"]}")" + printf ' },\n' + printf ' "source": {\n' + printf ' "incoming_dir": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["source_incoming_dir"]}")" + printf ' "image_count": %s\n' \ + "${_GENERATION_METADATA["source_image_count"]}" + printf ' },\n' + printf ' "generated": {\n' + printf ' "photo_count": %s,\n' \ + "${_GENERATION_METADATA["generated_photo_count"]}" + printf ' "thumb_count": %s,\n' \ + "${_GENERATION_METADATA["generated_thumb_count"]}" + printf ' "html_count": %s\n' \ + "${_GENERATION_METADATA["generated_html_count"]}" + printf ' },\n' + printf ' "tarball": {\n' + printf ' "included": %s,\n' \ + "$(_json_bool "${_GENERATION_METADATA["tarball_included"]}")" + printf ' "file": %s\n' \ + "$(_json_string "${_GENERATION_METADATA["tarball_file"]}")" + printf ' },\n' + printf ' "settings": {\n' + printf ' "title": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_title"]}")" + printf ' "height": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_height"]}")" + printf ' "thumbheight": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_thumbheight"]}")" + printf ' "maxpreviews": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_maxpreviews"]}")" + printf ' "image_jobs": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_image_jobs"]}")" + printf ' "random_seed": %s,\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_random_seed"]}")" + printf ' "shuffle": %s,\n' \ + "$(_json_bool "${_GENERATION_METADATA["settings_shuffle"]}")" + printf ' "splash_page": %s,\n' \ + "$(_json_bool "${_GENERATION_METADATA["settings_splash_page"]}")" + printf ' "stats_page": %s,\n' \ + "$(_json_bool "${_GENERATION_METADATA["settings_stats_page"]}")" + printf ' "original_basepath": %s\n' \ + "$(_json_string "${_GENERATION_METADATA["settings_original_basepath"]}")" + printf ' }\n' + printf '}\n' +} + +write_generation_metadata() { + local -r tarball_file="$1"; shift + + _collect_generation_metadata "$tarball_file" + { + _generation_metadata_json + } > "$DIST_DIR/shuriken.json" +} + +# Empty the volatile EXIF cache (./cache/exif, parallel to ./dist) so a --force +# run re-runs `identify` from scratch. Done once up front; the cache then +# repopulates and is reused for the rest of the run (one identify per photo). +clear_exif_cache() { + local -r cache_dir="$(dirname "$DIST_DIR")/cache/exif" + + log_verbose "Force generation; clearing EXIF cache $cache_dir" + rm -rf "$cache_dir" +} + +dry_run() { + # shellcheck disable=SC2034 + local -A dry_run_plan=() + + collect_dry_run_plan dry_run_plan + print_dry_run_plan dry_run_plan +} + +collect_dry_run_page_plan() { + local -r plan_name="$1"; shift + # shellcheck disable=SC2178 + local -n plan_ref="$plan_name" + local -r image_count="$1"; shift + local -i page_count=0 + local -i redirect_count=0 + + plan_ref["html_index_count"]=1 + plan_ref["page_count"]=0 + plan_ref["redirect_count"]=0 + plan_ref["details_count"]=0 + + if (( image_count > 0 )); then + page_count=$(( (image_count + MAXPREVIEWS - 1) / MAXPREVIEWS )) + redirect_count=$(( page_count * 4 + 2 )) + plan_ref["details_count"]="$image_count" + plan_ref["page_count"]="$page_count" + plan_ref["redirect_count"]="$redirect_count" + fi +} + +collect_dry_run_plan() { + local -r plan_name="$1"; shift + # shellcheck disable=SC2178 + local -n plan_ref="$plan_name" + local -i image_count=0 + + image_count=$(count_incoming_images) + plan_ref=() + plan_ref["config_source"]="$SHURIKEN_CONFIG_SOURCE" + plan_ref["incoming_dir"]="$INCOMING_DIR" + plan_ref["dist_dir"]="$DIST_DIR" + plan_ref["template_dir"]="$TEMPLATE_DIR" + plan_ref["title"]="$TITLE" + plan_ref["height"]="$HEIGHT" + plan_ref["thumbheight"]="$THUMBHEIGHT" + plan_ref["maxpreviews"]="$MAXPREVIEWS" + plan_ref["image_jobs"]="$IMAGE_JOBS" + plan_ref["random_seed"]="$RANDOM_SEED" + plan_ref["shuffle"]="$SHUFFLE" + plan_ref["splash_page"]="$SPLASH_PAGE" + plan_ref["stats_page"]="$STATS_PAGE" + plan_ref["image_count"]="$image_count" + plan_ref["tarball_include"]="$TARBALL_INCLUDE" + plan_ref["tarball_name_plan"]='not planned' + + if [ "$TARBALL_INCLUDE" = yes ]; then + plan_ref["tarball_name_plan"]=$(tarball_name_plan) + fi + + collect_dry_run_page_plan "$plan_name" "$image_count" +} + +print_dry_run_plan() { + local -r plan_name="$1"; shift + # shellcheck disable=SC2178 + local -n plan_ref="$plan_name" + + printf 'Dry run: no files will be written.\n' + printf 'Config source: %s\n' "${plan_ref["config_source"]}" + printf 'Incoming directory: %s\n' "${plan_ref["incoming_dir"]}" + printf 'Output directory: %s\n' "${plan_ref["dist_dir"]}" + printf 'Template directory: %s\n' "${plan_ref["template_dir"]}" + printf 'Title: %s\n' "${plan_ref["title"]}" + printf 'Height: %s\n' "${plan_ref["height"]}" + printf 'Thumb height: %s\n' "${plan_ref["thumbheight"]}" + printf 'Max previews per page: %s\n' "${plan_ref["maxpreviews"]}" + printf 'Image jobs: %s\n' "${plan_ref["image_jobs"]}" + printf 'Random seed: %s\n' "${plan_ref["random_seed"]}" + printf 'Shuffle: %s\n' "${plan_ref["shuffle"]}" + printf 'Splash page: %s\n' "${plan_ref["splash_page"]}" + printf 'Stats page: %s\n' "${plan_ref["stats_page"]}" + printf 'Image count: %s\n' "${plan_ref["image_count"]}" + printf 'Tarball setting: %s\n' "${plan_ref["tarball_include"]}" + printf 'Tarball name plan: %s\n' "${plan_ref["tarball_name_plan"]}" + + printf 'Planned directories:\n' + printf ' %s\n' "${plan_ref["dist_dir"]}" + printf ' %s/photos\n' "${plan_ref["dist_dir"]}" + printf ' %s/thumbs\n' "${plan_ref["dist_dir"]}" + printf ' %s/blurs\n' "${plan_ref["dist_dir"]}" + + printf 'Planned generated files:\n' + if [ "${plan_ref["splash_page"]}" = yes ]; then + printf ' %s/index.html (%s splash page)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}" + else + printf ' %s/index.html (%s album index redirect)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["html_index_count"]}" + fi + printf ' %s/favicon.ico\n' "${plan_ref["dist_dir"]}" + printf ' %s/shuriken.json\n' "${plan_ref["dist_dir"]}" + printf ' %s/photos/* (%s image files)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" + printf ' %s/thumbs/* (%s image files)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" + printf ' %s/blurs/* (%s image files)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" + printf ' %s/page-*.html (%s preview pages)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["page_count"]}" + printf ' %s/[page]-[image].html (%s view pages)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["image_count"]}" + printf ' %s/[page]-[image]-details.html (%s details pages)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["details_count"]}" + printf ' %s/[redirect].html (%s navigation redirects)\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["redirect_count"]}" + if [ "${plan_ref["stats_page"]}" = yes ]; then + # The exact filter mini-album set needs EXIF aggregation, which dry-run + # does not perform, so list them as a wildcard under stats/. + printf ' %s/stats/index.html (EXIF stats page)\n' \ + "${plan_ref["dist_dir"]}" + printf ' %s/stats/*/ (filter mini-albums)\n' \ + "${plan_ref["dist_dir"]}" + fi + if [ "${plan_ref["tarball_include"]}" = yes ]; then + printf ' %s/%s\n' \ + "${plan_ref["dist_dir"]}" "${plan_ref["tarball_name_plan"]}" + fi +} diff --git a/src/lib/album-render.source.sh b/src/lib/album-render.source.sh new file mode 100644 index 0000000..ff86727 --- /dev/null +++ b/src/lib/album-render.source.sh @@ -0,0 +1,739 @@ +# Maps each album photo filename to its view-page basename ("-") +# as assigned during render_album_pages. The stats filter mini-albums read this +# so each view page can link "Details" to the album's own details page for the +# photo. Declared globally so it always exists for callers even when no album +# was rendered. +declare -gA ALBUM_VIEW_PAGE_BY_PHOTO=() + +album_photo_files() { + local -r photos_dir="$1"; shift + + find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ + | maybe_shuffle +} + +start_preview_page() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r page_name="$1"; shift + local -r header_bar="$1"; shift + local background_image + + background_image=$(randomphoto "$photos_dir" "$page_name") + template header "$page_name.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + blurs_dir "$blurs_dir" \ + background_image "$background_image" \ + show_header_bar "$header_bar" +} + +finish_preview_page() { + local -r page_name="$1"; shift + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + + template footer "$page_name.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + tarball_name "$tarball_name" +} + +finish_preview_page_with_next() { + local -r page_name="$1"; shift + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r next_page="$1"; shift + local -r prev_page="$1"; shift + + template next "$page_name.html" \ + html_dir "$html_dir" \ + next "$next_page" \ + prev "$prev_page" + finish_preview_page "$page_name" "$html_dir" "$backhref" "$tarball_name" +} + +render_previous_page_link() { + local -r page_name="$1"; shift + local -r html_dir="$1"; shift + local -r prev_page="$1"; shift + + template prev "$page_name.html" \ + html_dir "$html_dir" \ + prev "$prev_page" +} + +# Assemble one complete preview page (page-N.html) in a single call: header, +# optional previous-page link, every thumbnail in order, then the footer (with +# a next-page link unless this is the last page). The appends to the one page +# file happen here, in sequence, so the per-page ordering is preserved even when +# this whole function runs as one backgrounded render job (parallelism is only +# ACROSS pages, never within a page). The page's photos are the trailing +# positional arguments; each photo's preview index is its 1-based position. +render_full_preview_page() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r thumbs_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_name="$1"; shift + local -r page_num="$1"; shift + local -r header_bar="$1"; shift + local -r prev_page="$1"; shift + local -r next_page="$1"; shift + local -i preview_num=0 + local photo + + start_preview_page \ + "$photos_dir" "$html_dir" "$blurs_dir" "$backhref" "$page_name" \ + "$header_bar" + if [ -n "$prev_page" ]; then + render_previous_page_link "$page_name" "$html_dir" "$prev_page" + fi + + for photo in "$@"; do + (( ++preview_num )) + render_preview_thumbnail \ + "$page_name" "$html_dir" "$thumbs_dir" "$backhref" \ + "$page_num" "$preview_num" "$photo" + done + + if [ -n "$next_page" ]; then + finish_preview_page_with_next \ + "$page_name" "$html_dir" "$backhref" "$tarball_name" \ + "$next_page" "$prev_page" + else + finish_preview_page "$page_name" "$html_dir" "$backhref" "$tarball_name" + fi +} + +# Enqueue one complete preview page as a background render job, throttled to +# IMAGE_JOBS via the shared template render job pool. Mirrors +# queue_album_view_render_job: wait for a free slot, background the whole-page +# assembly, then track its pid/label so a failed job flips render_failed and +# makes generation fail loudly. The page's photos are passed as trailing +# positional args; the background subshell forks a private copy of them, so the +# caller is free to reuse its per-page accumulator for the next page. +queue_preview_page_render_job() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r thumbs_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_name="$1"; shift + local -r page_num="$1"; shift + local -r header_bar="$1"; shift + local -r prev_page="$1"; shift + local -r next_page="$1"; shift + # shellcheck disable=SC2178 + local -n render_job_pids_ref="$1"; shift + # Passed by name to wait_for_template_render_job_slot. + # shellcheck disable=SC2034,SC2178 + local -n render_job_statuses_ref="$1"; shift + # Passed by name to wait_for_template_render_job_slot and assigned below. + # shellcheck disable=SC2034,SC2178 + local -n render_job_labels_ref="$1"; shift + # shellcheck disable=SC2178 + local -n render_failed_ref="$1"; shift + # Remaining positional args ("$@") are this page's photos in order. + + wait_for_template_render_job_slot \ + render_job_pids_ref \ + render_job_statuses_ref \ + render_job_labels_ref \ + render_failed_ref + render_full_preview_page \ + "$photos_dir" \ + "$html_dir" \ + "$thumbs_dir" \ + "$blurs_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_name" \ + "$page_num" \ + "$header_bar" \ + "$prev_page" \ + "$next_page" \ + "$@" & + render_job_pids_ref+=("$!") + render_job_labels_ref["$!"]="template render job for preview $page_name" +} + +render_preview_thumbnail() { + local -r page_name="$1"; shift + local -r html_dir="$1"; shift + local -r thumbs_dir="$1"; shift + local -r backhref="$1"; shift + local -r page_num="$1"; shift + local -r preview_num="$1"; shift + local -r photo_file="$1"; shift + local animation_class + + animation_class=$(random_animation_css_class slow "$photo_file") + template preview "$page_name.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + thumbs_dir "$thumbs_dir" \ + page_num "$page_num" \ + preview_num "$preview_num" \ + photo "$photo_file" \ + animation_class "$animation_class" +} + +render_view_page() { + local -r html_dir="$1"; shift + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_num="$1"; shift + local -r preview_num="$1"; shift + local -r photo_file="$1"; shift + local animation_class + local exif_tooltip_text + + template header "$page_num-$preview_num.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + blurs_dir "$blurs_dir" \ + background_image "$photo_file" \ + show_header_bar 'no' + + animation_class=$(random_animation_css_class fast "$photo_file") + # Reuse the same EXIF tooltip as the details page so hovering the image in + # the normal view shows the camera/exposure summary (reads the shared + # identify cache, so no extra ImageMagick work). + exif_tooltip_text=$( + photo_exif_tooltip_text "$photo_file" "$INCOMING_DIR/$photo_file" + ) + template view "$page_num-$preview_num.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + photos_dir "$photos_dir" \ + page_num "$page_num" \ + preview_num "$preview_num" \ + photo "$photo_file" \ + animation_class "$animation_class" \ + exif_tooltip "$exif_tooltip_text" + template footer "$page_num-$preview_num.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + tarball_name "$tarball_name" +} + +render_details_page() { + local -r html_dir="$1"; shift + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_num="$1"; shift + local -r preview_num="$1"; shift + local -r photo_file="$1"; shift + local animation_class + local exif_details_html + local exif_tooltip_text + + template header "$page_num-$preview_num-details.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + blurs_dir "$blurs_dir" \ + background_image "$photo_file" \ + show_header_bar 'no' + + animation_class=$(random_animation_css_class fast "$photo_file") + exif_details_html=$( + photo_exif_details_html "$photo_file" "$INCOMING_DIR/$photo_file" + ) + exif_tooltip_text=$( + photo_exif_tooltip_text "$photo_file" "$INCOMING_DIR/$photo_file" + ) + template details "$page_num-$preview_num-details.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + photos_dir "$photos_dir" \ + page_num "$page_num" \ + preview_num "$preview_num" \ + photo "$photo_file" \ + animation_class "$animation_class" \ + exif_details "$exif_details_html" \ + exif_tooltip "$exif_tooltip_text" + template footer "$page_num-$preview_num-details.html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + tarball_name "$tarball_name" +} + +render_photo_view_and_details() { + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_num="$1"; shift + local -r preview_num="$1"; shift + local -r photo="$1"; shift + + render_view_page \ + "$html_dir" \ + "$photos_dir" \ + "$blurs_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_num" \ + "$preview_num" \ + "$photo" + render_details_page \ + "$html_dir" \ + "$photos_dir" \ + "$blurs_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_num" \ + "$preview_num" \ + "$photo" +} + +record_rendered_view_page() { + # shellcheck disable=SC2178 + local -n view_pages_ref="$1"; shift + # shellcheck disable=SC2178 + local -n last_views_ref="$1"; shift + local -r page="$1"; shift + local -r preview="$1"; shift + + if [ -z "${last_views_ref[$page]+set}" ]; then + view_pages_ref+=("$page") + fi + last_views_ref["$page"]="$preview" +} + +# Render every navigation redirect for a single view page (the prev/next +# wrap-around stubs that bounce N-0 / N-(last+1) to the neighbouring page). +# Each redirect is its own self-contained file (the template overwrites it), so +# this whole group is safe to run as one independent background job; only the +# files for distinct pages are produced here. The wrap-around redirects for the +# very last page (0-MAXPREVIEWS and the loop-to-1 links) are emitted as part of +# that page's group. +render_page_view_redirects() { + local -r html_dir="$1"; shift + local -ri page="$1"; shift + local -ri lastview="$1"; shift + local -ri max_page="$1"; shift + local -r prevredirect="${page}-0" + local -r nextredirect="${page}-$(( lastview + 1 ))" + + template redirect "$prevredirect.html" \ + html_dir "$html_dir" \ + redirect_page "$(( page - 1 ))-${MAXPREVIEWS}" + template redirect "$prevredirect-details.html" \ + html_dir "$html_dir" \ + redirect_page "$(( page - 1 ))-${MAXPREVIEWS}-details" + + if (( page == max_page )); then + template redirect "0-$MAXPREVIEWS.html" \ + html_dir "$html_dir" \ + redirect_page "${page}-$lastview" + template redirect "0-$MAXPREVIEWS-details.html" \ + html_dir "$html_dir" \ + redirect_page "${page}-$lastview-details" + template redirect "$nextredirect.html" \ + html_dir "$html_dir" \ + redirect_page '1-1' + template redirect "$nextredirect-details.html" \ + html_dir "$html_dir" \ + redirect_page '1-1-details' + else + template redirect "$nextredirect.html" \ + html_dir "$html_dir" \ + redirect_page "$(( page + 1 ))-1" + template redirect "$nextredirect-details.html" \ + html_dir "$html_dir" \ + redirect_page "$(( page + 1 ))-1-details" + fi +} + +# Render all navigation redirects in parallel, one background job per view page, +# throttled to IMAGE_JOBS via the shared template render job pool. This blocks +# until every redirect is on disk (and fails loudly if any job failed), so +# callers can rely on the redirects existing once it returns -- the redirect +# groups are mutually independent, so only timing changes, not output. +render_view_redirects() { + local -r html_dir="$1"; shift + # shellcheck disable=SC2178 + local -n view_pages_ref="$1"; shift + # shellcheck disable=SC2178 + local -n redirect_last_views_ref="$1"; shift + local max_page + local page + # Render job pool, throttled to IMAGE_JOBS by the job-pool helpers. + # shellcheck disable=SC2034 + local -a render_job_pids=() + # shellcheck disable=SC2034 + local -A render_job_statuses=() + # shellcheck disable=SC2034 + local -A render_job_labels=() + local -i render_failed=0 + + if (( ${#view_pages_ref[@]} == 0 )); then + return + fi + + max_page=${view_pages_ref[$(( ${#view_pages_ref[@]} - 1 ))]} + + for page in "${view_pages_ref[@]}"; do + wait_for_template_render_job_slot \ + render_job_pids render_job_statuses render_job_labels render_failed + render_page_view_redirects \ + "$html_dir" "$page" "${redirect_last_views_ref[$page]}" \ + "$max_page" & + render_job_pids+=("$!") + # shellcheck disable=SC2034 + render_job_labels["$!"]="template render job for redirect page $page" + done + + wait_for_template_render_jobs \ + render_job_pids render_job_statuses render_job_labels render_failed + if (( render_failed != 0 )); then + return 1 + fi +} + +render_album_index_redirect() { + local -r html_dir="$1"; shift + + template 'redirect' 'index.html' \ + html_dir "$html_dir" \ + redirect_page 'page-1' +} + +render_album_splash_page() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local html='index.html' + local photo + + if (( $# > 0 )); then + html="$1" + shift + fi + + photo=$(random_splash_photo "$photos_dir" "$blurs_dir") + template 'splash' "$html" \ + html_dir "$html_dir" \ + backhref "$backhref" \ + blurs_dir "$blurs_dir" \ + background_image "$photo" \ + photos_dir "$photos_dir" \ + photo "$photo" \ + enter_page 'page-1' +} + +render_album_index() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + + if [ "$SPLASH_PAGE" = yes ]; then + render_album_splash_page "$photos_dir" "$html_dir" "$blurs_dir" \ + "$backhref" + else + render_album_index_redirect "$html_dir" + fi +} + +album_page_name() { + local -r page_num="$1"; shift + + printf 'page-%s\n' "$page_num" +} + +queue_album_view_render_job() { + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -r page_num="$1"; shift + local -r preview_num="$1"; shift + local -r photo="$1"; shift + # shellcheck disable=SC2178 + local -n render_job_pids_ref="$1"; shift + # Passed by name to wait_for_template_render_job_slot. + # shellcheck disable=SC2034,SC2178 + local -n render_job_statuses_ref="$1"; shift + # Passed by name to wait_for_template_render_job_slot and assigned below. + # shellcheck disable=SC2034,SC2178 + local -n render_job_labels_ref="$1"; shift + # shellcheck disable=SC2178 + local -n render_failed_ref="$1"; shift + + wait_for_template_render_job_slot \ + render_job_pids_ref \ + render_job_statuses_ref \ + render_job_labels_ref \ + render_failed_ref + render_photo_view_and_details \ + "$photos_dir" \ + "$blurs_dir" \ + "$html_dir" \ + "$backhref" \ + "$tarball_name" \ + "$page_num" \ + "$preview_num" \ + "$photo" & + render_job_pids_ref+=("$!") + render_job_labels_ref["$!"]="template render job for photo $photo" +} + +wait_for_album_view_render_jobs() { + # shellcheck disable=SC2178 + local -n render_job_pids_ref="$1"; shift + # Passed by name to wait_for_template_render_jobs. + # shellcheck disable=SC2034,SC2178 + local -n render_job_statuses_ref="$1"; shift + # Passed by name to wait_for_template_render_jobs. + # shellcheck disable=SC2034,SC2178 + local -n render_job_labels_ref="$1"; shift + # shellcheck disable=SC2178 + local -n render_failed_ref="$1"; shift + + wait_for_template_render_jobs \ + render_job_pids_ref \ + render_job_statuses_ref \ + render_job_labels_ref \ + render_failed_ref + if (( render_failed_ref != 0 )); then + return 1 + fi +} + +# Group the album's photos into pages of at most MAXPREVIEWS, in their final +# (shuffled/sorted) order. The result is emitted one line per page as a +# tab-separated record "\t\t..." so the caller can walk +# pages without keeping every page in memory at once. Order is fully +# deterministic (album_photo_files already applies the seeded shuffle), so the +# downstream parallelism only changes timing, never which photo lands where. +album_page_records() { + local -r photos_dir="$1"; shift + local photo + local -i num=1 + local -i count=0 + local line='' + + while IFS= read -r photo; do + if (( count == MAXPREVIEWS )); then + printf '%d\t%s\n' "$num" "$line" + (( ++num )) + count=0 + line='' + fi + if (( count == 0 )); then + line="$photo" + else + line="$line"$'\t'"$photo" + fi + (( ++count )) + done < <(album_photo_files "$photos_dir") + + if (( count > 0 )); then + printf '%d\t%s\n' "$num" "$line" + fi +} + +# Per-photo bookkeeping shared by the album loop: queue the view+details render +# job, record the page/preview as a rendered view page (for redirect generation) +# and remember the photo -> view-page mapping for the stats mini-albums. Kept +# separate from the preview-page assembly so each concern stays small. +_album_record_view_photo() { + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r html_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + local -ri page_num="$1"; shift + local -ri preview_num="$1"; shift + local -r photo="$1"; shift + local -r pids_name="$1"; shift + local -r statuses_name="$1"; shift + local -r labels_name="$1"; shift + local -r failed_name="$1"; shift + local -r view_pages_name="$1"; shift + local -r last_views_name="$1"; shift + + queue_album_view_render_job \ + "$photos_dir" "$blurs_dir" "$html_dir" "$backhref" "$tarball_name" \ + "$page_num" "$preview_num" "$photo" \ + "$pids_name" "$statuses_name" "$labels_name" "$failed_name" + record_rendered_view_page "$view_pages_name" "$last_views_name" \ + "$page_num" "$preview_num" + # Read later by the stats filter mini-albums (render_filter_pages) for + # their Details links; shellcheck cannot see that cross-function use. + # shellcheck disable=SC2034 + ALBUM_VIEW_PAGE_BY_PHOTO["$photo"]="$page_num-$preview_num" +} + +render_album_pages() { + local -r photos_dir="$1"; shift + local -r html_dir="$1"; shift + local -r thumbs_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -r backhref="$1"; shift + local -r tarball_name="$1"; shift + + local header_bar + local name + local next_name + local page_num + local photo + local prev_name='' + local record + local -i preview_num + # Passed by name to the render-pool helpers below. + # shellcheck disable=SC2034 + local -i render_failed=0 + # shellcheck disable=SC2034 + local -a render_job_pids=() + # shellcheck disable=SC2034 + local -A render_job_labels=() + # shellcheck disable=SC2034 + local -A render_job_statuses=() + # Passed by name to record_rendered_view_page and render_view_redirects. + # shellcheck disable=SC2034 + local -A rendered_last_views=() + # shellcheck disable=SC2034 + local -a rendered_view_pages=() + local -a page_photos=() + local -a page_records=() + + # Rebuild the photo -> view-page map for this album from scratch so a + # re-generate (or a smaller incoming set) does not keep stale entries. The + # per-photo entries are written in _album_record_view_photo, which shellcheck + # cannot see from here. + # shellcheck disable=SC2034 + ALBUM_VIEW_PAGE_BY_PHOTO=() + + # Materialise the full deterministic page layout first so each preview page + # knows up front whether it has a following page (and thus a "next" link). + mapfile -t page_records < <(album_page_records "$photos_dir") + + for record in "${page_records[@]}"; do + page_num=${record%%$'\t'*} + name=$(album_page_name "$page_num") + # The first page shows the header bar; later pages do not (matches the + # previous start_preview_page calls). + if [ -z "$prev_name" ]; then + header_bar='yes' + else + header_bar='no' + fi + + # Split the tab-separated photo list for this page into an array. + IFS=$'\t' read -r -a page_photos <<< "${record#*$'\t'}" + + preview_num=0 + for photo in "${page_photos[@]}"; do + (( ++preview_num )) + _album_record_view_photo \ + "$photos_dir" "$blurs_dir" "$html_dir" "$backhref" \ + "$tarball_name" "$page_num" "$preview_num" "$photo" \ + render_job_pids render_job_statuses render_job_labels \ + render_failed rendered_view_pages rendered_last_views + done + + # A page has a "next" link unless it is the last record. + next_name='' + if (( page_num < ${#page_records[@]} )); then + next_name=$(album_page_name "$(( page_num + 1 ))") + fi + queue_preview_page_render_job \ + "$photos_dir" "$html_dir" "$thumbs_dir" "$blurs_dir" "$backhref" \ + "$tarball_name" "$name" "$page_num" "$header_bar" \ + "$prev_name" "$next_name" \ + render_job_pids render_job_statuses render_job_labels \ + render_failed \ + "${page_photos[@]}" + + prev_name="$name" + done + + if ! wait_for_album_view_render_jobs \ + render_job_pids \ + render_job_statuses \ + render_job_labels \ + render_failed; then + return 1 + fi + render_view_redirects "$html_dir" rendered_view_pages rendered_last_views + render_album_index "$photos_dir" "$html_dir" "$blurs_dir" "$backhref" +} + +splash_photo_files() { + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local photo + + while IFS= read -r photo; do + if [ -f "$DIST_DIR/$blurs_dir/$photo" ]; then + printf '%s\n' "$photo" + fi + done < <( + find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ + | sort + ) +} + +random_splash_photo() { + local -r photos_dir="$1"; shift + local -r blurs_dir="$1"; shift + local -i index + local photo + local -a photos=() + + while IFS= read -r photo; do + photos+=("$photo") + done < <(splash_photo_files "$photos_dir" "$blurs_dir") + + if (( ${#photos[@]} == 0 )); then + printf 'ERROR: No splash photos found in %s with matching blurs in %s\n' \ + "$(_display_path "$DIST_DIR/$photos_dir")" \ + "$(_display_path "$DIST_DIR/$blurs_dir")" >&2 + return 1 + fi + + index=$(random_index "photo:$photos_dir:splash" "${#photos[@]}") + printf '%s\n' "${photos[index]}" +} + +randomphoto() { + local -r photos_dir="$1"; shift + local -r context="${1:-$photos_dir}" + local -i index + local photo + local -a photos=() + + while IFS= read -r photo; do + photos+=("$photo") + done < <( + find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ + | sort + ) + + if (( ${#photos[@]} == 0 )); then + printf 'ERROR: No photos found in %s\n' \ + "$(_display_path "$DIST_DIR/$photos_dir")" >&2 + return 1 + fi + + index=$(random_index "photo:$photos_dir:$context" "${#photos[@]}") + printf '%s\n' "${photos[index]}" +} diff --git a/src/lib/album.source.sh b/src/lib/album.source.sh index 6c78ffa..e4b5e9d 100644 --- a/src/lib/album.source.sh +++ b/src/lib/album.source.sh @@ -1,1182 +1,9 @@ -# Maps each album photo filename to its view-page basename ("-") -# as assigned during render_album_pages. The stats filter mini-albums read this -# so each view page can link "Details" to the album's own details page for the -# photo. Declared globally so it always exists for callers even when no album -# was rendered. -declare -gA ALBUM_VIEW_PAGE_BY_PHOTO=() - -album_photo_files() { - local -r photos_dir="$1"; shift - - find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ - | maybe_shuffle -} - -start_preview_page() { - local -r photos_dir="$1"; shift - local -r html_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r backhref="$1"; shift - local -r page_name="$1"; shift - local -r header_bar="$1"; shift - local background_image - - background_image=$(randomphoto "$photos_dir" "$page_name") - template header "$page_name.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - blurs_dir "$blurs_dir" \ - background_image "$background_image" \ - show_header_bar "$header_bar" -} - -finish_preview_page() { - local -r page_name="$1"; shift - local -r html_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - - template footer "$page_name.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - tarball_name "$tarball_name" -} - -finish_preview_page_with_next() { - local -r page_name="$1"; shift - local -r html_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r next_page="$1"; shift - local -r prev_page="$1"; shift - - template next "$page_name.html" \ - html_dir "$html_dir" \ - next "$next_page" \ - prev "$prev_page" - finish_preview_page "$page_name" "$html_dir" "$backhref" "$tarball_name" -} - -render_previous_page_link() { - local -r page_name="$1"; shift - local -r html_dir="$1"; shift - local -r prev_page="$1"; shift - - template prev "$page_name.html" \ - html_dir "$html_dir" \ - prev "$prev_page" -} - -# Assemble one complete preview page (page-N.html) in a single call: header, -# optional previous-page link, every thumbnail in order, then the footer (with -# a next-page link unless this is the last page). The appends to the one page -# file happen here, in sequence, so the per-page ordering is preserved even when -# this whole function runs as one backgrounded render job (parallelism is only -# ACROSS pages, never within a page). The page's photos are the trailing -# positional arguments; each photo's preview index is its 1-based position. -render_full_preview_page() { - local -r photos_dir="$1"; shift - local -r html_dir="$1"; shift - local -r thumbs_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r page_name="$1"; shift - local -r page_num="$1"; shift - local -r header_bar="$1"; shift - local -r prev_page="$1"; shift - local -r next_page="$1"; shift - local -i preview_num=0 - local photo - - start_preview_page \ - "$photos_dir" "$html_dir" "$blurs_dir" "$backhref" "$page_name" \ - "$header_bar" - if [ -n "$prev_page" ]; then - render_previous_page_link "$page_name" "$html_dir" "$prev_page" - fi - - for photo in "$@"; do - (( ++preview_num )) - render_preview_thumbnail \ - "$page_name" "$html_dir" "$thumbs_dir" "$backhref" \ - "$page_num" "$preview_num" "$photo" - done - - if [ -n "$next_page" ]; then - finish_preview_page_with_next \ - "$page_name" "$html_dir" "$backhref" "$tarball_name" \ - "$next_page" "$prev_page" - else - finish_preview_page "$page_name" "$html_dir" "$backhref" "$tarball_name" - fi -} - -# Enqueue one complete preview page as a background render job, throttled to -# IMAGE_JOBS via the shared template render job pool. Mirrors -# queue_album_view_render_job: wait for a free slot, background the whole-page -# assembly, then track its pid/label so a failed job flips render_failed and -# makes generation fail loudly. The page's photos are passed as trailing -# positional args; the background subshell forks a private copy of them, so the -# caller is free to reuse its per-page accumulator for the next page. -queue_preview_page_render_job() { - local -r photos_dir="$1"; shift - local -r html_dir="$1"; shift - local -r thumbs_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r page_name="$1"; shift - local -r page_num="$1"; shift - local -r header_bar="$1"; shift - local -r prev_page="$1"; shift - local -r next_page="$1"; shift - # shellcheck disable=SC2178 - local -n render_job_pids_ref="$1"; shift - # Passed by name to wait_for_template_render_job_slot. - # shellcheck disable=SC2034,SC2178 - local -n render_job_statuses_ref="$1"; shift - # Passed by name to wait_for_template_render_job_slot and assigned below. - # shellcheck disable=SC2034,SC2178 - local -n render_job_labels_ref="$1"; shift - # shellcheck disable=SC2178 - local -n render_failed_ref="$1"; shift - # Remaining positional args ("$@") are this page's photos in order. - - wait_for_template_render_job_slot \ - render_job_pids_ref \ - render_job_statuses_ref \ - render_job_labels_ref \ - render_failed_ref - render_full_preview_page \ - "$photos_dir" \ - "$html_dir" \ - "$thumbs_dir" \ - "$blurs_dir" \ - "$backhref" \ - "$tarball_name" \ - "$page_name" \ - "$page_num" \ - "$header_bar" \ - "$prev_page" \ - "$next_page" \ - "$@" & - render_job_pids_ref+=("$!") - render_job_labels_ref["$!"]="template render job for preview $page_name" -} - -render_preview_thumbnail() { - local -r page_name="$1"; shift - local -r html_dir="$1"; shift - local -r thumbs_dir="$1"; shift - local -r backhref="$1"; shift - local -r page_num="$1"; shift - local -r preview_num="$1"; shift - local -r photo_file="$1"; shift - local animation_class - - animation_class=$(random_animation_css_class slow "$photo_file") - template preview "$page_name.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - thumbs_dir "$thumbs_dir" \ - page_num "$page_num" \ - preview_num "$preview_num" \ - photo "$photo_file" \ - animation_class "$animation_class" -} - -render_view_page() { - local -r html_dir="$1"; shift - local -r photos_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r page_num="$1"; shift - local -r preview_num="$1"; shift - local -r photo_file="$1"; shift - local animation_class - local exif_tooltip_text - - template header "$page_num-$preview_num.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - blurs_dir "$blurs_dir" \ - background_image "$photo_file" \ - show_header_bar 'no' - - animation_class=$(random_animation_css_class fast "$photo_file") - # Reuse the same EXIF tooltip as the details page so hovering the image in - # the normal view shows the camera/exposure summary (reads the shared - # identify cache, so no extra ImageMagick work). - exif_tooltip_text=$( - photo_exif_tooltip_text "$photo_file" "$INCOMING_DIR/$photo_file" - ) - template view "$page_num-$preview_num.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - photos_dir "$photos_dir" \ - page_num "$page_num" \ - preview_num "$preview_num" \ - photo "$photo_file" \ - animation_class "$animation_class" \ - exif_tooltip "$exif_tooltip_text" - template footer "$page_num-$preview_num.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - tarball_name "$tarball_name" -} - -photo_cache_signature() { - local -r photo="$1"; shift - local -r photo_path="$1"; shift - local stat_output - - stat_output=$(stat -c '%s:%Y' "$photo_path") - printf '%s:%s\n' "$photo" "$stat_output" -} - -print_cached_photo_identify_output() { - local -r cache_file="$1"; shift - local line - local skipped_signature=no - - while IFS= read -r line || [ -n "$line" ]; do - if [ "$skipped_signature" = no ]; then - skipped_signature=yes - continue - fi - printf '%s\n' "$line" - done < "$cache_file" -} - -cached_photo_identify_output() { - local -r photo="$1"; shift - local -r photo_path="$1"; shift - local cache_dir - local cache_file - local cached_signature='' - local current_signature - - # Persist the EXIF cache in a volatile ./cache directory parallel to ./dist - # (the staging dir is a sibling of the final dist, so dirname "$DIST_DIR" is - # the working dir in both staging and direct contexts). Keeping it outside - # dist means it survives a fresh/cleared dist and is never deployed, so an - # unchanged photo skips the slow `identify -verbose` on every regenerate. - cache_dir="$(dirname "$DIST_DIR")/cache/exif" - cache_file="$cache_dir/$photo.txt" - current_signature=$(photo_cache_signature "$photo" "$photo_path") - - # Reuse the cache when its signature still matches the source file. --force - # is handled once up front by clear_exif_cache (which empties this directory), - # so the first call per photo then rebuilds it and the rest of the run reuses - # it -- exactly one identify per photo even under force. - if [ -f "$cache_file" ]; then - IFS= read -r cached_signature < "$cache_file" || true - if [ "$cached_signature" = "$current_signature" ]; then - print_cached_photo_identify_output "$cache_file" - return - fi - fi - - mkdir -p "$cache_dir" - printf '%s\n' "$current_signature" > "$cache_file" - imagemagick_identify -verbose "$photo_path" >> "$cache_file" 2>/dev/null \ - || true - print_cached_photo_identify_output "$cache_file" -} - -photo_exif_details_html() { - local -r photo="$1"; shift - local -r photo_path="$1"; shift - local key - local key_html - local line - local value - local value_html - local -i exif_count=0 - - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then - key="exif:${BASH_REMATCH[1]}" - value="${BASH_REMATCH[2]}" - key_html=$(_html_escape "$key") - value_html=$(_html_escape "$value") - - if (( exif_count == 0 )); then - printf '\n' - printf '\n' - fi - - printf '\n' \ - "$key_html" \ - "$value_html" - (( ++exif_count )) - fi - done < <(cached_photo_identify_output "$photo" "$photo_path") - - if (( exif_count == 0 )); then - printf '

No EXIF details available.

\n' - return - fi - - printf '\n' - printf '
%s%s
\n' -} - -_first_exif_value_to() { - local -n output_ref="$1"; shift - local -n exif_ref="$1"; shift - local key - - output_ref='' - for key in "$@"; do - if [ -n "${exif_ref[$key]:-}" ]; then - # shellcheck disable=SC2034 - output_ref="${exif_ref[$key]}" - return - fi - done -} - -_photo_exif_values_to() { - local -n output_ref="$1"; shift - local -r photo="$1"; shift - local -r photo_path="$1"; shift - local line - - output_ref=() - while IFS= read -r line; do - if [[ "$line" =~ ^[[:space:]]*exif:([^:]+):[[:space:]]*(.*)$ ]]; then - # output_ref writes to the caller-provided associative array. - # shellcheck disable=SC2034 - output_ref["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" - fi - done < <(cached_photo_identify_output "$photo" "$photo_path") -} - -_photo_exif_tooltip_text_from_values() { - local -r exif_name="$1"; shift - local -n values_ref="$exif_name" - local aperture - local camera - local date_time - local iso - local key - local make - local model - local separator='' - local shutter_speed - local -a tooltip_parts=() - - make="${values_ref[Make]:-}" - model="${values_ref[Model]:-}" - camera="$make" - if [ -n "$model" ]; then - if [ -n "$make" ]; then - case "$model" in - "$make"|"$make "*) - camera="$model" - ;; - *) - camera="$make $model" - ;; - esac - else - camera="$model" - fi - fi - - _first_exif_value_to aperture "$exif_name" FNumber ApertureValue - _first_exif_value_to iso "$exif_name" \ - ISOSpeedRatings PhotographicSensitivity ISO - _first_exif_value_to shutter_speed "$exif_name" \ - ExposureTime ShutterSpeedValue - _first_exif_value_to date_time "$exif_name" \ - DateTimeOriginal DateTimeDigitized DateTime - - if [ -n "$camera" ]; then - tooltip_parts+=("Camera: $camera") - fi - if [ -n "$aperture" ]; then - tooltip_parts+=("Aperture: $aperture") - fi - if [ -n "$iso" ]; then - tooltip_parts+=("ISO: $iso") - fi - if [ -n "$shutter_speed" ]; then - tooltip_parts+=("Shutter speed: $shutter_speed") - fi - if [ -n "$date_time" ]; then - tooltip_parts+=("Taken: $date_time") - fi - - for key in "${tooltip_parts[@]}"; do - printf '%s%s' "$separator" "$key" - separator='; ' - done - if (( ${#tooltip_parts[@]} > 0 )); then - printf '\n' - fi -} - -photo_exif_tooltip_text() { - local -r photo="$1"; shift - local -r photo_path="$1"; shift - # exif_values is populated and read through nameref helpers. - # shellcheck disable=SC2034 - local -A exif_values=() - - _photo_exif_values_to exif_values "$photo" "$photo_path" - _photo_exif_tooltip_text_from_values exif_values -} - -render_details_page() { - local -r html_dir="$1"; shift - local -r photos_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r page_num="$1"; shift - local -r preview_num="$1"; shift - local -r photo_file="$1"; shift - local animation_class - local exif_details_html - local exif_tooltip_text - - template header "$page_num-$preview_num-details.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - blurs_dir "$blurs_dir" \ - background_image "$photo_file" \ - show_header_bar 'no' - - animation_class=$(random_animation_css_class fast "$photo_file") - exif_details_html=$( - photo_exif_details_html "$photo_file" "$INCOMING_DIR/$photo_file" - ) - exif_tooltip_text=$( - photo_exif_tooltip_text "$photo_file" "$INCOMING_DIR/$photo_file" - ) - template details "$page_num-$preview_num-details.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - photos_dir "$photos_dir" \ - page_num "$page_num" \ - preview_num "$preview_num" \ - photo "$photo_file" \ - animation_class "$animation_class" \ - exif_details "$exif_details_html" \ - exif_tooltip "$exif_tooltip_text" - template footer "$page_num-$preview_num-details.html" \ - html_dir "$html_dir" \ - backhref "$backhref" \ - tarball_name "$tarball_name" -} - -create_photo_derivatives() { - local -r photos_dir="$1"; shift - local -r thumbs_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r photo="$1"; shift - local dirname - local height - - if [[ -f "$DIST_DIR/$thumbs_dir/$photo" \ - && -f "$DIST_DIR/$blurs_dir/$photo" ]]; then - log_verbose "Skipped existing thumb and blur" \ - "$(_display_path "$DIST_DIR/$thumbs_dir/$photo") and" \ - "$(_display_path "$DIST_DIR/$blurs_dir/$photo")" - return - fi - - dirname="$DIST_DIR/$thumbs_dir" - mkdir -p "$dirname" - log_info "Creating thumb $(_display_path "$DIST_DIR/$thumbs_dir/$photo")" - # Double the height, as CSS scales images based on boxing too. - height=$(( THUMBHEIGHT * 2 )) - imagemagick \ - "$DIST_DIR/$photos_dir/$photo" \ - -geometry "x$height" \ - "$DIST_DIR/$thumbs_dir/$photo" - - dirname="$DIST_DIR/$blurs_dir" - mkdir -p "$dirname" - log_info "Creating blur $(_display_path "$DIST_DIR/$blurs_dir/$photo")" - imagemagick \ - "$DIST_DIR/$thumbs_dir/$photo" \ - -flip \ - -blur 0x8 \ - "$DIST_DIR/$blurs_dir/$photo" -} - -create_all_photo_derivatives() { - local -r photos_dir="$1"; shift - local -r thumbs_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -i failed=0 - local -a image_job_pids=() - # Passed by name to wait_for_image_job_slot and wait_for_image_jobs. - # shellcheck disable=SC2034 - local -A image_job_labels=() - # Passed by name to wait_for_image_job_slot and wait_for_image_jobs. - # shellcheck disable=SC2034 - local -A image_job_statuses=() - local photo - - while IFS= read -r photo; do - wait_for_image_job_slot \ - image_job_pids \ - image_job_statuses \ - image_job_labels \ - failed - create_photo_derivatives "$photos_dir" "$thumbs_dir" "$blurs_dir" \ - "$photo" & - image_job_pids+=("$!") - # Read through a nameref in the job-pool helpers. - # shellcheck disable=SC2034 - image_job_labels["$!"]="image derivative job for photo $photo" - done < <( - find "$DIST_DIR/$photos_dir" -maxdepth 1 -type f -printf '%f\n' \ - | sort - ) - - wait_for_image_jobs \ - image_job_pids \ - image_job_statuses \ - image_job_labels \ - failed - if (( failed != 0 )); then - return 1 - fi -} - -render_photo_view_and_details() { - local -r photos_dir="$1"; shift - local -r blurs_dir="$1"; shift - local -r html_dir="$1"; shift - local -r backhref="$1"; shift - local -r tarball_name="$1"; shift - local -r page_num="$1"; shift - local -r preview_num="$1"; shift - local -r photo="$1"; shift - - render_view_page \ - "$html_dir" \ - "$photos_dir" \ - "$blurs_dir" \ - "$backhref" \ - "$tarball_name" \ - "$page_num" \ - "$preview_num" \ - "$photo" - render_details_page \ - "$html_dir" \ - "$photos_dir" \ - "$blurs_dir" \ - "$backhref" \ - "$tarball_name" \ - "$page_num" \ - "$preview_num" \ - "$photo" -} - -record_rendered_view_page() { - # shellcheck disable=SC2178 - local -n view_pages_ref="$1"; shift - # shellcheck disable=SC2178 - local -n last_views_ref="$1"; shift - local -r page="$1"; shift - local -r preview="$1"; shift - - if [ -z "${last_views_ref[$page]+set}" ]; then - view_page