#!/bin/bash
#
# Add XMP metadata to a series of JPEG images.
#
# 1)
#    addxmp
#    addxmp -m mask
#    addxmp <textfile>
#    addxmp -m mask <textfile>
#
# Reads from a file or stdin and adds (mostly Dublin Core) metadata
# from that file to the image files mentioned in it.
#
# If option -m is present, the metadata is only added to files that
# match the mask. The mask is a file name or file name pattern with
# wildcards *, ? and [...], as defined by the Bash shell.
#
# 2)
#    addxmp -c
#    addxmp -c <jpegfile>...
#
# Creates a template on stdout for metadata for the images in the
# current directory or for the images given on the command line.
#
# 3)
#    addxmp -r
#    addxmp -r <jpegfile>...
#
# Reads metadata from the images in the current directory and outputs
# to stdout a pre-filled template.
#
# Ad 1) Reads (from a file or from stdin) a list of file names (JPEG
# file) and descriptions to be added to them. Each line is either a
# field to add or the name of a file. The first letter of each line is
# the field name and determines what the line contains. E.g.:
#
#     L en
#     C Antibes, Alpes-Maritimes, France
#     F 12345.jpg
#     T Olive tree
#     D An olive tree in a square
#
#     F 12346.jpg
#     D The trunk of an olive tree
#
# Each line with an "F" gives a file name. Each file gets the same
# metadata as the previous file, except for fields that are overridden
# by lines directly after the file name. In the example above, the
# first file, 12345.jpg, gets language="en", coverage="Antibes,
# Alpes-Maritimes, France", title="Olive tree" and description="An
# olive tree in a square". The second file, 12346.jpg, gets the same
# language, coverage and title, but a different description;
# description="The trunk of an olive tree".
#
# To reset a field to nothing, leave it empty:
#
#     F 123.jpg
#     T Olive tree
#     D An olive tree
#     F 456.jpg
#     D
#
# The second file, 456.jpg, will get the same title as the first, but
# will not get a description.
#
# If a field is added to a file, it replaces the field that that file
# may already have. If a language is given, only the field in that
# language is replaced, otherwise the field is removed in all
# languages and replaced by one without a language.
#
# If a field is not added, such as the D (description) field for file
# 456.jpg above, any existing values for that field in the file are
# left unchanged. In other words, there is no way to remove an
# existing field, only to replace it.
#
# There must be one space between the field name and the value. (If
# the value is empty, the space may be omitted.)
#
# Values may be written over several lines by starting each line after
# the first with a "+" and a space, e.g.:
#
#     D This description
#     + takes two lines
#
# The lines will be concatenated with a space between them.
#
# The letters are:
#
#   L language. The value should be a locale tag such as "fr" (for
#     French) or "en-us" (for American English). This is the language
#     of the metadata, not the language of the image.
#
#   C coverage. The location where the photo was taken.
#
#   T title.
#
#   D description.
#
#   P publisher.
#
#   S series (Dublin Core: relation). The title for a series of
#     related photos, such as the name of the event where they were
#     taken, or the reason why they were taken.
#
#   R rights. A copyright statement, e.g., "Copyright © 2013 Bert Bos"
#
#   A author (Dublin Core: creator).
#
#   c contributor.
#
#   I identifier. See also N.
#
#   N name pattern. A regular expression that is applied to the file
#     name to yield the identifier. Only used if I (identifier) is
#     empty. The regular expression must have a parenthesized part and
#     the first such part is the identifier. The pattern is anchored
#     at the start of the file name. E.g., if the file name is
#     "img_1234.jpg" and the pattern is "img_([0-9]+)", the
#     parenthesized subpattern will match "1234" and that will thus be
#     the identifier for that file.
#
#   d date. Recommended format is YYYY-MM-DD or YYYY-MM-DDThh:mm:ssZ.
#
#   E date. When the digital version was created, if different from
#     field d. Can be used to record the date an analog photo was
#     scanned, e.g. Recommended format is YYYY-MM-DD or
#     YYYY-MM-DDThh:mm:ssZ.
#
#   z timezone. If d is not given, the date and time are read from the
#     photo's EXIF, in which case this timezone is added. Recommended
#     format is numeric, e.g., +02:00, or Z (for UTC)
#
#   K keywords (Dublin Core: subject). A list of keywords separated by
#     commas.
#
#   X longitude. This overrides any GPS data in the photo's EXIF.
#
#   Y latitude. This overrides any GPS data in the photo's EXIF.
#
#   Z altitude. This overrides any GPS data in the photo's EXIF.
#
#   s source. The source from which this image is derived.
#
#   # comment. Lines starting with "#" are ignored.
#
#   Empty lines are ignored.
#
# TODO: continuation lines that start with a space or tab.
#
# TODO: a way to delete an existing value without a language (i.e.,
# not even x-default) from a file.
#
# TODO: Allow commas in subjects ("K" lines). Commas are currently
# used to separate keywords.
#
# File names can occur multiple times. This is useful, e.g., to add
# metadata in multiple languages:
#
#     F 123.jpg
#     L en
#     D An olive tree
#     F 123.jpg
#     L fr
#     D Un olivier
#
# In addition to the fields given in the input, each file is also
# scanned for relevant EXIF data, such as camera make and GPS
# coordinates. That data is also added in the XMP. Fields given
# explicitly override fields found in the EXIF.
#
# Author: Bert Bos <bert@w3.org>
# Created: 14 March 2013

# Dublin Core properties
#
readonly DC="http://purl.org/dc/elements/1.1/"
readonly TITLE="${DC}title"
readonly CREATOR="${DC}creator"
readonly SUBJECT="${DC}subject"
readonly DESCRIPTION="${DC}description"
readonly PUBLISHER="${DC}publisher"
readonly CONTRIBUTOR="${DC}contributor"
readonly DATE="${DC}date"
readonly TYPE="${DC}type"
readonly FORMAT="${DC}format"
readonly IDENTIFIER="${DC}identifier"
readonly RELATION="${DC}relation"
readonly COVERAGE="${DC}coverage"
readonly RIGHTS="${DC}rights"
readonly SOURCE="${DC}source"

# PhotoRDF Technical properties
#
readonly TECH="http://www.w3.org/2000/PhotoRDF/technical-1-0#"
readonly CAMERA="${TECH}camera"
readonly FILM="${TECH}film"
readonly LENS="${TECH}lens"
readonly DEVEL_DATE="${TECH}devel-date"

