#!/bin/bash
#
# This script scans the given image files (JPEG or other formats),
# generates a thumbnail and a reduced
# version in JPEG of each, and generates an HTML page with all the
# thumbnails and links to the reduced version and the original.
#
# If the image contains PhotoRDF (wrapped in Adobe's XMP), it is used
# to generate a description.
#
# The HTML page with thumbnails is written to the first argument:
#
#    thumbnails index.html *.jpg
#
# Options: see the function "help" below.
#
# This script checks for the existence of some external programs
# (e.g., to convert images to JPEG, or to process XMP) and some
# functions and some image formats may not be supported if no suitable
# software is found.
#
# TO DO: handle file names with spaces.
#
# Author: Bert Bos <bert@w3.org>
# Created: 8 July 1999
# Version: $Date: 2025/06/17 14:47:45 $
#
# Copyright (c) 1995-2025 World Wide Web Consortium. All Rights
# Reserved. See
# http://www.w3.org/Consortium/Legal/copyright-software.html

set -o nounset

progname="${0##*/}"

# die -- print error message and exit
function die { echo "$progname: error: $*" >&2; exit 1; }

# Other parameters
#
w1=100				# Width of thumbnail
smallwd=640			# Max. width of smaller version
smallht=480			# Max. height of smaller version
thdir=thumb			# Name of thumbnail directory
smdir=small			# Name of smaller images directory
pagetitle="Thumbnails"		# Unless overridden by "-t"
language="en"			# Language of generated HTML pages
owner="Bert Bos"		# Copyright holder
email="bert@w3.org"		# Copyright holder's email or URL
preamble=			# Unless overridden by -p
style=				# Unless overridden by -e
interactive=false		# Whether to prompt for title, language, etc.
group=				# Group by series/year/month/day/nothing
samegrouping=false		# Try to use the grouping of the existing index
crop=100			# Percentage of image to show in thumbnail
up=".."				# Where the "up" link points to
xmplink=static			# Put the XMP data from each image in a file
htaccess=false			# Don't write a .htaccess file
prevlink=			# URL for <link rel=prev>
nextlink=			# URL for <link rel=next>
osm_zoom=-1			# Zoom for link to OpenStreetMap (-1 = no link)
declare -i max_procs=1		# Max # of parallel processes
trust_ext=false			# Use file(1) command or file extension
caption=127			# Bitset of what to put in the thumbnail caption
output=				# Output file name, "-" for stdout
OSM='http://www.openstreetmap.org/' # Base URL for Web-based OSM map
copyrightyear=`date +%Y`	# Current year for copyright

# Some constants for the interactive dialogs
#
d_backtitle="Thumbnails RDF/XMP"

# Arrays of command line arguments and metadata for each argument.
#
declare -a args smallnames thumbnames \
description coverage data description coverage date \
title identifier creator contributor contributor relation rights \
subject camera lens film devel_date latitude longitude auxlens size \
create_date city country state


# have -- check that program $1 exists
function have { type -t "$1" >/dev/null; }


# We need dc (for the latitude/longitude conversion).
#
if have dc; then :;
else die "Required program dc not found."
fi

# Constants (RDF field names). Note: Between rdfpic 2.0 and 2.1, we
# changed the names of the properties to lowercase, as is the
# convention for RDF.
#
dc='http://purl.org/dc/elements/1.1/'
tc='http://www.w3.org/2000/PhotoRDF/technical-1-0#'
exif="http://ns.adobe.com/exif/1.0/"
tiff='http://ns.adobe.com/tiff/1.0/'
xap='http://ns.adobe.com/xap/1.0/'
aux="http://ns.adobe.com/exif/1.0/aux/"
photoshop="http://ns.adobe.com/photoshop/1.0/"
# iptc="http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/"
dcDescription="${dc}description"
dcCoverage="${dc}coverage"
dcDate="${dc}date"
dcTitle="${dc}title"
dcIdentifier="${dc}identifier"
dcCreator="${dc}creator"
dcContributor="${dc}contributor"
dcRelation="${dc}relation"
dcRights="${dc}rights"
dcSubject="${dc}subject"
tcCamera="${tc}camera"
tcLens="${tc}lens"
tcFilm="${tc}film"
tcDevelDate="${tc}devel-date"
exifLatitude="${exif}GPSLatitude"
exifLongitude="${exif}GPSLongitude"
tiffMake="${tiff}Make"
tiffModel="${tiff}Model"
# exifDateTimeOriginal="${exif}DateTimeOriginal"
xapCreateDate="${xap}CreateDate"
auxLens="${aux}Lens"
photoshopcountry="${photoshop}Country"
photoshopstate="${photoshop}State"
photoshopcity="${photoshop}City"

# Localization
languages=(\
 "en"\
 "nl"\
 "fr"\
 "de")
Legend=(\
 "Legend"\
 "Legenda"\
 "L&eacute;gende"\
 "Legende")
Large=(\
 "large image"\
 "grote foto"\
 "grande photo"\
 "gro&szlig;e Foto")
Small=(\
 "small image (max ${smallwd}&times;${smallht})"\
 "kleine foto (max ${smallwd}&times;${smallht})"\
 "petite photo (${smallwd}&times;${smallht} max)"\
 "kleine Foto")
Raw=(\
 "raw RDF/XMP metadata"\
 "ruwe RDF/XMP metadata"\
 "metadata RDF/XMP cru"\
 "rohe RDF/XMP metadata")
Previous=(\
 "Previous"\
 "Vorige"\
 "Pr&eacute;c&eacute;dent"\
 "Vorige")
NoPrevious=(\
 "No previous"\
 "Geen vorige"\
 "Pas de pr&eacute;c&eacute;dent"\
 "Kein vorige")
Index=(\
 "Index"\
 "Index"\
 "Index"\
 "Index")
Next=(\
 "Next"\
 "Volgende"\
 "Suivant"\
 "N&auml;chste")
NoNext=(\
 "No next"\
 "Geen volgende"\
 "Pas de suivant"\
 "Kein n&auml;chste")
Image=(\
 "[image]"\
 "[foto]"\
 "[photo]"\
 "[Foto]")
Date=(\
 "Date:"\
 "Datum:"\
 "Date&nbsp;:"\
 "Datum:")
CreateDate=(\
 "Digitized:"\
 "Gedigitaliseerd:"\
 "Numérisé&nbsp;:"\
 "Digitalisiert:")
Place=(\
 "Place:"\
 "Plaats:"\
 "Lieu&nbsp;:"\
 "Ort:")
Series=(\
 "Series:"\
 "Serie:"\
 "S&eacute;rie&nbsp;:"\
 "Serie:")
Number=(\
 "Number:"\
 "Nummer:"\
 "Num&eacute;ro&nbsp;:"\
 "Nummer:")
Creator=(\
 "Photographer:"\
 "Fotograaf:"\
 "Photographe&nbsp;:"\
 "Fotograf:")
Contributor=(\
 "Contributor:"\
 "Medewerker:"\
 "Collaborateur:"\
 "Mitarbeiter:")
Rights=(\
 "Rights:"\
 "Rechten:"\
 "Droits&nbsp;:"\
 "Rechte:")
Keywords=(\
 "Keywords:"\
 "Trefwoorden:"\
 "Mots&nbsp;clés&nbsp;:"\
 "Schl&uuml;sselw&ouml;rter:")
Size=(\
 "Size:"\
 "Grootte:"\
 "Taille&nbsp;:"\
 "Gr&ouml;&szlig;e:")
Camera=(\
 "Camera:"\
 "Camera:"\
 "Appareil photo&nbsp;:"\
 "Kamera:")
Lens=(\
 "Lens:"\
 "Lens:"\
 "Objectif&nbsp;:"\
 "Objektiv:")
Film=(\
 "Film:"\
 "Film:"\
 "Pellicule&nbsp;:"\
 "Film:")
DevelDate=(\
 "Development date:"\
 "Ontwikkeldatum:"\
 "Date de developpement&nbsp;:"\
 "Entwiklungsdatum:")
Latitude=(\
 "Latitude:"\
 "Breedtegraad:"\
 "Latitude&nbsp;:"\
 "Geographische Breite:")
Longitude=(\
 "Longitude:"\
 "Lengtegraad:"\
 "Longitude&nbsp;:"\
 "Geographische Länge:")
Unknown=(\
 "Unkown"\
 "Onbekend"\
 "Inconnu"\
 "Unbekannt")

# A directory for temporary files. The directory will be removed when
# the script exits, so no need to remove temporary files after use.
#
TEMPDIR=`mktemp -d /tmp/thumb-XXXXXX 2>&1` || \
  die "Cannot create temporary directory"
trap "rm -rf $TEMPDIR" 0


# usage -- print usage message and exit
function usage
{
  echo "Usage: $progname [-ihgxyMdgGae] [-t title] [-l language] [-n owner] [-m e-mail] [-c percent] [-w width] [-p file] [-s style-link] [-U up-link] [-P prev-link] [-N next-link] [-z OSM-zoom] [-C caption-set] [-j jobs] output image [image...]"
  exit 1
}


# help -- explain the options
function help
{
  if have fmt; then fmt -w ${COLUMNS:-75}; else cat; fi <<EOF
$progname -- create thumbnails, description pages and an index of images

Options and arguments:
  -l language
	Output HTML in the given language. Currently supported
	are "en" (English), "fr" (French), "nl" (Dutch) and
	"de" (German). Default: en
  -t title
	Title of the generated thumbnails page. May contain
	inline mark-up. Default: "Thumbnails"
  -n name
	The name of the copyright owner, which will be printed at
	the bottom of the page.
	Default: "Bert Bos"
  -m email
	The email or URL of the copyright owner.
	Default: bert@w3.org
  -c percent
	Try to make thumbnails better by showing only the center
	of the image. 100 means the whole image is used
	(default), 80 means the image's four edges are cut off
	such that only 80% of its original width and height
	remain
  -p file
	Insert the contents of file above the thumbnails.
	Must be HTML that is valid in BODY.
	Default: none
  -w number
	Width or height (whichever is larger) of the thumbnails,
	in pixels.
	Default: 100
  -s style-link
	URI reference of an external style sheet. If present, all
	generated pages (thumbnail page and individual photo pages)
	will have a <link rel=stylesheet> with this URI. Otherwise,
	the script will insert its own built-in style.
  -g
	Group the thumbnails by series (dc:relation), add a subtitle
	before each group and a menu of series at the top.
  -G
	Group the thumbnails in the same way as in the existing index,
	overiding the options -g, -y, -M and -d. If the index does not
	exist, group according to the other options.
  -y
	Group the thumbnails by year (dc:date), add a subtitle
	before each group and a menu of years at the top.
	(-g, -y, -M and -d are mutually exclusive.)
  -M
	Group the thumbnails by month (dc:date), add a subtitle
	before each group and a menu of months at the top.
	(-g, -y, -M and -d are mutually exclusive.)
  -d
	Group the thumbnails by date (dc:date), add a subtitle
	before each group and a menu of days at the top.
	(-g, -y, -M and -d are mutually exclusive.)
  -C list
	What to include in the captions of the thumbnails on the index
	page. The list consists of numbers or keywords, separated by
	commas or spaces. The keywords and their equivalent numbers
	are: title or 1, number or 2, description or 4, images or 8,
	place or 16, date or 32, metadata or 64, file or 128. The
	keywords may be abbreviated. Other numbers are interpreted as
	the sum of the numbers above. E.g., "-C 7" means the same as
	"-C 1,2,4" or "-C t,n,de". Default: 127 (the sum of all of
	the above except file).
  -x
	Don't make a separate file with the XMP data, but assume the
	server can extract the XMP on request. (The Jigsaw server can
	do this, e.g.)
  -a
	Create or update the .htaccess file (for an Apache server)
	with some lines so that .jpg and .xmp files are served with
	the right media types.
  -U URL
	URI reference of a document that will be the "up" link from
	the thumbnails page. The default is "..". This will insert
	<link rel=up href=URL> in the thumbnails page.
  -P URL
	URI reference of a document that will be the "previous"
	document for the thumbnails page. No default. If present, this
	adds a <link rel=prev href=URL> to the thumbnails page.
  -N URL
	Analogous. If present, this adds a <link rel=next href=URL> to
	the thumbnails page.
  -z OSM-zoomlevel
	If a photo has latitude/longitude information, a "geo:" link
	is added. With a value for -z of 0 or more, a link to
	OpenStreetMap is added instead, with the given zoom level.
	Default: -1 (= add a "geo:" link).
  -j jobs
	Maximum number of parallel processes
  -e
	Rely on file extensions instead of calling the file(1) program
	to determine the format of each image.
  -i
	Interactive. The user will be prompted for language,
	title, name of owner, output file and image files.
  -h
	Help. Shows this text.
  output
	The name of the HTML file which will contain the
	thumbnails. Use "-" to print to standard output.
  image [image...]
	All remaining arguments are interpreted as image files to
	be processed. The program will create a small version
	(max. 640x480) in the directory "small", unless the
	original is already smaller than that. And it will create
	a thumbnail (max. 100x100) in the directory "thumb". It
	also creates one HTML file for each image file with the
	small version of the image and the description. If there
	is no description in XMP format inside the JPEG,
	the description will consist of just the file name.
EOF
  exit;
}


