summaryrefslogtreecommitdiff
path: root/src/lib/album-render.source.sh
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-16 18:19:51 +0300
committerPaul Buetow <paul@buetow.org>2026-06-16 18:19:51 +0300
commitfe59a701586972793ea110b465f65d47b825fb8e (patch)
tree87ca3f64b695a0a8fa63d578be1aa4937bb59e01 /src/lib/album-render.source.sh
parentcd7cf797bc633700c5ee7e986077f366c000f0b9 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'src/lib/album-render.source.sh')
-rw-r--r--src/lib/album-render.source.sh739
1 files changed, 739 insertions, 0 deletions
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 ("<page>-<preview>")
+# 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 "<page_num>\t<photo>\t<photo>..." 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]}"
+}