# Selected EXIF/XMP properties
#
readonly EXIF="http://ns.adobe.com/exif/1.0/"
readonly XMP="http://ns.adobe.com/xap/1.0/"
readonly GPSVersionID="${EXIF}GPSVersionID"	# Value should be 2.0.0.0
readonly GPSLatitude="${EXIF}GPSLatitude"	# DDD,MM,SSk or DDD,MM.mmk
readonly GPSLongitude="${EXIF}GPSLongitude"	# DDD,MM,SSk or DDD,MM.mmk
readonly GPSAltitudeRef="${EXIF}GPSAltitudeRef" # 0 above sea level, 1 below
readonly GPSAltitude="${EXIF}GPSAltitude"	# In meters
readonly GPSBearingRef="${EXIF}GPSDestBearingRef" # T(rue) or M(agnetic) North
readonly GPSBearing="${EXIF}GPSDestBearing"	# Compas direction [0.0,360.0)
readonly CREATEDATE="${XMP}CreateDate"		# DateTimeDigitized in EXIF

# Selected EXIF 2.21 properties
#
readonly EXIF221="http://cipa.jp/exif/1.0/"

# Selected properties in the TIFF namespace
#
readonly TIFF="http://ns.adobe.com/tiff/1.0/"
readonly MAKE="${TIFF}Make"
readonly MODEL="${TIFF}Model"

declare -i maxprocesses=1     # Max # of write-to-file processes in parallel
readonly clr_eol=$(tput el) # Clear until end of line
# sed option to get extended regexps
if sed -r p </dev/null 2>/dev/null; then ext="-r"; else ext="-E"; fi


# die -- print error message and exit
function die
{
  echo >&2
  echo "$@" >&2
  wait
  exit 1
}


# 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"; }


# usage -- print usage message and exit
function usage
{
  echo "Usage: $1 [-m mask] [file]"
  echo "   or: $1 -c [image [image...]]"
  echo "   or: $1 -r [image [image...]]"
}


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


########################################################################
##
##		 Routines related to print-template()
##
########################################################################


# print-template -- output a template
function print-template
{
  echo "# Template generated by $0 -c"
  echo "L nl"
  echo "C <town, province, country>"
  echo "P http://www.phonk.net/"
  echo "S <name of series/relation>"
  echo "A Bert Bos"
  echo "R Copyright ©" $(date +%Y) "Bert Bos"
  echo "N ([0-9a-z]+)-"
  echo "# c <contributor>"
  echo "# X longitude DDD,MM,SSk or DDD,MM.mmk or [-]DDD.dddd"
  echo "# Y latitude DDD,MM,SSk or DDD,MM.mmk or [-]DDD.dddd"
  echo "# Z altitude (meters above sea level)"
  echo "# d YYYY-MM-DD or YYYY-MM-DD HH:MM:SS+ZZ:ZZ (when the photo was taken)"
  echo "# E YYYY-MM-DD or YYYY-MM-DD HH:MM:SS+ZZ:ZZ (when it was digitized)"
  echo "# s source (original) image"
  echo "# z timezone"
  echo
  if [ $# == 0 ]; then		# No arguments: use images from directory
    for f in *.jpg; do
      echo -e "F $f\nT\nD\nK\n"
    done
  else				# With arguments: use those arguments
    for f; do
      echo -e "F $f\nT\nD\nK\n"
    done
  fi
}


########################################################################
##
##		 Routines related to read-template()
##
########################################################################


# scan-xmp -- extract XMP data from file $1
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
  tr '\000' '\n' <"$1" | \
    sed \
      -e '/<?xpacket [^>]*W5M0MpCehiHzreSzNTczkc9d/,/<?xpacket end/!d' \
      -e 's/.*<?xpacket begin/<?xpacket begin/' \
      -e "s/<?xpacket end=['\"][rw]['\"]?>.*/<?xpacket end='w'?>/"
}

if have xmp-scan; then
  function read-xmp-jpeg { xmp-scan "$1" || true; } # Ignore error if no XMP
elif have rdjpgxmp; then
  function read-xmp-jpeg { rdjpgxmp "$1" || echo -n; }
elif have exiv2; then
  function read-xmp-jpeg { exiv2 -pX "$1"; }
elif have exiftool; then
  function read-xmp-jpeg { exiftool -XMP -b "$1"; }
else
  function read-xmp-jpeg { scan-xmp "$1"; }
fi

if have xmp-scan; then
  function read-xmp-png { xmp-scan "$1" || true; }
elif have exiv2; then
  function read-xmp-png { exiv2 -pX "$1"; }
elif have exiftool; then
  function read-xmp-png { exiftool -XMP -b "$1"; }
else
  function read-xmp-png { scan-xmp "$1"; }
fi

if have xmp-scan; then
  function read-xmp-webp { xmp-scan "$1" || true; }
elif have webpmux; then
  function read-xmp-webp { webpmux -get xmp -o - -- "$1" 2>/dev/null||echo -n; }
elif have exiv2; then
  function read-xmp-webp { exiv2 -pX "$1"; }
elif have exiftool; then
  function read-xmp-webp { exiftool -XMP -b "$1"; }
else
  function read-xmp-webp { scan-xmp "$1"; }
fi

if have xmp-scan; then
  function read-xmp-any { xmp-scan "$1" || true; }
else
  function read-xmp-any { scan-xmp "$1"; }
fi


# read-xmp -- extract any XMP from image file $1
function read-xmp
{
  case `file --mime-type -b -L "$1"` in
    image/jpeg) read-xmp-jpeg "$1";;
    image/png) read-xmp-png "$1";;
    image/webp) read-xmp-webp "$1";;
    *) read-xmp-any "$1";;
  esac
}