# lock -- wait for exclusive access to file $1
function lock { until ln -s $$ "$1.lock" 2>/dev/null; do sleep 0.02; done; }


# unlock -- release exclusive access to file $1
function unlock { rm "$1.lock"; }


# semaphore-p -- decrement semaphore $1 by $2 (default 1)
function semaphore-p
{
  local -i n v=${2:-1}
  until lock "$1"; n=$(< "$1"); ((n >= v)); do unlock "$1"; sleep 0.1; done
  echo -n $((n - v)) >"$1"
  unlock "$1"
}


# semaphore-v -- increment semaphore $1 by $2 (default 1)
function semaphore-v
{
  lock "$1"
  local -i n=$(< "$1") v=${2:-1}
  echo -n $((n + v)) >"$1"
  unlock "$1"
}


# semaphore-new -- return a semaphore set to $1 (default 1) in $2 (default /tmp)
function semaphore-new
{
  local d
  if ! d=$(mktemp -d ${2:-/tmp}/s-XXXXX) 2>/dev/null; then return 1; fi
  if ! echo -n ${1:-1} >"$d/semaphore"; then rm -rf "$d"; return 1; fi
  echo "$d/semaphore"
}


# semaphore-delete -- delete a semaphore
function semaphore-delete { rm -f "$1" "$1.lock"; }


# get-date -- extract a year, month (yyyy-mm) or day (yyyy-mm-dd) from a string
function get-date
{
  local s=$1 what=$2 year month day

  if [[ "$s" =~ ([0-9][0-9][0-9][0-9])(-([0-9][0-9])(-([0-9][0-9]))?)? ]]; then
    case "$what" in
      year)
	echo ${BASH_REMATCH[1]};;
      month)
	echo ${BASH_REMATCH[1]}-${BASH_REMATCH[3]:-00};;
      day)
	echo ${BASH_REMATCH[1]}-${BASH_REMATCH[3]:-00}-${BASH_REMATCH[5]:-00};;
      *)
	die "Bug: parameter 2 of ${FUNCNAME[0]} must be year, month or day";;
    esac
  else
    echo ''			# No year found
  fi
}


# expand-links -- find text that looks like a URL and wrap it in <a></a>
function expand-links
{
  sed \
    -e 's|[Mm][Aa]Ii][Ll][Tt][Oo]:[^ ()"'\'']*|<a href="&">&</a>|g' \
    -e 's|[Uu][Rr][Nn]:[^ ()"'\'']*|<a href="&">&</a>|g' \
    -e 's|[A-Za-z][A-Za-z]*://[^ ()"'\'']*|<a href="&">&</a>|g' \
    <<<"$1"
}


# esc -- escape HTML's special characters in $1
function esc
{
  local h=${1//&/\&amp;}
  h=${h//</\&lt;}
  h=${h//>/\&gt;}
  h=${h//\"/\&quot;}
  echo "$h"
}


# abs_path -- return absolute path for file $1
function abs_path
{
  local r

  # realpath(1) is not available by default on MacOS, so define our
  # own replacement.
  cd `dirname "$1"`
  r=$PWD/${1##*/}
  cd $OLDPWD
  echo "$r"
}


# get_image_dimension -- return the width x height of a JPEG image
if have gm; then
  function get_image_dimension
  {
    gm identify -format '%wx%h' "$1"
  }
elif have convert; then
  function get_image_dimension
  {
    identify -format '%wx%h' "$1"
  }
elif have anytopnm; then
  function get_image_dimension
  {
    local fileinfo=`anytopnm "$1" | pnmfile`
    fileinfo=${fileinfo#*,}
    local -i w=${fileinfo%by*}
    fileinfo=${fileinfo#*by}
    local -i h=${fileinfo%maxval*}
    echo ${w}x${h}
  }
else
  die "Neither graphicsmagick, imagemagick nor netpbm found. Cannot determine the width/height of a JPEG"
fi


# scan-xmp -- extract XMP from an arbitrary file
if have xmp-scan; then
  function scan-xmp { xmp-scan "$1"; }
elif have exempi; then
  function scan-xmp
  {
    exempi -x -p "$1" | sed -n -e '/<\?xpacket begin/,/<\?xpacket end/p'
  }
else
  function scan-xmp
  {
    # tr is needed because of a bug in GNU sed 4.1: "." doesn't match
    # null bytes; and because it makes sed faster
    LC_ALL=C tr '\000' '\n' <"$1" | \
      LC_ALL=C sed \
    	    -e '/<?xpacket [^>]*W5M0MpCehiHzreSzNTczkc9d/,/<?xpacket end/!d' \
  	    -e 's/.*<?xpacket begin/<?xpacket begin/' \
  	    -e "s/<?xpacket end=['\"][rw]['\"]?>.*/<?xpacket end='w'?>/"
  }
fi


# extract_xmp_from_jpeg -- extract XMP from a JPEG file
if have rdjpgxmp; then
  function extract_xmp_from_jpeg { rdjpgxmp "$1"; }
elif have exiftool; then
  function extract_xmp_from_jpeg { exiftool -xmp -b "$1"; }
else
  function extract_xmp_from_jpeg { scan-xmp "$1"; }
fi


# extract_xmp_from_svg -- extract XMP from an SVG file
function extract_xmp_from_svg { scan-xmp "$1"; }


# extract_xmp_from_raw -- extract XMP from a camera raw file
function extract_xmp_from_raw { scan-xmp "$1"; }


# extract_xmp_from_ps -- extract XMP from a PDF or (Encapsulated) PostScript
function extract_xmp_from_ps { scan-xmp "$1"; }


# extract_xmp_from_png -- extract XMP from a PNG file
if have xmp-scan; then
  function extract_xmp_from_png { xmp-scan "$1"; }
elif have exiftool; then
  function extract_xmp_from_png { exiftool -xmp -b "$1"; }
else
  function extract_xmp_from_png { scan-xmp "$1"; }
fi


# extract_xmp_from_tiff -- extract XMP from a TIFF file
function extract_xmp_from_tiff { scan-xmp "$1"; }


# extract_xmp_from_webp -- extract XMP from a WebP file
if have webpmux; then
  function extract_xmp_from_webp { webpmux -get xmp -o - -- "$1" 2>/dev/null; }
elif have exiv2; then
  function extract_xmp_from_webp { exiv2 -pX -- "$1"; }
else
  function extract_xmp_from_webp { scan-xmp "$1"; }
fi


# xmp_to_key_value -- extract the interesting metadata for image $1 into arrays
if have xmptool; then
  function xmp_to_key_value
  {
    local -i i=$1
    local xmpfile=$2

    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    local line key lang value make= model=

    description[$i]=
    coverage[$i]=
    date[$i]=
    title[$i]=
    identifier[$i]=
    creator[$i]=
    contributor[$i]=
    contributor[$i]=
    relation[$i]=
    rights[$i]=
    subject[$i]=
    camera[$i]=
    lens[$i]=
    film[$i]=
    devel_date[$i]=
    latitude[$i]=
    longitude[$i]=
    auxlens[$i]=
    create_date[$i]=
    city[$i]=
    country[$i]=
    state[$i]=

    xmptool -v '*' "${xmpfile}" >$TMP
    while read line; do
      key=${line%%	*}
      h=${key%\[*}; lang=${key#$h}; key=$h
      value=${line#*	}
      case $key in
	$dcDescription)
	  if [[ "$lang" == *\[$language* || -z "${description[$i]}" ]]; then
	    description[$i]=$value
	  fi;;
	$dcCoverage)
	  if [[ "$lang" == *\[$language* || -z "${coverage[$i]}" ]]; then
	    coverage[$i]=$value
	  fi;;
	$dcDate)
	  if [[ "$lang" == *\[$language* || -z "${date[$i]}" ]]; then
	    date[$i]=$value
	  fi;;
	$dcTitle)
	  if [[ "$lang" == *\[$language* || -z "${title[$i]}" ]]; then
	    title[$i]=$value
	  fi;;
	$dcIdentifier)
	  if [[ "$lang" == *\[$language* || -z "${identifier[$i]}" ]]; then
 	    identifier[$i]=$value
	  fi;;
	$dcCreator)
	  if [[ "$lang" == *\[$language* || -z "${creator[$i]}" ]]; then
	    creator[$i]=$value
	  fi;;
	$dcContributor)
	  if [[ "$lang" == *\[$language* || -z "${contributor[$i]}" ]]; then
	    contributor[$i]=$value
	  fi;;
	$dcRelation)
	  if [[ "$lang" == *\[$language* || -z "${relation[$i]}" ]]; then
	    relation[$i]=$value
	  fi;;
	$dcRights)
	  if [[ "$lang" == *\[$language* || -z "${rights[$i]}" ]]; then
	    rights[$i]=$value
	  fi;;
	$dcSubject)
	  if [[ -z "${subject[$i]}" ]]; then
	    # We have no value yet, accept a value in any language.
	    subject[$i]=$value
	    subject_lang=$language # Empty or "[xx]"
	  elif [[ "$language" == "$subject_lang" ]]; then
	    # Same language as existing value, so add to it (with a ",").
	    subject[$i]+=", $value"
	  elif [[ "$lang" == *\[$language* ]]; then
	    # This is the desired language, but the existing value has
	    # a different language. Replace it.
	    subject[$i]=$value
	    subject_lang=$language
	  fi;;
	$tcCamera)
	  if [[ "$lang" == *\[$language* || -z "${camera[$i]}" ]]; then
	    camera[$i]=$value
	  fi;;
	$tcLens)
	  if [[ "$lang" == *\[$language* || -z "${lens[$i]}" ]]; then
	    lens[$i]=$value
	  fi;;
	$tcFilm)
	  if [[ "$lang" == *\[$language* || -z "${film[$i]}" ]]; then
	    film[$i]=$value
	  fi;;
	$tcDevelDate)
	  if [[ "$lang" == *\[$language* || -z "${devel_date[$i]}" ]]; then
	    devel_date[$i]=$value
	  fi;;
	$exifLatitude)
	  if [[ "$lang" == *\[$language* || -z "${latitude[$i]}" ]]; then
	    latitude[$i]=`minutes-to-dec "$value"`
	  fi;;
	$exifLongitude)
	  if [[ "$lang" == *\[$language* || -z "${longitude[$i]}" ]]; then
	    longitude[$i]=`minutes-to-dec "$value"`
	  fi;;
	$auxLens)
	  if [[ "$lang" == *\[$language* || -z "${auxlens[$i]}" ]]; then
	    auxlens[$i]=$value
	  fi;;
	$tiffMake)
	  if [[ "$lang" == *\[$language* || -z "$make" ]]; then
	    make=$value
	  fi;;
	$tiffModel)
	  if [[ "$lang" == *\[$language* || -z "$model" ]]; then
	    model=$value
	  fi;;
	$xapCreateDate)
	  if [[ "$lang" == *\[$language* || -z "${create_date[$i]}" ]]; then
	    create_date[$i]=$value
	  fi;;
	$photoshopcity)
	  if [[ "$lang" == *\[$language* || -z "${city[$i]}" ]]; then
	    city[$i]=$value
	  fi;;
	$photoshopstate)
	  if [[ "$lang" == *\[$language* || -z "${state[$i]}" ]]; then
	    state[$i]=$value
	  fi;;
	$photoshopcountry)
	  if [[ "$lang" == *\[$language* || -z "${country[$i]}" ]]; then
	    country[$i]=$value
	  fi;;
      esac
    done <$TMP

    # If data is missing, try other sources
    [[ -n "${lens[$i]}" ]] || lens[$i]=${auxlens[$i]}
    [[ -n "${camera[$i]}" ]] || camera[$i]="${make:+$make }$model"
    if [[ -z "${coverage[$i]}" ]]; then
      coverage[$i]="${city[$i]}, ${state[$i]}, ${country[$i]}"
      coverage[$i]=${coverage[$i]/, , /, } # Remove ", " if no state
      coverage[$i]=${coverage[$i]#, }	   # Remove ", " if no city
      coverage[$i]=${coverage[$i]%, }	   # Remove ", " if no country
    fi
  }
elif have exiftool; then
  function xmp_to_key_value
  {
    local -i i=$1
    local xmpfile=$2

    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    local line key lang value

    description[$i]=
    coverage[$i]=
    date[$i]=
    title[$i]=
    identifier[$i]=
    creator[$i]=
    contributor[$i]=
    contributor[$i]=
    relation[$i]=
    rights[$i]=
    subject[$i]=
    camera[$i]=
    lens[$i]=
    film[$i]=
    devel_date[$i]=
    latitude[$i]=
    longitude[$i]=
    auxlens[$i]=
    create_date[$i]=

    exiftool -t -c '%+.5f' "$xmpfile" >$TMP
    while IFS=$'\t' read key value; do
      h=${key% \(*}; lang=${key#h}; key=$h; # lang is like '(nl)'
      case "$key" in
	Description)
	  if [[ "$lang" == *\($language* || -z "${description[$i]}" ]]; then
	    description[$i]=$value
	  fi;;
	Coverage)
	  if [[ "$lang" == *\($language* || -z "${coverage[$i]}" ]]; then
	    coverage[$i]=$value
	  fi;;
	Date)
	  if [[ "$lang" == *\($language* || -z "${date[$i]}" ]]; then
	    value=${value/:/-}
	    value=${value/:/-}
	    value=${value/ /T}
	    date[$i]=$value
	  fi;;
	Title)
	  if [[ "$lang" == *\($language* || -z "${title[$i]}" ]]; then
	    title[$i]=$value
	  fi;;
	Identifier)
	  if [[ "$lang" == *\($language* || -z "${identifier[$i]}" ]]; then
 	    identifier[$i]=$value
	  fi;;
	Creator)
	  if [[ "$lang" == *\($language* || -z "${creator[$i]}" ]]; then
	    creator[$i]=$value
	  fi;;
	Contributor)
	  if [[ "$lang" == *\($language* || -z "${contributor[$i]}" ]]; then
	    contributor[$i]=$value
	  fi;;
	Relation)
	  if [[ "$lang" == *\($language* || -z "${relation[$i]}" ]]; then
	    relation[$i]=$value
	  fi;;
	Rights)
	  if [[ "$lang" == *\($language* || -z "${rights[$i]}" ]]; then
	    rights[$i]=$value
	  fi;;
	Subject)
	  if [[ "$lang" == *\($language* || -z "${subject[$i]}" ]]; then
	    subject[$i]=$value
	  fi;;
	Camera)
	  if [[ "$lang" == *\[$language* || -z "${camera[$i]}" ]]; then
	    camera[$i]=$value
	  fi;;
	# Lens)
	#   if [[ "$lang" == *\($language* || -z "${lens[$i]}" ]]; then
	#     lens[$i]=$value
	#   fi;;
	Film)
	  if [[ "$lang" == *\($language* || -z "${film[$i]}" ]]; then
	    film[$i]=$value
	  fi;;
	Devel-date)
	  if [[ "$lang" == *\($language* || -z "${devel_date[$i]}" ]]; then
	    value=${value/:/-}
	    value=${value/:/-}
	    value=${value/ /T}
	    devel_date[$i]=$value
	  fi;;
	GPS\ Latitude)
	  if [[ "$lang" == *\($language* || -z "${latitude[$i]}" ]]; then
	    latitude[$i]=${value#+}
	  fi;;
	GPS\ Longitude)
	  if [[ "$lang" == *\($language* || -z "${longitude[$i]}" ]]; then
	    longitude[$i]=${value#+}
	  fi;;
	Lens)
	  if [[ "$lang" == *\($language* || -z "${auxlens[$i]}" ]]; then
	    auxlens[$i]=$value
	  fi;;
	Create\ Date)
	  if [[ "$lang" == *\($language* || -z "${create_date[$i]}" ]]; then
	    value=${value/:/-}
	    value=${value/:/-}
	    value=${value/ /T}
	    create_date[$i]=$value
	  fi;;
      esac
    done <$TMP

    # If data is missing, try other sources
    [[ -n "${lens[$i]}" ]] || lens[$i]=${auxlens[$i]}
  }
else
  die "found neither xmptool nor exiftool"
fi



    # local field=${1##*/}
    # field=${field##*#}
    # # LC_ALL=C is needed, because the sed command on Mac OS X 10.6.8 cannot handle UTF-8
    # tr '\n' ' ' <$2 |\
    #  LC_ALL=C sed -n '
    #   # Remove <rdf:Description>, as it may interfere with old <dc:Description>
    #   s|<rdf:Description[^>]*>||g
    #   s|</rdf:Description[^>]*>||g
    #   # Remove Namespace prefixes
    #   s|<[a-zA-Z0-9_-]*:|<|g
    #   s|</[a-zA-Z0-9_-]*:|</|g
    #   # Reset substitution counter
    #   t here
    #   :here
    #   # Keep just the property element
    #   s|.*\(<'$field'[^a-zA-Z0-9_-].*</'$field'>\).*|\1|g
    #   # Quit if there was no such property
    #   t cont
    #   q
    #   :cont
    #   # Remove everything before wanted language
    #   s|.*\(<[^>]*xml:lang=.'$language'\)|\1|g
    #   # Remove everything after first closing tag
    #   s|</.*||
    #   # Remove all tags
    #   s|<[^>]*>||g
    #   # Collapse white space, escape double quotes
    #   s|   *| |g
    #   s| $||
    #   s|^ ||
    #   s|"|\&quot;|g
    #   p
    #  '


if have inkscape; then
  function svg-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    DISPLAY= inkscape --export-background='#ffffff' --export-type=png --export-filename=- "$3" >$TMP
    png-to-jpeg $1 $2 $TMP
  }
elif have gm; then
  function svg-to-jpeg
  {
    gm convert "$3" -resize $1x$2 jpeg:-
  }
elif have rsvg-convert; then
  function svg-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    rsvg-convert "$3" >$TMP
    png-to-jpeg $1 $2 $TMP
  }
else
  function svg-to-jpeg
  {
    echo "$3: error: no software available for SVG" >&2
    return 1
  }
fi

if have inkscape; then
  function ps-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    DISPLAY= inkscape -z -f "$3" -b '#ffffff' -e $TMP >/dev/null
    png-to-jpeg $1 $2 $TMP
  }
elif have gm; then
  function ps-to-jpeg
  {
    gm convert "$3" -resize $1x$2 jpeg:-
  }
elif have pdftoppm; then
  function ps-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    pdftoppm -singlefile "$3" $TMP
    pamscale -xysize $1 $2 $TMP | ppmtojpeg
  }
elif have gs; then
  function ps-to-jpeg
  {
    gs -sOutputFile=- -sDEVICE=ppm "$3" | pamscale -xysize $1 $2 | ppmtojpeg
  }
else
  function ps-to-jpeg
  {
    echo "$3: error: no software available for PDF or PostScript" >&2
    return 1
  }
fi

if have gm; then
  function jpeg-to-jpeg
  {
    gm convert "$3" -resize $1x$2 jpeg:-
  }
elif have convert; then
  function jpeg-to-jpeg
  {
    convert "$3" -resize $1x$2 jpeg:-
  }
elif have jpegtopnm && have wrjpgapp; then
  function jpeg-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    extract_xmp_from_jpeg "$3" >$TMP
    jpegtopnm "$3" | pamscale -xysize $1 $2 | pnmtojpeg | wrjpgapp -cfile $TMP
  }
elif have jpegtopnm; then
  function jpeg-to-jpeg
  {
    jpegtopnm "$3" 2>/dev/null | pamscale -xysize $1 $2 | pnmtojpeg
  }
else
  function jpeg-to-jpeg
  {
    echo "$3: error: no software available for JPEG" >&2
    return 1
  }
fi

if have gm; then
  function png-to-jpeg
  {
    gm convert "$3" -resize "$1x$2>" jpeg:-
  }
elif have convert; then
  function png-to-jpeg
  {
    convert "$3" -resize "$1x$2>" jpeg:-
  }
elif have pngtopnm && have wrjpgapp; then
  function png-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    extract_xmp_from_png "$3" >$TMP
    pngtopnm "$3" | pamscale -xysize $1 $2 | pnmtojpeg | wrjpgapp -cfile $TMP
  }
elif have pngtopnm; then
  function png-to-jpeg
  {
    pngtopnm "$3" | pamscale -xysize $1 $2 | pnmtojpeg
  }
else
  function png-to-jpeg
  {
    echo -e "$3: error: no software available for PNG" >&2
    return 1
  }
fi

if have gm; then
  function tiff-to-jpeg
  {
    gm convert "$3" -resize $1x$2 jpeg:-
  }
elif have convert; then
  function tiff-to-jpeg
  {
    convert "$3" -resize $1x$2 jpeg:-
  }
elif have tifftopnm && have wrjpgapp; then
  function tiff-to-jpeg
  {
    local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1
    extract_xmp_from_tiff "$3" >$TMP
    tifftopnm "$3" | pamscale -xysize $1 $2 | pnmtojpeg | wrjpgapp -cfile $TMP
  }
elif have tifftopnm; then
  function tiff-to-jpeg
  {
    tifftopnm "$3" | pamscale -xysize $1 $2 | pnmtojpeg
  }
else
  function tiff-to-jpeg
  {
    echo -e "$3: error: no software available for TIFF" >&2
    return 1
  }
fi

if have dcraw && have pamscale; then
  function raw-to-jpeg
  {
    dcraw -c -w "$3"| pamscale -xysize $1 $2 | pnmtojpeg
  }
else
  function raw-to-jpeg
  {
    echo -e "$3: error: no software available for raw image file" >&2
    return 1
  }
fi

if have dwebp && have pamscale; then
  function webp-to-jpeg
  {
    dwebp -quiet -ppm -o - -- "$3" | pamscale -xysize $1 $2 | pnmtojpeg
  }
elif have gm; then
  function webp-to-jpeg
  {
    gm convert "$3" -resize $1x$2 jpeg:-
  }
elif have convert; then
  function webp-to-jpeg
  {
    convert "$3" -resize $1x$2 jpeg:-
  }
else
  function webp-to-jpeg
  {
    echo -e "$3: error: no software available for WebP" >&2
    return 1
  }
fi


# scale -- scale image $3 to fit within $1 x $2 pixels and output as JPEG
function scale
{
  local TMP=`mktemp $TEMPDIR/XXXXXX` || return 1

  if $trust_ext; then
    case "$3" in
      *.svg|.SVG) svg-to-jpeg $1 $2 "$3";;
      *.pdf|*.PDF|*.ps|*.PS|*.eps|*.EPS) ps-to-jpeg $1 $2 "$3";;
      *.jpg|*.jpeg) jpeg-to-jpeg $1 $2 "$3";;
      *.png|*.PNG) png-to-jpeg $1 $2 "$3";;
      *.crw|*.CRW) raw-to-jpeg $1 $2 "$3";;
      *.tiff|*.TIFF) tiff-to-jpeg $1 $2 "$3";;
      *.webp|*.WEPB) webp-to-jpeg $1 $1 "$3";;
      *) echo -e "$3: error: unknown image format" >&2; return 1
    esac
  else
    case `file -b -L "$3"` in
      SVG*) svg-to-jpeg $1 $2 "$3";;
      PDF*|*PostScript*|*Postscript*) ps-to-jpeg $1 $2 "$3";;
      JPEG*) jpeg-to-jpeg $1 $2 "$3";;
      PNG*) png-to-jpeg $1 $2 "$3";;
      TIFF*) tiff-to-jpeg $1 $2 "$3";;
      *Web/P*) webp-to-jpeg $1 $1 "$3";;
      *raw\ image*) raw-to-jpeg $1 $2 "$3";;
      *) echo -e "$3: error: unknown image format" >&2; return 1
    esac
  fi
}