# read-metadata -- read metadata from $1 and print it, $2 is file with predicates
function read-metadata
{
  # $3 is a temporary file to keep each previous block of values in.

  # TODO: Remove newlines from values.
  # TODO: Speed this up by using more Bash variables and built-ins
  # instead of forking join, grep, sed, head, etc.?

  local TMP1=$(mktemp $TMPDIR/XXXXXX) || exit 1
  local TMP2=$(mktemp $TMPDIR/XXXXXX) || exit 1
  local TMP3=$(mktemp $TMPDIR/XXXXXX) || exit 1
  local langs first

  # Get the existing metadata from $1. Result is a three-column TSV
  # file: (predicate name, language, value). The Awk script puts
  # multiple values for the same property and language on the same
  # line, separated by commas; and puts the language in the 2nd column
  # (and the language x-default is removed).
  read-xmp "$1" | xmptool -c | xmptool -v '*' |
    awk -F $'\t' '
      prev == $1 {printf ", %s", $2; next}
      prev != "" {printf "\n"}
      {prev = $1}
      $1 ~ /\[x-default\]$/ {printf "%s\t\t%s", $1, $2; next}
      $1 !~ /\[.*\]$/ {printf "%s\t\t%s", $1, $2; next}
      {sub(/\[/, "\t"); sub(/\]/, ""); printf "%s", $0}' >$TMP1

  # Add the EXIF data to the end. It will be used if there is no
  # corresponding data in the XMP. copy-from-exif writes three columns
  # to $TMP3: predicate, the value prefixed with an "x", and the
  # language (here: x-default).
  copy-from-exif $TMP3 "$1" Z x-default
  sed -e 's/	x-default$//' \
      -e 's/	/		/' \
      -e 's/	x/	/' $TMP3 >>$TMP1

  # Sort the metadata file, keeping just the first if there are
  # multiple entries with the same property and language.
  sort -t$'\t' -k1,2 -u $TMP1 >$TMP2

  # Replace the RDF names with their corresponding letters from $2.
  # Output is a four-column TSV file: (letter, language dependence,
  # language, value), sorted on letter.
  join -t '	' -1 3 -2 1 -o 1.1,1.2,2.2,2.3 \
	<(sort -t '	' -k3 $2) $TMP2 | sort >$TMP3

  # Get one of the languages used for values for which the language
  # matters. Default to the language of the previous block, or x-default.
  lang=$(grep "	y	" $TMP3 | grep -v "		" | \
	   head -n 1 | cut -f 3 || sed -n -e '/^L/{s/^L *//;p;q;}' $3)
  lang=${lang:-x-default}

  # Replace the languages of values for which the language does not
  # matter by the language just found.
  sed -e "s/	n	[^	]*	/	n	$lang	/" $TMP3 >$TMP1
  mv $TMP1 $TMP3

  # Also set the language of each value that does not have a language
  # and that consists of a URL to the language just found.
  sed \
      -e "s|		https://|	$lang	https://|" \
      -e "s|		http://|	$lang	http://|" $TMP3 >$TMP1
  mv $TMP1 $TMP3

  # Set the language of values that don't have a language yet to x-default.
  sed -e 's/		/	x-default	/' $TMP3 >$TMP1
  mv $TMP1 $TMP3

  # Find all languages used in the file's XMP.
  langs=`cut -f3 $TMP3 | sort -u`

  # Output all values in each language, with empty values if there is
  # none. If there is no XMP at all in the file, use the default
  # language found above, so that the loop runs at least once and
  # outputs empty fields.
  first=true
  for lang in ${langs:-$lang}; do
    echo
    echo "F $1"

    # If this is the first of the languages, generate the full list of
    # fields, with empty values if there is no value for that field in
    # TMP3. Otherwise just generate the fields that have values.
    if $first; then
      (
	echo "L $lang"
	{ grep "	$lang	" $TMP3 || true; } | \
	  join -t '	' -a 1 -o 1.1,2.4 $2 - | \
	  sed -e 's/	/ /' -e 's/ $//'
      ) | \
	sort >$TMP2
    first=false
    else
      echo "L $lang"
      { grep "	$lang	" $TMP3 || true; } | \
	cut -d$'\t' -f1,4 | \
	sed -e 's/	/ /' -e 's/ $//'
    fi

    # Filter out the fields that are the same as in the previously
    # printed block.
    comm -13 $3 $TMP2

    # Remember the values of this block for the next invocation of
    # this function.
    cat $TMP2 >$3
  done

  rm -f $TMP1 $TMP2 $TMP3
}


# read-template -- create metadata template from existing metadata
function read-template
{
  local f g
  local -i i
  local TMP1=$(mktemp $TMPDIR/XXXXXX) || exit 1
  local TMP2=$(mktemp $TMPDIR/XXXXXX) || exit 1

  # Write a TSV file to join with in read-metadata(), sorted on the
  # first letter.
  cat >$TMP1 <<-EOF
	A	y	http://purl.org/dc/elements/1.1/creator
	C	y	http://purl.org/dc/elements/1.1/coverage
	D	y	http://purl.org/dc/elements/1.1/description
	E	n	http://ns.adobe.com/xap/1.0/CreateDate
	I	n	http://purl.org/dc/elements/1.1/identifier
	K	y	http://purl.org/dc/elements/1.1/subject
	P	y	http://purl.org/dc/elements/1.1/publisher
	R	y	http://purl.org/dc/elements/1.1/rights
	S	y	http://purl.org/dc/elements/1.1/relation
	T	y	http://purl.org/dc/elements/1.1/title
	X	n	http://ns.adobe.com/exif/1.0/GPSLongitude
	Y	n	http://ns.adobe.com/exif/1.0/GPSLatitude
	Z	n	http://ns.adobe.com/exif/1.0/GPSAltitude
	c	y	http://purl.org/dc/elements/1.1/contributor
	d	n	http://purl.org/dc/elements/1.1/date
	s	n	http://purl.org/dc/elements/1.1/source"
	EOF

  # TMP2 is a temporary file to keep each previous block of values in,
  # for use by read-metadata.

  echo "# Template generated by $0 -r"
  if [ $# == 0 ]; then		# No arguments: read all images in the directory
    for f in *.jpg; do
      read-metadata "$f" $TMP1 $TMP2
    done
  else				# With arguments: read the arguments
    for f; do
      read-metadata "$f" $TMP1 $TMP2
    done
  fi

  rm -f $TMP1 $TMP2
}


########################################################################
##
##		     Routines related to apply()
##
########################################################################

if have wrjpgxmp; then
  function write-xmp-jpeg { wrjpgxmp "$1"; }
elif have exiv2; then
  function write-xmp-jpeg
  {
    local TMP=$(mktemp $TMPDIR/write-XXXX) || exit 1
    cp "$1" $TMP
    exiv2 -i XX- $TMP
    cat $TMP
    rm $TMP
  }
elif have exiftool; then
  # Exiftool only copies tags it knows :-(
  function write-xmp-jpeg { exiftool -tagsFromFile - -all:all -o - "$1"; }
else
  function write-xmp-jpeg { die "$0: No tool found to write XMP into JPEG. Need  wrjpgxmp, exiv2 or exiftool."; }
fi

if have exiv2; then
  function write-xmp-png
  {
    local TMP=$(mktemp $TMPDIR/write-XXXX) || exit 1
    cp "$1" $TMP
    exiv2 -i XX- $TMP
    cat $TMP
    rm $TMP
  }
elif have exiftool; then
  # Exiftool only copies tags it knows :-(
  function write-xmp-png { exiftool -tagsFromFile - -all:all -o - "$1"; }
else
  function write-xmp-png { die "$0: No tool found to write XMP into PNG. Need exiv2 or exiftool."; }
fi

if have webpmux; then
  function write-xmp-webp { webpmux -set xmp - -o - -- "$1"; }
elif have exiv2; then
  function write-xmp-webp
  {
    local TMP=$(mktemp $TMPDIR/write-XXXX) || exit 1
    cp "$1" $TMP
    exiv2 -i XX- $TMP
    cat $TMP
    rm $TMP
  }
elif have exiftool; then
  # Exiftool only copies tags it knows :-(
  function write-xmp-webp { exiftool -tagsFromFile - -all:all -o - "$1"; }
else
  function write-xmp-webp { die "$0: No tool found to write XMP into WebP. Need  webpmux, exiv2 or exiftool,"; }
fi

if have exiv2; then
  function write-xmp-tiff
  {
    local TMP=$(mktemp $TMPDIR/write-XXXX) || exit 1
    cp "$1" $TMP
    exiv2 -i XX- $TMP
    cat $TMP
    rm $TMP
  }
elif have exiftool; then
  # Exiftool only copies tags it knows :-(
  function write-xmp-tiff { exiftool -tagsFromFile - -all:all -o - "$1"; }
else
  function write-xmp-tiff { die "$0: No tool found to write XMP into TIFF/DNG. Need exiv2 or exiftool."; }
fi

if have exiv2; then
  function write-xmp-crw
  {
    local TMP=$(mktemp $TMPDIR/write-XXXX) || exit 1
    cp "$1" $TMP
    exiv2 -i XX- $TMP
    cat $TMP
    rm $TMP
  }
elif have exiftool; then
  # Exiftool only copies tags it knows :-(
  function write-xmp-crw { exiftool -tagsFromFile - -all:all -o - "$1"; }
else
  function write-xmp-crw { die "$0: No tool found to write XMP into CRW. Need exiv2 or exiftool."; }
fi


# write-xmp -- read XMP from stdin and image from $1, output merged to stdout
function write-xmp
{
  xmptool -c |
    case `file --mime-type -b -L "$1"` in
      image/jpeg) write-xmp-jpeg "$1";;
      image/png) write-xmp-png "$1";;
      image/webp) write-xmp-webp "$1";;
      image/tiff) write-xmp-tiff "$1";; # Could be DNG as well
      image/x-canon-crw) write-xmp-crw "$1";;
      *) die "$0: Can only write metadata to JPEG, PNG, WebP, DNG or CRW images";;
    esac ||
    exit 1
}