# crop_and_scale -- scale the center $1 % of a JPEG image $4 to $2x$3
if have gm; then
  function crop_and_scale
  {
    [[ -s "$4" ]] || return 1

    local fileinfo=`get_image_dimension "$4"`
    local -i w=${fileinfo%x*}
    local -i h=${fileinfo#*x}
    local -i shavew=$((w * (100 - $1) / 200))
    local -i shaveh=$((h * (100 - $1) / 200))
    gm convert "$4" -shave ${shavew}x${shaveh} -resize $2x$3 -strip -
  }
elif have convert; then
  function crop_and_scale
  {
    [[ -s "$4" ]] || return 1

    local fileinfo=`get_image_dimension "$4"`
    local -i w=${fileinfo%x*}
    local -i h=${fileinfo#*x}
    local -i shavew=$((w * (100 - $1) / 200))
    local -i shaveh=$((h * (100 - $1) / 200))
    convert "$4" -shave ${shavew}x${shaveh} -resize $2x$3 -strip -
  }
elif have jpegtopnm; then
  function crop_and_scale
  {
    [[ -s "$4" ]] || return 1

    local fileinfo=`get_image_dimension "$4"`
    local -i w=${fileinfo%x*}
    local -i h=${fileinfo#*x}
    local -i l=$(((w - (w * crop / 100)) / 2))
    local -i t=$(((h - (h * crop / 100)) / 2))
    jpegtopnm "$4" 2>/dev/null |\
       pnmcut -left $l -right -$l -top $t -bottom -$t |\
       pamscale -xysize $1 $2 | pnmtojpeg
  }
else
  die "No software available to scale a JPEG"
fi


# output_header -- Write the header part of the output
function output_header
{
  # Note: 'float: left' causes a bug in Netscape 4 and Opera 3.5 & 3.6
  # (all images are in a single row, instead of as many as needed).
  # You may want to comment it out until there are newer versions of
  # those browsers.

  local heading="$1"
  local title=`echo "$heading" | sed -e 's/<[^>]*>//g'`
  local fontsize=$((w1/10))
  local boxwd=$((15*w1/10))

  echo "<!doctype html public '-//W3C//DTD HTML 4.0//EN'"
  echo "  'http://www.w3.org/TR/REC-html40/strict.dtd'>"
  echo "<html lang=\"$language\">"
  echo "<meta http-equiv=content-type content='text/html; charset=utf-8'>"
  echo "<meta name=viewport content=\"width=device-width\">"
  echo "<meta name=generator content=\"thumbnails-xmp\">"
  echo "<title>$(esc "$title")</title>"
  if [ -z "$style" ]; then
    cat <<-EOF
	<style type="text/css">
	  body {background: #555; color: #DDD; font-family: sans-serif;
	    margin: 1rem}
	  h1 {font-size: xx-large; text-align: end; margin: 1rem 0 2.6rem}
	  h2 {margin: 2.6rem 0 1rem; text-align: end; border-bottom: thin solid;
	    padding-bottom: 0.5rem}
	  ul.series {columns: 30em}
	  ul.day {columns: 7.5em}
	  ul.month, ul.year {columns: 6em}
	  .cards {display: flex; flex-flow: row wrap; gap: 1em}
	  .thumb {background: #EEE; color: #333;
	    width: ${boxwd}px; height: ${boxwd}px;
	    font: ${fontsize}px/1.1 sans-serif; text-align: center;
	    border: 1px outset #FFF; display: grid; grid: "a" 1fr "b" auto "c" 1fr;
	    gap: 1px; border-radius: $((w1/25))px}
	  .thumb p, .thumb h3 {margin: 0}
	  .thumb h3 {grid-area: a; font: inherit; box-sizing: border-box;
	     height: 100%; padding: 0.3em 0.3em 0; overflow: hidden}
	  .imgwrap {grid-area: b}
	  .thumb img {border: 1px inset #FFF; display: block; margin: 0 auto;
	    width: auto; height: auto; max-width: ${w1}px; max-height: ${w1}px}
	  .thumb > p {grid-area: c; box-sizing: border-box; height: 100%;
	    overflow: hidden; align-content: end; padding: 0.3em 0.3em 0}
	  .thumb > p::after {content: " "; display: block; height: 0.3em}
	  a:link, a:visited {color: inherit; text-decoration: underline}
	  address {margin: 2.6rem 0 1rem; text-align: end}
	</style>
	EOF
  else
    echo "<link rel=stylesheet href=\"$style\">"
  fi
  echo "<link rel=up href=\"$(esc "$up")\">"
  if [ ! -z "$prevlink" ]; then
    echo "<link rel=prev href=\"$(esc "$prevlink")\">"
  fi
  if [ ! -z "$nextlink" ]; then
    echo "<link rel=next href=\"$(esc "$nextlink")\">"
  fi
  if [ ! -z "$email" ]; then
    echo -e "<link rel=author href=\"$(esc "$email")\"\c"
    if [ ! -z "$owner" ]; then echo -e " title=\"$(esc "$owner")\"\c"; fi
    echo ">"
  fi
  echo
  echo "<body class=index>"
  echo "<h1>$(esc "$heading")</h1>"
}


# output_footer -- output copyright message, etc. at the end
function output_footer
{
  echo "<address>"
  if [ -z "$email" ]; then
    echo "Copyright &copy; $copyrightyear $owner"
  else
    echo "Copyright &copy; $copyrightyear <a href=\"$(esc "$email")\">$owner</a>"
  fi
  echo "</address>"
  echo "</body>"
  echo "</html>"
}


# Interactive functions, using dialog, ssft.sh or just read, depending
# on what is available.

if have ssft.sh && { [[ -n "${DISPLAY:-}" ]] || ! have dialog; }; then

  . ssft.sh
  TEXTDOMAIN=			# TODO: Provide translations
  TEXTDOMAINDIR=
  [ -n "${SSFT_FRONTEND:-}" ] || SSFT_FRONTEND="$(ssft_choose_frontend)"

  # ask_pagetitle -- put up a dialog with an input box for the page title
  function ask_pagetitle
  {
    SSFT_DEFAULT=$pagetitle
    ssft_read_string "$d_backtitle" "Title for thumbnails page" && \
      pagetitle=$SSFT_RESULT || exit
  }

  # ask_owner_and_email -- put up a dialog for the copyright owner
  function ask_owner_and_email
  {
    SSFT_DEFAULT=$owner
    ssft_read_string "$d_backtitle" "Copyright owner" && \
      owner=$SSFT_RESULT && \
      SSFT_DEFAULT=$email && \
      ssft_read_string "$d_backtitle" "Email address or URL" && \
      email=$SSFT_RESULT || exit
  }

  # ask_images -- Ask for the names of all the JPEG images to process
  function ask_images
  {
    SSFT_DEFAULT=${images:-*.jpg}
    ssft_read_string "$d_backtitle" "The images" && \
      images=$SSFT_RESULT || exit
  }

  # ask_output -- put up a dialog for the name of the generated thumbnails page
  function ask_output
  {
    SSFT_DEFAULT=${output:-}
    ssft_read_string "$d_backtitle" \
      "Output file name (\"-\" for standard out)" && \
      output=$SSFT_RESULT || exit
  }

  # ask_language -- present the choice of output languages
  function ask_language
  {
    case "$language" in
      en) SSFT_DEFAULT="en - English";;
      nl) SSFT_DEFAULT="nl - Dutch";;
      fr) SSFT_DEFAULT="fr - French";;
      de) SSFT_DEFAULT="de - German";;
    esac
    ssft_select_single "$d_backtitle" "Language for HTML pages" \
		       "en - English" "nl - Dutch" "fr - French" \
		       "de - German" && \
      language=${SSFT_RESULT%% *} || exit
  }

  # ask_preamble -- ask for the name of a file to insert
  function ask_preamble
  {
    SSFT_DEFAULT=${preamble:-<none>}
    ssft_read_string "$d_backtitle" \
      "File to insert at the top of the index page"
    if [[ $? != 0 ]]; then exit
    elif [[ "$SSFT_RESULT" != "<none>" ]]; then preamble=$SSFT_RESULT
    fi
  }

  # ask_percentage -- put up a dialog for the percentage crop of thumbnails
  function ask_percentage
  {
    local stop=false

    SSFT_DEFAULT=$crop
    until $stop; do
      if ! ssft_read_string \
	   "$d_backtitle" \
	   "Percentage of photo shown in thumbnail (100 = everything)"; then
	exit
      elif [[ "$SSFT_RESULT" == [1-9] || "$SSFT_RESULT" == [0-9][0-9] || \
		"$SSFT_RESULT" == 100 ]]; then
	crop=$SSFT_RESULT
	stop=true
      fi
    done
  }

  # ask_thumbnail_size -- put up a dialog for the size of thumbnails
  function ask_thumbnail_size
  {
    local stop=false

    SSFT_DEFAULT=$w1
    until $stop; do
      if ! ssft_read_string \
	   "$d_backtitle" \
	   "Thumbnail size in pixels (1-999)"; then
	exit
      elif [[ "$SSFT_RESULT" == [1-9] || "$SSFT_RESULT" == [0-9][0-9] || \
		"$SSFT_RESULT" == [0-9][0-9][0-9] ]]; then
	w1=$SSFT_RESULT
	stop=true
      fi
    done
  }

  # ask_caption -- ask how much to include in the thumbnail captions
  function ask_caption
  {
    SSFT_DEFAULT=
    (( $caption & 1 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Title"$'\n'
    (( $caption & 2 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Number"$'\n'
    (( $caption & 4 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Description"$'\n'
    (( $caption & 8 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Links to images"$'\n'
    (( $caption & 16 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Place"$'\n'
    (( $caption & 32 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Date"$'\n'
    (( $caption & 64 )) && SSFT_DEFAULT="${SSFT_DEFAULT}Link to metadata"$'\n'
    (( $caption & 128 )) && SSFT_DEFAULT="${SSFT_DEFAULT}File name"$'\n'
    ssft_select_multiple "$d_backtitle" \
      "What to include in the thumbnail captions?" \
      "Title" \
      "Number" \
      "Description" \
      "Links to images" \
      "Place" \
      "Date" \
      "Link to metadata" \
      "File name"
    caption=0
    [[ "$SSFT_RESULT" =~ Title ]] && ((caption += 1))
    [[ "$SSFT_RESULT" =~ Number ]] && ((caption += 2))
    [[ "$SSFT_RESULT" =~ Description ]] && ((caption += 4))
    [[ "$SSFT_RESULT" =~ images ]] && ((caption += 8))
    [[ "$SSFT_RESULT" =~ Place ]] && ((caption += 16))
    [[ "$SSFT_RESULT" =~ Date ]] && ((caption += 32))
    [[ "$SSFT_RESULT" =~ metadata ]] && ((caption += 64))
    [[ "$SSFT_RESULT" =~ File ]] && ((caption += 128))
  }

  # ask_grouping -- ask if the thumbnails should be grouped
  function ask_grouping
  {
    case $group in
      '') SSFT_DEFAULT="Do not group";;
      year) SSFT_DEFAULT="By year";;
      month) SSFT_DEFAULT="By month";;
      day) SSFT_DEFAULT="By day";;
      series) SSFT_DEFAULT="By series";;
    esac
    ssft_select_single "$d_backtitle" "Group thumbnails?" \
      "Do not group" "By year" "By month" "By day" "By series" && \
      case "$SSFT_RESULT" in
	"Do not group") group=;;
	"By year") group=year;;
	"By month") group=month;;
	"By day") group=day;;
	"By series") group=series;;
      esac || \
	exit
  }

  # ask_xmplink -- ask if the XMP should be made available in a separate file
  function ask_xmplink
  {
    ssft_yesno "$d_backtitle" "Create XMP (sidecar) file?"
    case $? in
      0) xmplink=static;;
      1) xmplink=dynamic;;
      *) exit;;
    esac
  }

  # ask_style -- ask for the URL of an external style sheet
  function ask_style
  {
    SSFT_DEFAULT=${style:-<none>}
    ssft_read_string "$d_backtitle" "URL of external style sheet"
    if [[ $? != 0 ]]; then exit
    elif [[ "$SSFT_RESULT" != "<none>" ]]; then style=$SSFT_RESULT
    fi
  }

  # ask_nav_links -- ask for the URL of "rel=up/prev/next" links
  function ask_nav_links
  {
    SSFT_DEFAULT=${up:-<none>}
    ssft_read_string "$d_backtitle" "rel=up URL"
    if [[ $? != 0 ]]; then exit
    elif [[ "$SSFT_RESULT" != "<none>" ]]; then up=$SSFT_RESULT
    fi
    SSFT_DEFAULT=${prevlink:-<none>}
    ssft_read_string "$d_backtitle" "rel=prev URL"
    if [[ $? != 0 ]]; then exit
    elif [[ "$SSFT_RESULT" != "<none>" ]]; then prevlink=$SSFT_RESULT
    fi
    SSFT_DEFAULT=${nextlink:-<none>}
    ssft_read_string "$d_backtitle" "rel=next URL"
    if [[ $? != 0 ]]; then exit
    elif [[ "$SSFT_RESULT" != "<none>" ]]; then nextlink=$SSFT_RESULT
    fi
  }

  # ask_confirmation -- ask if the text $1 is correct
  function ask_confirmation
  {
    ssft_yesno "$d_backtitle" "$1"
    case $? in
      0) return 0;;
      1) return 1;;
      *) exit;;
    esac
  }