# set-field -- set field $2 to value $3 with language $4 in database file $1
function set-field
{
  # The database is just a list of field names and values, one per line
  echo -e "$2\tx$3\t$4" >>$1
}


# set-bag -- set field $2 to the comma-sep values $3 with lang $4 in database $1
function set-bag
{

  # The database is just a list of field names and values, one per
  # line. Insert an "x" before the value, to make sure the column
  # isn't empty, because "read" in add-fields-to-xmp cannot read empty
  # fields.
  if [[ -z "$3" ]]; then
    echo -e "$2\tx$3\t$4"
  else
    local head tail=$3
    while [[ -n "$tail" ]]; do
      head=${tail%%,*}
      tail=${tail#$head}
      tail=${tail#,}
      while [[ "$tail" != "${tail# }" ]]; do tail=${tail# }; done
      echo -e "$2\tx$head\t$4"
    done
  fi >>$1
}


# add-fields-to-xmp -- add fields from file $1 to XMP file $2, output to stdout
function add-fields-to-xmp
{
  local TMP=$(mktemp $TMPDIR/set-XXXX) || exit 1
  local xmpfile=$2
  local prevfield= field value lang r h

  while IFS=$'\t' read field value lang; do

    value=${value#x}

    # Guess that a value that starts with http:/https: is a resource
    case "$value" in
      http:*|https:*) r=-r;;
      *) r=;;
    esac

    if [[ "$prevfield" == "$field" ]]; then
      # This field is the same as the previous line, which means it
      # represents an additional value in a Bag of values. Add it to
      # the field.
      xmptool -w -l "$lang" $r -- "$field" "$value" "$xmpfile"
    elif [[ -z "$value" ]]; then
      # An empty value. Delete the field in the given language.
      xmptool -d -l "$lang" -- "$field" $xmpfile
    else
      # A non-empty value. Replace the field in the given language.
      xmptool -d -l "$lang" -- "$field" $xmpfile |
	xmptool -w -l "$lang" $r -- "$field" "$value"
    fi >$TMP

    # Swap the roles of TMP and xmpfile
    h=$xmpfile; xmpfile=$TMP; TMP=$h

    prevfield=$field

  done <$1

  cat $xmpfile
}


# set-lat-or-long -- set latitude or longitude after checking the syntax
function set-lat-or-long
{
  local xmp=$1 field=$2 value=$3
  local d m

  if [ "$value" == "" ]; then
    set-field $xmp "$field" "" "" # x-default
  elif [[ "$field" == "$GPSLatitude" ]]; then
    if [[ "$value" =~ ^[0-9]+,[0-9]+,?[0-9]*\.?[0-9]*[NS]$ ]]; then
      # ^[0-9]+,[0-9]+(,[0-9]+)?(\.[0-9]+)?[NS]$
      set-field $xmp "$field" "$value" "" # x-default
    elif [[ "$value" =~ ^[0-9]+\.?[0-9]*$ ]]; then
      # ^[0-9]+(\.[0-9]+)?$
      m=$(dc <<<"$value 1%4k60*p"); d=${2%.*}
      set-field $xmp "$field" "${d},${m}N" "" # x-default
    elif [[ "$value" =~ ^-[0-9]+\.?[0-9]*$ ]]; then
      # ^-[0-9]+(\.[0-9]+)?$
      m=$(dc <<<"${2#-} 1%4k60*p"); d=${2#-}; d=${d%.*}
      set-field $xmp "$field" "${d},${m}S" "" # x-default
    elif [[ "$value" =~ ^[0-9]+,[0-9]*\.[0-9]+[NS]$ ]]; then
      set-field $xmp "$field" "$value" "" # x-default
    else
      die "$0: Error: lat/long must be like 6,58,6W or 43,58.1N or -7.956"
    fi
  elif [[ "$field" == "$GPSLongitude" ]]; then
    if [[ "$value" =~ ^[0-9]+,[0-9]+,?[0-9]*\.?[0-9]*[EW]$ ]]; then
      # ^[0-9]+,[0-9]+(,[0-9]+)?(\.[0-9]+)?[EW]$
      set-field $xmp "$field" "$value" "" # x-default
    elif [[ "$value" =~ ^[0-9]+\.?[0-9]*$ ]]; then
      # ^[0-9]+(\.[0-9]+)?$
      m=$(dc <<<"$value 1%4k60*p"); d=${2%.*}
      set-field $xmp "$field" "${d},${m}E" "" # x-default
    elif [[ "$value" =~ ^-[0-9]+\.?[0-9]*$ ]]; then
      # ^-[0-9]+(\.[0-9]+)?$
      m=$(dc <<<"${2#-} 1%4k60*p"); d=${2#-}; d=${d%.*}
      set-field $xmp "$field" "${d},${m}W" "" # x-default
    elif [[ "$value" =~ ^[0-9]+,[0-9]*\.[0-9]+[EW]$ ]]; then
      set-field $xmp "$field" "$value" "" # x-default
    else
      die "$0: Error: lat/long must be like 6,58,6W or 43,58.1N or -7.956"
    fi
  else
    die "$0: Cannot happen!"
  fi
  set-field $xmp "$GPSVersionID" "2.0.0.0" "" # x-default
}


# set-altitude -- set altitude to $2, in meters (19.5m) or rational (38/2)
function set-altitude
{
  local xmp=$1 value=$2
  local ref

  case "$value" in
    "") ref=;;			    # Empty value
    -*/*) ref=1; value=${value#-};; # Negative rational value
    +*/*) ref=0; value=${value#+};; # Positive rational value
    */*) ref=0;;		    # Positive rational value
    -*) ref=1; value=${value#-}; value=`dc <<<"${value%m} 100*1/p"`/100;;
    +*) ref=0; value=${value#+}; value=`dc <<<"${value%m} 100*1/p"`/100;;
    *) ref=0; value=`dc <<<"${value%m} 100*1/p"`/100;; # Positive meters
  esac
  set-field $xmp "$GPSAltitudeRef" "$ref" "" # x-default
  set-field $xmp "$GPSAltitude" "$value" "" # x-default
  set-field $xmp "$GPSVersionID" "2.0.0.0" "" # x-default
}


# set-bearingref -- set bearing reference direction after checking syntax
function set-bearingref
{
  local xmp=$1 value=$2

  case "$value" in
    "") set-field $xmp "$GPSBearingRef" "" "";; # x-default
    T|t) set-field $xmp "$GPSBearingRef" T "";; # x-default
    M|m) set-field $xmp "$GPSBearingRef" M "";; # x-default
    *) die "$0: Error: --bearingref must be \"T\" or \"M\"";;
  esac
}


# set-bearing -- set bearing after checking syntax
function set-bearing
{
  local xmp=$1 value=$2
  local n=${value%.*}
  local h=${value#$n}
  local d=1

  if [ "$value" == "" ]; then
    set-field $xmp "$GPSBearing" "" "" # x-default
  else
    case "$n" in
      [0-9]|[0-9][0-9]|[0-9][0-9][0-9]) ;;
      *) die "$0: Error Bearing value must be a number";;
    esac
    h=${h#.}
    n=${n}${h}
    while [ ! -z "$h" ]; do
      case "$h" in
	[0-9]*) d=${d}0; h=${h#?};;
	*) die "$0: Error Bearing value must be a number";;
      esac
    done
    set-field $xmp "$GPSBearing" $n/$d "" # x-default
  fi
}


if have exiftool; then
  function read-exif-jpeg
  {
    # Convert to the format output by jhead
    exiftool -EXIF:\* -IPTC:\* -GPS\* "$1" |
      sed -e 's/^Make/Camera Make/' \
	  -e '/^GPS .*S$/{s/ S$//; s/:/: S/;}' \
	  -e '/^GPS .*N$/{s/ N$//; s/:/: N/;}' \
	  -e '/^GPS .*W$/{s/ W$//; s/:/: W/;}' \
	  -e '/^GPS .*E$/{s/ E$//; s/:/: E/;}' \
	  -e "/^GPS /{s/ deg/d/; s/'/m/; s/\"/s/;}" \
	  -e '/^GPS Altitude.*Above Sea Level/s/ Above Sea Level//' \
	  -e '/^GPS Altitude.*Below Sea Level/{s/: /: -/;s/ Below Sea Level//;}'
  }
elif have jhead; then
  function read-exif-jpeg { jhead "$1"; }
elif have exiftags; then
  function read-exif-jpeg
  {
    # Convert to the format output by jhead
    # Note: exiftags 1.01 prints altitudes below sea level incorrectly.
    exiftags "$1" |
      sed -e '/^Latitude:/{s/^/GPS /;s/'$'\xB0''/d/;s/'\''/m/;s/$/s/;}' \
	  -e '/^Longitude:/{s/^/GPS /;s/'$'\xB0''/d/;s/'\''/m/;s/$/s/;}' \
	  -e '/^Altitude:/s/^/GPS /' \
	  -e 's/^Image Created:/Date\/Time:/'
  }
elif have exif; then
  function read-exif-jpeg
  {
    # Convert to the format output by jhead
    exif -m "$1" |
      sed -e 's/'$'\t''/: /' \
	  -e '/^North or South Latitude: N/,/^Latitude:/s/Latitude: */Latitude: N /' \
	  -e '/^North or South Latitude: S/,/^Latitude:/s/Latitude: */Latitude: S /' \
	  -e '/^East or West Longitude: E/,/^Longitude: /s/Longitude: */Longitude: E /' \
	  -e '/^East or West Longitude: W/,/^Longitude: /s/Longitude: */Longitude: W /' \
	  -e '/^Altitude Reference: Sea level reference/,/^Altitude:/s/Altitude: */Altitude: -/' \
	  -e '/^Latitude:/{s/,/d/; s/,/m/; s/$/s/;}' \
	  -e '/^Longitude:/{s/,/d/; s/,/m/; s/$/s/;}' \
	  -e 's/^Altitude/GPS Altitude/' \
	  -e 's/^Longitude/GPS Longitude/' \
	  -e 's/^Latitude/GPS Latitude/' \
	  -e 's/^Manufacturer:/Camera make:/' \
	  -e 's/^Model:/Camera model:/' \
	  -e 's|^Date and Time|Date/Time|'
  }
elif have exiv2; then
  function read-exif-jpeg
  {
    exiv2 -P EIlt "$1" |
      sed -e 's/ */: /' \
	  -e 's/^Exif.Photo.DateTimeOriginal:/Date\/Time:/' \
	  -e 's/^Exif.Image.Make:/Camera Make:/' \
	  -e 's/^Exif.Image.Model:/Camera Model:/' \
	  -e 's/^Exif.GPSInfo.GPSLatitude:/GPS Latitude:/' \
	  -e 's/^Exif.GPSInfo.GPSLongitude:/GPS Longitude:/' \
	  -e 's/^Exif.GPSInfo.GPSAltitude:/GPS Altitude:/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearingRef:/GPSDestBearingRef =/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearing:/GPSDestBearing =/'
    }
else
  function read-exif-jpeg { die "$0: No tools found to read EXIF from JPEG"; }
fi

if have exiftool; then
  function read-exif-webp
  {
    exiftool "$1" |
      sed -e 's/^Make/Camera Make/' \
	  -e '/^GPS .*S$/{s/ S$//; s/:/: S/;}' \
	  -e '/^GPS .*N$/{s/ N$//; s/:/: N/;}' \
	  -e '/^GPS .*W$/{s/ W$//; s/:/: W/;}' \
	  -e '/^GPS .*E$/{s/ E$//; s/:/: E/;}' \
	  -e "/^GPS /{s/ deg/d/; s/'/m/; s/\"/s/;}" \
	  -e '/^GPS Altitude.*Above Sea Level/s/ Above Sea Level//' \
	  -e '/^GPS Altitude.*Below Sea Level/{s/: /: -/;s/ Below Sea Level//;}'
  }
elif have exiv2; then
  function read-exif-webp
  {
    exiv2 -P EIlt "$1" |
      sed -e 's/ */: /' \
	  -e 's/^Exif.Photo.DateTimeOriginal:/Date\/Time:/' \
	  -e 's/^Exif.Image.Make:/Camera Make:/' \
	  -e 's/^Exif.Image.Model:/Camera Model:/' \
	  -e 's/^Exif.GPSInfo.GPSLatitude:/GPS Latitude:/' \
	  -e 's/^Exif.GPSInfo.GPSLongitude:/GPS Longitude:/' \
	  -e 's/^Exif.GPSInfo.GPSAltitude:/GPS Altitude:/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearingRef:/GPSDestBearingRef =/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearing:/GPSDestBearing =/'
    }
else
  function read-exif-webp { die "$0: No tools found to read EXIF from WebP"; }
fi

if have exiftool; then
  function read-exif-tiff
  {
    exiftool "$1" |
      sed -e 's/^Make/Camera Make/' \
	  -e '/^GPS .*S$/{s/ S$//; s/:/: S/;}' \
	  -e '/^GPS .*N$/{s/ N$//; s/:/: N/;}' \
	  -e '/^GPS .*W$/{s/ W$//; s/:/: W/;}' \
	  -e '/^GPS .*E$/{s/ E$//; s/:/: E/;}' \
	  -e "/^GPS /{s/ deg/d/; s/'/m/; s/\"/s/;}" \
	  -e '/^GPS Altitude.*Above Sea Level/s/ Above Sea Level//' \
	  -e '/^GPS Altitude.*Below Sea Level/{s/: /: -/;s/ Below Sea Level//;}'
  }
elif have exiv2; then
  function read-exif-tiff
  {
    exiv2 -P EIlt "$1" |
      sed -e 's/ */: /' \
	  -e 's/^Exif.Photo.DateTimeOriginal:/Date\/Time:/' \
	  -e 's/^Exif.Image.Make:/Camera Make:/' \
	  -e 's/^Exif.Image.Model:/Camera Model:/' \
	  -e 's/^Exif.GPSInfo.GPSLatitude:/GPS Latitude:/' \
	  -e 's/^Exif.GPSInfo.GPSLongitude:/GPS Longitude:/' \
	  -e 's/^Exif.GPSInfo.GPSAltitude:/GPS Altitude:/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearingRef:/GPSDestBearingRef =/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearing:/GPSDestBearing =/'
    }
else
  function read-exif-tiff { die "$0: No tools found to read EXIF from TIFF"; }
fi

if have exiftool; then
  function read-exif-crw
  {
    exiftool "$1" |
      sed -e 's/^Make/Camera Make/' \
	  -e '/^GPS .*S$/{s/ S$//; s/:/: S/;}' \
	  -e '/^GPS .*N$/{s/ N$//; s/:/: N/;}' \
	  -e '/^GPS .*W$/{s/ W$//; s/:/: W/;}' \
	  -e '/^GPS .*E$/{s/ E$//; s/:/: E/;}' \
	  -e "/^GPS /{s/ deg/d/; s/'/m/; s/\"/s/;}" \
	  -e '/^GPS Altitude.*Above Sea Level/s/ Above Sea Level//' \
	  -e '/^GPS Altitude.*Below Sea Level/{s/: /: -/;s/ Below Sea Level//;}'
  }
elif have exiv2; then
  function read-exif-crw
  {
    exiv2 -P EIlt "$1" |
      sed -e 's/ */: /' \
	  -e 's/^Exif.Photo.DateTimeOriginal:/Date\/Time:/' \
	  -e 's/^Exif.Image.Make:/Camera Make:/' \
	  -e 's/^Exif.Image.Model:/Camera Model:/' \
	  -e 's/^Exif.GPSInfo.GPSLatitude:/GPS Latitude:/' \
	  -e 's/^Exif.GPSInfo.GPSLongitude:/GPS Longitude:/' \
	  -e 's/^Exif.GPSInfo.GPSAltitude:/GPS Altitude:/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearingRef:/GPSDestBearingRef =/' \
	  -e 's/^Exif.GPSInfo.GPSDestBearing:/GPSDestBearing =/'
    }
else
  function read-exif-crw { die "$0: No tools found to read EXIF from CRW"; }
fi


# read-exif -- extract some relevant info from the EXIF in image $1
function read-exif
{
  case `file --mime-type -b -L "$1"` in
    image/jpeg) read-exif-jpeg "$1";;
    image/webp) read-exif-webp "$1";;
    image/tiff) read-exif-tiff "$1";; # Could be DNG as well
    image/x-canon-crw) read-exif-crw "$1";;
    *) echo -n ;;
  esac
}