elif have dialog; then

  # ask_pagetitle -- put up a dialog with an input box for the page title
  function ask_pagetitle
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    dialog --backtitle "$d_backtitle"\
      --inputbox "Title for thumbnails page" 10 60 "$pagetitle" 2>$TMP
    case $? in
      0) pagetitle="$(<$TMP)";;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_owner_and_email -- put up a dialog for the copyright owner
  function ask_owner_and_email
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    dialog --backtitle "$d_backtitle"\
      --form "Copyright owner" 11 60 2\
      "Name" 1 1 "$owner" 1 17 37 5000\
      "Email or URL" 2 1 "$email" 2 17 37 5000 2>$TMP
    case $? in
      0) IFS=$'\n' read -d $'\0' owner email <$TMP;; # OK, read two lines
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_images -- Ask for the names of all the JPEG images to process
  function ask_images
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    dialog --backtitle "$d_backtitle"\
      --inputbox "The images (e.g., \"*.jpg\")" 10 70 "$images" 2>$TMP
    case $? in
      0) images="$(<$TMP)";;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_output -- put up a dialog for the name of the generated thumbnails page
  function ask_output
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local done=false

    until [ $done == true ]; do
      until dialog --backtitle "$d_backtitle"\
        --extra-button --extra-label "Browse"\
	--inputbox "Output file name"\
	10 60 "$output" 2>$TMP
      do
	case $? in
	  1|255) exit;;			# Cancel|ESC
	  3) dialog --backtitle "Output file name"\
		     --fselect "$(abs_path "${output:-./}")" 9 60 2>$TMP
	     case $? in
	       0) output="$(<$TMP)";;
	       1|255) ;;
	       *) die "Bug! Cannot happen!";;
	     esac;;
	  *) die "Bug! Cannot happen!";;
	esac
      done
      output="$(<$TMP)"
      if [ -z "$output" ]; then
	dialog --backtitle "$d_backtitle"\
		--msgbox "The file name cannot be empty. If you want output\
       to be printed to standard output, use a single dash (\"-\")"\
		12 60
      else
	done=true
      fi
    done
  }

  # ask_language -- present the choice of output languages
  function ask_language
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local en=off fr=off nl=off de=off
    case "$language" in
      en) en=on;;
      fr) fr=on;;
      nl) nl=on;;
      de) de=on;;
    esac
    dialog --backtitle "$d_backtitle"\
      --radiolist "Language for HTML pages" 12 40 5\
      en "English" $en\
      de "German [Deutsch]" $de\
      fr "French [Français]" $fr\
      nl "Dutch [Nederlands]" $nl 2>$TMP
    case $? in
      0) language="$(<$TMP)";;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_preamble -- ask for the name of a file to insert
  function ask_preamble
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    until dialog --backtitle "$d_backtitle"\
		  --extra-button --extra-label "Browse"\
		  --inputbox "Optional file with text to insert"\
		  10 60 "$preamble" 2>$TMP
    do
      case $? in
	1|255) exit;;				# Cancel|ESC
	3) dialog --backtitle "$d_backtitle"\
		   --fselect "$(abs_path "${preamble:-./}")" 9 60 2>$TMP
	   case $? in
	     0) preamble="$(<$TMP)";;
	     1|255) ;;
	     *) die "Bug! Cannot happen!";;
	   esac;;
	*) die "Bug! Cannot happen!";;
      esac
    done
    preamble="$(<$TMP)"
  }

  # ask_percentage -- put up a dialog for the percentage crop of thumbnails
  function ask_percentage
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    dialog --backtitle "$d_backtitle"\
      --rangebox "Percentage of photo shown in thumbnail (100 = everything)" \
      6 70 0 100 "$crop" 2>$TMP
    case $? in
      0) read crop <$TMP;;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_thumbnail_size -- put up a dialog for the size of thumbnails
  function ask_thumbnail_size
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local again=true
    while [ $again = true ]; do
      dialog --backtitle "$d_backtitle"\
        --max-input 3 --trim\
	--inputbox "Size of thumbnails (in pixels)" 10 40 "$w1" 2>$TMP
      case $? in
	0) w1="$(<$TMP)";;	# OK
	1|255) exit;;		# Cancel|ESC
	*) die "Bug! Cannot happen!";;
      esac
      case "$w1" in
	[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]) again=false;;
	*) dialog --backtitle "$d_backtitle"\
             --msgbox "The size must be between 1 and 9999."\
	     12 60
	   again=true;;
      esac
    done
  }

  # ask_caption -- ask how much to include in the thumbnail captions
  function ask_caption
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local -a b
    local a

    for a in 1 2 4 8 16 32 64 128; do
      if ((caption & a)); then b[$a]=on; else b[$a]=off; fi
    done
    dialog --backtitle "$d_backtitle" \
	   --checklist "Include in thumbnail captions:" 14 60 8 \
      "1" "Title" ${b[1]} \
      "2" "Number" ${b[2]} \
      "4" "Description" ${b[4]} \
      "8" "Links to images" ${b[8]} \
      "16" "Place" ${b[16]} \
      "32" "Date" ${b[32]} \
      "64" "Link to metadata" ${b[64]} \
      "128" "File name" 2>$TMP
    case $? in
      0) ;;				# OK
      1|255) exit;;			# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
    caption=0
    for a in $(<$TMP); do ((caption += a)); done
  }

  # ask_grouping -- ask if the thumbnails should be grouped
  function ask_grouping
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local none=off series=off year=off month=off day=off
    case "$group" in
      series) series=on;;
      year) year=on;;
      month) month=on;;
      day) day=on;;
      *) none=on;;
    esac
    dialog --backtitle "$d_backtitle"\
     --radiolist "Group thumbnails?" 12 60 5\
     "" "Don't group" $none\
     "series" "By series" $series\
     "year" "By year" $year\
     "month" "By month" $month\
     "day" "By day" $day 2>$TMP
    case $? in
      0) group=$(<$TMP);;		# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_xmplink -- ask if the XMP should be made available in a separate file
  function ask_xmplink
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    local static=off dynamic=off
    case $xmplink in
      static) static=on;;
      dynamic) dynamic=on;;
      *) die 'Cannot happen! Impossible value in function ask_xmplink';;
    esac
    dialog --backtitle "$d_backtitle"\
      --radiolist "Make each image's XMP data available in a file?" 9 64 2\
      "static" "Yes, make an XMP file for each image" $static\
      "dynamic" "No, let the server extract the XMP on demand" $dynamic 2>$TMP
    case $? in
      0) xmplink=$(<$TMP);;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die 'Cannot happen! Impossible exit code in function ask_xmplink';;
    esac
  }

  # ask_style -- ask for the URL of an external style sheet
  function ask_style
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    dialog --backtitle "$d_backtitle"\
     --inputbox "Optional external style sheet" 10 60 "$style" 2>$TMP
    case $? in
      0) style="$(<$TMP)";;	# OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_nav_links -- ask for the URL of "rel=up/prev/next" links
  function ask_nav_links
  {
    local TMP=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."
    #dialog --backtitle "$d_backtitle"\
    # --inputbox "Link for rel=up" 10 60 "$up" 2>$TMP
    dialog --backtitle "$d_backtitle"\
     --form "Navigation links" 10 60 3\
     "<link rel=up>" 1 1 "$up" 1 17 37 5000\
     "<link rel=prev>" 2 1 "$prevlink" 2 17 37 5000\
     "<link rel=next>" 3 1 "$nextlink" 3 17 37 5000 2>$TMP
    case $? in
      0) IFS=$'\n' read -d $'\0' up prevlink nextlink <$TMP;; # OK
      1|255) exit;;		# Cancel|ESC
      *) die "Bug! Cannot happen!";;
    esac
  }

  # ask_confirmation -- ask if the text $1 is correct
  function ask_confirmation
  {
    dialog --backtitle "$d_backtitle" --yesno "$1" 0 0
  }

else

  # ask_pagetitle -- put up a dialog with an input box for the page title
  function ask_pagetitle
  {
    read -e -p "Page title [$pagetitle] : "
    [[ -z "$REPLY" ]] || pagetitle="$REPLY"
  }

  # ask_owner_and_email -- put up a dialog for the copyright owner
  function ask_owner_and_email
  {
    read -e -p "Owner [$owner] : "
    [[ -z "$REPLY" ]] || owner=$REPLY
    read -e -p "Email or URL [$email] : "
    [[ -z "$REPLY" ]] || email=$REPLY
  }

  # ask_images -- Ask for the names of all the JPEG images to process
  function ask_images
  {
    read -e -p "Images [$images] : "
    [[ -z "$REPLY" ]] || images=$REPLY
  }

  # ask_output -- put up a dialog for the name of the generated thumbnails page
  function ask_output
  {
    while true; do
      read -e -p "Output file ${output:+[$output]} : "
      [[ -z "$REPLY" ]] || output=$REPLY
      [[ -n "$output" ]] && break
      echo "Output file name cannot be empty. Use \"-\" for standard out"
    done
  }

  # ask_language -- present the choice of output languages
  function ask_language
  {
    read -e -p "Language [$language] : "
    [[ -z "$REPLY" ]] || language=$REPLY
  }

  # ask_preamble -- ask for the name of a file to insert
  function ask_preamble
  {
    read -e -p "Preamble [$preamble] : "
    [[ -z "$REPLY" ]] || preamble=$REPLY
  }

  # ask_percentage -- put up a dialog for the percentage crop of thumbnails
  function ask_percentage
  {
    read -e -p "Crop percentage (100 = no crop) [$crop] :"
    [[ -z "$REPLY" ]] || crop=$REPLY
  }

  # ask_thumbnail_size -- put up a dialog for the size of thumbnails
  function ask_thumbnail_size
  {
    read -e -p "Thumbnail size in pixels [$w1] : "
    [[ -z "$REPLY" ]] || w1=$REPLY
  }

  # ask_caption -- ask how much to include in the thumbnail captions
  function ask_caption
  {
    local h r v d
    local -i c=0
    local -a labels=([1]="Title" [2]="Number" [4]="Description"
		     [8]="Links to images" [16]="Place" [32]="Date"
		     [64]="Link to metadata" [128]="File name")

    echo "Include in thumbnail captions:"
    for h in 1 2 4 8 16 32 64 128; do
      if (( caption & h )); then d=y; else d=n; fi
      read -e -p "${labels[$h]} [$d] " r
      if [[ ${r:-$d} =~ ^[yY] ]]; then ((c |= h)); fi
    done
    caption=$c
  }

  # ask_grouping -- ask if the thumbnails should be grouped
  function ask_grouping
  {
    local h
    select h in "Don't group" "Group by year" "Group by month" "Group by day" \
			      "Group by series"; do
      case "$REPLY" in
	1) group=; break;;
	2) group=year; break;;
	3) group=month; break;;
	4) group=day; break;;
	5) group=series; break;;
      esac
    done
  }

  # ask_xmplink -- ask if the XMP should be made available in a separate file
  function ask_xmplink
  {
    local default
    [[ $xmplink == static ]] && default=Y || default=N
    read -e -p "Create XMP (sidecar) file [$default] ? "
    case "$REPLY" in
      y*|Y*) xmplink=static;;
      n*|N*) xmplink=dynamic;;
    esac
  }

  # ask_style -- ask for the URL of an external style sheet
  function ask_style
  {
    read -e -p "Style URL [$style] : "
    [[ -z "$REPLY" ]] || style=$REPLY
  }

  # ask_nav_links -- ask for the URL of "rel=up/prev/next" links
  function ask_nav_links
  {
    read -e -p "rel=up URL [$up] : "
    [[ -z "$REPLY" ]] || up="$REPLY"
    read -e -p "rel=prev URL [$prevlink] : "
    [[ -z "$REPLY" ]] || prevlink="$REPLY"
    read -e -p "rel=next URL [$nextlink] : "
    [[ -z "$REPLY" ]] || nextlink="$REPLY"
  }

  # ask_confirmation -- ask if the text $1 is correct
  function ask_confirmation
  {
    echo -e "$1"
    while true; do
      read -e -p "[Y] ? "
      case "$REPLY" in
	''|y*|Y*) return 0;;
	n*|N*) return 1;;
      esac
    done
  }

fi


# minutes-to-dec -- convert latitude or longitude to decimal form
function minutes-to-dec
{
  local h d m s c
  case "$1" in
    *[NESW])			# E.g., "53,2.4522600000N"
      h=${1%?}; c=${1#${h}}
      d=${h%%,*}; h=${h#${d}}; h=${h#,}
      m=${h%%,*}; h=${h#${m}}; h=${h#,}
      s=${h}; h=${h#${s}}
      if [ -z "$h" ]; then
	case "$c" in
	  N|E) echo "5k${d:-0} ${m:-0} ${s:-0} 60/+60/+p" | dc;;
	  S|W) echo "5k0 ${d:-0} ${m:-0} ${s:-0} 60/+60/+-p" | dc;;
	esac
      fi;;
    [NESW]*)			# E.g.: "N 53d  2.461m  0s"
      set -- $1
      d=${2%d}
      m=${3%m}
      s=${4%s}
      case "$1" in
	N|E) echo "5k${d:-0} ${m:-0} ${s:-0} 60/+60/+p" | dc;;
	S|W) echo "5k0 ${d:-0} ${m:-0} ${s:-0} 60/+60/+-p" | dc;;
      esac
  esac
}