# copy-from-exif -- get data from EXIF data in $2, add it to database file $1
function copy-from-exif
{
  local timezone=$3
  local defaultlang=$4
  local inf=`read-exif "$2"`
  local make=`sed -n -e '/^Camera [Mm]ake/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local camera=`sed -n -e '/^Camera [Mm]odel/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local date=`sed -n -e '/^Date\/Time/{s/[^:]*: *//p;q;}' <<<"$inf"`
  # local latref=`sed -n -e '/^ *GPSLatitudeRef *=/{s/[^=]*=//p;q;}' <<<"$inf"`
  local latitude=`sed -n -e '/^GPS Latitude *:/{s/[^:]*: *//p;q;}' <<<"$inf"`
  # local longref=`sed -n -e '/^ *GPSLongitudeRef *=/{s/[^=]*=//p;q;}' <<<"$inf"`
  local longitude=`sed -n -e '/^GPS Longitude *:/{s/[^:]*: *//p;q;}' <<<"$inf"`
  # local altref=`sed -n -e '/^ *GPSAltitudeRef *=/{s/[^=]*=//p;q;}' <<<"$inf"`
  local altitude=`sed -n -e '/^GPS Altitude *:/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local bearing=`sed -n -e '/^ *GPSDestBearing *=/{s/[^=]*=//p;q;}' <<<"$inf"`
  local bearingref=`sed -n -e '/^ *GPSDestBearingRef *=/{s/[^=]*=//p;q;}' <<<"$inf"`
  # exiftool gives more precise info:
  local created=`sed -n -e '/^Create Date/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local title=`sed -n -e '/^Headline/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local description=`sed -n -e '/^Caption-Abstract/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local country=`sed -n -e '/^Country/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local province=`sed -n -e '/^Provence/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local city=`sed -n -e '/^City/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local lang=`sed -n -e '/^LanguageIdentifier/{s/[^:]*: *//p;q;}' <<<"$inf"`
  local offset=`sed -n -e '/^Offset Time *:/{s/[^:]*: //p;q;}' <<<"$inf"`
  local offsetdigitized=`sed -n -e '/^Offset Time Digitized/{s/[^:]*: //p;q;}' <<<"$inf"`

  local coverage="$city${province:+, }$province${country:+, }$country"
  coverage=${coverage#, }
  lang=${lang:-$defaultlang}

  if [ -n "$title" ]; then
    set-field $1 "$TITLE" "$title" "$lang"
  fi
  if [ -n "$description" ]; then
    set-field $1 "$DESCRIPTION" "$description" "$lang"
  fi
  if [ -n "$coverage" ]; then
    set-field $1 "$COVERAGE" "$coverage" "$lang"
  fi
  if [ ! -z "$camera" ]; then
    camera=${camera##$make }	# Remove duplicate maker ("Canon")
    set-field $1 "$CAMERA" "${make:+$make }$camera" "" # x-default
    set-field $1 "$MAKE" "$make" "" # x-default
    set-field $1 "$MODEL" "$camera" "" # x-default
  fi
  case "x$timezone" in (x+*|x-*|xZ|x);; (*) timezone=" $timezone";; esac
  if [ ! -z "$date" ]; then
    date=${date/:/-}; date=${date/:/-}; date=${date/ /T} # To ISO format
    if ! [[ "$date" =~ ([-+][0-9:]+|Z)$ ]]; then
      if [ -n "$offset" ]; then date=$date$offset
      else date=$date$timezone; fi
    fi
    set-field $1 "$DATE" "$date" "" # x-default
  fi
  if [ ! -z "$created" ]; then
    created=${created/:/-}; created=${created/:/-}; created=${created/ /T}
    if ! [[ "$created" =~ ([-+][0-9:]+|Z)$ ]]; then
      if [ -n "$offsetdigitized" ]; then created=$created$offsetdigitized
      else created=$created$timezone; fi
    fi
    set-field $1 "$CREATEDATE" "$created" "" # x-default
  fi
  if [ ! -z "$altitude" ]; then
    set-altitude $1 "$altitude"
  fi
  if [ ! -z "$latitude" ]; then	# E.g., "N 7d 51m 8.52s"
    local c h d m s
    c=${latitude:0:1}; h=${latitude:1}; h=${h# }; h=${h# }; h=${h# }
    d=${h%d*}; h=${h#*d}; m=${h%m*}; h=${h#*m}; s=${h%s*}
    [[ "$d" =~ ^[0-9]+$ && "$m" =~ ^\ *[0-9]+$ && "$s" =~ ^\ *[0-9.]+$ ]] ||
      die "$2: Unrecognized values for latitude: $latitude"
    m=`dc <<<"6k $m $s 60/+ p"`
    set-field $1 "$GPSLatitude" "$d,$m$c" "" # x-default
  fi
  if [ ! -z "$longitude" ]; then
    local c h d m s
    c=${longitude:0:1}; h=${longitude:1}; h=${h# }; h=${h# }; h=${h# }
    d=${h%d*}; h=${h#*d}; m=${h%m*}; h=${h#*m}; s=${h%s*}
    [[ "$d" =~ ^[0-9]+$ && "$m" =~ ^\ *[0-9]+$ && "$s" =~ ^\ *[0-9.]+$ ]] ||
      die "$2: Unrecognized value for longitude: $longitude"
    m=`dc <<<"6k $m $s 60/+ p"`
    set-field $1 "$GPSLongitude" "$d,$m$c" "" # x-default
  fi
  if [ ! -z "$bearing" ]; then
    local n=${bearing%%/*} d=#{bearing##*/}
    set-field $1 "$GPSBearing" `dc <<<"6k $n $d / p"` "" # x-default
  fi
  if [ ! -z "$bearingref" ]; then
    set-field $1 "$GPSBearing" ${bearingref//\"/} "" # x-default
  fi
}


# from-name -- use part of $3 (given by pattern $2) as identifier
function from-name
{
  local xmp=$1 pattern=$2 file=$3
  local ident

  case "$pattern" in
    *\(*\)*)
      ident=`sed $ext -e "s/^$pattern.*/\\1/" <<<"${file##*/}"`
      set-field $xmp "$IDENTIFIER" "$ident" "" # x-default
      ;;
    *)
      echo "Pattern has no parenthesized sub-expression: $pattern" >&2
      ;;
  esac
}


# write-to-file -- add all known metadata to the file $1
function write-to-file
{
  local file=$1 input=$2
  local lang=$3 coverage=$4 title=$5
  local desc=$6 publisher=$7 relation=$8 creator=$9
  local rights=${10} ident=${11} name=${12} longitude=${13}
  local latitude=${14} altitude=${15} date=${16} contrib=${17}
  local subject=${18} source=${19} timezone=${20} createdate=${21}

  semaphore-p $NPROCS		# Get a free process slot
  echo -e "\rWriting to $file $clr_eol\c"
  (
    lock "$file"		# Get exlusive access to $file
    trap 'rm $db $TMP $xmpfile; unlock "$file"; semaphore-v $NPROCS' EXIT

    local db=$(mktemp $TMPDIR/db-XXXX) || exit 1
    local TMP=$(mktemp $TMPDIR/xmp-XXXX) || exit 1
    local xmpfile=$(mktemp $TMPDIR/xmp-XXXX) || exit 1

    # Get the existing XMP from the file into $xmp
    #
    read-xmp "$file" >$xmpfile

    # Add to the DB all the data that was passed in.
    #
    lang=${lang#x}
    lang=${lang:-x-default}
    copy-from-exif $db "$file" "${timezone#x}" "$lang"
    [[ -n "$title" ]]      && set-field $db "$TITLE" "${title#x}" "$lang"
    [[ -n "$creator" ]]    && set-field $db "$CREATOR" "${creator#x}" "$lang"
    [[ -n "$subject" ]]    && set-bag $db "$SUBJECT" "${subject#x}" "$lang"
    [[ -n "$desc" ]]       && set-field $db "$DESCRIPTION" "${desc#x}" "$lang"
    [[ -n "$publisher" ]]  && set-field $db "$PUBLISHER" "${publisher#x}" "$lang"
    [[ -n "$contrib" ]]    && set-field $db "$CONTRIBUTOR" "${contrib#x}" "$lang"
    [[ -n "$date" ]]       && set-field $db "$DATE" "${date#x}" "" # x-default
    [[ -n "$ident" ]]      && set-field $db "$IDENTIFIER" "${ident#x}" "" # x-default
    [[ -n "$relation" ]]   && set-field $db "$RELATION" "${relation#x}" "$lang"
    [[ -n "$coverage" ]]   && set-field $db "$COVERAGE" "${coverage#x}" "$lang"
    [[ -n "$rights" ]]     && set-field $db "$RIGHTS" "${rights#x}" "$lang"
    [[ -n "$source" ]]     && set-field $db "$SOURCE" "${source#x}" "$lang"
    [[ -n "$createdate" ]] && set-field $db "$CREATEDATE" "${createdate#x}" "" # x-default
    [[ -n "$latitude" ]]   && set-lat-or-long $db "$GPSLatitude" "${latitude#x}"
    [[ -n "$longitude" ]]  && set-lat-or-long $db "$GPSLongitude" "${longitude#x}"
    [[ -n "$altitude" ]]   && set-altitude $db "${altitude#x}"
    # [[ -n "$bearingref" ]] && set-bearingref $db "$bearingref"
    # [[ -n "$bearing" ]]    && set-bearing $db "$bearing"
    [[ -n "$name" ]] && [[ -z "${ident#x}" ]] && from-name $db "${name#x}" "$file"

    # Add the standard type and format.
    #
    set-field $db "$FORMAT" `file --mime-type -b -L "$1"` "" # x-default
    set-field $db "$TYPE" "image" "" # x-default

    # Overwrite the XMP with the collected fields and write the XMP
    # back to the image.
    cp "$file" $TMP &&
      add-fields-to-xmp $db $xmpfile | write-xmp "$TMP" >"$file" ||
	{ cat $TMP >"$file"; echo "An error occurred" >&2; exit 1; }
  ) &
}


# apply -- read the descriptions from file and apply them
function apply
{
  local input=${1:-}
  local key line prevkey= file=
  local language= coverage= title=
  local description= publisher= relation= creator=
  local rights= identifier= name= longitude=
  local latitude= altitude= date= contributor= subject=
  local source= timezone= createdate=
  local -i lineno=0

  trap 'semaphore-delete $NPROCS' RETURN
  local NPROCS=$(semaphore-new $maxprocesses $TMPDIR) || return 1

  # If there is an argument, that is the file to read from, otherwise stdin
  if [ -n "$input" ]; then exec <"$input"; fi

  # Loop over all input lines
  while read key line; do
    ((++lineno))
    if [ "$key" == "+" ]; then	# Continuation line
      key=$prevkey
      line=$prevline\ $line
    else
      prevkey=$key
    fi
    prevline=$line
    case "$key" in
      "") ;;			# Skip empty line
      "#"*) ;;			# Skip comment
      L) language=x$line;;	# Add an "x" so it is guaranteed not empty
      C) coverage=x$line;;
      T) title=x$line;;
      D) description=x$line;;
      P) publisher=x$line;;
      S) relation=x$line;;
      A) creator=x$line;;
      R) rights=x$line;;
      I) identifier=x$line;;
      N) name=x$line;;
      X) longitude=x$line;;
      Y) latitude=x$line;;
      Z) altitude=x$line;;
      d) date=x$line;;
      c) contributor=x$line;;
      K) subject=x$line;;
      s) source=x$line;;
      z) timezone=x$line;;
      E) createdate=x$line;;
      F) if [ -n "$file" ] && [ -e "$file" ] && [[ "$file" == $mask ]]; then
	   write-to-file "$file" "$input" \
			 "$language" "$coverage" "$title" \
			 "$description" "$publisher" "$relation" "$creator" \
			 "$rights" "$identifier" "$name" "$longitude" \
			 "$latitude" "$altitude" "$date" "$contributor" \
			 "$subject" "$source" "$timezone" "$createdate"
	 fi
	 file=$line;;
      *) die "${input:-stdin}:$lineno: Illegal field name \"$key\"";;
    esac
  done
  if [ -n "$file" ] && [ -e "$file" ] && [[ "$file" == $mask ]]; then
    write-to-file "$file" "$input" \
		  "$language" "$coverage" "$title" \
		  "$description" "$publisher" "$relation" "$creator" \
		  "$rights" "$identifier" "$name" "$longitude" \
		  "$latitude" "$altitude" "$date" "$contributor" \
		  "$subject" "$source" "$timezone" "$createdate"
  fi

  wait
  echo
}


# Main body

# Find and reduce the effect of bugs:
# - Treat unset variables as an error (-u).
# - Exit immediately on an error (-e)
# - A pipeline fails if any command in it fails (-o pipefail)
# - Do not automatically export variables to the environment (+a)
set -u -e -o pipefail +a

# Avoid problems with unknown or erroneous character encodings.
export LC_ALL=C

# Check prerequisites
# for f in jhead rdjpgxmp xmptool wrjpgxmp; do
for f in xmptool ; do
  if ! have "$f"; then die "Could not find $f"; fi
done

# Make a directory for temporary files
trap 'rm -rf $TMPDIR' 0
TMPDIR=`mktemp -d /tmp/addxmp-XXXXXX` || exit 1

action=
mask=
while getopts ":hcrj:m:" flag; do
  case $flag in
    c) if [ "$action" ]; then usage ${0##*/} >&2; exit 1
       else action=print-template
       fi;;
    r) if [ "$action" ]; then usage ${0##*/} >&2; exit 1
       else action=read-template
       fi;;
    j) maxprocesses=$OPTARG;;
    m) mask=$OPTARG;;
    h) usage ${0##*/}; exit;;
    ?) usage ${0##*/} >&2; exit 1;;
  esac
done
if [[ -n "$mask" ]] && [[ -n "$action" ]]; then usage ${0##*/}; exit; fi
if [[ -z "$mask" ]]; then mask='*'; fi
shift $((OPTIND - 1))

case "$action" in
  print-template) print-template "$@";;
  read-template) read-template "$@";;
  *) apply "$@";;
esac