# write_page -- write a page about image number $i
function write_page
{
  local -i i=$1
  local prev=$2 next=$3 smallname=$4 metalink=$5 first=$6 last=$7

  local title=$(esc "${title[$i]:-${identifier[$i]:-${args[$i]##*/}}}")
  local description=$(esc "${description[$i]}")

  # Write HTML
  echo "<!doctype html public '-//W3C//DTD HTML 4.0//EN'"
  echo "  'http://www.w3.org/TR/REC-html40/strict.dtd'>"
  echo
  echo "<html lang=\"$(esc "$language")\">"
  echo "<head profile=\"http://www.w3.org/2006/03/hcard\">"
  echo "<meta http-equiv=content-type content='text/html; charset=utf-8'>"
  echo "<meta name=viewport content=\"width=device-width\">"
  echo "<meta name=generator content=\"thumbnails-xmp\">"

  # Header
  echo "<title>$title</title>"
  [[ -n "$prev" ]] && echo "<link rel=prev href=\"$(esc "$prev")\">"
  [[ -n "$next" ]] && echo "<link rel=next href=\"$(esc "$next")\">"
  echo "<link rel=first href=\"$(esc "$first")\">"
  echo "<link rel=last href=\"$(esc "$last")\">"
  echo "<link rel=up href=\"$(esc "$output")\">"
  if [ ! -z "$email" ]; then
    echo -e "<link rel=author href=\"$(esc "$email")\"\c"
    if [ ! -z "$owner" ]; then echo -e " title=\"$(esc "$owner")\"\c"; fi
    echo ">"
  fi
  echo "<link rel=meta href=\"$(esc "$metalink")\">"
  if [ -z "$style" ]; then
    cat <<-EOF
	<style type="text/css">
	  body {background: #555; color: #DDD; font: 1em/1.2 sans-serif;
	  margin: 1rem; text-align: center}
	  address {margin: 2.6em 0}
	  h1, p {margin: 1.3rem 3rem}
	  img {border: thick solid white; box-sizing: border-box;
	    max-width: 100%; display: block; margin: 0 auto}
	  .nav {float: left; font-size: 2em}
	  a {color: inherit; text-decoration: underline}
	  a[rel=meta] {float: right}
	  a[href]:active {background: #FFF}
	  a[href]:hover {color: #FFF; outline: thin dotted}
	  a:not([href]) {color: gray}
	  .desc {font-style: italic}
	  svg {vertical-align: text-bottom}
	  table {display: block; columns: 20em; margin: 2em 0}
	  tbody {display: block}
	  tr {display: block; text-align: center}
	  td, body.photo th {display: inline}
	  @media (min-width: 720px) {
	    table {margin: 2em 4rem}
	    .nav {font-size: 3em}
	    a[rel=prev], a[rel=next] {position: fixed; top: calc(50vh - 0.6em)}
	    a[rel=prev] {left: 1rem}
	    a[rel=next] {right: 1rem}
	  }
	  @media print { .nav {display: none} }
	</style>
	EOF
  else
    echo "<link rel=stylesheet href=\"$(esc "$style")\">"
  fi
  echo

  # Body
  echo "<body class=\"photo geo\">"
  echo "<nav id=global-nav>"
  if [ -z "$prev" ]; then
    echo "<a class=nav rel=prev"
    echo " title=\"${NoPrevious[$lang]}\"><svg height=\"1em\""
    echo " viewbox=\"0 0 6 6\"><desc>${NoPrevious[$lang]}</desc><path"
    echo " style=\"stroke:currentColor;stroke-width:0.8;fill:none\""
    echo " d=\"M1,3 5,1V5Z\"/></svg></a>"
  else
    echo "<a class=nav rel=prev accesskey=P href=\"$(esc "$prev")\""
    echo " title=\"${Previous[$lang]}\"><svg height=\"1em\""
    echo " viewbox=\"0 0 6 6\"><desc>${Previous[$lang]}</desc><path"
    echo " style=\"stroke:currentColor;stroke-width:0.8;fill:currentColor\""
    echo " d=\"M1,3 5,1V5Z\"/></svg></a>"
  fi
  echo "<a class=nav rel=up accesskey=I href=\"$(esc "$output")\""
  echo " title=\"${Index[$lang]}\"><svg height=\"1em\""
  echo " viewBox=\"0 0 6 6\"><desc>${Index[$lang]}</desc><path"
  echo " style=\"stroke:currentColor;stroke-width:0.8;fill:currentColor\""
  echo " d=\"M1,1h1v1h-1z M4,1h1v1h-1z M1,4h1v1h-1z M4,4h1v1h-1z\"/></svg></a>"
  if [ -z "$next" ]; then
    echo "<a class=nav rel=next"
    echo " title=\"${NoNext[$lang]}\"><svg"
    echo " height=\"1em\" viewbox=\"0 0 6 6\"><desc>${NoNext[$lang]}</desc>"
    echo " <path style=\"stroke:currentColor;stroke-width:0.8;fill:none\""
    echo " d=\"M5,3 1,5V1Z\"/></svg></a>"
  else
    echo "<a class=nav rel=next accesskey=N href=\"$(esc "$next")\""
    echo " title=\"${Next[$lang]}\"><svg height=\"1em\""
    echo " viewbox=\"0 0 6 6\"><desc>${Next[$lang]}</desc><path"
    echo " style=\"stroke:currentColor;stroke-width:0.8;fill:currentColor\""
    echo " d=\"M5,3 1,5V1Z\"/></svg></a>"
  fi
  echo "<a class=nav rel=meta accesskey=R href=\"$(esc "$metalink")\""
  echo " title=\"${Raw[$lang]}\"><svg height=\"1em\""
  echo " viewbox=\"0 0 6 6\"><desc>${Raw[$lang]}</desc><path"
  echo " style=\"stroke:currentColor;stroke-width:0.8;fill:currentColor\""
  echo " d=\"M1,3 3,1 5,3 3,5Z\"/></svg></a>"
  echo "</nav>"
  echo
  echo "<h1>$title</h1>"
  echo
  echo "<p><a accesskey=M href=\"$(esc "${args[$i]}")\"><img alt=\"${Image[$lang]}\""
  echo " src=\"$(esc "$smallname")\"></a>"
  echo
  echo "<p class=\"desc\">$(expand-links "$description")"
  echo
  echo "<table>"
  [[ -z "${date[$i]}" ]] || \
    echo "<tr><th>${Date[$lang]}<td>$(esc "${date[$i]}")"
  [[ -z "${coverage[$i]}" ]] || \
    echo "<tr><th>${Place[$lang]}<td>$(esc "${coverage[$i]}")"
  [[ -z "${relation[$i]}" ]] || \
    echo "<tr><th>${Series[$lang]}<td>$(expand-links "$(esc "${relation[$i]}")")"
  [[ -z "${identifier[$i]}" ]] || \
    echo "<tr><th>${Number[$lang]}<td>$(esc "${identifier[$i]}")"
  [[ -z "${creator[$i]}" ]] || \
    echo "<tr><th>${Creator[$lang]}<td>$(esc "${creator[$i]}")"
  [[ -z "${contributor[$i]}" ]] || \
    echo "<tr><th>${Contributor[$lang]}<td>$(esc "${contributor[$i]}")"
  [[ -z "${create_date[$i]}" ]] || \
    [[ "${create_date[$i]}" == "${date[$i]}" ]] || \
    echo "<tr><th>${CreateDate[$lang]}<td>$(esc "${create_date[$i]}")"
  [[ -z "${camera[$i]}" ]] || \
    echo "<tr><th>${Camera[$lang]}<td>$(esc "${camera[$i]}")"
  [[ -z "${lens[$i]}" ]] || \
    echo "<tr><th>${Lens[$lang]}<td>$(esc "${lens[$i]}")"
  [[ -z "${film[$i]}" ]] || \
    echo "<tr><th>${Film[$lang]}<td>$(esc "${film[$i]}")"
  [[ -z "${devel_date[$i]}" ]] || \
    echo "<tr><th>${DevelDate[$lang]}<td>$(esc "${devel_date[$i]}")"

  if [ ! -z "${latitude[$i]}" ] && [ ! -z "${longitude[$i]}" ]; then
    echo "<tr><th>${Latitude[$lang]}<td class=latitude><a href="
    if [ "$osm_zoom" -ge 0 ]; then
      echo " \"$OSM?mlat=${latitude[$i]}&amp;mlon=${longitude[$i]}&amp;zoom=$osm_zoom\""
    else
      echo "\"geo:${latitude[$i]},${longitude[$i]}\""
    fi
    echo " >${latitude[$i]} &#9873;</a>"
    echo "<tr><th>${Longitude[$lang]}<td class=longitude><a href="
    if [ "$osm_zoom" -ge 0 ]; then
      echo " \"$OSM?mlat=${latitude[$i]}&amp;mlon=${longitude[$i]}&amp;zoom=$osm_zoom\""
    else
      echo "\"geo:${latitude[$i]},${longitude[$i]}\""
    fi
    echo " >${longitude[$i]} &#9873;</a>"
  fi
  [[ -z "${subject[$i]}" ]] || \
    echo "<tr><th>${Keywords[$lang]}<td>$(esc "${subject[$i]}")"
  [[ -z "${rights[$i]}" ]] || \
    echo "<tr><th>${Rights[$lang]}<td>$(esc "${rights[$i]}")"
  echo "<tr><th>${Size[$lang]}<td>${size[$i]}&nbsp;KB"
  echo "</table>"
}


# write_thumb_box -- write HTML fragment with a thumbnail
function write_thumb_box
{
  local -i i=$2
  local mainpage=$1 smallname=$3 thumbname=$4 metalink=$5 caption=$6
  local -i smallsize=$(((`wc -c <"$smallname"` + 512) / 1024))
  local fileinfo=`get_image_dimension "$4"`
  local -i w=${fileinfo%x*}
  local -i h=${fileinfo#*x}
  local s="" sep="<span class=sep> · </span>"
  local alt=${description[$i]:-${identifier[$i]:-${args[$i]##*/}}}

  echo
  echo "<div class=thumb>"
  if (( $caption & 1 )) && [[ -n "${title[$i]}" ]]; then
    echo " <h3>$(esc "${title[$i]}")</h3>"
  fi
  echo " <div class=imgwrap>"
  echo "  <p><a href=\"$(esc "$mainpage")\"><img"
  echo "   src=\"$(esc "$thumbname")\""
  echo "   alt=\"${Image[$lang]}\""
  echo "   title=\"$(esc "$alt")\""
  echo "   width=$w height=$h"
  echo "   ></a>"
  echo " </div>"
  echo -e " <p>"
  if (( $caption & 128 )); then
    echo "  <span class=filename>$(esc "${args[$i]}")</span>"
    s=$sep
  fi
  if (( $caption & 2 )) && [[ -n "${identifier[$i]}" ]]; then
    echo "  <span class=num>$s$(esc "${identifier[$i]}")</span>"
    s=$sep
  fi
  if (( $caption & 8 )); then
    echo "  <span class=sizes>$s<a title=\"${Large[$lang]}\""
    echo -e "   href=\"$(esc "${args[$i]}")\">${size[$i]}&nbsp;KB</a>\c"
    # Don't link to the small version if the small version is actually larger
    if [ $smallsize -lt ${size[$i]} ]; then
      echo -e "\n  $s<a title=\"${Small[$lang]}\""
      echo -e "   href=\"$(esc "$smallname")\">${smallsize}&nbsp;KB</a>\c"
    fi
    echo "</span>"
    s=$sep
  fi
  if (( $caption & 4 )) && [[ -n "${description[$i]}" ]]; then
    echo "  <span class=desc>$s$(expand-links "$(esc "${description[$i]}")")</span>"
    s=$sep
  fi
  if (( $caption & 32 )) && [[ -n "${date[$i]}" ]]; then
    echo "  <span class=date>$s$(esc "${date[$i]}")</span>"
    s=$sep
  fi
  if (( $caption & 16 )) && [[ -n "${coverage[$i]}" ]]; then
    echo "  <span class=place>$s$(esc "${coverage[$i]}")</span>"
    s=$sep
  fi
  if (( $caption & 64 )); then
    echo "  <span class=rdf>$s<a title=\"${Raw[$lang]}\""
    echo "   href=\"$(esc "$metalink")\">RDF</a></span>"
  fi
  echo "</div>"
}


declare -i groupcount=1 lang i j k
declare -a order		# Map indexes of args[] into desired order
TMP2=`mktemp -q $TEMPDIR/XXXXXX` || die "Cannot create temporary file."

# Parse options
#
while getopts "hagyMdGxieC:t:l:n:m:c:w:p:s:N:P:U:z:j:" flag; do
  case $flag in
    "i") interactive=true;;
    "t") pagetitle="$OPTARG";;
    "l") language="$OPTARG";;
    "n") owner="$OPTARG";;
    "m") email="$OPTARG";;
    "c") crop="$OPTARG";;
    "w") w1="$OPTARG";;
    "p") preamble="$OPTARG";;
    "s") style="$OPTARG";;
    "g") group=series;;
    "y") group=year;;
    "M") group=month;;
    "d") group=day;;
    "G") samegrouping=true;;
    "x") xmplink=dynamic;;
    "a") htaccess=true;;
    "N") nextlink="$OPTARG";;
    "P") prevlink="$OPTARG";;
    "U") up="$OPTARG";;
    "z") osm_zoom="$OPTARG";;
    "j") max_procs=$OPTARG;;
    "e") trust_ext=true;;
    "C") caption=$OPTARG;;
    "h") help;;
    "?") usage;;
  esac
done

((max_procs >= 1)) || die "max processes (-j) must be >= 1"

shift $((OPTIND - 1))

# Parse the name of the HTML file to output to
#
if [[ $# -ge 1 ]]; then output=$1; shift; fi

# With -G or -i, set the grouping to that of the existing index
#
if $samegrouping || $interactive; then
  if [[ -f "$output" ]]; then
    if grep -F -q '<ul class="series' "$output"; then group=series
    elif grep -F -q '<ul class="day' "$output"; then group=day
    elif grep -F -q '<ul class="month' "$output"; then group=month
    elif grep -F -q '<ul class="year' "$output"; then group=year
    fi
  fi
fi

# If the -C option is a list, convert it to a single number.
#
caption=${caption//,/ }		# Commas to spaces
i=0
for f in $caption; do
  case $f in
    t|ti|tit|titl|title)
      ((i |= 1));;		# title
    n|nu|num|numb|numbe|number)
      ((i |= 2));;		# number
    de|des|desc|descr|descri|descrip|descript|descripti|descriptio|description)
      ((i |= 4));;		# description
    i|im|ima|imag|image|images)
      ((i |= 8));;		# images
    p|pl|pla|plac|place)
      ((i |= 16));;		# place
    da|dat|date)
      ((i |= 32));;		# date
    m|me|met|meta|metad|metada|metadat|metadata)
      ((i |= 64));;		# metadata
    f|fi|fil|file)
      ((i |= 128));;		# file
    [0-9]|[0-9][0-9]|[0-9][0-9][0-9])
      ((i |= f));;		# a number
    *)
      die "Unrecognized or ambiguous keyword in -C: \"$f\"";;
  esac
done
caption=$i

# If interactive, prompt for all parameters. Loop until user is
# satisfied.
#
while $interactive; do
  images="$@"
  ask_pagetitle
  ask_language
  ask_owner_and_email
  ask_output
  ask_images
  ask_percentage
  ask_thumbnail_size
  ask_caption
  ask_preamble
  ask_style
  ask_nav_links
  ask_grouping
  ask_xmplink
  ask_confirmation "Are these correct?\n\n\
Title      : $pagetitle\n\
Language   : $language\n\
Owner      : $owner\n\
E-mail     : $email\n\
Output     : $output\n\
Crop       : $crop%\n\
Thumbnail  : $w1 x $w1 px\n\
Captions   : $caption\n\
Preamble   : $preamble\n\
Style sheet: $style\n\
Up link    : $up\n\
Prev link  : $prevlink\n\
Next link  : $nextlink\n\
Group by   : $group\n\
XMP link   : $xmplink" && interactive=false
  set -- $images
done

[[ -n "$output" ]] || usage

# Check for a common mistake:
#
if [[ -e "$output" && `file -b -L "$output"` != *[SH][GT]ML*text* ]]; then
  die "\`$output' exists but is not an HTML file; did you mean index.html?"
fi

# Output "-" means standard output.
#
[[ "$output" != "-" ]] || output=/dev/stdout

# Translate name of the language into an index into language array
#
((lang = ${#languages[*]} - 1))
while [ "${languages[$lang]}" != "$language" -a $lang -gt 0 ]; do
  ((lang = $lang - 1))
done

# The email address may also be a URL. A URL is used as is, an email
# address is prefixed with "mailto:".
if [[ "$email" =~ ^[a-zA-Z]+: ]]; then # It's a URL
  :
elif [[ "$email" =~ @ ]]; then	# It's an email address, make it a URL
  email=mailto:$email
elif [[ -n "$email" ]]; then	# It's neither, but not empty
  echo "Warning: \"$email\" doesn't look like an email address or a URL" >&2
fi

# Display progress on stdout if it is a terminal, otherwise on stderr
# if it is a terminal, otherwise do not display progress.
#
if [[ -t 1 ]]; then function trace { echo "$@"; }
elif [[ -t 2 ]]; then function trace { echo "$@" >&2; }
else function trace { :; }
fi

# Determine the file formats of the arguments and store them in
# format[]. Remove files in unknown formats and store the remaining
# ones in args[].
#
trace -n "Check args    "
args=()
if $trust_ext; then
  for f; do
    case "$f" in
      *.svg|*.SVG) format+=(svg); args+=("$f");;
      *.pdf|*.PDF|*.ps|*.PS|*.eps|*.EPS) format+=(ps); args+=("$f");;
      *.jpg|*.jpeg|*.JPG|*.JPEG) format+=(jpeg); args+=("$f");;
      *.png|*.PNG) format+=(png); args+=("$f");;
      *.crw|*.CRW) format+=(raw); args+=("$f");;
      *.tif|*.TIF|*.tiff|*.TIFF) format+=(tiff); args+=("$f");;
      *.webp|*.WEBP) format+=(webp); args+=("$f");;
      *) echo "$f: unsupported format, skipped" >&2;;
    esac
    trace -n .
  done
else
  for f; do
    case `file -b -L "$f"` in
      SVG*) format+=(svg); args+=("$f");;
      PDF*|*PostScript*|*Postscript*) format+=(ps); args+=("$f");;
      JPEG*) format+=(jpeg); args+=("$f");;
      PNG*) format+=(png); args+=("$f");;
      *raw\ image*) format+=(raw); args+=("$f");;
      TIFF*) format+=(tiff); args+=("$f");;
      *Web/P*) format+=(webp); args+=("$f");;
      *) echo "$f: unsupported format, skipped" >&2;;
    esac
    trace -n .
  done
fi
trace

if [[ ${#args[@]} == 0 ]]; then die "Need at least one image."; fi

# Derive the paths of small images and thumbnails. If an argument
# includes a directory, form the name by replacing slashes, dots and
# spaces in the directory part by underlines. And if argument does not
# end in .jpg or .jpeg, add .jpg at the end.
#
for ((i = 0; i < ${#args[@]}; i++)); do
  f=${args[$i]};
  base=${f##*/}; dir=${f%$base}; f=${dir//[ .\/]/_}$base
  if ! [[ "$f" =~ \.jpe?g$ ]]; then f=$f.jpg; fi
  smallnames[$i]=$smdir/$f
  thumbnames[$i]=$thdir/$f
done

# Extract XMP from each image, store in a temporary file.
#
trace -n "Extract XMP   "
NPROCS=$(semaphore-new $max_procs $TEMPDIR 2>&1) || die "$NPROCS"
for ((i = 0; i < ${#args[@]}; i++)); do
  semaphore-p $NPROCS		# Acquire a process slot
  {
    f=${args[$i]}
    xmpfile=$TEMPDIR`abs_path "$f"`.xmp
    mkdir -p ${xmpfile%/*}
    case ${format[$i]} in
      svg) extract_xmp_from_svg "$f" >"$xmpfile";;
      ps) extract_xmp_from_ps "$f" >"$xmpfile";;
      jpeg) extract_xmp_from_jpeg "$f" >"$xmpfile";;
      png) extract_xmp_from_png "$f" >"$xmpfile";;
      raw) extract_xmp_from_raw "$f" >"$xmpfile";;
      tiff) extract_xmp_from_tiff "$f" >"$xmpfile";;
      webp) extract_xmp_from_webp "$f" >"$xmpfile";;
      *) die "Bug! Cannot happen!";;
    esac
    trace -n .
    semaphore-v $NPROCS		# Release our process slot
  } &
done
wait
trace

# Extract the properties that interest us from that XMP into several
# arrays.
#
trace -n "Parse XMP     "
for ((i = 0; i < ${#args[@]}; i++)); do
  f=${args[$i]}
  xmpfile=$TEMPDIR`abs_path "$f"`.xmp
  xmp_to_key_value "$i" "$xmpfile"
  size[$i]=$(((`wc -c <"$f"` + 512) / 1024)) # size in KB
  trace -n .
done
trace

# Sort by series or by date
#
if [ -z "$group" ]; then
  for ((i = 0; i < ${#args[@]}; i++)); do order[$i]=$i; done
else
  trace -n "Sort          "

  for ((i = 0; i < ${#args[@]}; i++)); do
    f=${args[$i]}
    if [[ "$group" == series ]]; then
      echo -e "${relation[$i]//[$'\r'$'\n'$'\t']/ }\t$f\t$i">>$TMP2
    else
      g=$(get-date "${date[$i]}" "$group") # Get yyyy-mm-dd, yyyy-mm or yyyy
      echo -e "${g:=${Unknown[$lang]}}\t$f\t$i" >>$TMP2
    fi
    trace -n .
  done

  # New list of images, sorted by group
  #
  for i in `sort -bdfs -t $'\t' -k1,1 $TMP2 | cut -f3`; do order+=($i); done

  trace
fi

# Create or update XMP files
#
if [ $xmplink = static ]; then
  trace -n "XMP files     "
  for ((i = 0; i < ${#args[@]}; i++)); do
    f=${args[$i]}
    xmpfile=$TEMPDIR`abs_path "$f"`.xmp
    metalink=${f}.xmp
    if ! [ "$metalink" -nt "$f" ]; then cp "$xmpfile" "$metalink"; fi
    trace -n .
  done
  trace
fi

# Create or update small images
#
trace -n "Small images  "
mkdir -p $smdir
NPROCS=$(semaphore-new $max_procs $TEMPDIR 2>&1) || die "$NPROCS"
for ((i = 0; i < ${#args[@]}; i++)); do
  f=${args[$i]}
  smallname=${smallnames[$i]}

  if ! [ "$smallname" -nt "$f" ]; then # Including when $smallname doesn't exist
    semaphore-p $NPROCS
    {
      scale $smallwd $smallht "$f" >"$smallname"
      trace -n .
      semaphore-v $NPROCS
    } &
  else
    trace -n .
  fi
done
wait
trace

# Create or update thumbnails
#
trace -n "Thumbnails    "
mkdir -p $thdir
NPROCS=$(semaphore-new $max_procs $TEMPDIR 2>&1) || die "$NPROCS"
for ((i = 0; i < ${#args[@]}; i++)); do
  f=${args[$i]}
  smallname=${smallnames[$i]}
  thumbname=${thumbnames[$i]}

  if [ -s "$smallname" ]; then
    if ! [ "$thumbname" -nt "$smallname" ]; then
      semaphore-p $NPROCS
      {
	crop_and_scale $crop $w1 $w1 "$smallname" >"$thumbname"
	trace -n .
	semaphore-v $NPROCS
      } &
    else
      trace -n .
    fi
  fi
done
wait
trace

# Create HTML pages
#
trace -n "HTML pages    "
NPROCS=$(semaphore-new $max_procs $TEMPDIR 2>&1) || die "$NPROCS"
first=${args[${order[0]}]##*/}.html # TODO: assumes those are readable images
((j = ${#args[@]} - 1))
last=${args[${order[$j]}]###*/}.html
mainpage=
for ((i = 0; i < ${#args[@]}; i++)); do
  k=${order[$i]}
  f=${args[$k]}
  smallname=${smallnames[$k]}

  if [ -s "$smallname" ]; then
    ((j = i + 1))
    ((j < ${#args[@]})) && next=${args[${order[$j]}]##*/}.html || next=
    prev=$mainpage
    mainpage=${f##*/}.html
    metalink=${f}.xmp
    if [ $xmplink == dynamic ]; then metalink="$f;application%2Frdf%2Bxml"; fi

    semaphore-p $NPROCS
    {
      write_page $k "$prev" "$next" "$smallname" "$metalink" \
		 "$first" "$last" >"$mainpage"
      trace -n .
      semaphore-v $NPROCS
    } &
  fi
done
wait
trace

# Write the index page
#
output_header "$pagetitle" >"$output"

# Make a menu of groups
#
if [ -n "$group" ]; then
  echo "<ul class=\"$group toc\">" >>"$output"
  sort -b -d -f -t $'\t' -k1,1 -u $TMP2 | cut -f1 | cat -n | \
    sed \
      -e 's/&/\&amp;/g' \
      -e 's/</\&lt;/g' \
      -e 's/>/\&gt;/g' \
      -e 's/"/\&quot;/g' \
      -e 's/^\( *[0-9]*	\)@@@$/\1.../' \
      -e 's/^\( *[0-9][0-9]*	\)$/\1'"${Unknown[$lang]}}}"'/' \
      -e 's/^ *\([0-9][0-9]*\)	/ <li><a href="#s\1">/' \
      -e 's/$/<\/a>/' >>"$output"
  echo "</ul>" >>"$output"
  echo >>"$output"
fi

# Print preamble, if any
#
if [ -f "$preamble" ]; then
  echo "<div class=preamble>"
  cat "$preamble"
  echo "</div>"
fi >>"$output"

# Write a thumbnail box for each image
#
trace -n "Index         "
[[ -z "$group" ]] && echo "<div class=\"wide cards\">" >>"$output"
prevgroup="	"
for ((i = 0; i < ${#args[@]}; i++)); do
  k=${order[$i]}
  f=${args[$k]}
  smallname=${smallnames[$k]}

  if [ -s "$smallname" ]; then
    mainpage=${f##*/}.html
    thumbname=${thumbnames[$k]}
    metalink=${f}.xmp
    if [ $xmplink == dynamic ]; then metalink="$f;application%2Frdf%2Bxml"; fi

    # If images are grouped, insert a group title, if needed
    if [ -n "$group" ]; then
      if [[ "$group" == series ]]; then	g=${relation[$k]}
      else g=$(get-date "${date[$k]}" "$group")
      fi
      h=${g:-${Unknown[$lang]}}
      if [ "$h" != "$prevgroup" ]; then
	[[ "$prevgroup" == "	" ]] || echo "</div><!--series-->"
	echo
	echo "<h2 id=s$groupcount>$(esc "$h")</h2>"
	echo "<div class=\"series cards wide\">"
	((groupcount++))
	prevgroup="$h"
      fi >>"$output"
    fi

    write_thumb_box "$mainpage" "$k" "$smallname" "$thumbname" "$metalink" \
		    "$caption" >>"$output"
    trace -n .
  fi
done
trace
echo >>"$output"

if [[ -n "$group" ]]; then
  echo "</div><!--series-->" >>"$output"
else
  echo "</div>" >>"$output"
fi
output_footer >>"$output"

# If a .htaccess file for Apache was requested, write or update it.
#
if $htaccess; then
  [[ -f .htaccess ]] || touch .htaccess
  egrep -q -i '^[ 	]*AddCharset[ 	]+UTF-8[ 	]+\.xmp\>' .htaccess ||\
   echo 'AddCharset UTF-8 .xmp' >>.htaccess
  egrep -q -i '^[ 	]*AddType[ 	]+application/rdf\+xml[ 	]+.xmp\>' .htaccess ||\
   echo 'AddType application/rdf+xml .xmp' >>.htaccess
fi
