summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorzachir <zachir@librem.one>2022-10-09 01:10:41 -0500
committerzachir <zachir@librem.one>2022-10-09 01:10:41 -0500
commit91cdbd9be0d677c8a832e85de9d55aecf2c236fd (patch)
tree9d43e1514d70f8cdd7ac82c6adc92e2583456780
initialize repo
-rw-r--r--.gitignore41
-rw-r--r--README.md3
-rwxr-xr-xbetterdiscordctl661
-rwxr-xr-xbooksplit52
-rwxr-xr-xcompiler52
-rwxr-xr-xdmenu_books7
-rwxr-xr-xdmenu_keepass30
-rwxr-xr-xdmenumount75
-rwxr-xr-xdmenurecord132
-rwxr-xr-xdmenuumount44
-rwxr-xr-xdmenuunicode18
-rwxr-xr-xexiftool7153
-rwxr-xr-xflatdark.sh19
-rwxr-xr-xflattheme.sh8
-rwxr-xr-xhotkeys.sh5
-rwxr-xr-xlaunch_polybar.sh9
-rwxr-xr-xmailsync90
-rwxr-xr-xs6-user-update31
-rwxr-xr-xslider124
-rwxr-xr-xvimv45
-rwxr-xr-xwaybar-dwl.sh176
21 files changed, 8775 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0e07f08
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# other git directories
+dwmblocks/
+scripts/
+
+# other directories
+ignore/
+shortcmds/
+testing/
+
+# binaries + symlinks
+AHM 5050 v3 Standalone
+BULLDOG Standalone
+Blacksun Standalone
+RVXX EX Standalone
+ReAmp Studio R1 Standalone
+The Crown EX Standalone
+biber
+device-flasher.linux
+kdenlive*
+lf
+net.downloadhelper.coapp
+schildichat
+sheepshaver
+ubports-installer*
+volsv
+xmobar
+xmonad
+
+# local package managers
+.crates*
+LibreGaming
+Qminimize
+chardetect
+fyrox-template
+jp.py
+protonup
+pyrsa*
+qbpm
+razer-cli
+rst*.py
+simple-term-menu
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..74c1aa1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+These are scripts that I use, that are made by other people; some I have
+modified, some I have not. I make no guarantees for any of these, even that I
+will fix them.
diff --git a/betterdiscordctl b/betterdiscordctl
new file mode 100755
index 0000000..00d9a19
--- /dev/null
+++ b/betterdiscordctl
@@ -0,0 +1,661 @@
+#!/usr/bin/env bash
+
+set -ueo pipefail
+shopt -s dotglob extglob nullglob
+
+# Constants
+VERSION=1.7.0
+SOURCE=$(readlink -f "${BASH_SOURCE[0]}")
+DISABLE_UPGRADE=
+
+# Options
+cmd=status
+verbosity=0
+scan=(/opt /usr/share /usr/lib64)
+flavors=('' canary ptb)
+discord=
+modules=
+bd_repo='https://github.com/rauenzi/BetterDiscordApp'
+bd_repo_branch=injector
+bd=
+copy_bd=
+snap=
+flatpak=
+upgrade_url='https://github.com/bb010g/betterdiscordctl/raw/master/betterdiscordctl'
+
+# Variables
+flavor=
+core=
+xdg_config=${XDG_CONFIG_HOME:-$HOME/.config}
+data=${XDG_DATA_HOME:-$HOME/.local/share}/betterdiscordctl
+snap_bin=snap
+flatpak_bin=flatpak
+
+show_help() {
+ cat << EOF
+Usage: ${0##*/} [COMMAND] [OPTION...]
+
+Options:
+ -V, --version Display version info and exit
+ -h, --help Display this help message and exit
+ -v, --verbose Increase verbosity
+ -s, --scan=DIRECTORIES Colon-separated list of directories to scan for
+ a Discord installation
+ (default '/opt:/usr/share')
+ -f, --flavors=FLAVORS Colon-separated list of Discord flavors
+ (default ':canary:ptb')
+ -d, --discord=DIRECTORY Use the specified Discord directory
+ (requires --modules)
+ -m, --modules=DIRECTORY Use the specified Discord modules directory
+ -r, --bd-repo=REPOSITORY Use the specified Git repo for BetterDiscord
+ --bd-repo-branch=BRANCH Use the specified Git branch for BetterDiscord
+ (default 'stable16')
+ -b, --betterdiscord=DIRECTORY Use the specified BetterDiscord directory
+ -c, --copy-bd Copy BD directory instead of symlinking
+ --snap[=COMMAND] Use the Snap version of Discord (optionally
+ using the specified snap command)
+ --flatpak[=COMMAND] Use the Flatpak version of Discord (optionally
+ using the specified flatpak command)
+ --upgrade-url=URL Custom URL to upgrade betterdiscordctl with
+
+Commands:
+ status (default) Show the current Discord patch state.
+ install Install BetterDiscord.
+ reinstall Reinstall BetterDiscord.
+ update Update BetterDiscord.
+ uninstall Uninstall BetterDiscord.
+ upgrade Update betterdiscordctl.
+EOF
+}
+
+verbose() {
+ if (( verbosity >= $1 )); then
+ shift
+ printf '%s\n' "$1" >&2
+ fi
+}
+
+die() {
+ while [ $# -gt 0 ]; do
+ printf '%s\n' "$1" >&2
+ shift
+ done
+ exit 1
+}
+
+die_with_help() {
+ die "$@" 'Use "--help" for more information.'
+}
+
+die_non_empty() {
+ die_with_help "ERROR: \"$1\" requires a non-empty option argument."
+}
+
+while :; do
+ if [[ -z ${1+x} ]]; then break; fi
+ case $1 in
+ status|install|reinstall|update|uninstall|upgrade)
+ cmd=$1
+ ;;
+ -V|--version)
+ printf 'betterdiscordctl %s\n' "$VERSION" >&2
+ exit
+ ;;
+ -h|-\?|--help)
+ show_help; exit
+ ;;
+ -v|--verbose)
+ ((++verbosity))
+ ;;
+ -s|--scan)
+ if [[ ${2+x} ]]; then IFS=':' read -ra scan <<< "$2"; shift
+ else die_non_empty '--scan'; fi
+ ;;
+ --scan=?*)
+ IFS=':' read -ra scan <<< "${1#*=}"
+ ;;
+ --scan=)
+ die_non_empty '--scan'
+ ;;
+ -f|--flavors)
+ if [[ ${2+x} ]]; then IFS=':' read -ra flavors <<< "$2"; shift
+ else die_non_empty '--flavors'; fi
+ ;;
+ --flavors=?*)
+ IFS=':' read -ra flavors <<< "${1#*=}"
+ ;;
+ --flavors=)
+ die_non_empty '--flavors'
+ ;;
+ -d|--discord)
+ if [[ ${2+x} ]]; then discord=$2; shift
+ else die_non_empty '--discord'; fi
+ ;;
+ --discord=?*)
+ discord=${1#*=}
+ ;;
+ --discord=)
+ die_non_empty '--discord'
+ ;;
+ -m|--modules)
+ if [[ ${2+x} ]]; then modules=$2; shift
+ else die_non_empty '--modules'; fi
+ ;;
+ --modules=?*)
+ modules=${1#*=}
+ ;;
+ --modules=)
+ die_non_empty '--modules'
+ ;;
+ --bd-repo-branch)
+ if [[ ${2+x} ]]; then bd_repo_branch=$2; shift
+ else die_non_empty '--bd-repo-branch'; fi
+ ;;
+ --bd-repo-branch=?*)
+ bd_repo_branch=${1#*=}
+ ;;
+ --bd-repo-branch=)
+ die_non_empty '--bd-repo-branch'
+ ;;
+ -r|--bd-repo)
+ if [[ ${2+x} ]]; then bd_repo=$2; shift
+ else die_non_empty '--bd-repo'; fi
+ ;;
+ --bd-repo=?*)
+ bd_repo=${1#*=}
+ ;;
+ --bd-repo=)
+ die_non_empty '--bd-repo'
+ ;;
+ -b|--betterdiscord)
+ if [[ ${2+x} ]]; then bd=$2; shift
+ else die_non_empty '--betterdiscord'; fi
+ ;;
+ --betterdiscord=?*)
+ bd=${1#*=}
+ ;;
+ --betterdiscord=)
+ die_non_empty '--betterdiscord'
+ ;;
+ -c|--copy-bd)
+ copy_bd=yes
+ ;;
+ --snap)
+ snap=yes
+ copy_bd=yes
+ ;;
+ --snap=?*)
+ snap=yes
+ copy_bd=yes
+ snap_bin=${1#*=}
+ ;;
+ --snap=)
+ die_non_empty '--snap'
+ ;;
+ --flatpak)
+ flatpak=yes
+ copy_bd=yes
+ ;;
+ --flatpak=?*)
+ flatpak=yes
+ copy_bd=yes
+ flatpak_bin=${1#*=}
+ ;;
+ --flatpak=)
+ die_non_empty '--flatpak'
+ ;;
+ --upgrade-url)
+ if [[ ${2+x} ]]; then upgrade_url=$2; shift
+ else die_non_empty '--upgrade-url'; fi
+ ;;
+ --upgrade-url=?*)
+ upgrade_url=${1#*=}
+ ;;
+ --upgrade-url=)
+ die_non_empty '--upgrade-url'
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -?*)
+ printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
+ ;;
+ *)
+ break
+ esac
+ shift
+done
+
+mkdir -p "$data"
+[[ -f $data/bd_map ]] || touch "$data/bd_map"
+
+# Commands
+
+bdc_status() {
+ index_mod=no
+ linked_dir=no
+ linked_repo=no
+ if [[ -d $core/injector ]]; then
+ if [[ -h $core/injector ]]; then
+ linked_dir=$(readlink "$core/injector")
+ if pushd "$core/injector" >/dev/null; then
+ linked_repo=$(git remote get-url origin 2>/dev/null || printf 'no\n')
+ popd >/dev/null
+ else
+ linked_dir="(broken link) $linked_dir"
+ fi
+ fi
+ fi
+ if [[ ! -f $core/index.js ]]; then
+ index_mod='(missing) no'
+ else
+ grep -q 'injector' "$core/index.js" && index_mod=yes
+ fi
+
+ printf 'Discord: %s
+Modules: %s
+Index modified: %s
+Linked injector directory: %s
+Linked injector repository: %s\n' \
+ "$discord" "$modules" "$index_mod" "$linked_dir" "$linked_repo"
+}
+
+bdc_install() {
+ [[ -d $core/injector ]] && die 'ERROR: Already installed.'
+
+ # Clean up legacy cruft
+ if [[ -d $core/core ]]; then
+ printf 'Removing legacy core directory...\n' >&2
+ rm -rf "$core/core"
+ fi
+
+ bd_patch
+ bd_injector
+
+ printf 'Installed. (Restart Discord if necessary.)\n' >&2
+}
+
+bdc_reinstall() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ bdc_kill
+
+ verbose 1 'V: Removing old injector folder.'
+ rm -rf "$core/injector"
+
+ bd_patch
+ bd_injector
+
+ printf 'Reinstalled.\n' >&2
+}
+
+bdc_update() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ if ! pushd "$core/injector" >/dev/null; then
+ if [[ -h $core/injector ]]; then
+ die 'ERROR: BetterDiscord injector symbolic link is broken.'
+ else
+ die 'ERROR: BetterDiscord injector location is not a directory.'
+ fi
+ fi
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ printf 'Updating Git repository...\n' >&2
+ git fetch origin "$bd_repo_branch"
+ git reset --hard FETCH_HEAD
+ else
+ printf 'WARN: No Git repository found.\n' >&2
+ fi
+ popd >/dev/null
+}
+
+bdc_uninstall() {
+ [[ -d $core/injector ]] || die 'Not installed.'
+
+ bdc_kill
+ bd_unpatch
+
+ # Remove managed BD repo if applicable
+ bd_n=$(bd_map_get_dir "$discord" | bd_map_entry_n)
+ bd_map_remove "$discord"
+ if [[ -z $(bd_map_get_n "$bd_n") ]]; then
+ verbose 2 "VV: Removing $data/bd/$bd_n"
+ rm -rf "$data/bd/$bd_n"
+ fi
+
+ printf 'Uninstalled.\n' >&2
+}
+
+bdc_upgrade() {
+ if [[ $DISABLE_UPGRADE ]]; then
+ die 'ERROR: Upgrading has been disabled.' \
+ 'If you installed this from a package, its maintainer should keep it up to date.'
+ fi
+
+ github_version=$(curl -NLSs "$upgrade_url" | sed -n 's/^VERSION=//p')
+ if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
+ die "ERROR: GitHub couldn't be reached to check the version."
+ fi
+ verbose 2 "VV: Script location: $SOURCE"
+ verbose 2 "VV: Upgrade URL: $upgrade_url"
+ verbose 1 "V: Local version: $VERSION"
+ verbose 1 "V: GitHub version: $github_version"
+ semver_diff=$(Semver::compare "$github_version" "$VERSION")
+ if [[ $semver_diff -eq 1 ]]; then
+ printf 'Downloading betterdiscordctl...\n' >&2
+ if curl -LSso "$SOURCE" "$upgrade_url"; then
+ printf 'Successfully updated betterdiscordctl.\n' >&2
+ else
+ die 'ERROR: Failed to update betterdiscordctl.' \
+ 'You may want to rerun this command with sudo.'
+ fi
+ else
+ if [[ $semver_diff -eq 0 ]]; then
+ printf 'betterdiscordctl is already the latest version (%s).\n' "$VERSION" >&2
+ else
+ printf 'Local version (%s) is higher than GitHub version (%s).\n' "$VERSION" "$github_version" >&2
+ fi
+ fi
+}
+
+# Implementation functions
+
+bdc_main() {
+ if [[ -z $discord ]]; then
+ if [[ $snap ]]; then bdc_snap
+ elif [[ $flatpak ]]; then bdc_flatpak
+ else bdc_scan; fi
+ else
+ flavor=$flavors
+ # --discord and --modules
+ [[ -z $modules ]] && die_with_help 'ERROR: "--discord" requires "--modules" to also be set.'
+ [[ -d $discord ]] || die 'ERROR: Discord installation not found.'
+ [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
+ fi
+ [[ -d $discord ]] || die 'ERROR: Discord installation not found. Try specifying it with "--discord".'
+ core=$modules/discord_desktop_core
+ [[ -d $core ]] || die "ERROR: Directory 'discord_desktop_core' not found in $(readlink -f "$modules")"
+}
+
+bdc_scan() {
+ for scandir in "${scan[@]}"; do
+ verbose 2 "VV: Scanning $scandir"
+ for flavor in "${flavors[@]}"; do
+ verbose 2 "VV: Trying flavor '$flavor'"
+ shopt -s nocaseglob
+ for discord in "$scandir"/discord?(-)"$flavor"; do
+ shopt -u nocaseglob
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ discord_config=$xdg_config/discord${flavor,,}
+ if [[ ! -d $discord_config ]]; then
+ printf 'WARN: Config directory not found for Discord %s (%s, %s).\n' \
+ "$flavor" "$discord" "$discord_config" >&2
+ continue 2
+ fi
+ if [[ -z $modules ]]; then
+ bdc_find_modules
+ else
+ # --modules
+ [[ -d $modules ]] || die 'ERROR: Discord modules directory not found.'
+ fi
+ break 3
+ fi
+ done
+ done
+ done
+}
+
+bdc_find_modules() {
+ declare -a all_modules
+ all_modules=("$discord_config/"+([0-9]).+([0-9]).+([0-9])/modules)
+ ((${#all_modules[@]})) || die 'ERROR: Discord modules directory not found.'
+ modules=${all_modules[-1]}
+ verbose 1 "V: Found modules in $modules"
+}
+
+bdc_snap() {
+ # shellcheck disable=SC2016
+ # Expansion should happen inside snap's shell.
+ snap_location=$("$snap_bin" run --shell discord <<< $'printf -- \'%s\n\' "$SNAP" 1>&3' 3>&1)
+ discord=${snap_location:?}/usr/share/discord
+ verbose 2 "VV: Checking $discord"
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ # shellcheck disable=SC2016
+ # Expansion should happen inside snap's shell.
+ xdg_config=$("$snap_bin" run --shell discord <<< $'printf -- \'%s/.config\n\' "$SNAP_USER_DATA" 1>&3' 3>&1)
+ discord_config=$xdg_config/discord
+ bdc_find_modules
+ else
+ die 'ERROR: Discord installation not found.'
+ fi
+}
+
+bdc_flatpak() {
+ flatpak_version=$("$flatpak_bin" --version | sed -n 's/Flatpak //p')
+ if [[ $(Semver::compare "$flatpak_version" '1.0.0') -eq -1 ]]; then
+ die 'ERROR: You are using an unsupported version of Flatpak.' \
+ 'See https://github.com/bb010g/betterdiscordctl/issues/45'
+ fi
+ # flatpak sucks and doesn't use stderr for warnings.
+ # https://github.com/flatpak/flatpak/blob/13e449b/app/flatpak-main.c#L259-L286
+ # This really should be better for directories with newlines, but...
+ # We're just going to grab the last line and hope for the best.
+ flatpak_location=$("$flatpak_bin" info --show-location com.discordapp.Discord)
+ flatpak_location=${flatpak_location##*$'\n'}
+ if [[ -d ${flatpak_location:?}/files/discord ]]; then
+ discord=$flatpak_location/files/discord
+ else
+ discord=$flatpak_location/files/extra
+ fi
+ verbose 2 "VV: Checking $discord"
+ if [[ -d $discord ]]; then
+ verbose 1 "V: Using Discord at $discord"
+ # shellcheck disable=SC2016
+ # Expansion should happen inside flatpak's shell.
+ flatpak_config=$("$flatpak_bin" run --command=sh com.discordapp.Discord -c $'printf -- \'%s\n\' "$XDG_CONFIG_HOME"')
+ discord_config=${flatpak_config:-$HOME/.var/app/com.discordapp.Discord/config}/discord
+ if [[ ! -d $discord_config ]]; then
+ printf 'WARN: Config directory not found for Discord (%s, %s).\n' "$discord" "$discord_config" >&2
+ fi
+ bdc_find_modules
+ else
+ die 'ERROR: Discord installation not found.'
+ fi
+}
+
+bdc_kill() {
+ declare process_name=Discord
+ [[ $flavor ]] && process_name+=" $flavor"
+ printf 'Killing %s processes...\n' "$process_name" >&2
+ pkill -exi -KILL "discord-?$flavor" || printf 'No active processes found.\n' >&2
+}
+
+bd_injector() {
+ if [[ -z $bd ]]; then
+ bd=$data/bd/$(bd_map_add "$discord" "$bd_repo")
+ if [[ ! -d $bd ]]; then
+ printf 'Cloning %s...\n' "$bd_repo" >&2
+ git clone "$bd_repo" -b "$bd_repo_branch" --depth=1 --single-branch "$bd"
+ fi
+ fi
+
+ if [[ $copy_bd ]]; then
+ verbose 1 'V: Copying BetterDiscord injector...'
+ cp -r "$bd" "$core/injector"
+ else
+ verbose 1 'V: Linking BetterDiscord injector...'
+ ln -s "$bd" "$core/injector"
+ fi
+}
+
+bd_patch() {
+ if ! grep -q 'injector' "$core/index.js"; then
+ verbose 1 'V: Injecting into index.js...'
+ sed -i "$core/index.js" \
+ -e "1i require('./injector');" \
+ -e "s/core'/core.asar'/"
+ fi
+}
+
+bd_unpatch() {
+ verbose 1 'V: Removing BetterDiscord injection...'
+ sed -i "$core/index.js" \
+ -e '/injector/d' \
+ -e "s/core'/core.asar'/"
+ rm -rf "$core/injector"
+}
+
+bd_map_entry_n() {
+ sed 's/^.*\t\t.*\t\(.*\)$/\1/' "$@"
+}
+
+bd_map_fresh() {
+ verbose 2 'VV: Generating fresh bd_map number...'
+ bd_map_entry_n "$data/bd_map" | sort | awk \
+ 'BEGIN {max=-1} NF != 0 {if ($1>max+1) {exit}; max=$1} END {print max+1}'
+}
+
+bd_map_add() {
+ entry=$(bd_map_get_repo "$2")
+ if [[ $entry ]]; then
+ num=$(head -n1 <<< "$entry" | bd_map_entry_n)
+ else
+ num=$(bd_map_fresh)
+ fi
+ printf '%s\t\t%s\t%s\n' "$1" "$2" "$num" >> "$data/bd_map"
+ printf '%s\n' "$num"
+}
+
+bd_map_get_dir() {
+ grep -F "$1"$'\t\t' "$data/bd_map"
+}
+
+bd_map_get_repo() {
+ grep -F $'\t\t'"$1"$'\t' "$data/bd_map"
+}
+
+bd_map_get_n() {
+ grep $'\t'"$1\$" "$data/bd_map"
+}
+
+bd_map_remove() {
+ sed -i "$data/bd_map" -e "\\%$1\\t\\t%d"
+}
+
+# Included from https://github.com/bb010g/Semver.sh , under the MIT License.
+
+Semver::validate() {
+ # shellcheck disable=SC2064
+ trap "$(shopt -p extglob)" RETURN
+ shopt -s extglob
+
+ declare normal=${1%%[+-]*}
+ declare extra=${1:${#normal}}
+
+ declare major=${normal%%.*}
+ if [[ $major != +([0-9]) ]]; then echo "Semver::validate: invalid major: $major" >&2; return 1; fi
+ normal=${normal:${#major}+1}
+ declare minor=${normal%%.*}
+ if [[ $minor != +([0-9]) ]]; then echo "Semver::validate: invalid minor: $minor" >&2; return 1; fi
+ declare patch=${normal:${#minor}+1}
+ if [[ $patch != +([0-9]) ]]; then echo "Semver::validate: invalid patch: $patch" >&2; return 1; fi
+
+ declare -r ident="+([0-9A-Za-z-])"
+ declare pre=${extra%%+*}
+ declare pre_len=${#pre}
+ if [[ $pre_len -gt 0 ]]; then
+ pre=${pre#-}
+ if [[ $pre != $ident*(.$ident) ]]; then echo "Semver::validate: invalid pre-release: $pre" >&2; return 1; fi
+ fi
+ declare build=${extra:pre_len}
+ if [[ ${#build} -gt 0 ]]; then
+ build=${build#+}
+ if [[ $build != $ident*(.$ident) ]]; then echo "Semver::validate: invalid build metadata: $build" >&2; return 1; fi
+ fi
+
+ if [[ $2 ]]; then
+ echo "$2=(${major@Q} ${minor@Q} ${patch@Q} ${pre@Q} ${build@Q})"
+ else
+ echo "$1"
+ fi
+}
+
+Semver::compare() {
+ declare -a x y
+ eval "$(Semver::validate "$1" x)"
+ eval "$(Semver::validate "$2" y)"
+
+ declare x_i y_i i
+ for i in 0 1 2; do
+ x_i=${x[i]}; y_i=${y[i]}
+ if [[ $x_i -eq $y_i ]]; then continue; fi
+ if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
+ if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
+ done
+
+ x_i=${x[3]}; y_i=${y[3]}
+ if [[ -z $x_i && $y_i ]]; then echo 1; return; fi
+ if [[ $x_i && -z $y_i ]]; then echo -1; return; fi
+
+ declare -a x_pre; declare x_len
+ declare -a y_pre; declare y_len
+ IFS=. read -ra x_pre <<< "$x_i"; x_len=${#x_pre[@]}
+ IFS=. read -ra y_pre <<< "$y_i"; y_len=${#y_pre[@]}
+
+ if (( x_len > y_len )); then echo 1; return; fi
+ if (( x_len < y_len )); then echo -1; return; fi
+
+ for (( i=0; i < x_len; i++ )); do
+ x_i=${x_pre[i]}; y_i=${y_pre[i]}
+ if [[ $x_i = "$y_i" ]]; then continue; fi
+
+ declare num_x num_y
+ num_x=$([[ $x_i = +([0-9]) ]] && echo "$x_i")
+ num_y=$([[ $y_i = +([0-9]) ]] && echo "$y_i")
+ if [[ $num_x && $num_y ]]; then
+ if [[ $x_i -gt $y_i ]]; then echo 1; return; fi
+ if [[ $x_i -lt $y_i ]]; then echo -1; return; fi
+ else
+ if [[ $num_y ]]; then echo 1; return; fi
+ if [[ $num_x ]]; then echo -1; return; fi
+ if [[ $x_i > $y_i ]]; then echo 1; return; fi
+ if [[ $x_i < $y_i ]]; then echo -1; return; fi
+ fi
+ done
+
+ echo 0
+}
+
+# Run command
+
+case "$cmd" in
+ status)
+ bdc_main
+ bdc_status
+ ;;
+ install)
+ bdc_main
+ bdc_install
+ ;;
+ reinstall)
+ bdc_main
+ bdc_reinstall
+ ;;
+ update)
+ bdc_main
+ bdc_update
+ ;;
+ uninstall)
+ bdc_main
+ bdc_uninstall
+ ;;
+ upgrade)
+ bdc_upgrade
+ ;;
+ *)
+ die "ERROR: Unknown command: $cmd"
+ ;;
+esac
diff --git a/booksplit b/booksplit
new file mode 100755
index 0000000..14187ab
--- /dev/null
+++ b/booksplit
@@ -0,0 +1,52 @@
+#!/bin/sh
+#==================================
+# Copyright Luke Smith
+# This notice by Zachary Smith (unrelated)
+# booksplit is designed to split audiobooks into chapters
+# it can also be used to split whole albums into songs
+#==================================
+
+# Requires ffmpeg (audio splitting) and my `tag` wrapper script.
+
+[ ! -f "$2" ] && printf "The first file should be the audio, the second should be the timecodes.\\n" && exit
+
+echo "Enter the album/book title:"; read -r booktitle
+
+echo "Enter the artist/author:"; read -r author
+
+echo "Enter the publication year:"; read -r year
+
+inputaudio="$1"
+
+# Get a safe file name from the book.
+escbook="$(echo "$booktitle" | iconv -cf UTF-8 -t ASCII//TRANSLIT | tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed "s/-\+/-/g;s/\(^-\|-\$\)//g")"
+
+! mkdir -p "$escbook" && echo "Do you have write access in this directory?" && exit 1
+
+# As long as the extension is in the tag script, it'll work.
+ext="mp3"
+#ext="${1#*.}"
+
+# Get the total number of tracks from the number of lines.
+total="$(wc -l < "$2")"
+
+while read -r x;
+do
+ end="$(echo "$x" | cut -d' ' -f1)"
+
+ [ -n "$start" ] &&
+ echo "From $start to $end; $track $title"
+ file="$escbook/$(printf "%.2d" "$track")-$esctitle.$ext"
+ [ -n "$start" ] && echo "Splitting \"$title\"..." &&
+ ffmpeg -nostdin -y -loglevel -8 -i "$inputaudio" -ss "$start" -to "$end" -vn -acodec mp3 "$file" &&
+ echo "Tagging \"$title\"..." && mid3v2 -a "$author" -A "$booktitle" -t "$title" -T "$track" -y "$year" "$file"
+ title="$(echo "$x" | cut -d' ' -f 2-)"
+ esctitle="$(echo "$title" | iconv -cf UTF-8 -t ASCII//TRANSLIT | tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | sed "s/-\+/-/g;s/\(^-\|-\$\)//g")"
+ track="$((track+1))"
+ start="$end"
+done < "$2"
+# The last track must be done outside the loop.
+echo "From $start to the end: $title"
+file="$escbook/$(printf "%.2d" "$track")-$esctitle.$ext"
+echo "Splitting \"$title\"..." && ffmpeg -nostdin -y -loglevel -8 -i "$inputaudio" -ss "$start" -vn -acodec mp3 "$file" &&
+ echo "Tagging \"$title\"..." && mid3v2 -a "$author" -A "$booktitle" -t "$title" -T "$track" -y "$year" "$file"
diff --git a/compiler b/compiler
new file mode 100755
index 0000000..1892d5f
--- /dev/null
+++ b/compiler
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+# This script will compile or run another finishing operation on a document. I
+# have this script run via vim.
+#
+# Compiles .tex. groff (.mom, .ms), .rmd, .md. Opens .sent files as sent
+# presentations. Runs scripts based on extention or shebang
+#
+# Note that .tex files which you wish to compile with XeLaTeX should have the
+# string "xelatex" somewhere in a comment/command in the first 5 lines.
+
+file=$(readlink -f "$1")
+dir=${file%/*}
+base="${file%.*}"
+ext="${file##*.}"
+
+cd "$dir" || exit
+
+textype() { \
+ command="pdflatex"
+ ( sed 5q "$file" | grep -i -q 'xelatex' ) && command="xelatex"
+ $command --output-directory="$dir" "$base" &&
+ grep -i addbibresource "$file" >/dev/null &&
+ biber --input-directory "$dir" "$base" &&
+ $command --output-directory="$dir" "$base" &&
+ $command --output-directory="$dir" "$base"
+}
+
+case "$ext" in
+ [0-9]) preconv "$file" | refer -PS -e | groff -mandoc -T pdf > "$base".pdf ;;
+ c) cc "$file" -o "$base" && "$base" ;;
+ go) go run "$file" ;;
+ h) sudo make install ;;
+ m) octave "$file" ;;
+ md) if [ -x "$(command -v lowdown)" ]; then
+ lowdown -d nointem -e super "$file" -Tms | groff -mpdfmark -ms -kept > "$base".pdf
+ elif [ -x "$(command -v groffdown)" ]; then
+ groffdown -i "$file" | groff > "$base.pdf"
+ else
+ pandoc "$file" --pdf-engine=xelatex -o "$base".pdf
+ fi ; ;;
+ mom) preconv "$file" | refer -PS -e | groff -mom -kept -T pdf > "$base".pdf ;;
+ ms) preconv "$file" | refer -PS -e | groff -me -ms -kept -T pdf > "$base".pdf ;;
+ py) python "$file" ;;
+ [rR]md) Rscript -e "rmarkdown::render('$file', quiet=TRUE)" ;;
+ rs) cargo build ;;
+ sass) sassc -a "$file" "$base.css" ;;
+ scad) openscad -o "$base".stl "$file" ;;
+ sent) setsid -f sent "$file" 2>/dev/null ;;
+ tex) textype "$file" ;;
+ *) sed 1q "$file" | grep "^#!/" | sed "s/^#!//" | xargs -r -I % "$file" ;;
+esac
diff --git a/dmenu_books b/dmenu_books
new file mode 100755
index 0000000..367ee37
--- /dev/null
+++ b/dmenu_books
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+BOOK=`ls ~/Documents/shared | dmenu`
+
+[ -z "$BOOK" ] && exit 1
+
+zathura "~/Documents/shared/$BOOK"
diff --git a/dmenu_keepass b/dmenu_keepass
new file mode 100755
index 0000000..d87a4c8
--- /dev/null
+++ b/dmenu_keepass
@@ -0,0 +1,30 @@
+#!/sbin/sh
+
+HAS_KEYFILE="y"
+
+PASS_DIR="$HOME/Pass"
+[ -n "$HAS_KEYFILE" ] && \
+ KEYFILE=`ls $PASS_DIR | barmenu $@ -p 'keyfile:'` && \
+ [ -n "$KEYFILE" ] && \
+ KEYFILE_FLAG="-k" && \
+ echo "$KEYFILE" || \
+ exit 2
+
+PASS_DB=`ls $PASS_DIR | barmenu $@ -p 'database:'` && \
+ [ -f "$PASS_DIR/$PASS_DB" ] &&
+ echo "$PASS_DB" || \
+ exit 1
+
+
+PASS_WD=`barmenu -P $@ -p 'password:'`
+
+PASS_LS=`echo "$PASS_WD" | keepassxc-cli ls -Rf "$KEYFILE_FLAG" "$PASS_DIR/$KEYFILE" "$PASS_DIR/$PASS_DB"`
+
+PASSWORD_SEL=`echo "$PASS_LS" | barmenu $@ -p password`
+
+PASS_ATTR=`echo "username\npassword" | barmenu $@ -p "which attribute to copy?"` && \
+ [ -z "$PASS_ATTR" ] && \
+ PASS_ATTR="password"
+
+echo "$PASS_WD" | keepassxc-cli clip -a "$PASS_ATTR" "$KEYFILE_FLAG" "$PASS_DIR/$KEYFILE" "$PASS_DIR/$PASS_DB" "$PASSWORD_SEL" 15
+
diff --git a/dmenumount b/dmenumount
new file mode 100755
index 0000000..af6c390
--- /dev/null
+++ b/dmenumount
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+# Gives a dmenu -prompt to mount unmounted drives and Android phones. If
+# they're in /etc/fstab, they'll be mounted automatically. Otherwise, you'll
+# be prompted to give a mountpoint from already existsing directories. If you
+# input a novel directory, it will prompt you to create that directory.
+
+getmount() { \
+ [ -z "$chosen" ] && exit 1
+ # shellcheck disable=SC2086
+ mp="$(find $1 2>/dev/null | dmenu -p "Type in mount point.")" || exit 1
+ test -z "$mp" && exit 1
+ if [ ! -d "$mp" ]; then
+ mkdiryn=$(printf "No\\nYes" | dmenu -p "$mp does not exist. Create it?") || exit 1
+ [ "$mkdiryn" = "Yes" ] && (mkdir -p "$mp" || doas mkdir -p "$mp")
+ fi
+ }
+
+mountusb() { \
+ chosen="$(echo "$usbdrives" | dmenu -p "Mount which drive?")" || exit 1
+ chosen="$(echo "$chosen" | awk '{print $1}')"
+ echo "$chosen"
+ doas mount "$chosen" 2>/dev/null && notify-send "💻 USB mounting" "$chosen mounted." && exit 0
+ alreadymounted=$(lsblk -nrpo "name,type,mountpoint" | awk '$3!~/\/boot|\/home$|SWAP/&&length($3)>1{printf "-not ( -path *%s -prune ) ",$3}')
+ getmount "/mnt /media /mount /home -maxdepth 3 -type d -empty $alreadymounted"
+ partitiontype="$(lsblk -no "fstype" "$chosen")"
+ case "$partitiontype" in
+ "vfat") doas mount -t vfat "$chosen" "$mp" -o rw,umask=0000;;
+ "exfat") doas mount.exfat "$chosen" "$mp" -o uid="$(id -u)",gid="$(id -g)";;
+ *) doas mount "$chosen" "$mp"; user="$(whoami)"; ug="$(groups | awk '{print $1}')"; doas chown "$user":"$ug" "$mp";;
+ esac
+ notify-send "💻 USB mounting" "$chosen mounted to $mp."
+ }
+
+mountandroid() { \
+ chosen="$(echo "$anddrives" | dmenu -p "Which Android device?")" || exit 1
+ chosen="$(echo "$chosen" | cut -d : -f 1)"
+ getmount "$HOME -maxdepth 3 -type d"
+ simple-mtpfs --device "$chosen" "$mp"
+ echo "OK" | dmenu -p "Tap Allow on your phone if it asks for permission and then press enter" || exit 1
+ simple-mtpfs --device "$chosen" "$mp"
+ notify-send "🤖 Android Mounting" "Android device mounted to $mp."
+ }
+
+asktype() { \
+ choice="$(printf "USB\\nAndroid" | dmenu -p "Mount a USB drive or Android device?")" || exit 1
+ case $choice in
+ USB) mountusb ;;
+ Android) mountandroid ;;
+ esac
+ }
+
+anddrives=$(simple-mtpfs -l 2>/dev/null)
+alldrives="$(lsblk -rpo "name,type,size,mountpoint" | grep 'part\|rom\|crypt' | awk '$4==""{printf "%s (%s)\n",$1,$3}')"
+
+for i in $alldrives; do
+ echo "$i" | grep -qi '([0-9.]*[mgt])' && continue
+ if ! `blkid $i | grep -q 'crypto_LUKS'`; then
+ usbdrives="$(echo "$alldrives" | grep "$i")\n$usbdrives"
+ fi
+done
+
+if [ -z "$usbdrives" ]; then
+ [ -z "$anddrives" ] && echo "No USB drive or Android device detected" && exit
+ echo "Android device(s) detected."
+ mountandroid
+else
+ if [ -z "$anddrives" ]; then
+ echo "USB drive(s) detected."
+ mountusb
+ else
+ echo "Mountable USB drive(s) and Android device(s) detected."
+ asktype
+ fi
+fi
diff --git a/dmenurecord b/dmenurecord
new file mode 100755
index 0000000..e177b03
--- /dev/null
+++ b/dmenurecord
@@ -0,0 +1,132 @@
+#!/bin/sh
+
+# Usage:
+# `$0`: Ask for recording type via dmenu
+# `$0 screencast`: Record both audio and screen
+# `$0 video`: Record only screen
+# `$0 audio`: Record only audio
+# `$0 kill`: Kill existing recording
+#
+# If there is already a running instance, user will be prompted to end it.
+
+SCREENCAST_DIR="$HOME/Public/Videos"
+
+updateicon() { \
+ echo "$1" > /tmp/recordingicon
+ pkill -RTMIN+9 "${STATUSBAR:-dwmblocks}"
+ }
+
+killrecording() {
+ recpid="$(cat /tmp/recordingpid)"
+ # kill with SIGTERM, allowing finishing touches.
+ kill -15 "$recpid"
+ rm -f /tmp/recordingpid
+ updateicon ""
+ pkill -RTMIN+9 "${STATUSBAR:-dwmblocks}"
+ # even after SIGTERM, ffmpeg may still run, so SIGKILL it.
+ sleep 3
+ kill -9 "$recpid"
+ notify-send "recording killed"
+ exit
+ }
+
+screencast() { \
+ ffmpeg -y \
+ -f x11grab \
+ -framerate 60 \
+ -s "$(xdpyinfo | awk '/dimensions/ {print $2;}')" \
+ -i "$DISPLAY" \
+ -f pulse -i default \
+ -f pulse -i alsa_output.pci-0000_00_1b.0.analog-stereo.monitor \
+ -r 30 \
+ -c:v h264 -crf 0 -preset ultrafast -c:a aac \
+ "$SCREENCAST_DIR/screencast-$(date '+%y%m%d-%H%M-%S').mp4" &
+ echo $! > /tmp/recordingpid
+ notify-send "recording started (screencast)"
+ updateicon "⏺️🎙️"
+ }
+
+video() { ffmpeg \
+ -f x11grab \
+ -s "$(xdpyinfo | awk '/dimensions/ {print $2;}')" \
+ -i "$DISPLAY" \
+ -c:v libx264 -qp 0 -r 30 \
+ "$SCREENCAST_DIR/video-$(date '+%y%m%d-%H%M-%S').mkv" &
+ echo $! > /tmp/recordingpid
+ notify-send "recording started (video)"
+ updateicon "⏺️"
+ }
+
+webcamhidef() { ffmpeg \
+ -f v4l2 \
+ -i /dev/video0 \
+ -video_size 1920x1080 \
+ "$SCREENCAST_DIR/webcam-$(date '+%y%m%d-%H%M-%S').mkv" &
+ echo $! > /tmp/recordingpid
+ notify-send "recording started (webcamhidef)"
+ updateicon "🎥"
+ }
+
+webcam() { ffmpeg \
+ -f v4l2 \
+ -i /dev/video0 \
+ -video_size 640x480 \
+ "$SCREENCAST_DIR/webcam-$(date '+%y%m%d-%H%M-%S').mkv" &
+ echo $! > /tmp/recordingpid
+ notify-send "recording started (webcam)"
+ updateicon "🎥"
+ }
+
+
+audio() { \
+ ffmpeg \
+ -f alsa -i default \
+ -c:a flac \
+ "$SCREENCAST_DIR/audio-$(date '+%y%m%d-%H%M-%S').flac" &
+ echo $! > /tmp/recordingpid
+ notify-send "recording started (audio)"
+ updateicon "🎙️"
+ }
+
+askrecording() { \
+ choice=$(printf "screencast\\nvideo\\nvideo selected\\naudio\\nwebcam\\nwebcam (hi-def)" | dmenu -i -p "Select recording style:")
+ case "$choice" in
+ screencast) screencast;;
+ audio) audio;;
+ video) video;;
+ *selected) videoselected;;
+ webcam) webcam;;
+ "webcam (hi-def)") webcamhidef;;
+ esac
+ }
+
+asktoend() { \
+ response=$(printf "No\\nYes" | dmenu -i -p "Recording still active. End recording?") &&
+ [ "$response" = "Yes" ] && killrecording
+ }
+
+videoselected()
+{
+ slop -f "%x %y %w %h" > /tmp/slop
+ read -r X Y W H < /tmp/slop
+ rm /tmp/slop
+
+ ffmpeg \
+ -f x11grab \
+ -framerate 60 \
+ -video_size "$W"x"$H" \
+ -i :0.0+"$X,$Y" \
+ -c:v libx264 -qp 0 -r 30 \
+ "$SCREENCAST_DIR/box-$(date '+%y%m%d-%H%M-%S').mkv" &
+ echo $! > /tmp/recordingpid
+ updateicon "⏺️"
+}
+
+case "$1" in
+ screencast) screencast;;
+ audio) audio;;
+ video) video;;
+ *selected) videoselected;;
+ kill) killrecording;;
+ *) ([ -f /tmp/recordingpid ] && asktoend && exit) || askrecording;;
+esac
diff --git a/dmenuumount b/dmenuumount
new file mode 100755
index 0000000..0584cb4
--- /dev/null
+++ b/dmenuumount
@@ -0,0 +1,44 @@
+#!/bin/sh
+
+# A dmenu prompt to unmount drives.
+# Provides you with mounted partitions, select one to unmount.
+# Drives mounted at /, /boot and /home will not be options to unmount.
+
+unmountusb() {
+ [ -z "$drives" ] && exit
+ chosen="$(echo "$drives" | dmenu -p "Unmount which drive?")" || exit 1
+ chosen="$(echo "$chosen" | awk '{print $1}')"
+ [ -z "$chosen" ] && exit
+ doas umount "$chosen" && notify-send "💻 USB unmounting" "$chosen unmounted."
+ }
+
+unmountandroid() { \
+ chosen="$(awk '/simple-mtpfs/ {print $2}' /etc/mtab | dmenu -p "Unmount which device?")" || exit 1
+ [ -z "$chosen" ] && exit
+ doas umount -l "$chosen" && notify-send "🤖 Android unmounting" "$chosen unmounted."
+ }
+
+asktype() { \
+ choice="$(printf "USB\\nAndroid" | dmenu -p "Unmount a USB drive or Android device?")" || exit 1
+ case "$choice" in
+ USB) unmountusb ;;
+ Android) unmountandroid ;;
+ esac
+ }
+
+drives=$(lsblk -nrpo "name,type,size,mountpoint" | awk '$4!~/\/boot|\/efi|\/home$|SWAP/&&length($4)>1{printf "%s (%s)\n",$4,$3}')
+
+if ! grep simple-mtpfs /etc/mtab; then
+ [ -z "$drives" ] && echo "No drives to unmount." && exit
+ echo "Unmountable USB drive detected."
+ unmountusb
+else
+ if [ -z "$drives" ]
+ then
+ echo "Unmountable Android device detected."
+ unmountandroid
+ else
+ echo "Unmountable USB drive(s) and Android device(s) detected."
+ asktype
+ fi
+fi
diff --git a/dmenuunicode b/dmenuunicode
new file mode 100755
index 0000000..704c809
--- /dev/null
+++ b/dmenuunicode
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+# The famous "get a menu of emojis to copy" script.
+
+# Get user selection via dmenu from emoji file.
+chosen=$(cut -d ';' -f1 ~/.local/share/larbs/chars/* | dmenu -i -l 30 | sed "s/ .*//")
+
+# Exit if none chosen.
+[ -z "$chosen" ] && exit
+
+# If you run this command with an argument, it will automatically insert the
+# character. Otherwise, show a message that the emoji has been copied.
+if [ -n "$1" ]; then
+ xdotool type "$chosen"
+else
+ printf "$chosen" | xclip -selection clipboard
+ notify-send "'$chosen' copied to clipboard." &
+fi
diff --git a/exiftool b/exiftool
new file mode 100755
index 0000000..ea840d0
--- /dev/null
+++ b/exiftool
@@ -0,0 +1,7153 @@
+#!/usr/bin/perl -w
+#------------------------------------------------------------------------------
+# File: exiftool
+#
+# Description: Read/write meta information
+#
+# Revisions: Nov. 12/03 - P. Harvey Created
+# (See html/history.html for revision history)
+#------------------------------------------------------------------------------
+use strict;
+require 5.004;
+
+my $version = '12.12';
+
+# add our 'lib' directory to the include list BEFORE 'use Image::ExifTool'
+my $exeDir;
+BEGIN {
+ # (undocumented -xpath option added in 11.91, must come before other options)
+ $Image::ExifTool::exePath = @ARGV && lc($ARGV[0]) eq '-xpath' && shift() ? $^X : $0;
+ # get exe directory
+ $exeDir = ($Image::ExifTool::exePath =~ /(.*)[\\\/]/) ? $1 : '.';
+ if (-l $0) {
+ my $lnk = eval { readlink $0 };
+ if (defined $lnk) {
+ my $lnkDir = ($lnk =~ /(.*)[\\\/]/) ? $1 : '.';
+ $exeDir = (($lnk =~ m(^/)) ? '' : $exeDir . '/') . $lnkDir;
+ }
+ }
+ # add lib directory at start of include path
+ unshift @INC, ($0 =~ /(.*)[\\\/]/) ? "$1/lib" : './lib';
+ # load or disable config file if specified
+ if (@ARGV and lc($ARGV[0]) eq '-config') {
+ shift;
+ $Image::ExifTool::configFile = shift;
+ }
+}
+use Image::ExifTool qw{:Public};
+
+# function prototypes
+sub SigInt();
+sub SigCont();
+sub Cleanup();
+sub GetImageInfo($$);
+sub SetImageInfo($$$);
+sub DoHardLink($$$$$);
+sub CleanXML($);
+sub EncodeXML($);
+sub FormatXML($$$);
+sub EscapeJSON($;$);
+sub FormatJSON($$$);
+sub PrintCSV();
+sub AddGroups($$$$);
+sub ConvertBinary($);
+sub IsEqual($$);
+sub Infile($;$);
+sub AddSetTagsFile($;$);
+sub DoSetFromFile($$$);
+sub CleanFilename($);
+sub SetWindowTitle($);
+sub ProcessFiles($;$);
+sub ScanDir($$;$);
+sub FindFileWindows($$);
+sub FileNotFound($);
+sub PreserveTime();
+sub AbsPath($);
+sub MyConvertFileName($$);
+sub SuggestedExtension($$$);
+sub LoadPrintFormat($);
+sub FilenameSPrintf($;$@);
+sub NextUnusedFilename($;$);
+sub CreateDirectory($);
+sub OpenOutputFile($;@);
+sub AcceptFile($);
+sub SlurpFile($$);
+sub FilterArgfileLine($);
+sub ReadStayOpen($);
+sub PrintTagList($@);
+sub PrintErrors($$$);
+
+$SIG{INT} = 'SigInt'; # do cleanup on Ctrl-C
+$SIG{CONT} = 'SigCont'; # (allows break-out of delays)
+END {
+ Cleanup();
+}
+
+# declare all static file-scope variables
+my @commonArgs; # arguments common to all commands
+my @condition; # conditional processing of files
+my @csvFiles; # list of files when reading with CSV option (in ExifTool Charset)
+my @csvTags; # order of tags for first file with CSV option (lower case)
+my @delFiles; # list of files to delete
+my @dynamicFiles; # list of -tagsFromFile files with dynamic names and -TAG<=FMT pairs
+my @efile; # files for writing list of error/fail/same file names
+my @exclude; # list of excluded tags
+my (@echo3, @echo4);# stdout and stderr echo after processing is complete
+my @files; # list of files and directories to scan
+my @moreArgs; # more arguments to process after -stay_open -@
+my @newValues; # list of new tag values to set
+my @requestTags; # tags to request (for -p or -if option arguments)
+my @srcFmt; # source file name format strings
+my @tags; # list of tags to extract
+my %appended; # list of files appended to
+my %countLink; # count hard and symbolic links made
+my %created; # list of files we created
+my %csvTags; # lookup for all found tags with CSV option (lower case keys)
+my %database; # lookup for database information based on file name (in ExifTool Charset)
+my %filterExt; # lookup for filtered extensions
+my %ignore; # directory names to ignore
+my %preserveTime; # preserved timestamps for files
+my %printFmt; # the contents of the print format file
+my %setTags; # hash of list references for tags to set from files
+my %setTagsList; # list of other tag lists for multiple -tagsFromFile from the same file
+my %usedFileName; # lookup for file names we already used in TestName feature
+my %utf8FileName; # lookup for file names that are UTF-8 encoded
+my %warnedOnce; # lookup for once-only warnings
+my %wext; # -W extensions to write
+my $allGroup; # show group name for all tags
+my $altEnc; # alternate character encoding if not UTF-8
+my $argFormat; # use exiftool argument-format output
+my $binaryOutput; # flag for binary output (undef or 1, or 0 for binary XML/PHP)
+my $binaryStdout; # flag set if we output binary to stdout
+my $binSep; # separator used for list items in binary output
+my $binTerm; # terminator used for binary output
+my $comma; # flag set if we need a comma in JSON output
+my $count; # count of files scanned when reading or deleting originals
+my $countBad; # count of files with errors
+my $countBadCr; # count files not created due to errors
+my $countBadWr; # count write errors
+my $countCopyWr; # count of files copied without being changed
+my $countDir; # count of directories scanned
+my $countFailed; # count files that failed condition
+my $countGoodCr; # count files created OK
+my $countGoodWr; # count files written OK
+my $countNewDir; # count of directories created
+my $countSameWr; # count files written OK but not changed
+my $critical; # flag for critical operations (disable CTRL-C)
+my $csv; # flag for CSV option (set to "CSV", or maybe "JSON" when writing)
+my $csvAdd; # flag to add CSV information to existing lists
+my $csvDelim; # delimiter for CSV files
+my $csvSaveCount; # save counter for last CSV file loaded
+my $deleteOrig; # 0=restore original files, 1=delete originals, 2=delete w/o asking
+my $disableOutput; # flag to disable normal output
+my $doSetFileName; # flag set if FileName may be written
+my $doUnzip; # flag to extract info from .gz and .bz2 files
+my ($end,$endDir,%endDir); # flags to end processing
+my $escapeC; # C-style escape
+my $escapeHTML; # flag to escape printed values for html
+my $evalWarning; # warning from eval
+my $executeID; # -execute ID number
+my $failCondition; # flag to fail -if condition
+my $fastCondition; # flag for fast -if condition
+my $fileHeader; # header to print to output file (or console, once)
+my $fileTrailer; # trailer for output file
+my $filtered; # flag indicating file was filtered by name
+my $filterFlag; # file filter flag (0x01=deny extensions, 0x02=allow extensions, 0x04=add ext)
+my $fixLen; # flag to fix description lengths when writing alternate languages
+my $forcePrint; # string to use for missing tag values (undef to not print them)
+my $helped; # flag to avoid printing help if no tags specified
+my $html; # flag for html-formatted output (2=html dump)
+my $interrupted; # flag set if CTRL-C is pressed during a critical process
+my $isWriting; # flag set if we are writing tags
+my $joinLists; # flag set to join list values into a single string
+my $json; # flag for JSON/PHP output format (1=JSON, 2=PHP)
+my $langOpt; # language option
+my $listItem; # item number for extracting single item from a list
+my $listSep; # list item separator (', ' by default)
+my $mt; # main ExifTool object
+my $multiFile; # non-zero if we are scanning multiple files
+my $outFormat; # -1=Canon format, 0=same-line, 1=tag names, 2=values only
+my $outOpt; # output file or directory name
+my $overwriteOrig; # flag to overwrite original file (1=overwrite, 2=in place)
+my $pause; # pause before returning
+my $preserveTime; # flag to preserve times of updated files (2=preserve FileCreateDate only)
+my $progress; # flag to calculate total files to process (0=calculate but don't display)
+my $progressCount; # count of files processed
+my $progressMax; # total number of files to process
+my $progStr; # progress message string
+my $quiet; # flag to disable printing of informational messages / warnings
+my $rafStdin; # File::RandomAccess for stdin (if necessary to rewind)
+my $recurse; # recurse into subdirectories (2=also hidden directories)
+my $rtnVal; # command return value (0=success)
+my $rtnValPrev; # previous command return value (0=success)
+my $saveCount; # count the number of times we will/did call SaveNewValues()
+my $scanWritable; # flag to process only writable file types
+my $sectHeader; # current section header for -p option
+my $sectTrailer; # section trailer for -p option
+my $seqFileBase; # sequential file number at start of directory
+my $seqFileNum; # sequential file number used for %C
+my $setCharset; # character set setting ('default' if not set and -csv -b used)
+my $showGroup; # number of group to show (may be zero or '')
+my $showTagID; # non-zero to show tag ID's
+my $stayOpenBuff='';# buffer for -stay_open file
+my $stayOpenFile; # name of the current -stay_open argfile
+my $structOpt; # output structured XMP information (JSON and XML output only)
+my $tabFormat; # non-zero for tab output format
+my $tagOut; # flag for separate text output file for each tag
+my $textOut; # extension for text output file (or undef for no output)
+my $textOverwrite; # flag to overwrite existing text output file (2=append, 3=over+append)
+my $tmpFile; # temporary file to delete on exit
+my $tmpText; # temporary text file
+my $validFile; # flag indicating we processed a valid file
+my $verbose; # verbose setting
+my $vout; # verbose output file reference (\*STDOUT or \*STDERR)
+my $windowTitle; # title for console window
+my $isBinary; # true if value is a SCALAR ref
+my $xml; # flag for XML-formatted output
+
+# flag to keep the input -@ argfile open:
+# 0 = normal behaviour
+# 1 = received "-stay_open true" and waiting for argfile to keep open
+# 2 = currently reading from STAYOPEN argfile
+# 3 = waiting for -@ to switch to a new STAYOPEN argfile
+my $stayOpen = 0;
+
+my $rtnValApp = 0; # app return value (0=success)
+my $curTitle = ''; # current window title
+
+# lookup for O/S names which may use a backslash as a directory separator
+# (ref File::Spec of PathTools-3.2701)
+my %hasBackslash = ( MSWin32 => 1, os2 => 1, dos => 1, NetWare => 1, symbian => 1, cygwin => 1 );
+
+# lookup for O/S names which use CR/LF newlines
+my $isCRLF = { MSWin32 => 1, os2 => 1, dos => 1 }->{$^O};
+
+# lookup for JSON characters that we escape specially
+my %jsonChar = ( '"'=>'"', '\\'=>'\\', "\t"=>'t', "\n"=>'n', "\r"=>'r' );
+
+# lookup for C-style escape sequences
+my %escC = ( "\n" => '\n', "\r" => '\r', "\t" => '\t', '\\' => '\\\\');
+my %unescC = ( a => "\a", b => "\b", f => "\f", n => "\n", r => "\r",
+ t => "\t", 0 => "\0", '\\' => '\\' );
+
+# options requiring additional arguments
+# (used only to skip over these arguments when reading -stay_open ARGFILE)
+# (arg is converted to lower case then tested again unless an entry was found with the same case)
+my %optArgs = (
+ '-tagsfromfile' => 1, '-addtagsfromfile' => 1, '-alltagsfromfile' => 1,
+ '-@' => 1,
+ '-api' => 1,
+ '-c' => 1, '-coordformat' => 1,
+ '-charset' => 0, # (optional arg; OK because arg cannot begin with "-")
+ '-config' => 1,
+ '-csvdelim' => 1,
+ '-d' => 1, '-dateformat' => 1,
+ '-D' => 0, # necessary to avoid matching lower-case equivalent
+ '-echo' => 1, '-echo1' => 1, '-echo2' => 1, '-echo3' => 1, '-echo4' => 1,
+ '-efile' => 1, '-efile1' => 1, '-efile2' => 1, '-efile3' => 1, '-efile4' => 1,
+ '-efile!' => 1, '-efile1!' => 1, '-efile2!' => 1, '-efile3!' => 1, '-efile4!' => 1,
+ '-ext' => 1, '--ext' => 1, '-ext+' => 1, '--ext+' => 1,
+ '-extension' => 1, '--extension' => 1, '-extension+' => 1, '--extension+' => 1,
+ '-fileorder' => 1, '-fileorder0' => 1, '-fileorder1' => 1, '-fileorder2' => 1,
+ '-fileorder3' => 1, '-fileorder4' => 1, '-fileorder5' => 1,
+ '-geotag' => 1,
+ '-globaltimeshift' => 1,
+ '-i' => 1, '-ignore' => 1,
+ '-if' => 1, '-if0' => 1, '-if1' => 1, '-if2' => 1, '-if3' => 1, '-if4' => 1, '-if5' => 1,
+ '-lang' => 0, # (optional arg; cannot begin with "-")
+ '-listitem' => 1,
+ '-o' => 1, '-out' => 1,
+ '-p' => 1, '-printformat' => 1,
+ '-P' => 0,
+ '-password' => 1,
+ '-require' => 1,
+ '-sep' => 1, '-separator' => 1,
+ '-srcfile' => 1,
+ '-stay_open' => 1,
+ '-use' => 1,
+ '-userparam' => 1,
+ '-w' => 1, '-w!' => 1, '-w+' => 1, '-w+!' => 1, '-w!+' => 1,
+ '-textout' => 1, '-textout!' => 1, '-textout+' => 1, '-textout+!' => 1, '-textout!+' => 1,
+ '-tagout' => 1, '-tagout!' => 1, '-tagout+' => 1, '-tagout+!' => 1, '-tagout!+' => 1,
+ '-wext' => 1,
+ '-wm' => 1, '-writemode' => 1,
+ '-x' => 1, '-exclude' => 1,
+ '-X' => 0,
+);
+
+# recommended packages and alternatives
+my @recommends = qw(
+ Archive::Zip
+ Compress::Zlib
+ Digest::MD5
+ Digest::SHA
+ IO::Compress::Bzip2
+ POSIX::strptime
+ Unicode::LineBreak
+ IO::Compress::RawDeflate
+ IO::Uncompress::RawInflate
+ Win32::API
+ Win32::FindFile
+ Win32API::File
+);
+my %altRecommends = (
+ 'POSIX::strptime' => 'Time::Piece', # (can use Time::Piece instead of POSIX::strptime)
+);
+
+my %unescapeChar = ( 't'=>"\t", 'n'=>"\n", 'r'=>"\r" );
+
+# special subroutines used in -if condition
+sub Image::ExifTool::EndDir() { return $endDir = 1 }
+sub Image::ExifTool::End() { return $end = 1 }
+
+# exit routine
+sub Exit {
+ if ($pause) {
+ if (eval { require Term::ReadKey }) {
+ print STDERR "-- press any key --";
+ Term::ReadKey::ReadMode('cbreak');
+ Term::ReadKey::ReadKey(0);
+ Term::ReadKey::ReadMode(0);
+ print STDERR "\b \b" x 20;
+ } else {
+ print STDERR "-- press RETURN --\n";
+ <STDIN>;
+ }
+ }
+ exit shift;
+}
+# my warning and error routines (NEVER say "die"!)
+sub Warn {
+ if ($quiet < 2 or $_[0] =~ /^Error/) {
+ my $oldWarn = $SIG{'__WARN__'};
+ delete $SIG{'__WARN__'};
+ warn(@_);
+ $SIG{'__WARN__'} = $oldWarn if defined $oldWarn;
+ }
+}
+sub Error { Warn @_; $rtnVal = 1; }
+sub WarnOnce($) {
+ Warn(@_) and $warnedOnce{$_[0]} = 1 unless $warnedOnce{$_[0]};
+}
+
+# define signal handlers and cleanup routine
+sub SigInt() {
+ $critical and $interrupted = 1, return;
+ Cleanup();
+ exit 1;
+}
+sub SigCont() { }
+sub Cleanup() {
+ $mt->Unlink($tmpFile) if defined $tmpFile;
+ $mt->Unlink($tmpText) if defined $tmpText;
+ undef $tmpFile;
+ undef $tmpText;
+ PreserveTime() if %preserveTime;
+ SetWindowTitle('');
+}
+
+#------------------------------------------------------------------------------
+# main script
+#
+
+# isolate arguments common to all commands
+if (grep /^-common_args$/i, @ARGV) {
+ my (@newArgs, $common);
+ foreach (@ARGV) {
+ if (/^-common_args$/i) {
+ $common = 1;
+ } elsif ($common) {
+ push @commonArgs, $_;
+ } else {
+ push @newArgs, $_;
+ }
+ }
+ @ARGV = @newArgs if $common;
+}
+
+#..............................................................................
+# loop over sets of command-line arguments separated by "-execute"
+Command: for (;;) {
+
+if (@echo3) {
+ my $str = join "\n", @echo3, "\n";
+ $str =~ s/\$\{status\}/$rtnVal/ig;
+ print STDOUT $str;
+}
+if (@echo4) {
+ my $str = join "\n", @echo4, "\n";
+ $str =~ s/\$\{status\}/$rtnVal/ig;
+ print STDERR $str;
+}
+
+$rafStdin->Close() if $rafStdin;
+undef $rafStdin;
+
+# save our previous return codes
+$rtnValPrev = $rtnVal;
+$rtnValApp = $rtnVal if $rtnVal;
+
+# exit Command loop now if we are all done processing commands
+last unless @ARGV or not defined $rtnVal or $stayOpen >= 2 or @commonArgs;
+
+# attempt to restore text mode for STDOUT if necessary
+if ($binaryStdout) {
+ binmode(STDOUT,':crlf') if $] >= 5.006 and $isCRLF;
+ $binaryStdout = 0;
+}
+
+# flush console and print "{ready}" message if -stay_open is in effect
+if ($stayOpen >= 2) {
+ if ($quiet and not defined $executeID) {
+ # flush output if possible
+ eval { require IO::Handle } and STDERR->flush(), STDOUT->flush();
+ } else {
+ eval { require IO::Handle } and STDERR->flush();
+ my $id = defined $executeID ? $executeID : '';
+ my $save = $|;
+ $| = 1; # turn on output autoflush for stdout
+ print "{ready$id}\n";
+ $| = $save; # restore original autoflush setting
+ }
+}
+
+# initialize necessary static file-scope variables
+# (not done: @commonArgs, @moreArgs, $critical, $binaryStdout, $helped,
+# $interrupted, $mt, $pause, $rtnValApp, $rtnValPrev, $stayOpen, $stayOpenBuff, $stayOpenFile)
+undef @condition;
+undef @csvFiles;
+undef @csvTags;
+undef @delFiles;
+undef @dynamicFiles;
+undef @echo3;
+undef @echo4;
+undef @efile;
+undef @exclude;
+undef @files;
+undef @newValues;
+undef @srcFmt;
+undef @tags;
+undef %appended;
+undef %countLink;
+undef %created;
+undef %csvTags;
+undef %database;
+undef %endDir;
+undef %filterExt;
+undef %ignore;
+undef %printFmt;
+undef %preserveTime;
+undef %setTags;
+undef %setTagsList;
+undef %usedFileName;
+undef %utf8FileName;
+undef %warnedOnce;
+undef %wext;
+undef $allGroup;
+undef $altEnc;
+undef $argFormat;
+undef $binaryOutput;
+undef $binSep;
+undef $binTerm;
+undef $comma;
+undef $csv;
+undef $csvAdd;
+undef $deleteOrig;
+undef $disableOutput;
+undef $doSetFileName;
+undef $doUnzip;
+undef $end;
+undef $endDir;
+undef $escapeHTML;
+undef $escapeC;
+undef $evalWarning;
+undef $executeID;
+undef $failCondition;
+undef $fastCondition;
+undef $fileHeader;
+undef $filtered;
+undef $fixLen;
+undef $forcePrint;
+undef $joinLists;
+undef $langOpt;
+undef $listItem;
+undef $multiFile;
+undef $outOpt;
+undef $preserveTime;
+undef $progress;
+undef $progressCount;
+undef $progressMax;
+undef $recurse;
+undef $scanWritable;
+undef $sectHeader;
+undef $setCharset;
+undef $showGroup;
+undef $showTagID;
+undef $structOpt;
+undef $tagOut;
+undef $textOut;
+undef $textOverwrite;
+undef $tmpFile;
+undef $tmpText;
+undef $validFile;
+undef $verbose;
+undef $windowTitle;
+
+$count = 0;
+$countBad = 0;
+$countBadCr = 0;
+$countBadWr = 0;
+$countCopyWr = 0;
+$countDir = 0;
+$countFailed = 0;
+$countGoodCr = 0;
+$countGoodWr = 0;
+$countNewDir = 0;
+$countSameWr = 0;
+$csvDelim = ',';
+$csvSaveCount = 0;
+$fileTrailer = '';
+$filterFlag = 0;
+$html = 0;
+$isWriting = 0;
+$json = 0;
+$listSep = ', ';
+$outFormat = 0;
+$overwriteOrig = 0;
+$progStr = '';
+$quiet = 0;
+$rtnVal = 0;
+$saveCount = 0;
+$sectTrailer = '';
+$seqFileBase = 0;
+$seqFileNum = 0;
+$tabFormat = 0;
+$vout = \*STDOUT;
+$xml = 0;
+
+# define local variables used only in this command loop
+my @fileOrder; # tags to use for ordering of input files
+my $fileOrderFast; # -fast level for -fileOrder option
+my $addGeotime; # automatically added geotime argument
+my $doGlob; # flag set to do filename wildcard expansion
+my $endOfOpts; # flag set if "--" option encountered
+my $escapeXML; # flag to escape printed values for xml
+my $setTagsFile; # filename for last TagsFromFile option
+my $sortOpt; # sort option is used
+my $srcStdin; # one of the source files is STDIN
+my $useMWG; # flag set if we are using any MWG tag
+
+my ($argsLeft, @nextPass, $badCmd);
+my $pass = 0;
+
+# for Windows, use globbing for wildcard expansion if available - MK/20061010
+if ($^O eq 'MSWin32' and eval { require File::Glob }) {
+ # override the core glob forcing case insensitivity
+ import File::Glob qw(:globally :nocase);
+ $doGlob = 1;
+}
+
+$mt = new Image::ExifTool; # create ExifTool object
+
+# don't extract duplicates by default unless set by UserDefined::Options
+$mt->Options(Duplicates => 0) unless %Image::ExifTool::UserDefined::Options
+ and defined $Image::ExifTool::UserDefined::Options{Duplicates};
+
+# default is to join lists if the List option was set to zero in the config file
+$joinLists = 1 if defined $mt->Options('List') and not $mt->Options('List');
+
+# preserve FileCreateDate if possible
+if (not $preserveTime and $^O eq 'MSWin32') {
+ $preserveTime = 2 if eval { require Win32::API } and eval { require Win32API::File };
+}
+
+# parse command-line options in 2 passes...
+# pass 1: set all of our ExifTool options
+# pass 2: print all of our help and informational output (-list, -ver, etc)
+for (;;) {
+
+ # execute the command now if no more arguments or -execute is used
+ if (not @ARGV or ($ARGV[0] =~ /^(-|\xe2\x88\x92)execute(\d+)?$/i and not $endOfOpts)) {
+ if (@ARGV) {
+ $executeID = $2; # save -execute number for "{ready}" response
+ $helped = 1; # don't show help if we used -execute
+ $badCmd and shift, $rtnVal=1, next Command;
+ } elsif ($stayOpen >= 2) {
+ ReadStayOpen(\@ARGV); # read more arguments from -stay_open file
+ next;
+ } elsif ($badCmd) {
+ undef @commonArgs; # all done. Flush common arguments
+ $rtnVal = 1;
+ next Command;
+ }
+ if ($pass == 0) {
+ # insert common arguments now if not done already
+ if (@commonArgs and not defined $argsLeft) {
+ # count the number of arguments remaining for subsequent commands
+ $argsLeft = scalar(@ARGV) + scalar(@moreArgs);
+ unshift @ARGV, @commonArgs;
+ # all done with commonArgs if this is the end of the command
+ undef @commonArgs unless $argsLeft;
+ next;
+ }
+ # check if we have more arguments now than we did before we processed
+ # the common arguments. If so, then we have an infinite processing loop
+ if (defined $argsLeft and $argsLeft < scalar(@ARGV) + scalar(@moreArgs)) {
+ Warn "Ignoring -common_args from $ARGV[0] onwards to avoid infinite recursion\n";
+ while ($argsLeft < scalar(@ARGV) + scalar(@moreArgs)) {
+ @ARGV and shift(@ARGV), next;
+ shift @moreArgs;
+ }
+ }
+ # require MWG module if used in any argument
+ # (note: doesn't cover the -p option because these tags will be parsed on the 2nd pass)
+ $useMWG = 1 if not $useMWG and grep /^mwg:/i, @tags, @requestTags;
+ if ($useMWG) {
+ require Image::ExifTool::MWG;
+ Image::ExifTool::MWG::Load();
+ }
+ # update necessary variables for 2nd pass
+ if (defined $forcePrint) {
+ unless (defined $mt->Options('MissingTagValue')) {
+ $mt->Options(MissingTagValue => '-');
+ }
+ $forcePrint = $mt->Options('MissingTagValue');
+ }
+ }
+ if (@nextPass) {
+ # process arguments which were deferred to the next pass
+ unshift @ARGV, @nextPass;
+ undef @nextPass;
+ undef $endOfOpts;
+ ++$pass;
+ next;
+ }
+ @ARGV and shift; # remove -execute from argument list
+ last; # process the command now
+ }
+ $_ = shift;
+ next if $badCmd; # flush remaining arguments if aborting this command
+
+ # allow funny dashes (nroff dash bug for cut-n-paste from pod)
+ if (not $endOfOpts and s/^(-|\xe2\x88\x92)//) {
+ s/^\xe2\x88\x92/-/; # translate double-dash too
+ if ($_ eq '-') {
+ $pass or push @nextPass, '--';
+ $endOfOpts = 1;
+ next;
+ }
+ my $a = lc $_;
+ if (/^list([wfrdx]|wf|g(\d*))?$/i) {
+ $pass or push @nextPass, "-$_";
+ my $type = lc($1 || '');
+ if (not $type or $type eq 'w' or $type eq 'x') {
+ my $group;
+ if ($ARGV[0] and $ARGV[0] =~ /^(-|\xe2\x88\x92)(.+):(all|\*)$/i) {
+ if ($pass == 0) {
+ $useMWG = 1 if lc($2) eq 'mwg';
+ push @nextPass, shift;
+ next;
+ }
+ $group = $2;
+ shift;
+ $group =~ /IFD/i and Warn("Can't list tags for specific IFD\n"), next;
+ $group =~ /^(all|\*)$/ and undef $group;
+ } else {
+ $pass or next;
+ }
+ $helped = 1;
+ if ($type eq 'x') {
+ require Image::ExifTool::TagInfoXML;
+ my %opts;
+ $opts{Flags} = 1 if defined $forcePrint;
+ $opts{NoDesc} = 1 if $outFormat > 0;
+ $opts{Lang} = $langOpt;
+ Image::ExifTool::TagInfoXML::Write(undef, $group, %opts);
+ next;
+ }
+ my $wr = ($type eq 'w');
+ my $msg = ($wr ? 'Writable' : 'Available') . ($group ? " $group" : '') . ' tags';
+ PrintTagList($msg, $wr ? GetWritableTags($group) : GetAllTags($group));
+ # also print shortcuts if listing all tags
+ next if $group or $wr;
+ my @tagList = GetShortcuts();
+ PrintTagList('Command-line shortcuts', @tagList) if @tagList;
+ next;
+ }
+ $pass or next;
+ $helped = 1;
+ if ($type eq 'wf') {
+ my @wf;
+ CanWrite($_) and push @wf, $_ foreach GetFileType();
+ PrintTagList('Writable file extensions', @wf);
+ } elsif ($type eq 'f') {
+ PrintTagList('Supported file extensions', GetFileType());
+ } elsif ($type eq 'r') {
+ PrintTagList('Recognized file extensions', GetFileType(undef, 0));
+ } elsif ($type eq 'd') {
+ PrintTagList('Deletable groups', GetDeleteGroups());
+ } else { # 'g(\d*)'
+ # list all groups in specified family
+ my $family = $2 || 0;
+ PrintTagList("Groups in family $family", GetAllGroups($family));
+ }
+ next;
+ }
+ if ($a eq 'ver') {
+ $pass or push(@nextPass,'-ver'), next;
+ my $libVer = $Image::ExifTool::VERSION;
+ my $str = $libVer eq $version ? '' : " [Warning: Library version is $libVer]";
+ if ($verbose) {
+ print "ExifTool version $version$str$Image::ExifTool::RELEASE\n";
+ printf "Perl version %s%s\n", $], (defined ${^UNICODE} ? " (-C${^UNICODE})" : '');
+ print "Platform: $^O\n";
+ print "Optional libraries:\n";
+ foreach (@recommends) {
+ next if /^Win32/ and $^O ne 'MSWin32';
+ my $ver = eval "require $_ and \$${_}::VERSION";
+ my $alt = $altRecommends{$_};
+ # check for alternative if primary not available
+ $ver = eval "require $alt and \$${alt}::VERSION" and $_ = $alt if not $ver and $alt;
+ printf " %-28s %s\n", $_, $ver || '(not installed)';
+ }
+ if ($verbose > 1) {
+ print "Include directories:\n";
+ print " $_\n" foreach @INC;
+ }
+ } else {
+ print "$version$str$Image::ExifTool::RELEASE\n";
+ }
+ $helped = 1;
+ next;
+ }
+ if (/^(all|add)?tagsfromfile(=.*)?$/i) {
+ $setTagsFile = $2 ? substr($2,1) : (@ARGV ? shift : '');
+ if ($setTagsFile eq '') {
+ Error("File must be specified for -tagsFromFile option\n");
+ $badCmd = 1;
+ next;
+ }
+ # create necessary lists, etc for this new -tagsFromFile file
+ AddSetTagsFile($setTagsFile, { Replace => ($1 and lc($1) eq 'add') ? 0 : 1 } );
+ next;
+ }
+ if ($a eq '@') {
+ my $argFile = shift or Error("Expecting filename for -\@ option\n"), $badCmd=1, next;
+ # switch to new ARGFILE if using chained -stay_open options
+ if ($stayOpen == 1) {
+ # defer remaining arguments until we close this argfile
+ @moreArgs = @ARGV;
+ undef @ARGV;
+ } elsif ($stayOpen == 3) {
+ if ($stayOpenFile and $stayOpenFile ne '-' and $argFile eq $stayOpenFile) {
+ # don't allow user to switch to the same -stay_open argfile
+ # because it will result in endless recursion
+ $stayOpen = 2;
+ Warn "Ignoring request to switch to the same -stay_open ARGFILE ($argFile)\n";
+ next;
+ }
+ close STAYOPEN;
+ $stayOpen = 1; # switch to this -stay_open file
+ }
+ my $fp = ($stayOpen == 1 ? \*STAYOPEN : \*ARGFILE);
+ unless ($mt->Open($fp, $argFile)) {
+ unless ($argFile !~ /^\// and $mt->Open($fp, "$exeDir/$argFile")) {
+ Error "Error opening arg file $argFile\n";
+ $badCmd = 1;
+ next
+ }
+ }
+ if ($stayOpen == 1) {
+ $stayOpenFile = $argFile; # remember the name of the file we have open
+ $stayOpenBuff = ''; # initialize buffer for reading this file
+ $stayOpen = 2;
+ $helped = 1;
+ ReadStayOpen(\@ARGV);
+ next;
+ }
+ my (@newArgs, $didBOM);
+ foreach (<ARGFILE>) {
+ # filter Byte Order Mark if it exists from start of UTF-8 text file
+ unless ($didBOM) {
+ s/^\xef\xbb\xbf//;
+ $didBOM = 1;
+ }
+ $_ = FilterArgfileLine($_);
+ push @newArgs, $_ if defined $_;
+ }
+ close ARGFILE;
+ unshift @ARGV, @newArgs;
+ next;
+ }
+ /^(-?)(a|duplicates)$/i and $mt->Options(Duplicates => ($1 ? 0 : 1)), next;
+ if ($a eq 'api') {
+ my $opt = shift;
+ defined $opt or Error("Expected OPT[=VAL] argument for -api option\n"), $badCmd=1, next;
+ my $val = ($opt =~ s/=(.*)//s) ? $1 : 1;
+ # empty string means an undefined value unless ^= is used
+ $val = undef unless $opt =~ s/\^$// or length $val;
+ $mt->Options($opt => $val);
+ next;
+ }
+ /^arg(s|format)$/i and $argFormat = 1, next;
+ /^b(inary)?$/i and $mt->Options(Binary => 1, NoPDFList => 1), $binaryOutput = 1, next;
+ if (/^c(oordFormat)?$/i) {
+ my $fmt = shift;
+ $fmt or Error("Expecting coordinate format for -c option\n"), $badCmd=1, next;
+ $mt->Options('CoordFormat', $fmt);
+ next;
+ }
+ if ($a eq 'charset') {
+ my $charset = (@ARGV and $ARGV[0] !~ /^(-|\xe2\x88\x92)/) ? shift : undef;
+ if (not $charset) {
+ $pass or push(@nextPass, '-charset'), next;
+ my %charsets;
+ $charsets{$_} = 1 foreach values %Image::ExifTool::charsetName;
+ PrintTagList('Available character sets', sort keys %charsets);
+ $helped = 1;
+ } elsif ($charset !~ s/^(\w+)=// or lc($1) eq 'exiftool') {
+ {
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ undef $evalWarning;
+ $mt->Options(Charset => $charset);
+ }
+ if ($evalWarning) {
+ warn $evalWarning;
+ } else {
+ $setCharset = $mt->Options('Charset');
+ }
+ } else {
+ # set internal encoding of specified metadata type
+ my $type = { id3 => 'ID3', iptc => 'IPTC', exif => 'EXIF', filename => 'FileName',
+ photoshop => 'Photoshop', quicktime => 'QuickTime', riff=>'RIFF' }->{lc $1};
+ $type or Warn("Unknown type for -charset option: $1\n"), next;
+ $mt->Options("Charset$type" => $charset);
+ }
+ next;
+ }
+ /^config$/i and Warn("Ignored -config option (not first on command line)\n"), shift, next;
+ if (/^csv(\+?=.*)?$/i) {
+ my $csvFile = $1;
+ # must process on 2nd pass so -f and -charset options are available
+ unless ($pass) {
+ push @nextPass, "-$_";
+ if ($csvFile) {
+ push @newValues, { SaveCount => ++$saveCount }; # marker to save new values now
+ $csvSaveCount = $saveCount;
+ }
+ next;
+ }
+ if ($csvFile) {
+ $csvFile =~ s/^(\+?=)//;
+ $csvAdd = 2 if $1 eq '+=';
+ $vout = \*STDERR if $srcStdin;
+ $verbose and print $vout "Reading CSV file $csvFile\n";
+ my $msg;
+ if ($mt->Open(\*CSVFILE, $csvFile)) {
+ binmode CSVFILE;
+ require Image::ExifTool::Import;
+ $msg = Image::ExifTool::Import::ReadCSV(\*CSVFILE, \%database, $forcePrint, $csvDelim);
+ close(CSVFILE);
+ } else {
+ $msg = "Error opening CSV file '${csvFile}'";
+ }
+ $msg and Warn("$msg\n");
+ $isWriting = 1;
+ }
+ $csv = 'CSV';
+ next;
+ }
+ if (/^csvdelim$/i) {
+ $csvDelim = shift;
+ defined $csvDelim or Error("Expecting argument for -csvDelim option\n"), $badCmd=1, next;
+ $csvDelim =~ /"/ and Error("CSV delimiter can not contain a double quote\n"), $badCmd=1, next;
+ my %unescape = ( 't'=>"\t", 'n'=>"\n", 'r'=>"\r", '\\' => '\\' );
+ $csvDelim =~ s/\\(.)/$unescape{$1}||"\\$1"/sge;
+ $mt->Options(CSVDelim => $csvDelim);
+ next;
+ }
+ if (/^d$/ or $a eq 'dateformat') {
+ my $fmt = shift;
+ $fmt or Error("Expecting date format for -d option\n"), $badCmd=1, next;
+ $mt->Options('DateFormat', $fmt);
+ next;
+ }
+ (/^D$/ or $a eq 'decimal') and $showTagID = 'D', next;
+ /^delete_original(!?)$/i and $deleteOrig = ($1 ? 2 : 1), next;
+ (/^e$/ or $a eq '-composite') and $mt->Options(Composite => 0), next;
+ (/^-e$/ or $a eq 'composite') and $mt->Options(Composite => 1), next;
+ (/^E$/ or $a eq 'escapehtml') and require Image::ExifTool::HTML and $escapeHTML = 1, next;
+ ($a eq 'ec' or $a eq 'escapec') and $escapeC = 1, next;
+ ($a eq 'ex' or $a eq 'escapexml') and $escapeXML = 1, next;
+ if (/^echo(\d)?$/i) {
+ my $n = $1 || 1;
+ my $arg = shift;
+ next unless defined $arg;
+ $n > 4 and Warn("Invalid -echo number\n"), next;
+ if ($n > 2) {
+ $n == 3 ? push(@echo3, $arg) : push(@echo4, $arg);
+ } else {
+ print {$n==2 ? \*STDERR : \*STDOUT} $arg, "\n";
+ }
+ $helped = 1;
+ next;
+ }
+ if (/^(ee|extractembedded)$/i) {
+ $mt->Options(ExtractEmbedded => 1);
+ $mt->Options(Duplicates => 1);
+ next;
+ }
+ if (/^efile(\d)?(!)?$/i) {
+ my $arg = shift;
+ defined $arg or Error("Expecting file name for -$_ option\n"), $badCmd=1, next;
+ $efile[0] = $arg if not $1 or $1 & 0x01;
+ $efile[1] = $arg if $1 and $1 & 0x02;
+ $efile[2] = $arg if $1 and $1 & 0x04;
+ unlink $arg if $2;
+ next;
+ }
+ # (-execute handled at top of loop)
+ if (/^-?ext(ension)?(\+)?$/i) {
+ my $ext = shift;
+ defined $ext or Error("Expecting extension for -ext option\n"), $badCmd=1, next;
+ my $flag = /^-/ ? 0 : ($2 ? 2 : 1);
+ $filterFlag |= (0x01 << $flag);
+ $ext =~ s/^\.//; # remove leading '.' if it exists
+ $filterExt{uc($ext)} = $flag ? 1 : 0;
+ next;
+ }
+ if (/^f$/ or $a eq 'forceprint') {
+ $forcePrint = 1;
+ next;
+ }
+ if (/^F([-+]?\d*)$/ or /^fixbase([-+]?\d*)$/i) {
+ $mt->Options(FixBase => $1);
+ next;
+ }
+ if (/^fast(\d*)$/i) {
+ $mt->Options(FastScan => (length $1 ? $1 : 1));
+ next;
+ }
+ if (/^fileorder(\d*)$/i) {
+ push @fileOrder, shift if @ARGV;
+ my $num = $1 || 0;
+ $fileOrderFast = $num if not defined $fileOrderFast or $fileOrderFast > $num;
+ next;
+ }
+ $a eq 'globaltimeshift' and $mt->Options(GlobalTimeShift => shift), next;
+ if (/^(g)(roupHeadings|roupNames)?([\d:]*)$/i) {
+ $showGroup = $3 || 0;
+ $allGroup = ($2 ? lc($2) eq 'roupnames' : $1 eq 'G');
+ $mt->Options(SavePath => 1) if $showGroup =~ /\b5\b/;
+ $mt->Options(SaveFormat => 1) if $showGroup =~ /\b6\b/;
+ next;
+ }
+ if ($a eq 'geotag') {
+ my $trkfile = shift;
+ unless ($pass) {
+ # defer to next pass so the filename charset is available
+ push @nextPass, '-geotag', $trkfile;
+ next;
+ }
+ $trkfile or Error("Expecting file name for -geotag option\n"), $badCmd=1, next;
+ # allow wildcards in filename
+ if ($trkfile =~ /[*?]/) {
+ # CORE::glob() splits on white space, so use File::Glob if possible
+ my @trks;
+ if ($^O eq 'MSWin32' and eval { require Win32::FindFile }) {
+ # ("-charset filename=UTF8" must be set for this to work with Unicode file names)
+ @trks = FindFileWindows($mt, $trkfile);
+ } elsif (eval { require File::Glob }) {
+ @trks = File::Glob::bsd_glob($trkfile);
+ } else {
+ @trks = glob($trkfile);
+ }
+ @trks or Error("No matching file found for -geotag option\n"), $badCmd=1, next;
+ push @newValues, 'geotag='.shift(@trks) while @trks > 1;
+ $trkfile = pop(@trks);
+ }
+ $_ = "geotag=$trkfile";
+ # (fall through!)
+ }
+ if (/^h$/ or $a eq 'htmlformat') {
+ require Image::ExifTool::HTML;
+ $html = $escapeHTML = 1;
+ $json = $xml = 0;
+ next;
+ }
+ (/^H$/ or $a eq 'hex') and $showTagID = 'H', next;
+ if (/^htmldump([-+]?\d+)?$/i) {
+ $verbose = ($verbose || 0) + 1;
+ $html = 2;
+ $mt->Options(HtmlDumpBase => $1) if defined $1;
+ next;
+ }
+ if (/^i(gnore)?$/i) {
+ my $dir = shift;
+ defined $dir or Error("Expecting directory name for -i option\n"), $badCmd=1, next;
+ $ignore{$dir} = 1;
+ next;
+ }
+ if (/^if(\d*)$/i) {
+ my $cond = shift;
+ $fastCondition = $1 if length $1;
+ defined $cond or Error("Expecting expression for -if option\n"), $badCmd=1, next;
+ # prevent processing file unnecessarily for simple case of failed '$ok' or 'not $ok'
+ $cond =~ /^\s*(not\s*)\$ok\s*$/i and ($1 xor $rtnValPrev) and $failCondition=1;
+ # add to list of requested tags
+ push @requestTags, $cond =~ /\$\{?((?:[-\w]+:)*[-\w?*]+)/g;
+ push @condition, $cond;
+ next;
+ }
+ if (/^j(son)?(\+?=.*)?$/i) {
+ if ($2) {
+ # must process on 2nd pass because we need -f and -charset options
+ unless ($pass) {
+ push @nextPass, "-$_";
+ push @newValues, { SaveCount => ++$saveCount }; # marker to save new values now
+ $csvSaveCount = $saveCount;
+ next;
+ }
+ my $jsonFile = $2;
+ $jsonFile =~ s/^(\+?=)//;
+ $csvAdd = 2 if $1 eq '+=';
+ $vout = \*STDERR if $srcStdin;
+ $verbose and print $vout "Reading JSON file $jsonFile\n";
+ my $chset = $mt->Options('Charset');
+ my $msg;
+ if ($mt->Open(\*JSONFILE, $jsonFile)) {
+ binmode JSONFILE;
+ require Image::ExifTool::Import;
+ $msg = Image::ExifTool::Import::ReadJSON(\*JSONFILE, \%database, $forcePrint, $chset);
+ close(JSONFILE);
+ } else {
+ $msg = "Error opening JSON file '${jsonFile}'";
+ }
+ $msg and Warn("$msg\n");
+ $isWriting = 1;
+ $csv = 'JSON';
+ } else {
+ $json = 1;
+ $html = $xml = 0;
+ $mt->Options(Duplicates => 1);
+ require Image::ExifTool::XMP; # for FixUTF8()
+ }
+ next;
+ }
+ /^(k|pause)$/i and $pause = 1, next;
+ (/^l$/ or $a eq 'long') and --$outFormat, next;
+ (/^L$/ or $a eq 'latin') and $mt->Options(Charset => 'Latin'), next;
+ if ($a eq 'lang') {
+ $langOpt = (@ARGV and $ARGV[0] !~ /^(-|\xe2\x88\x92)/) ? shift : undef;
+ if ($langOpt) {
+ # make lower case and use underline as a separator (eg. 'en_ca')
+ $langOpt =~ tr/-A-Z/_a-z/;
+ $mt->Options(Lang => $langOpt);
+ next if $langOpt eq $mt->Options('Lang');
+ } else {
+ $pass or push(@nextPass, '-lang'), next;
+ }
+ my $langs = "Available languages:\n";
+ $langs .= " $_ - $Image::ExifTool::langName{$_}\n" foreach @Image::ExifTool::langs;
+ $langs =~ tr/_/-/; # display dashes instead of underlines in language codes
+ $langs = Image::ExifTool::HTML::EscapeHTML($langs) if $escapeHTML;
+ $langs = $mt->Decode($langs, 'UTF8');
+ $langOpt and Error("Invalid or unsupported language '${langOpt}'.\n$langs"), $badCmd=1, next;
+ print $langs;
+ $helped = 1;
+ next;
+ }
+ if ($a eq 'listitem') {
+ my $li = shift;
+ defined $li and Image::ExifTool::IsInt($li) or Warn("Expecting integer for -listItem option\n"), next;
+ $mt->Options(ListItem => $li);
+ $listItem = $li;
+ next;
+ }
+ /^(m|ignoreminorerrors)$/i and $mt->Options(IgnoreMinorErrors => 1), next;
+ /^(n|-printconv)$/i and $mt->Options(PrintConv => 0), next;
+ /^(-n|printconv)$/i and $mt->Options(PrintConv => 1), next;
+ $a eq 'nop' and $helped=1, next; # (undocumented) no operation, added in 11.25
+ if (/^o(ut)?$/i) {
+ $outOpt = shift;
+ defined $outOpt or Error("Expected output file or directory name for -o option\n"), $badCmd=1, next;
+ CleanFilename($outOpt);
+ # verbose messages go to STDERR of output is to console
+ $vout = \*STDERR if $vout =~ /^-(\.\w+)?$/;
+ next;
+ }
+ /^overwrite_original$/i and $overwriteOrig = 1, next;
+ /^overwrite_original_in_place$/i and $overwriteOrig = 2, next;
+ if (/^p$/ or $a eq 'printformat') {
+ my $fmt = shift;
+ if ($pass) {
+ LoadPrintFormat($fmt);
+ # load MWG module now if necessary
+ if (not $useMWG and grep /^mwg:/i, @requestTags) {
+ $useMWG = 1;
+ require Image::ExifTool::MWG;
+ Image::ExifTool::MWG::Load();
+ }
+ } else {
+ # defer to next pass so the filename charset is available
+ push @nextPass, '-p', $fmt;
+ }
+ next;
+ }
+ (/^P$/ or $a eq 'preserve') and $preserveTime = 1, next;
+ /^password$/i and $mt->Options(Password => shift), next;
+ if (/^progress(:.*)?$/i) {
+ if ($1) {
+ $windowTitle = substr $1, 1;
+ $windowTitle = 'ExifTool %p%%' unless length $windowTitle;
+ $windowTitle =~ /%\d*[bpr]/ and $progress = 0 unless defined $progress;
+ } else {
+ $progress = 1;
+ $verbose = 0 unless defined $verbose;
+ }
+ $progressCount = 0;
+ next;
+ }
+ /^q(uiet)?$/i and ++$quiet, next;
+ /^r(ecurse)?(\.?)$/i and $recurse = ($2 ? 2 : 1), next;
+ if ($a eq 'require') { # (undocumented) added in version 8.65
+ my $ver = shift;
+ unless (defined $ver and Image::ExifTool::IsFloat($ver)) {
+ Error("Expecting version number for -require option\n");
+ $badCmd = 1;
+ next;
+ }
+ unless ($Image::ExifTool::VERSION >= $ver) {
+ Error("Requires ExifTool version $ver or later\n");
+ $badCmd = 1;
+ }
+ next;
+ }
+ /^restore_original$/i and $deleteOrig = 0, next;
+ (/^S$/ or $a eq 'veryshort') and $outFormat+=2, next;
+ /^s(hort)?(\d*)$/i and $outFormat = $2 eq '' ? $outFormat + 1 : $2, next;
+ /^scanforxmp$/i and $mt->Options(ScanForXMP => 1), next;
+ if (/^sep(arator)?$/i) {
+ my $sep = $listSep = shift;
+ defined $listSep or Error("Expecting list item separator for -sep option\n"), $badCmd=1, next;
+ $sep =~ s/\\(.)/$unescapeChar{$1}||$1/sge; # translate escape sequences
+ (defined $binSep ? $binTerm : $binSep) = $sep;
+ $mt->Options(ListSep => $listSep);
+ $joinLists = 1;
+ # also split when writing values
+ my $listSplit = quotemeta $listSep;
+ # a space in the string matches zero or more whitespace characters
+ $listSplit =~ s/(\\ )+/\\s\*/g;
+ # but a single space alone matches one or more whitespace characters
+ $listSplit = '\\s+' if $listSplit eq '\\s*';
+ $mt->Options(ListSplit => $listSplit);
+ next;
+ }
+ /^(-)?sort$/i and $sortOpt = $1 ? 0 : 1, next;
+ if ($a eq 'srcfile') {
+ @ARGV or Warn("Expecting FMT for -srcfile option\n"), next;
+ push @srcFmt, shift;
+ next;
+ }
+ if ($a eq 'stay_open') {
+ my $arg = shift;
+ defined $arg or Warn("Expecting argument for -stay_open option\n"), next;
+ if ($arg =~ /^(1|true)$/i) {
+ if (not $stayOpen) {
+ $stayOpen = 1;
+ } elsif ($stayOpen == 2) {
+ $stayOpen = 3; # chained -stay_open options
+ } else {
+ Warn "-stay_open already active\n";
+ }
+ } elsif ($arg =~ /^(0|false)$/i) {
+ if ($stayOpen >= 2) {
+ # close -stay_open argfile and process arguments up to this point
+ close STAYOPEN;
+ push @ARGV, @moreArgs;
+ undef @moreArgs;
+ } elsif (not $stayOpen) {
+ Warn("-stay_open wasn't active\n");
+ }
+ $stayOpen = 0;
+ } else {
+ Warn "Invalid argument for -stay_open\n";
+ }
+ next;
+ }
+ if (/^(-)?struct$/i) {
+ $mt->Options(Struct => $1 ? 0 : 1);
+ next;
+ }
+ /^t(ab)?$/ and $tabFormat = 1, next;
+ if (/^T$/ or $a eq 'table') {
+ $tabFormat = $forcePrint = 1; $outFormat+=2; ++$quiet;
+ next;
+ }
+ if (/^(u)(nknown(2)?)?$/i) {
+ my $inc = ($3 or (not $2 and $1 eq 'U')) ? 2 : 1;
+ $mt->Options(Unknown => $mt->Options('Unknown') + $inc);
+ next;
+ }
+ if ($a eq 'use') {
+ my $module = shift;
+ $module or Error("Expecting module name for -use option\n"), $badCmd=1, next;
+ lc $module eq 'mwg' and $useMWG = 1, next;
+ $module =~ /[^\w:]/ and Error("Invalid module name: $module\n"), $badCmd=1, next;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ unless (eval "require Image::ExifTool::$module" or
+ eval "require $module" or
+ eval "require '${module}'")
+ {
+ Error("Error using module $module\n");
+ $badCmd = 1;
+ }
+ next;
+ }
+ if ($a eq 'userparam') {
+ my $opt = shift;
+ defined $opt or Error("Expected parameter for -userParam option\n"), $badCmd=1, next;
+ $opt =~ /=/ or $opt .= '=1';
+ $mt->Options(UserParam => $opt);
+ next;
+ }
+ if (/^v(erbose)?(\d*)$/i) {
+ $verbose = ($2 eq '') ? ($verbose || 0) + 1 : $2;
+ next;
+ }
+ if (/^(w|textout|tagout)([!+]*)$/i) {
+ $textOut = shift || Warn("Expecting output extension for -$_ option\n");
+ my ($t1, $t2) = ($1, $2);
+ $textOverwrite = 0;
+ $textOverwrite += 1 if $t2 =~ /!/; # overwrite
+ $textOverwrite += 2 if $t2 =~ /\+/; # append
+ if ($t1 ne 'W' and lc($t1) ne 'tagout') {
+ undef $tagOut;
+ } elsif ($textOverwrite >= 2 and $textOut !~ /%[-+]?\d*[.:]?\d*[lu]?[tgs]/) {
+ $tagOut = 0; # append tags to one file
+ } else {
+ $tagOut = 1; # separate file for each tag
+ }
+ next;
+ }
+ if (/^(-?)(wext|tagoutext)$/i) {
+ my $ext = shift;
+ defined $ext or Error("Expecting extension for -wext option\n"), $badCmd=1, next;
+ my $flag = 1;
+ $1 and $wext{'*'} = 1, $flag = -1;
+ $ext =~ s/^\.//;
+ $wext{lc $ext} = $flag;
+ next;
+ }
+ if ($a eq 'wm' or $a eq 'writemode') {
+ my $wm = shift;
+ defined $wm or Error("Expecting argument for -$_ option\n"), $badCmd=1, next;
+ $wm =~ /^[wcg]*$/i or Error("Invalid argument for -$_ option\n"), $badCmd=1, next;
+ $mt->Options(WriteMode => $wm);
+ next;
+ }
+ if (/^x$/ or $a eq 'exclude') {
+ my $tag = shift;
+ defined $tag or Error("Expecting tag name for -x option\n"), $badCmd=1, next;
+ $tag =~ s/\ball\b/\*/ig; # replace 'all' with '*' in tag names
+ if ($setTagsFile) {
+ push @{$setTags{$setTagsFile}}, "-$tag";
+ } else {
+ push @exclude, $tag;
+ }
+ next;
+ }
+ (/^X$/ or $a eq 'xmlformat') and $xml = 1, $html = $json = 0, $mt->Options(Duplicates => 1), next;
+ if (/^php$/i) {
+ $json = 2;
+ $html = $xml = 0;
+ $mt->Options(Duplicates=>1);
+ next;
+ }
+ if (/^z(ip)?$/i) {
+ $doUnzip = 1;
+ $mt->Options(Compress => 1, XMPShorthand => 1);
+ $mt->Options(Compact => 1) unless $mt->Options('Compact');
+ next;
+ }
+ $_ eq '' and push(@files, '-'), $srcStdin = 1, next; # read STDIN
+ length $_ eq 1 and $_ ne '*' and Error("Unknown option -$_\n"), $badCmd=1, next;
+ if (/^[^<]+(<?)=(.*)/s) {
+ my $val = $2;
+ if ($1 and length($val) and ($val eq '@' or not defined FilenameSPrintf($val))) {
+ # save count of new values before a dynamic value
+ push @newValues, { SaveCount => ++$saveCount };
+ }
+ push @newValues, $_;
+ if (/^mwg:/i) {
+ $useMWG = 1;
+ } elsif (/^([-\w]+:)*(filename|directory|testname)\b/i) {
+ $doSetFileName = 1;
+ } elsif (/^([-\w]+:)*(geotag|geotime|geosync)\b/i) {
+ if (lc $2 eq 'geotime') {
+ $addGeotime = '';
+ } else {
+ # add geotag/geosync commands first
+ unshift @newValues, pop @newValues;
+ if (lc $2 eq 'geotag' and (not defined $addGeotime or $addGeotime) and length $val) {
+ $addGeotime = ($1 || '') . 'Geotime<DateTimeOriginal#';
+ }
+ }
+ }
+ } else {
+ # assume '-tagsFromFile @' if tags are being redirected
+ # and -tagsFromFile hasn't already been specified
+ AddSetTagsFile($setTagsFile = '@') if not $setTagsFile and /(<|>)/;
+ if ($setTagsFile) {
+ push @{$setTags{$setTagsFile}}, $_;
+ if (/>/) {
+ $useMWG = 1 if /^(.*>\s*)?mwg:/si;
+ if (/\b(filename|directory|testname)#?$/i) {
+ $doSetFileName = 1;
+ } elsif (/\bgeotime#?$/i) {
+ $addGeotime = '';
+ }
+ } else {
+ $useMWG = 1 if /^([^<]+<\s*(.*\$\{?)?)?mwg:/si;
+ if (/^([-\w]+:)*(filename|directory|testname)\b/i) {
+ $doSetFileName = 1;
+ } elsif (/^([-\w]+:)*geotime\b/i) {
+ $addGeotime = '';
+ }
+ }
+ } else {
+ my $lst = s/^-// ? \@exclude : \@tags;
+ unless (/^([-\w*]+:)*([-\w*?]+)#?$/) {
+ Warn(qq(Invalid TAG name: "$_"\n));
+ }
+ push @$lst, $_; # (push everything for backward compatibility)
+ }
+ }
+ } else {
+ unless ($pass) {
+ # defer to next pass so the filename charset is available
+ push @nextPass, $_;
+ next;
+ }
+ if ($doGlob and /[*?]/) {
+ if ($^O eq 'MSWin32' and eval { require Win32::FindFile }) {
+ push @files, FindFileWindows($mt, $_);
+ } else {
+ # glob each filespec if necessary - MK/20061010
+ push @files, File::Glob::bsd_glob($_);
+ }
+ $doGlob = 2;
+ } else {
+ push @files, $_;
+ $srcStdin = 1 if $_ eq '-';
+ }
+ }
+}
+
+# set "OK" UserParam based on result of last command
+$mt->Options(UserParam => 'OK=' . (not $rtnValPrev));
+
+# set verbose output to STDERR if output could be to console
+$vout = \*STDERR if $srcStdin and ($isWriting or @newValues);
+$mt->Options(TextOut => $vout) if $vout eq \*STDERR;
+
+# change default EXIF string encoding if MWG used
+if ($useMWG and not defined $mt->Options('CharsetEXIF')) {
+ $mt->Options(CharsetEXIF => 'UTF8');
+}
+
+# print help
+unless ((@tags and not $outOpt) or @files or @newValues) {
+ if ($doGlob and $doGlob == 2) {
+ Warn "No matching files\n";
+ $rtnVal = 1;
+ next;
+ }
+ if ($outOpt) {
+ Warn "Nothing to write\n";
+ $rtnVal = 1;
+ next;
+ }
+ unless ($helped) {
+ # catch warnings if we have problems running perldoc
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ my $dummy = \*SAVEERR; # avoid "used only once" warning
+ unless ($^O eq 'os2') {
+ open SAVEERR, ">&STDERR";
+ open STDERR, '>/dev/null';
+ }
+ if (system('perldoc',$0)) {
+ print "Syntax: exiftool [OPTIONS] FILE\n\n";
+ print "Consult the exiftool documentation for a full list of options.\n";
+ }
+ unless ($^O eq 'os2') {
+ close STDERR;
+ open STDERR, '>&SAVEERR';
+ }
+ }
+ next;
+}
+
+# do sanity check on -delete_original and -restore_original
+if (defined $deleteOrig and (@newValues or @tags)) {
+ if (not @newValues) {
+ my $verb = $deleteOrig ? 'deleting' : 'restoring from';
+ Warn "Can't specify tags when $verb originals\n";
+ } elsif ($deleteOrig) {
+ Warn "Can't use -delete_original when writing.\n";
+ Warn "Maybe you meant -overwrite_original ?\n";
+ } else {
+ Warn "It makes no sense to use -restore_original when writing\n";
+ }
+ $rtnVal = 1;
+ next;
+}
+
+if ($overwriteOrig > 1 and $outOpt) {
+ Warn "Can't overwrite in place when -o option is used\n";
+ $rtnVal = 1;
+ next;
+}
+
+if ($tagOut and ($csv or %printFmt or $tabFormat or $xml or ($verbose and $html))) {
+ Warn "Sorry, -W may not be combined with -csv, -htmlDump, -j, -p, -t or -X\n";
+ $rtnVal = 1;
+ next;
+}
+
+if ($csv and $csv eq 'CSV' and not $isWriting) {
+ if ($textOut) {
+ Warn "Sorry, -w may not be combined with -csv\n";
+ $rtnVal = 1;
+ next;
+ }
+ if ($binaryOutput) {
+ $binaryOutput = 0;
+ $setCharset = 'default' unless defined $setCharset;
+ }
+ require Image::ExifTool::XMP if $setCharset;
+}
+
+if ($escapeHTML or $json) {
+ # must be UTF8 for HTML conversion and JSON output
+ $mt->Options(Charset => 'UTF8') if $json;
+ # use Escape option to do our HTML escaping unless XML output
+ $mt->Options(Escape => 'HTML') if $escapeHTML and not $xml;
+} elsif ($escapeXML and not $xml) {
+ $mt->Options(Escape => 'XML');
+}
+
+# set sort option
+if ($sortOpt) {
+ # (note that -csv sorts alphabetically by default anyway if more than 1 file)
+ my $sort = ($outFormat > 0 or $xml or $json or $csv) ? 'Tag' : 'Descr';
+ $mt->Options(Sort => $sort, Sort2 => $sort);
+}
+
+# set $structOpt in case set by API option
+if ($mt->Options('Struct') and not $structOpt) {
+ $structOpt = $mt->Options('Struct');
+ require 'Image/ExifTool/XMPStruct.pl';
+}
+
+# set up for RDF/XML, JSON and PHP output formats
+if ($xml) {
+ require Image::ExifTool::XMP; # for EscapeXML()
+ my $charset = $mt->Options('Charset');
+ # standard XML encoding names for supported Charset settings
+ # (ref http://www.iana.org/assignments/character-sets)
+ my %encoding = (
+ UTF8 => 'UTF-8',
+ Latin => 'windows-1252',
+ Latin2 => 'windows-1250',
+ Cyrillic => 'windows-1251',
+ Greek => 'windows-1253',
+ Turkish => 'windows-1254',
+ Hebrew => 'windows-1255',
+ Arabic => 'windows-1256',
+ Baltic => 'windows-1257',
+ Vietnam => 'windows-1258',
+ MacRoman => 'macintosh',
+ );
+ # switch to UTF-8 if we don't have a standard encoding name
+ unless ($encoding{$charset}) {
+ $charset = 'UTF8';
+ $mt->Options(Charset => $charset);
+ }
+ # set file header/trailer for XML output
+ $fileHeader = "<?xml version='1.0' encoding='$encoding{$charset}'?>\n" .
+ "<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n";
+ $fileTrailer = "</rdf:RDF>\n";
+ # extract as a list unless short output format
+ $joinLists = 1 if $outFormat > 0;
+ $mt->Options(List => 1) unless $joinLists;
+ $showGroup = $allGroup = 1; # always show group 1
+ # set binaryOutput flag to 0 or undef (0 = output encoded binary in XML)
+ $binaryOutput = ($outFormat > 0 ? undef : 0) if $binaryOutput;
+ $showTagID = 'D' if $tabFormat and not $showTagID;
+} elsif ($json) {
+ if ($json == 1) { # JSON
+ $fileHeader = '[';
+ $fileTrailer = "]\n";
+ } else { # PHP
+ $fileHeader = 'Array(';
+ $fileTrailer = ");\n";
+ }
+ # allow binary output in a text-mode file when -php/-json and -b used together
+ # (this works because PHP strings are simple arrays of bytes, and CR/LF
+ # won't be messed up in the text mode output because they are converted
+ # to escape sequences in the strings)
+ if ($binaryOutput) {
+ $binaryOutput = 0;
+ require Image::ExifTool::XMP if $json == 1; # (for EncodeBase64)
+ }
+ $mt->Options(List => 1) unless $joinLists;
+ $mt->Options(Duplicates => 0) unless defined $showGroup;
+ $showTagID = 'D' if $tabFormat and not $showTagID;
+} elsif ($structOpt) {
+ $mt->Options(List => 1);
+} else {
+ $joinLists = 1; # join lists for all other unstructured output formats
+}
+
+if ($argFormat) {
+ $outFormat = 3;
+ $allGroup = 1 if defined $showGroup;
+}
+
+# change to forward slashes if necessary in all filenames (like CleanFilename)
+if ($hasBackslash{$^O}) {
+ tr/\\/\// foreach @files;
+}
+
+# can't do anything if no file specified
+unless (@files) {
+ unless ($outOpt) {
+ if ($doGlob and $doGlob == 2) {
+ Warn "No matching files\n";
+ } else {
+ Warn "No file specified\n";
+ }
+ $rtnVal = 1;
+ next;
+ }
+ push @files, ''; # create file from nothing
+}
+
+# set Verbose and HtmlDump options
+if ($verbose) {
+ $disableOutput = 1 unless @tags or @exclude or $tagOut;
+ undef $binaryOutput unless $tagOut; # disable conflicting option
+ if ($html) {
+ $html = 2; # flag for html dump
+ $mt->Options(HtmlDump => $verbose);
+ } else {
+ $mt->Options(Verbose => $verbose) unless $tagOut;
+ }
+} elsif (defined $verbose) {
+ # auto-flush output when -v0 is used
+ require FileHandle;
+ STDOUT->autoflush(1);
+ STDERR->autoflush(1);
+}
+
+# validate all tags we're writing
+my $needSave = 1;
+if (@newValues) {
+ # assume -geotime value if -geotag specified without -geotime
+ if ($addGeotime) {
+ AddSetTagsFile($setTagsFile = '@') unless $setTagsFile and $setTagsFile eq '@';
+ push @{$setTags{$setTagsFile}}, $addGeotime;
+ $verbose and print $vout qq{Argument "-$addGeotime" is assumed\n};
+ }
+ my %setTagsIndex;
+ # add/delete option lookup
+ my %addDelOpt = ( '+' => 'AddValue', '-' => 'DelValue', "\xe2\x88\x92" => 'DelValue' );
+ $saveCount = 0;
+ foreach (@newValues) {
+ if (ref $_ eq 'HASH') {
+ # save new values now if we stored a "SaveCount" marker
+ if ($$_{SaveCount}) {
+ $saveCount = $mt->SaveNewValues();
+ $needSave = 0;
+ # insert marker to load values from CSV file now if this was the CSV file
+ push @dynamicFiles, \$csv if $$_{SaveCount} == $csvSaveCount;
+ }
+ next;
+ }
+ /(.*?)=(.*)/s or next;
+ my ($tag, $newVal) = ($1, $2);
+ $tag =~ s/\ball\b/\*/ig; # replace 'all' with '*' in tag names
+ $newVal eq '' and undef $newVal unless $tag =~ s/\^([-+]*)$/$1/; # undefined to delete tag
+ if ($tag =~ /^(All)?TagsFromFile$/i) {
+ defined $newVal or Error("Need file name for -tagsFromFile\n"), next Command;
+ ++$isWriting;
+ if ($newVal eq '@' or not defined FilenameSPrintf($newVal)) {
+ push @dynamicFiles, $newVal;
+ next; # set tags from dynamic file later
+ }
+ unless ($mt->Exists($newVal) or $newVal eq '-') {
+ Warn "File '${newVal}' does not exist for -tagsFromFile option\n";
+ $rtnVal = 1;
+ next Command;
+ }
+ my $setTags = $setTags{$newVal};
+ # do we have multiple -tagsFromFile options with this file?
+ if ($setTagsList{$newVal}) {
+ # use the tags set in the i-th occurrence
+ my $i = $setTagsIndex{$newVal} || 0;
+ $setTagsIndex{$newVal} = $i + 1;
+ $setTags = $setTagsList{$newVal}[$i] if $setTagsList{$newVal}[$i];
+ }
+ # set specified tags from this file
+ unless (DoSetFromFile($mt, $newVal, $setTags)) {
+ $rtnVal = 1;
+ next Command;
+ }
+ $needSave = 1;
+ next;
+ }
+ my %opts = ( Shift => 0 ); # shift values if possible instead of adding/deleting
+ # allow writing of 'unsafe' tags unless specified by wildcard
+ $opts{Protected} = 1 unless $tag =~ /[?*]/;
+
+ if ($tag =~ s/<// and defined $newVal) {
+ if (defined FilenameSPrintf($newVal)) {
+ SlurpFile($newVal, \$newVal) or next; # read file data into $newVal
+ } else {
+ $tag =~ s/([-+]|\xe2\x88\x92)$// and $opts{$addDelOpt{$1}} = 1;
+ # verify that this tag can be written
+ my $result = Image::ExifTool::IsWritable($tag);
+ if ($result) {
+ $opts{ProtectSaved} = $saveCount; # protect new values set after this
+ # add to list of dynamic tag values
+ push @dynamicFiles, [ $tag, $newVal, \%opts ];
+ ++$isWriting;
+ } elsif (defined $result) {
+ Warn "Tag '${tag}' is not writable\n";
+ } else {
+ Warn "Tag '${tag}' does not exist\n";
+ }
+ next;
+ }
+ }
+ if ($tag =~ s/([-+]|\xe2\x88\x92)$//) {
+ $opts{$addDelOpt{$1}} = 1; # set AddValue or DelValue option
+ # set $newVal to '' if deleting nothing
+ $newVal = '' if $1 eq '-' and not defined $newVal;
+ }
+ if ($escapeC and defined $newVal) {
+ $newVal =~ s/\\(x([0-9a-fA-F]{2})|.)/$2 ? chr(hex($2)) : $unescC{$1} || $1/seg;
+ }
+ my ($rtn, $wrn) = $mt->SetNewValue($tag, $newVal, %opts);
+ $needSave = 1;
+ ++$isWriting if $rtn;
+ $wrn and Warn "Warning: $wrn\n";
+ }
+ # exclude specified tags
+ foreach (@exclude) {
+ $mt->SetNewValue($_, undef, Replace => 2);
+ $needSave = 1;
+ }
+ unless ($isWriting or $outOpt or @tags) {
+ Warn "Nothing to do.\n";
+ $rtnVal = 1;
+ next;
+ }
+} elsif (grep /^(\*:)?\*$/, @exclude) {
+ Warn "All tags excluded -- nothing to do.\n";
+ $rtnVal = 1;
+ next;
+}
+if ($isWriting and @tags and not $outOpt) {
+ my ($tg, $s) = @tags > 1 ? ("$tags[0] ...", 's') : ($tags[0], '');
+ Warn "Ignored superfluous tag name$s or invalid option$s: -$tg\n";
+}
+# save current state of new values if setting values from target file
+# or if we may be translating to a different format
+$mt->SaveNewValues() if $outOpt or (@dynamicFiles and $needSave);
+
+$multiFile = 1 if @files > 1;
+@exclude and $mt->Options(Exclude => \@exclude);
+
+undef $binaryOutput if $html;
+
+if ($binaryOutput) {
+ $outFormat = 99; # shortest possible output format
+ $mt->Options(PrintConv => 0);
+ unless ($textOut or $binaryStdout) {
+ binmode(STDOUT);
+ $binaryStdout = 1;
+ $mt->Options(TextOut => ($vout = \*STDERR));
+ }
+ # disable conflicting options
+ undef $showGroup;
+}
+
+# sort by groups to look nicer depending on options
+if (defined $showGroup and not (@tags and $allGroup) and ($sortOpt or not defined $sortOpt)) {
+ $mt->Options(Sort => "Group$showGroup");
+}
+
+if (defined $textOut) {
+ CleanFilename($textOut); # make all forward slashes
+ # add '.' before output extension if necessary
+ $textOut = ".$textOut" unless $textOut =~ /[.%]/ or defined $tagOut;
+}
+
+# determine if we should scan for only writable files
+if ($outOpt) {
+ my $type = GetFileType($outOpt);
+ if ($type) {
+ unless (CanWrite($type)) {
+ Warn "Can't write $type files\n";
+ $rtnVal = 1;
+ next;
+ }
+ $scanWritable = $type unless CanCreate($type);
+ } else {
+ $scanWritable = 1;
+ }
+ $isWriting = 1; # set writing flag
+} elsif ($isWriting or defined $deleteOrig) {
+ $scanWritable = 1;
+}
+
+# initialize alternate encoding flag
+$altEnc = $mt->Options('Charset');
+undef $altEnc if $altEnc eq 'UTF8';
+
+# set flag to fix description lengths if necessary
+if (not $altEnc and $mt->Options('Lang') ne 'en' and eval { require Encode }) {
+ # (note that Unicode::GCString is part of the Unicode::LineBreak package)
+ $fixLen = eval { require Unicode::GCString } ? 2 : 1;
+}
+
+# sort input files if specified
+if (@fileOrder) {
+ my @allFiles;
+ ProcessFiles($mt, \@allFiles);
+ my $sortTool = new Image::ExifTool;
+ $sortTool->Options(FastScan => $fileOrderFast) if $fileOrderFast;
+ $sortTool->Options(PrintConv => $mt->Options('PrintConv'));
+ $sortTool->Options(Duplicates => 0);
+ my (%sortBy, %isFloat, @rev, $file);
+ # save reverse sort flags
+ push @rev, (s/^-// ? 1 : 0) foreach @fileOrder;
+ foreach $file (@allFiles) {
+ my @tags;
+ my $info = $sortTool->ImageInfo(Infile($file,1), @fileOrder, \@tags);
+ # get values of all tags (or '~' to sort last if not defined)
+ foreach (@tags) {
+ $_ = $$info{$_}; # put tag value into @tag list
+ defined $_ or $_ = '~', next;
+ $isFloat{$_} = Image::ExifTool::IsFloat($_);
+ # pad numbers to 12 digits to keep them sequential
+ s/(\d+)/(length($1) < 12 ? '0'x(12-length($1)) : '') . $1/eg unless $isFloat{$_};
+ }
+ $sortBy{$file} = \@tags; # save tag values for each file
+ }
+ # sort in specified order
+ @files = sort {
+ my ($i, $cmp);
+ for ($i=0; $i<@rev; ++$i) {
+ my $u = $sortBy{$a}[$i];
+ my $v = $sortBy{$b}[$i];
+ if (not $isFloat{$u} and not $isFloat{$v}) {
+ $cmp = $u cmp $v; # alphabetically
+ } elsif ($isFloat{$u} and $isFloat{$v}) {
+ $cmp = $u <=> $v; # numerically
+ } else {
+ $cmp = $isFloat{$u} ? -1 : 1; # numbers first
+ }
+ return $rev[$i] ? -$cmp : $cmp if $cmp;
+ }
+ return $a cmp $b; # default to sort by name
+ } @allFiles;
+} elsif (defined $progress) {
+ # expand FILE argument to count the number of files to process
+ my @allFiles;
+ ProcessFiles($mt, \@allFiles);
+ @files = @allFiles;
+}
+# set file count for progress message
+$progressMax = scalar @files if defined $progress;
+
+# store duplicate database information under absolute path
+my @dbKeys = keys %database;
+if (@dbKeys) {
+ if (eval { require Cwd }) {
+ undef $evalWarning;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ foreach (@dbKeys) {
+ my $db = $database{$_};
+ tr/\\/\// and $database{$_} = $db; # allow for backslashes in SourceFile
+ # (punt on using ConvertFileName here, so $absPath may be a mix of encodings)
+ my $absPath = AbsPath($_);
+ if (defined $absPath) {
+ $database{$absPath} = $db unless $database{$absPath};
+ if ($verbose and $verbose > 1) {
+ print $vout "Imported entry for '${_}' (full path: '${absPath}')\n";
+ }
+ } elsif ($verbose and $verbose > 1) {
+ print $vout "Imported entry for '${_}' (non-existent file)\n";
+ }
+ }
+ }
+}
+
+# process all specified files
+ProcessFiles($mt);
+
+if ($filtered and not $validFile) {
+ Warn "No file with specified extension\n";
+ $rtnVal = 1;
+}
+
+# print CSV information if necessary
+PrintCSV() if $csv and not $isWriting;
+
+# print folder/file trailer if necessary
+print $sectTrailer if $sectTrailer and not $textOut;
+print $fileTrailer if $fileTrailer and not $textOut and not $fileHeader;
+
+my $totWr = $countGoodWr + $countBadWr + $countSameWr + $countCopyWr +
+ $countGoodCr + $countBadCr;
+
+if (defined $deleteOrig) {
+
+ # print summary and delete requested files
+ unless ($quiet) {
+ printf "%5d directories scanned\n", $countDir if $countDir;
+ printf "%5d directories created\n", $countNewDir if $countNewDir;
+ printf "%5d files failed condition\n", $countFailed if $countFailed;
+ printf "%5d image files found\n", $count;
+ }
+ if (@delFiles) {
+ # verify deletion unless "-delete_original!" was specified
+ if ($deleteOrig == 1) {
+ printf '%5d originals will be deleted! Are you sure [y/n]? ', scalar(@delFiles);
+ my $response = <STDIN>;
+ unless ($response =~ /^(y|yes)\s*$/i) {
+ Warn "Originals not deleted.\n";
+ next;
+ }
+ }
+ $countGoodWr = $mt->Unlink(@delFiles);
+ $countBad = scalar(@delFiles) - $countGoodWr;
+ }
+ if ($quiet) {
+ # no more messages
+ } elsif ($count and not $countGoodWr and not $countBad) {
+ printf "%5d original files found\n", $countGoodWr; # (this will be 0)
+ } elsif ($deleteOrig) {
+ printf "%5d original files deleted\n", $countGoodWr if $count;
+ printf "%5d originals not deleted due to errors\n", $countBad if $countBad;
+ } else {
+ printf "%5d image files restored from original\n", $countGoodWr if $count;
+ printf "%5d files not restored due to errors\n", $countBad if $countBad;
+ }
+
+} elsif ((not $binaryStdout or $verbose) and not $quiet) {
+
+ # print summary
+ my $tot = $count + $countBad;
+ if ($countDir or $totWr or $countFailed or $tot > 1 or $textOut or %countLink) {
+ my $o = (($html or $json or $xml or %printFmt or $csv) and not $textOut) ? \*STDERR : $vout;
+ printf($o "%5d directories scanned\n", $countDir) if $countDir;
+ printf($o "%5d directories created\n", $countNewDir) if $countNewDir;
+ printf($o "%5d files failed condition\n", $countFailed) if $countFailed;
+ printf($o "%5d image files created\n", $countGoodCr) if $countGoodCr;
+ printf($o "%5d image files updated\n", $countGoodWr) if $totWr - $countGoodCr - $countBadCr - $countCopyWr;
+ printf($o "%5d image files unchanged\n", $countSameWr) if $countSameWr;
+ printf($o "%5d image files %s\n", $countCopyWr, $overwriteOrig ? 'moved' : 'copied') if $countCopyWr;
+ printf($o "%5d files weren't updated due to errors\n", $countBadWr) if $countBadWr;
+ printf($o "%5d files weren't created due to errors\n", $countBadCr) if $countBadCr;
+ printf($o "%5d image files read\n", $count) if $tot>1 or ($countDir and not $totWr);
+ printf($o "%5d files could not be read\n", $countBad) if $countBad;
+ printf($o "%5d output files created\n", scalar(keys %created)) if $textOut;
+ printf($o "%5d output files appended\n", scalar(keys %appended)) if %appended;
+ printf($o "%5d hard links created\n", $countLink{Hard} || 0) if $countLink{Hard} or $countLink{BadHard};
+ printf($o "%5d hard links could not be created\n", $countLink{BadHard}) if $countLink{BadHard};
+ printf($o "%5d symbolic links created\n", $countLink{Sym} || 0) if $countLink{Sym} or $countLink{BadSym};
+ printf($o "%5d symbolic links could not be created\n", $countLink{BadSym}) if $countLink{BadSym};
+ }
+}
+
+# set error status if we had any errors or if all files failed the "-if" condition
+if ($countBadWr or $countBadCr or $countBad) {
+ $rtnVal = 1;
+} elsif ($countFailed and not ($count or $totWr) and not $rtnVal) {
+ $rtnVal = 2;
+}
+
+# clean up after each command
+Cleanup();
+
+} # end "Command" loop ........................................................
+
+close STAYOPEN if $stayOpen >= 2;
+
+Exit $rtnValApp; # all done
+
+
+#------------------------------------------------------------------------------
+# Get image information from EXIF data in file (or write file if writing)
+# Inputs: 0) ExifTool object reference, 1) file name
+sub GetImageInfo($$)
+{
+ my ($et, $orig) = @_;
+ my (@foundTags, $info, $file, $ind);
+
+ # set window title for this file if necessary
+ if (defined $windowTitle) {
+ my $prog = $progressMax ? "$progressCount/$progressMax" : '0/0';
+ my $title = $windowTitle;
+ my ($num, $denom) = split '/', $prog;
+ my $frac = $num / ($denom || 1);
+ my $n = $title =~ s/%(\d+)b/%b/ ? $1 : 20; # length of bar
+ my $bar = int($frac * $n + 0.5);
+ my %lkup = (
+ b => ('I' x $bar) . ('.' x ($n - $bar)), # (undocumented)
+ f => $orig,
+ p => int(100 * $frac + 0.5),
+ r => $prog,
+ '%'=> '%',
+ );
+ $title =~ s/%([%bfpr])/$lkup{$1}/eg;
+ SetWindowTitle($title);
+ }
+ unless (length $orig or $outOpt) {
+ Warn qq(Error: Zero-length file name - ""\n);
+ ++$countBad;
+ return;
+ }
+ # determine the name of the source file based on the original input file name
+ if (@srcFmt) {
+ my ($fmt, $first);
+ foreach $fmt (@srcFmt) {
+ $file = $fmt eq '@' ? $orig : FilenameSPrintf($fmt, $orig);
+ # use this file if it exists
+ $et->Exists($file) and undef($first), last;
+ $verbose and print $vout "Source file $file does not exist\n";
+ $first = $file unless defined $first;
+ }
+ $file = $first if defined $first;
+ my ($d, $f) = Image::ExifTool::SplitFileName($orig);
+ $et->Options(UserParam => "OriginalDirectory#=$d");
+ $et->Options(UserParam => "OriginalFileName#=$f");
+ } else {
+ $file = $orig;
+ }
+
+ my $pipe = $file;
+ if ($doUnzip) {
+ # pipe through gzip or bzip2 if necessary
+ if ($file =~ /\.gz$/i) {
+ $pipe = qq{gzip -dc "$file" |};
+ } elsif ($file =~ /\.bz2$/i) {
+ $pipe = qq{bzip2 -dc "$file" |};
+ }
+ }
+ # evaluate -if expression for conditional processing
+ if (@condition) {
+ unless ($file eq '-' or $et->Exists($file)) {
+ Warn "Error: File not found - $file\n";
+ EFile($file);
+ FileNotFound($file);
+ ++$countBad;
+ return;
+ }
+ my $result;
+
+ unless ($failCondition) {
+ # catch run time errors as well as compile errors
+ undef $evalWarning;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+
+ my (%info, $condition);
+ # extract information and build expression for evaluation
+ my $opts = { Duplicates => 1, RequestTags => \@requestTags, Verbose => 0, HtmlDump => 0 };
+ $$opts{FastScan} = $fastCondition if defined $fastCondition;
+ # return all tags but explicitly mention tags on command line so
+ # requested images will generate the appropriate warnings
+ @foundTags = ('*', @tags) if @tags;
+ $info = $et->ImageInfo(Infile($pipe,$isWriting), \@foundTags, $opts);
+ foreach $condition (@condition) {
+ my $cond = $et->InsertTagValues(\@foundTags, $condition, \%info);
+ {
+ # set package so eval'd functions are in Image::ExifTool namespace
+ package Image::ExifTool;
+
+ my $self = $et;
+ #### eval "-if" condition (%info, $self)
+ $result = eval $cond;
+
+ $@ and $evalWarning = $@;
+ }
+ if ($evalWarning) {
+ # fail condition if warning is issued
+ undef $result;
+ if ($verbose) {
+ chomp $evalWarning;
+ $evalWarning =~ s/ at \(eval .*//s;
+ Warn "Condition: $evalWarning - $file\n";
+ }
+ }
+ last unless $result;
+ }
+ undef @foundTags if $fastCondition; # ignore if we didn't get all tags
+ }
+ unless ($result) {
+ $verbose and print $vout "-------- $file (failed condition)$progStr\n";
+ EFile($file, 2);
+ ++$countFailed;
+ return;
+ }
+ # can't make use of $info if verbose because we must reprocess
+ # the file anyway to generate the verbose output
+ undef $info if $verbose or defined $fastCondition;
+ }
+ if (defined $deleteOrig) {
+ print $vout "======== $file$progStr\n" if defined $verbose;
+ ++$count;
+ my $original = "${file}_original";
+ $et->Exists($original) or return;
+ if ($deleteOrig) {
+ $verbose and print $vout "Scheduled for deletion: $original\n";
+ push @delFiles, $original;
+ } elsif ($et->Rename($original, $file)) {
+ $verbose and print $vout "Restored from $original\n";
+ ++$countGoodWr;
+ } else {
+ Warn "Error renaming $original\n";
+ EFile($file);
+ ++$countBad;
+ }
+ return;
+ }
+ ++$seqFileNum; # increment our file counter
+
+ my $lineCount = 0;
+ my ($fp, $outfile, $append);
+ if ($textOut and $verbose and not $tagOut) {
+ ($fp, $outfile, $append) = OpenOutputFile($orig);
+ $fp or EFile($file), ++$countBad, return;
+ # delete file if we exit prematurely (unless appending)
+ $tmpText = $outfile unless $append;
+ $et->Options(TextOut => $fp);
+ }
+
+ if ($isWriting) {
+ print $vout "======== $file$progStr\n" if defined $verbose;
+ SetImageInfo($et, $file, $orig);
+ $info = $et->GetInfo('Warning', 'Error');
+ PrintErrors($et, $info, $file);
+ # close output text file if necessary
+ if ($outfile) {
+ undef $tmpText;
+ close($fp);
+ $et->Options(TextOut => $vout);
+ if ($info->{Error}) {
+ $et->Unlink($outfile); # erase bad file
+ } elsif ($append) {
+ $appended{$outfile} = 1 unless $created{$outfile};
+ } else {
+ $created{$outfile} = 1;
+ }
+ }
+ return;
+ }
+
+ # extract information from this file
+ unless ($file eq '-' or $et->Exists($file)) {
+ Warn "Error: File not found - $file\n";
+ FileNotFound($file);
+ $outfile and close($fp), undef($tmpText), $et->Unlink($outfile);
+ EFile($file);
+ ++$countBad;
+ return;
+ }
+ # print file/progress message
+ my $o;
+ unless ($binaryOutput or $textOut or %printFmt or $html > 1 or $csv) {
+ if ($html) {
+ require Image::ExifTool::HTML;
+ my $f = Image::ExifTool::HTML::EscapeHTML($file);
+ print "<!-- $f -->\n";
+ } elsif (not ($json or $xml)) {
+ $o = \*STDOUT if ($multiFile and not $quiet) or $progress;
+ }
+ }
+ $o = \*STDERR if $progress and not $o;
+ $o and print $o "======== $file$progStr\n";
+ if ($info) {
+ # get the information we wanted
+ if (@tags and not %printFmt) {
+ @foundTags = @tags;
+ $info = $et->GetInfo(\@foundTags);
+ }
+ } else {
+ # request specified tags unless using print format option
+ my $oldDups = $et->Options('Duplicates');
+ if (%printFmt) {
+ $et->Options(Duplicates => 1);
+ $et->Options(RequestTags => \@requestTags);
+ } else {
+ @foundTags = @tags;
+ }
+ # extract the information
+ $info = $et->ImageInfo(Infile($pipe), \@foundTags);
+ $et->Options(Duplicates => $oldDups);
+ }
+ # all done now if we already wrote output text file (eg. verbose option)
+ if ($fp) {
+ if ($outfile) {
+ $et->Options(TextOut => \*STDOUT);
+ undef $tmpText;
+ if ($info->{Error}) {
+ close($fp);
+ $et->Unlink($outfile); # erase bad file
+ } else {
+ ++$lineCount; # output text file (likely) is not empty
+ }
+ }
+ if ($info->{Error}) {
+ Warn "Error: $info->{Error} - $file\n";
+ EFile($file);
+ ++$countBad;
+ return;
+ }
+ }
+
+ # print warnings to stderr if using binary output
+ # (because we are likely ignoring them and piping stdout to file)
+ # or if there is none of the requested information available
+ if ($binaryOutput or not %$info) {
+ my $errs = $et->GetInfo('Warning', 'Error');
+ PrintErrors($et, $errs, $file) and EFile($file), $rtnVal = 1;
+ } elsif ($et->GetValue('Error') or ($$et{Validate} and $et->GetValue('Warning'))) {
+ $rtnVal = 1;
+ }
+
+ # open output file (or stdout if no output file) if not done already
+ unless ($outfile or $tagOut) {
+ ($fp, $outfile, $append) = OpenOutputFile($orig);
+ $fp or EFile($file), ++$countBad, return;
+ $tmpText = $outfile unless $append;
+ }
+
+ # print the results for this file
+ if (%printFmt) {
+ # output using print format file (-p) option
+ my ($type, $doc, $grp, $lastDoc, $cache);
+ $fileTrailer = '';
+ # repeat for each embedded document if necessary
+ if ($et->Options('ExtractEmbedded')) {
+ # (cache tag keys if there are sub-documents)
+ $lastDoc = $$et{DOC_COUNT} and $cache = { };
+ } else {
+ $lastDoc = 0;
+ }
+ for ($doc=0; $doc<=$lastDoc; ++$doc) {
+ my $skipBody;
+ foreach $type (qw(HEAD SECT IF BODY ENDS TAIL)) {
+ my $prf = $printFmt{$type} or next;
+ next if $type eq 'BODY' and $skipBody;
+ if ($lastDoc) {
+ if ($doc) {
+ next if $type eq 'HEAD' or $type eq 'TAIL'; # only repeat SECT/IF/BODY/ENDS
+ $grp = "Doc$doc";
+ } else {
+ $grp = 'Main';
+ }
+ }
+ my @lines;
+ my $opt = $type eq 'IF' ? 'Silent' : 'Warn'; # silence "IF" warnings
+ foreach (@$prf) {
+ my $line = $et->InsertTagValues(\@foundTags, $_, $opt, $grp, $cache);
+ if ($type eq 'IF') {
+ $skipBody = 1 unless defined $line;
+ } elsif (defined $line) {
+ push @lines, $line;
+ }
+ }
+ $lineCount += scalar @lines;
+ if ($type eq 'SECT') {
+ my $thisHeader = join '', @lines;
+ if ($sectHeader and $sectHeader ne $thisHeader) {
+ print $fp $sectTrailer if $sectTrailer;
+ undef $sectHeader;
+ }
+ $sectTrailer = '';
+ print $fp $sectHeader = $thisHeader unless $sectHeader;
+ } elsif ($type eq 'ENDS') {
+ $sectTrailer .= join '', @lines if defined $sectHeader;
+ } elsif ($type eq 'TAIL') {
+ $fileTrailer .= join '', @lines;
+ } elsif (@lines) {
+ print $fp @lines;
+ }
+ }
+ }
+ delete $printFmt{HEAD} unless $outfile; # print header only once per output file
+ my $errs = $et->GetInfo('Warning', 'Error');
+ PrintErrors($et, $errs, $file) and EFile($file);
+ } elsif (not $disableOutput) {
+ my ($tag, $line, %noDups, %csvInfo, $bra, $ket, $sep);
+ if ($fp) {
+ # print file header (only once)
+ if ($fileHeader) {
+ print $fp $fileHeader;
+ undef $fileHeader unless $textOut;
+ }
+ if ($html) {
+ print $fp "<table>\n";
+ } elsif ($xml) {
+ my $f = $file;
+ CleanXML(\$f);
+ print $fp "\n<rdf:Description rdf:about='${f}'";
+ print $fp "\n xmlns:et='http://ns.exiftool.ca/1.0/'";
+ print $fp " et:toolkit='Image::ExifTool $Image::ExifTool::VERSION'";
+ # define namespaces for all tag groups
+ my (%groups, @groups, $grp0, $grp1);
+ foreach $tag (@foundTags) {
+ ($grp0, $grp1) = $et->GetGroup($tag);
+ unless ($grp1) {
+ next unless defined $forcePrint;
+ $grp0 = $grp1 = 'Unknown';
+ }
+ next if $groups{$grp1};
+ # include family 0 and 1 groups in URI except for internal tags
+ # (this will put internal tags in the "XML" group on readback)
+ $groups{$grp1} = $grp0;
+ push @groups, $grp1;
+ AddGroups($$info{$tag}, $grp0, \%groups, \@groups) if ref $$info{$tag};
+ }
+ foreach $grp1 (@groups) {
+ my $grp = $groups{$grp1};
+ unless ($grp eq $grp1 and $grp =~ /^(ExifTool|File|Composite|Unknown)$/) {
+ $grp .= "/$grp1";
+ }
+ print $fp "\n xmlns:$grp1='http://ns.exiftool.ca/$grp/1.0/'";
+ }
+ print $fp '>' if $outFormat < 1; # finish rdf:Description token unless short format
+ $ind = $outFormat >= 0 ? ' ' : ' ';
+ } elsif ($json) {
+ # set delimiters for JSON or PHP output
+ ($bra, $ket, $sep) = $json == 1 ? ('{','}',':') : ('Array(',')',' =>');
+ print $fp ",\n" if $comma;
+ print $fp qq($bra\n "SourceFile"$sep ), EscapeJSON(MyConvertFileName($et,$file));
+ $comma = 1;
+ $ind = (defined $showGroup and not $allGroup) ? ' ' : ' ';
+ } elsif ($csv) {
+ my $file2 = MyConvertFileName($et, $file);
+ $database{$file2} = \%csvInfo;
+ push @csvFiles, $file2;
+ }
+ }
+ # suppress duplicates manually in JSON and short XML output
+ my $noDups = ($json or ($xml and $outFormat > 0));
+ my $printConv = $et->Options('PrintConv');
+ my $lastGroup = '';
+ my $i = -1;
+TAG: foreach $tag (@foundTags) {
+ ++$i; # keep track on index in @foundTags
+ my $tagName = GetTagName($tag);
+ my ($group, $valList);
+ # get the value for this tag
+ my $val = $$info{$tag};
+ # set flag if this is binary data
+ $isBinary = (ref $val eq 'SCALAR' and defined $binaryOutput);
+ if (ref $val) {
+ # happens with -X, -j or -php when combined with -b:
+ if (defined $binaryOutput and not $binaryOutput and $$et{TAG_INFO}{$tag}{Protected}) {
+ # avoid extracting Protected binary tags (eg. data blocks) [insider information]
+ my $lcTag = lc $tag;
+ $lcTag =~ s/ .*//;
+ next unless $$et{REQ_TAG_LOOKUP}{$lcTag};
+ }
+ $val = ConvertBinary($val); # convert SCALAR references
+ if ($structOpt) {
+ # serialize structure if necessary
+ $val = Image::ExifTool::XMP::SerializeStruct($val) unless $xml or $json;
+ } elsif (ref $val eq 'ARRAY') {
+ if (defined $listItem) {
+ # take only the specified item
+ $val = $$val[$listItem];
+ # join arrays of simple values (with newlines for binary output)
+ } elsif ($binaryOutput) {
+ if ($tagOut) {
+ $valList = $val;
+ $val = shift @$valList;
+ } else {
+ $val = join defined $binSep ? $binSep : "\n", @$val;
+ }
+ } elsif ($joinLists) {
+ $val = join $listSep, @$val;
+ }
+ }
+ }
+ if (not defined $val) {
+ # ignore tags that weren't found unless necessary
+ next if $binaryOutput;
+ if (defined $forcePrint) {
+ $val = $forcePrint; # forced to print all tag values
+ } elsif (not $csv) {
+ next;
+ }
+ }
+ if (defined $showGroup) {
+ $group = $et->GetGroup($tag, $showGroup);
+ # look ahead to see if this tag may suppress a priority tag in
+ # the same group, and if so suppress this tag instead
+ next if $noDups and $tag =~ /^(.*?) ?\(/ and defined $$info{$1} and
+ $group eq $et->GetGroup($1, $showGroup);
+ $group = 'Unknown' if not $group and ($xml or $json or $csv);
+ if ($fp and not ($allGroup or $csv)) {
+ if ($lastGroup ne $group) {
+ if ($html) {
+ my $cols = 1;
+ ++$cols if $outFormat==0 or $outFormat==1;
+ ++$cols if $showTagID;
+ print $fp "<tr><td colspan=$cols bgcolor='#dddddd'>$group</td></tr>\n";
+ } elsif ($json) {
+ print $fp "\n $ket" if $lastGroup;
+ print $fp ',' if $lastGroup or $comma;
+ print $fp qq(\n "$group"$sep $bra);
+ undef $comma;
+ undef %noDups; # allow duplicate names in different groups
+ } else {
+ print $fp "---- $group ----\n";
+ }
+ $lastGroup = $group;
+ }
+ undef $group; # undefine so we don't print it below
+ }
+ }
+
+ ++$lineCount; # we are printing something meaningful
+
+ # loop through list values when -b -W used
+ for (;;) {
+ if ($tagOut) {
+ # determine suggested extension for output file
+ my $ext = SuggestedExtension($et, \$val, $tagName);
+ if (%wext and ($wext{$ext} || $wext{'*'} || -1) < 0) {
+ if ($verbose and $verbose > 1) {
+ print $vout "Not writing $ext output file for $tagName\n";
+ }
+ next TAG;
+ }
+ my @groups = $et->GetGroup($tag);
+ $outfile and close($fp), undef($tmpText); # (shouldn't happen)
+ ($fp, $outfile, $append) = OpenOutputFile($orig, $tagName, \@groups, $ext);
+ $fp or ++$countBad, next TAG;
+ $tmpText = $outfile unless $append;
+ }
+ # write binary output
+ if ($binaryOutput) {
+ print $fp $val;
+ print $fp $binTerm if defined $binTerm;
+ if ($tagOut) {
+ if ($append) {
+ $appended{$outfile} = 1 unless $created{$outfile};
+ } else {
+ $created{$outfile} = 1;
+ }
+ close($fp);
+ undef $tmpText;
+ $verbose and print $vout "Wrote $tagName to $outfile\n";
+ undef $outfile;
+ undef $fp;
+ next TAG unless $valList and @$valList;
+ $val = shift @$valList;
+ next; # loop over values of List tag
+ }
+ next TAG;
+ }
+ last;
+ }
+ # save information for CSV output
+ if ($csv) {
+ my $tn = $tagName;
+ $tn .= '#' if $tag =~ /#/; # add ValueConv "#" suffix if used
+ my $gt = $group ? "$group:$tn" : $tn;
+ # (tag-name case may be different if some tags don't exist
+ # in a file, so all logic must use lower-case tag names)
+ my $lcTag = lc $gt;
+ # override existing entry only if top priority
+ next if defined $csvInfo{$lcTag} and $tag =~ /\(/;
+ $csvInfo{$lcTag} = $val;
+ if (defined $csvTags{$lcTag}) {
+ # overwrite with actual extracted tag name
+ # (note: can't check "if defined $val" here because -f may be used)
+ $csvTags{$lcTag} = $gt if defined $$info{$tag};
+ next;
+ }
+ # must check for "Unknown" group (for tags that don't exist)
+ if ($group and defined $csvTags[$i] and $csvTags[$i] =~ /^(.*):$tn$/i) {
+ next if $group eq 'Unknown'; # nothing more to do if we don't know tag group
+ if ($1 eq 'unknown') {
+ # replace unknown entry in CSV tag lookup and list
+ delete $csvTags{$csvTags[$i]};
+ $csvTags{$lcTag} = defined($val) ? $gt : '';
+ $csvTags[$i] = $lcTag;
+ next;
+ }
+ }
+ # (don't save unextracted tag name unless -f was used)
+ $csvTags{$lcTag} = defined($val) ? $gt : '';
+ if (@csvFiles == 1) {
+ push @csvTags, $lcTag; # save order of tags for first file
+ } elsif (@csvTags) {
+ undef @csvTags;
+ }
+ next;
+ }
+
+ # get description if we need it (use tag name if $outFormat > 0)
+ my $desc = $outFormat > 0 ? $tagName : $et->GetDescription($tag);
+
+ if ($xml) {
+ # RDF/XML output format
+ my $tok = "$group:$tagName";
+ if ($outFormat > 0) {
+ if ($structOpt and ref $val) {
+ $val = Image::ExifTool::XMP::SerializeStruct($val);
+ }
+ if ($escapeHTML) {
+ $val =~ tr/\0-\x08\x0b\x0c\x0e-\x1f/./;
+ Image::ExifTool::XMP::FixUTF8(\$val) unless $altEnc;
+ $val = Image::ExifTool::HTML::EscapeHTML($val, $altEnc);
+ } else {
+ CleanXML(\$val);
+ }
+ unless ($noDups{$tok}) {
+ # manually un-do CR/LF conversion in Windows because output
+ # is in text mode, which will re-convert newlines to CR/LF
+ $isCRLF and $val =~ s/\x0d\x0a/\x0a/g;
+ print $fp "\n $tok='${val}'";
+ # XML does not allow duplicate attributes
+ $noDups{$tok} = 1;
+ }
+ next;
+ }
+ my ($xtra, $valNum, $descClose);
+ if ($showTagID) {
+ my ($id, $lang) = $et->GetTagID($tag);
+ if ($id =~ /^\d+$/) {
+ $id = sprintf("0x%.4x", $id) if $showTagID eq 'H';
+ } else {
+ $id = Image::ExifTool::XMP::FullEscapeXML($id);
+ }
+ $xtra = " et:id='${id}'";
+ $xtra .= " xml:lang='${lang}'" if $lang;
+ } else {
+ $xtra = '';
+ }
+ if ($tabFormat) {
+ my $table = $et->GetTableName($tag);
+ my $index = $et->GetTagIndex($tag);
+ $xtra .= " et:table='${table}'";
+ $xtra .= " et:index='${index}'" if defined $index;
+ }
+ my $lastVal = $val;
+ for ($valNum=0; $valNum<2; ++$valNum) {
+ $val = FormatXML($val, $ind, $group);
+ # manually un-do CR/LF conversion in Windows because output
+ # is in text mode, which will re-convert newlines to CR/LF
+ $isCRLF and $val =~ s/\x0d\x0a/\x0a/g;
+ if ($outFormat >= 0) {
+ # normal output format (note: this will give
+ # non-standard RDF/XML if there are any attributes)
+ print $fp "\n <$tok$xtra$val</$tok>";
+ last;
+ } elsif ($valNum == 0) {
+ CleanXML(\$desc);
+ if ($xtra) {
+ print $fp "\n <$tok>";
+ print $fp "\n <rdf:Description$xtra>";
+ $descClose = "\n </rdf:Description>";
+ } else {
+ print $fp "\n <$tok rdf:parseType='Resource'>";
+ $descClose = '';
+ }
+ # print tag Description
+ print $fp "\n <et:desc>$desc</et:desc>";
+ if ($printConv) {
+ # print PrintConv value
+ print $fp "\n <et:prt$val</et:prt>";
+ $val = $et->GetValue($tag, 'ValueConv');
+ $val = '' unless defined $val;
+ # go back to print ValueConv value only if different
+ next unless IsEqual($val, $lastVal);
+ print $fp "$descClose\n </$tok>";
+ last;
+ }
+ }
+ # print ValueConv value
+ print $fp "\n <et:val$val</et:val>";
+ print $fp "$descClose\n </$tok>";
+ last;
+ }
+ next;
+ } elsif ($json) {
+ # JSON or PHP output format
+ my $tok = $allGroup ? "$group:$tagName" : $tagName;
+ # (removed due to backward incompatibility)
+ # $tok .= '#' if $tag =~ /#/; # add back '#' suffix if used
+ next if $noDups{$tok};
+ $noDups{$tok} = 1;
+ print $fp ',' if $comma;
+ print $fp qq(\n$ind"$tok"$sep );
+ if ($showTagID or $outFormat < 0) {
+ $val = { val => $val };
+ if ($showTagID) {
+ my $id = $et->GetTagID($tag);
+ $id = sprintf('0x%.4x', $id) if $showTagID eq 'H' and $id =~ /^\d+$/;
+ $$val{id} = $id;
+ }
+ if ($tabFormat) {
+ $$val{table} = $et->GetTableName($tag);
+ my $index = $et->GetTagIndex($tag);
+ $$val{index} = $index if defined $index;
+ }
+ if ($outFormat < 0) {
+ $$val{desc} = $desc;
+ if ($printConv) {
+ my $num = $et->GetValue($tag, 'ValueConv');
+ $$val{num} = $num if defined $num and not IsEqual($num, $$val{val});
+ }
+ }
+ }
+ FormatJSON($fp, $val, $ind);
+ $comma = 1;
+ next;
+ }
+ my $id;
+ if ($showTagID) {
+ $id = $et->GetTagID($tag);
+ if ($id =~ /^(\d+)(\.\d+)?$/) { # only print numeric ID's
+ $id = sprintf("0x%.4x", $1) if $showTagID eq 'H';
+ } else {
+ $id = '-';
+ }
+ }
+
+ if ($escapeC) {
+ $val =~ s/([\0-\x1f\\\x7f])/$escC{$1} || sprintf('\x%.2x', ord $1)/eg;
+ } else {
+ # translate unprintable chars in value and remove trailing spaces
+ $val =~ tr/\x01-\x1f\x7f/./;
+ $val =~ s/\x00//g;
+ $val =~ s/\s+$//;
+ }
+
+ if ($html) {
+ print $fp "<tr>";
+ print $fp "<td>$group</td>" if defined $group;
+ print $fp "<td>$id</td>" if $showTagID;
+ print $fp "<td>$desc</td>" if $outFormat <= 1;
+ print $fp "<td>$val</td></tr>\n";
+ } else {
+ my $buff = '';
+ if ($tabFormat) {
+ $buff = "$group\t" if defined $group;
+ $buff .= "$id\t" if $showTagID;
+ if ($outFormat <= 1) {
+ $buff .= "$desc\t$val\n";
+ } elsif (defined $line) {
+ $line .= "\t$val";
+ } else {
+ $line = $val;
+ }
+ } elsif ($outFormat < 0) { # long format
+ $buff = "[$group] " if defined $group;
+ $buff .= "$id " if $showTagID;
+ $buff .= "$desc\n $val\n";
+ } elsif ($outFormat == 0 or $outFormat == 1) {
+ my $wid;
+ my $len = 0;
+ if (defined $group) {
+ $buff = sprintf("%-15s ", "[$group]");
+ $len = 16;
+ }
+ if ($showTagID) {
+ $wid = ($showTagID eq 'D') ? 5 : 6;
+ $len += $wid + 1;
+ ($wid = $len - length($buff) - 1) < 1 and $wid = 1;
+ $buff .= sprintf "%${wid}s ", $id;
+ }
+ $wid = 32 - (length($buff) - $len);
+ # pad description to a constant length
+ # (get actual character length when using alternate languages
+ # because these descriptions may contain UTF8-encoded characters)
+ my $padLen = $wid;
+ if (not $fixLen) {
+ $padLen -= length $desc;
+ } elsif ($fixLen == 1) {
+ $padLen -= length Encode::decode_utf8($desc);
+ } else {
+ my $gcstr = eval { new Unicode::GCString(Encode::decode_utf8($desc)) };
+ if ($gcstr) {
+ $padLen -= $gcstr->columns;
+ } else {
+ $padLen -= length Encode::decode_utf8($desc);
+ Warn "Warning: Unicode::GCString problem. Columns may be misaligned\n";
+ $fixLen = 1;
+ }
+ }
+ $padLen = 0 if $padLen < 0;
+ $buff .= $desc . (' ' x $padLen) . ": $val\n";
+ } elsif ($outFormat == 2) {
+ $buff = "[$group] " if defined $group;
+ $buff .= "$id " if $showTagID;
+ $buff .= "$tagName: $val\n";
+ } elsif ($argFormat) {
+ $buff = '-';
+ $buff .= "$group:" if defined $group;
+ $tagName .= '#' if $tag =~ /#/; # add '#' suffix if used
+ $buff .= "$tagName=$val\n";
+ } else {
+ $buff = "$group " if defined $group;
+ $buff .= "$id " if $showTagID;
+ $buff .= "$val\n";
+ }
+ print $fp $buff;
+ }
+ if ($tagOut) {
+ if ($append) {
+ $appended{$outfile} = 1 unless $created{$outfile};
+ } else {
+ $created{$outfile} = 1;
+ }
+ close($fp);
+ undef $tmpText;
+ $verbose and print $vout "Wrote $tagName to $outfile\n";
+ undef $outfile;
+ undef $fp;
+ }
+ }
+ if ($fp) {
+ if ($html) {
+ print $fp "</table>\n";
+ } elsif ($xml) {
+ # close rdf:Description element
+ print $fp $outFormat < 1 ? "\n</rdf:Description>\n" : "/>\n";
+ } elsif ($json) {
+ print $fp "\n $ket" if $lastGroup;
+ print $fp "\n$ket";
+ $comma = 1;
+ } elsif ($tabFormat and $outFormat > 1) {
+ print $fp "$line\n" if defined $line;
+ }
+ }
+ }
+ if ($outfile) {
+ # write section and file trailers before closing the file
+ print $fp $sectTrailer and $sectTrailer = '' if $sectTrailer;
+ print $fp $fileTrailer if $fileTrailer;
+ close($fp);
+ undef $tmpText;
+ if ($lineCount) {
+ if ($append) {
+ $appended{$outfile} = 1 unless $created{$outfile};
+ } else {
+ $created{$outfile} = 1;
+ }
+ } else {
+ $et->Unlink($outfile) unless $append; # don't keep empty output files
+ }
+ undef $comma;
+ }
+ ++$count;
+}
+
+#------------------------------------------------------------------------------
+# Set information in file
+# Inputs: 0) ExifTool object reference, 1) source file name
+# 2) original source file name ('' to create from scratch)
+# Returns: true on success
+sub SetImageInfo($$$)
+{
+ my ($et, $file, $orig) = @_;
+ my ($outfile, $restored, $isTemporary, $isStdout, $outType, $tagsFromSrc);
+ my ($hardLink, $symLink, $testName, $sameFile);
+ my $infile = $file; # save infile in case we change it again
+
+ # clean up old temporary file if necessary
+ if (defined $tmpFile) {
+ $et->Unlink($tmpFile);
+ undef $tmpFile;
+ }
+ # clear any existing errors or warnings since we check these on return
+ delete $$et{VALUE}{Error};
+ delete $$et{VALUE}{Warning};
+
+ # first, try to determine our output file name so we can return quickly
+ # if it already exists (note: this test must be delayed until after we
+ # set tags from dynamic files if writing FileName or Directory)
+ if (defined $outOpt) {
+ if ($outOpt =~ /^-(\.\w+)?$/) {
+ # allow output file type to be specified with "-o -.EXT"
+ $outType = GetFileType($outOpt) if $1;
+ $outfile = '-';
+ $isStdout = 1;
+ } else {
+ $outfile = FilenameSPrintf($outOpt, $orig);
+ if ($outfile eq '') {
+ Warn "Error: Can't create file with zero-length name from $orig\n";
+ EFile($infile);
+ ++$countBadCr;
+ return 0;
+ }
+ }
+ if (not $isStdout and ($et->IsDirectory($outfile) or $outfile =~ /\/$/)) {
+ $outfile .= '/' unless $outfile =~ /\/$/;
+ my $name = $file;
+ $name =~ s/^.*\///s; # remove directory name
+ $outfile .= $name;
+ } else {
+ my $srcType = GetFileType($file) || '';
+ $outType or $outType = GetFileType($outfile);
+ if ($outType and ($srcType ne $outType or $outType eq 'ICC') and $file ne '-') {
+ unless (CanCreate($outType)) {
+ my $what = $srcType ? 'other types' : 'scratch';
+ WarnOnce "Error: Can't create $outType files from $what\n";
+ EFile($infile);
+ ++$countBadCr;
+ return 0;
+ }
+ if ($file ne '') {
+ # restore previous new values unless done already
+ $et->RestoreNewValues() unless $restored;
+ $restored = 1;
+ # translate to this type by setting specified tags from file
+ my @setTags = @tags;
+ foreach (@exclude) {
+ push @setTags, "-$_";
+ }
+ # force some tags to be copied for certain file types
+ my %forceCopy = (
+ ICC => 'ICC_Profile',
+ VRD => 'CanonVRD',
+ DR4 => 'CanonDR4',
+ );
+ push @setTags, $forceCopy{$outType} if $forceCopy{$outType};
+ # assume "-tagsFromFile @" unless -tagsFromFile already specified
+ # (%setTags won't be empty if -tagsFromFile used)
+ if (not %setTags or (@setTags and not $setTags{'@'})) {
+ return 0 unless DoSetFromFile($et, $file, \@setTags);
+ } elsif (@setTags) {
+ # add orphaned tags to existing "-tagsFromFile @" for this file only
+ push @setTags, @{$setTags{'@'}};
+ $tagsFromSrc = \@setTags;
+ }
+ # all done with source file -- create from meta information alone
+ $file = '';
+ }
+ }
+ }
+ unless ($isStdout) {
+ $outfile = NextUnusedFilename($outfile);
+ if ($et->Exists($outfile) and not $doSetFileName) {
+ Warn "Error: '${outfile}' already exists - $infile\n";
+ EFile($infile);
+ ++$countBadWr;
+ return 0;
+ }
+ }
+ } elsif ($file eq '-') {
+ $isStdout = 1;
+ }
+ # set tags from destination file if required
+ if (@dynamicFiles) {
+ # restore previous values if necessary
+ $et->RestoreNewValues() unless $restored;
+ my ($dyFile, %setTagsIndex);
+ foreach $dyFile (@dynamicFiles) {
+ if (not ref $dyFile) {
+ my ($fromFile, $setTags);
+ if ($dyFile eq '@') {
+ $fromFile = $orig;
+ $setTags = $tagsFromSrc || $setTags{$dyFile};
+ } else {
+ $fromFile = FilenameSPrintf($dyFile, $orig);
+ defined $fromFile or EFile($infile), ++$countBadWr, return 0;
+ $setTags = $setTags{$dyFile};
+ }
+ # do we have multiple -tagsFromFile options with this file?
+ if ($setTagsList{$dyFile}) {
+ # use the tags set in the i-th occurrence
+ my $i = $setTagsIndex{$dyFile} || 0;
+ $setTagsIndex{$dyFile} = $i + 1;
+ $setTags = $setTagsList{$dyFile}[$i] if $setTagsList{$dyFile}[$i];
+ }
+ # set new values values from file
+ return 0 unless DoSetFromFile($et, $fromFile, $setTags);
+ } elsif (ref $dyFile eq 'ARRAY') {
+ # a dynamic file containing a simple tag value
+ my $fname = FilenameSPrintf($$dyFile[1], $orig);
+ my ($buff, $rtn, $wrn);
+ my $opts = $$dyFile[2];
+ if (defined $fname and SlurpFile($fname, \$buff)) {
+ $verbose and print $vout "Reading $$dyFile[0] from $fname\n";
+ ($rtn, $wrn) = $et->SetNewValue($$dyFile[0], $buff, %$opts);
+ $wrn and Warn "$wrn\n";
+ }
+ # remove this tag if we couldn't set it properly
+ $rtn or $et->SetNewValue($$dyFile[0], undef, Replace => 2,
+ ProtectSaved => $$opts{ProtectSaved});
+ next;
+ } elsif (ref $dyFile eq 'SCALAR') {
+ # set new values from CSV or JSON database
+ my ($f, $found, $tag);
+ undef $evalWarning;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ # force UTF-8 if the database was JSON
+ my $old = $et->Options('Charset');
+ $et->Options(Charset => 'UTF8') if $csv eq 'JSON';
+ # read tags for SourceFile '*' plus the specific file
+ foreach $f ('*', MyConvertFileName($et, $file)) {
+ my $csvInfo = $database{$f};
+ unless ($csvInfo) {
+ next if $f eq '*';
+ # check absolute path
+ # (punt on using ConvertFileName here, so $absPath may be a mix of encodings)
+ my $absPath = AbsPath($f);
+ next unless defined $absPath and $csvInfo = $database{$absPath};
+ }
+ $found = 1;
+ $verbose and print $vout "Setting new values from $csv database\n";
+ foreach $tag (sort keys %$csvInfo) {
+ next if $tag =~ /\b(SourceFile|Directory|FileName)$/i; # don't write these
+ my ($rtn, $wrn) = $et->SetNewValue($tag, $$csvInfo{$tag},
+ Protected => 1, AddValue => $csvAdd,
+ ProtectSaved => $csvSaveCount);
+ $wrn and Warn "$wrn\n" if $verbose;
+ }
+ }
+ $et->Options(Charset => $old) if $csv eq 'JSON';
+ unless ($found) {
+ Warn("No SourceFile '${file}' in imported $csv database\n");
+ my $absPath = AbsPath($file);
+ Warn("(full path: '${absPath}')\n") if defined $absPath and $absPath ne $file;
+ return 0;
+ }
+ }
+ }
+ }
+ if ($isStdout) {
+ # write to STDOUT
+ $outfile = \*STDOUT;
+ unless ($binaryStdout) {
+ binmode(STDOUT);
+ $binaryStdout = 1;
+ }
+ } else {
+ # get name of hard link if we are creating one
+ $hardLink = $et->GetNewValues('HardLink');
+ $symLink = $et->GetNewValues('SymLink');
+ $testName = $et->GetNewValues('TestName');
+ $hardLink = FilenameSPrintf($hardLink, $orig) if defined $hardLink;
+ $symLink = FilenameSPrintf($symLink, $orig) if defined $symLink;
+ # determine what our output file name should be
+ my $newFileName = $et->GetNewValues('FileName');
+ my $newDir = $et->GetNewValues('Directory');
+ if (defined $testName) {
+ my $err;
+ $err = "You shouldn't write FileName or Directory with TestFile" if defined $newFileName or defined $newDir;
+ $err = "The -o option shouldn't be used with TestFile" if defined $outfile;
+ $err and Warn("Error: $err - $infile\n"), EFile($infile), ++$countBadWr, return 0;
+ $testName = FilenameSPrintf($testName, $orig);
+ $testName = Image::ExifTool::GetNewFileName($file, $testName) if $file ne '';
+ }
+ if (defined $newFileName or defined $newDir or ($doSetFileName and defined $outfile)) {
+ if ($newFileName) {
+ $newFileName = FilenameSPrintf($newFileName, $orig);
+ if (defined $outfile) {
+ $outfile = Image::ExifTool::GetNewFileName($file, $outfile) if $file ne '';
+ $outfile = Image::ExifTool::GetNewFileName($outfile, $newFileName);
+ } elsif ($file ne '') {
+ $outfile = Image::ExifTool::GetNewFileName($file, $newFileName);
+ }
+ }
+ if ($newDir) {
+ $newDir = FilenameSPrintf($newDir, $orig);
+ $outfile = Image::ExifTool::GetNewFileName(defined $outfile ? $outfile : $file, $newDir);
+ }
+ $outfile = NextUnusedFilename($outfile, $infile);
+ if ($et->Exists($outfile)) {
+ if ($infile eq $outfile) {
+ undef $outfile; # not changing the file name after all
+ # (allow for case-insensitive filesystems)
+ } elsif ($et->IsSameFile($infile, $outfile)) {
+ $sameFile = $outfile; # same file, but the name has a different case
+ } else {
+ Warn "Error: '${outfile}' already exists - $infile\n";
+ EFile($infile);
+ ++$countBadWr;
+ return 0;
+ }
+ }
+ }
+ if (defined $outfile) {
+ $verbose and print $vout "'${infile}' --> '${outfile}'\n";
+ # create output directory if necessary
+ CreateDirectory($outfile);
+ # set temporary file (automatically erased on abnormal exit)
+ $tmpFile = $outfile if defined $outOpt;
+ }
+ unless (defined $tmpFile) {
+ # count the number of tags and pseudo-tags we are writing
+ my ($numSet, $numPseudo) = $et->CountNewValues();
+ if ($et->Exists($file)) {
+ unless ($numSet) {
+ # no need to write if no tags set
+ print $vout "Nothing changed in $file\n" if defined $verbose;
+ EFile($infile, 1);
+ ++$countSameWr;
+ return 1;
+ }
+ } elsif (CanCreate($file)) {
+ if ($numSet == $numPseudo) {
+ # no need to write if no real tags
+ Warn("Error: Nothing to write - $file\n");
+ EFile($infile, 1);
+ ++$countBadWr;
+ return 0;
+ }
+ unless (defined $outfile) {
+ # create file from scratch
+ $outfile = $file;
+ $file = '';
+ }
+ } else {
+ # file doesn't exist, and we can't create it
+ Warn "Error: File not found - $file\n";
+ EFile($infile);
+ FileNotFound($file);
+ ++$countBadWr;
+ return 0;
+ }
+ # quickly rename file and/or set file date if this is all we are doing
+ if ($numSet == $numPseudo) {
+ my $r1 = $et->SetFileModifyDate($file,undef,'FileCreateDate');
+ my $r2 = $et->SetFileModifyDate($file);
+ my $r3 = $et->SetSystemTags($file);
+ my $r4 = 0;
+ $r4 = $et->SetFileName($file, $outfile) if defined $outfile;
+ if ($r1 > 0 or $r2 > 0 or $r3 > 0 or $r4 > 0) {
+ ++$countGoodWr;
+ } elsif ($r1 < 0 or $r2 < 0 or $r3 < 0 or $r4 < 0) {
+ EFile($infile);
+ ++$countBadWr;
+ return 0;
+ } else {
+ EFile($infile, 1);
+ ++$countSameWr;
+ }
+ if (defined $hardLink or defined $symLink or defined $testName) {
+ my $src = (defined $outfile and $r4 > 0) ? $outfile : $file;
+ DoHardLink($et, $src, $hardLink, $symLink, $testName);
+ }
+ return 1;
+ }
+ if (not defined $outfile or defined $sameFile) {
+ # write to a truly temporary file
+ $outfile = "${file}_exiftool_tmp";
+ if ($et->Exists($outfile)) {
+ Warn("Error: Temporary file already exists: $outfile\n");
+ EFile($infile);
+ ++$countBadWr;
+ return 0;
+ }
+ $isTemporary = 1;
+ }
+ # new output file is temporary until we know it has been written properly
+ $tmpFile = $outfile;
+ }
+ }
+ # rewrite the file
+ my $success = $et->WriteInfo(Infile($file), $outfile, $outType);
+
+ # create hard link if specified
+ if ($success and (defined $hardLink or defined $symLink or defined $testName)) {
+ my $src = defined $outfile ? $outfile : $file;
+ DoHardLink($et, $src, $hardLink, $symLink, $testName);
+ }
+
+ # get file time if preserving it
+ my ($aTime, $mTime, $cTime, $doPreserve);
+ $doPreserve = $preserveTime unless $file eq '';
+ if ($doPreserve and $success) {
+ ($aTime, $mTime, $cTime) = $et->GetFileTime($file);
+ # don't override date/time values written by the user
+ undef $cTime if $$et{WRITTEN}{FileCreateDate};
+ if ($$et{WRITTEN}{FileModifyDate} or $doPreserve == 2) {
+ if (defined $cTime) {
+ undef $aTime; # only preserve FileCreateDate
+ undef $mTime;
+ } else {
+ undef $doPreserve; # (nothing to preserve)
+ }
+ }
+ }
+
+ if ($success == 1) {
+ # preserve the original file times
+ if (defined $tmpFile) {
+ if ($et->Exists($file)) {
+ $et->SetFileTime($tmpFile, $aTime, $mTime, $cTime) if $doPreserve;
+ if ($isTemporary) {
+ # preserve original file attributes if possible
+ $et->CopyFileAttrs($file, $outfile);
+ # move original out of the way
+ my $original = "${file}_original";
+ if (not $overwriteOrig and not $et->Exists($original)) {
+ # rename the file and check again to be sure the file doesn't exist
+ # (in case, say, the filesystem truncated the file extension)
+ if (not $et->Rename($file, $original) or $et->Exists($file)) {
+ Error "Error renaming $file\n";
+ return 0;
+ }
+ }
+ my $dstFile = defined $sameFile ? $sameFile : $file;
+ if ($overwriteOrig > 1) {
+ # copy temporary file over top of original to preserve attributes
+ my ($err, $buff);
+ my $newFile = $tmpFile;
+ $et->Open(\*NEW_FILE, $newFile) or Error("Error opening $newFile\n"), return 0;
+ binmode(NEW_FILE);
+
+ #..........................................................
+ # temporarily disable CTRL-C during this critical operation
+ $critical = 1;
+ undef $tmpFile; # handle deletion of temporary file ourself
+ if ($et->Open(\*ORIG_FILE, $file, '>')) {
+ binmode(ORIG_FILE);
+ while (read(NEW_FILE, $buff, 65536)) {
+ print ORIG_FILE $buff or $err = 1;
+ }
+ close(NEW_FILE);
+ close(ORIG_FILE) or $err = 1;
+ if ($err) {
+ Warn "Couldn't overwrite in place - $file\n";
+ unless ($et->Rename($newFile, $file) or
+ ($et->Unlink($file) and $et->Rename($newFile, $file)))
+ {
+ Error("Error renaming $newFile to $file\n");
+ undef $critical;
+ SigInt() if $interrupted;
+ return 0;
+ }
+ } else {
+ $et->SetFileModifyDate($file, $cTime, 'FileCreateDate', 1);
+ $et->SetFileModifyDate($file, $mTime, 'FileModifyDate', 1);
+ $et->Unlink($newFile);
+ if ($doPreserve) {
+ $et->SetFileTime($file, $aTime, $mTime, $cTime);
+ # save time to set it later again to patch OS X 10.6 bug
+ $preserveTime{$file} = [ $aTime, $mTime, $cTime ];
+ }
+ }
+ ++$countGoodWr;
+ } else {
+ close(NEW_FILE);
+ Warn "Error opening $file for writing\n";
+ EFile($infile);
+ $et->Unlink($newFile);
+ ++$countBadWr;
+ }
+ undef $critical; # end critical section
+ SigInt() if $interrupted; # issue delayed SIGINT if necessary
+ #..........................................................
+
+ # simply rename temporary file to replace original
+ # (if we didn't already rename it to add "_original")
+ } elsif ($et->Rename($tmpFile, $dstFile)) {
+ ++$countGoodWr;
+ } else {
+ my $newFile = $tmpFile;
+ undef $tmpFile; # (avoid deleting file if we get interrupted)
+ # unlink may fail if already renamed or no permission
+ if (not $et->Unlink($file)) {
+ Warn "Error renaming temporary file to $dstFile\n";
+ EFile($infile);
+ $et->Unlink($newFile);
+ ++$countBadWr;
+ # try renaming again now that the target has been deleted
+ } elsif (not $et->Rename($newFile, $dstFile)) {
+ Warn "Error renaming temporary file to $dstFile\n";
+ EFile($infile);
+ # (don't delete tmp file now because it is all we have left)
+ ++$countBadWr;
+ } else {
+ ++$countGoodWr;
+ }
+ }
+ } elsif ($overwriteOrig) {
+ # erase original file
+ $et->Unlink($file) or Warn "Error erasing original $file\n";
+ ++$countGoodWr;
+ } else {
+ ++$countGoodCr;
+ }
+ } else {
+ # this file was created from scratch, not edited
+ ++$countGoodCr;
+ }
+ } else {
+ ++$countGoodWr;
+ }
+ } elsif ($success) {
+ EFile($infile, 1);
+ if ($isTemporary) {
+ # just erase the temporary file since no changes were made
+ $et->Unlink($tmpFile);
+ ++$countSameWr;
+ } else {
+ $et->SetFileTime($outfile, $aTime, $mTime, $cTime) if $doPreserve;
+ if ($overwriteOrig) {
+ $et->Unlink($file) or Warn "Error erasing original $file\n";
+ }
+ ++$countCopyWr;
+ }
+ print $vout "Nothing changed in $file\n" if defined $verbose;
+ } else {
+ EFile($infile);
+ $et->Unlink($tmpFile) if defined $tmpFile;
+ ++$countBadWr;
+ }
+ undef $tmpFile;
+ return $success;
+}
+
+#------------------------------------------------------------------------------
+# Make hard link and handle TestName if specified
+# Inputs: 0) ExifTool ref, 1) source file name, 2) HardLink name,
+# 3) SymLink name, 4) TestFile name
+sub DoHardLink($$$$$)
+{
+ my ($et, $src, $hardLink, $symLink, $testName) = @_;
+ if (defined $hardLink) {
+ $hardLink = NextUnusedFilename($hardLink);
+ if ($et->SetFileName($src, $hardLink, 'Link') > 0) {
+ $countLink{Hard} = ($countLink{Hard} || 0) + 1;
+ } else {
+ $countLink{BadHard} = ($countLink{BadHard} || 0) + 1;
+ }
+ }
+ if (defined $symLink) {
+ $symLink = NextUnusedFilename($symLink);
+ if ($et->SetFileName($src, $symLink, 'SymLink') > 0) {
+ $countLink{Sym} = ($countLink{Sym} || 0) + 1;
+ } else {
+ $countLink{BadSym} = ($countLink{BadSym} || 0) + 1;
+ }
+ }
+ if (defined $testName) {
+ $testName = NextUnusedFilename($testName, $src);
+ if ($usedFileName{$testName}) {
+ $et->Warn("File '${testName}' would exist");
+ } elsif ($et->SetFileName($src, $testName, 'Test', $usedFileName{$testName}) == 1) {
+ $usedFileName{$testName} = 1;
+ $usedFileName{$src} = 0;
+ }
+ }
+}
+
+#------------------------------------------------------------------------------
+# Clean string for XML (also removes invalid control chars and malformed UTF-8)
+# Inputs: 0) string ref
+# Returns: nothing, but input string is escaped
+sub CleanXML($)
+{
+ my $strPt = shift;
+ # translate control characters that are invalid in XML
+ $$strPt =~ tr/\0-\x08\x0b\x0c\x0e-\x1f/./;
+ # fix malformed UTF-8 characters
+ Image::ExifTool::XMP::FixUTF8($strPt) unless $altEnc;
+ # escape necessary characters for XML
+ $$strPt = Image::ExifTool::XMP::EscapeXML($$strPt);
+}
+
+#------------------------------------------------------------------------------
+# Encode string for XML
+# Inputs: 0) string ref
+# Returns: encoding used (and input string is translated)
+sub EncodeXML($)
+{
+ my $strPt = shift;
+ if ($$strPt =~ /[\0-\x08\x0b\x0c\x0e-\x1f]/ or
+ (not $altEnc and Image::ExifTool::XMP::IsUTF8($strPt) < 0))
+ {
+ # encode binary data and non-UTF8 with special characters as base64
+ $$strPt = Image::ExifTool::XMP::EncodeBase64($$strPt);
+ # #ATV = Alexander Vonk, private communication
+ return 'http://www.w3.org/2001/XMLSchema#base64Binary'; #ATV
+ } elsif ($escapeHTML) {
+ $$strPt = Image::ExifTool::HTML::EscapeHTML($$strPt, $altEnc);
+ } else {
+ $$strPt = Image::ExifTool::XMP::EscapeXML($$strPt);
+ }
+ return ''; # not encoded
+}
+
+#------------------------------------------------------------------------------
+# Format value for XML output
+# Inputs: 0) value, 1) indentation, 2) group
+# Returns: formatted value
+sub FormatXML($$$)
+{
+ local $_;
+ my ($val, $ind, $grp) = @_;
+ my $gt = '>';
+ if (ref $val eq 'ARRAY') {
+ # convert ARRAY into an rdf:Bag
+ my $val2 = "\n$ind <rdf:Bag>";
+ foreach (@$val) {
+ $val2 .= "\n$ind <rdf:li" . FormatXML($_, "$ind ", $grp) . "</rdf:li>";
+ }
+ $val = "$val2\n$ind </rdf:Bag>\n$ind";
+ } elsif (ref $val eq 'HASH') {
+ $gt = " rdf:parseType='Resource'>";
+ my $val2 = '';
+ foreach (sort keys %$val) {
+ # (some variable-namespace XML structure fields may have a different group)
+ my $tok = /:/ ? $_ : ($grp . ':' . $_);
+ $val2 .= "\n$ind <$tok" . FormatXML($$val{$_}, "$ind ", $grp) . "</$tok>";
+ }
+ $val = "$val2\n$ind";
+ } else {
+ # (note: SCALAR reference should have already been converted)
+ my $enc = EncodeXML(\$val);
+ $gt = " rdf:datatype='${enc}'>\n" if $enc; #ATV
+ }
+ return $gt . $val;
+}
+
+#------------------------------------------------------------------------------
+# Escape string for JSON or PHP
+# Inputs: 0) string, 1) flag to force numbers to be quoted too
+# Returns: Escaped string (quoted if necessary)
+sub EscapeJSON($;$)
+{
+ my ($str, $quote) = @_;
+ unless ($quote) {
+ # JSON boolean (true or false)
+ return lc($str) if $str =~ /^(true|false)$/i and $json < 2;
+ # JSON/PHP number (see json.org for numerical format)
+ # return $str if $str =~ /^-?(\d|[1-9]\d+)(\.\d+)?(e[-+]?\d+)?$/i;
+ # (these big numbers caused problems for some JSON parsers, so be more conservative)
+ return $str if $str =~ /^-?(\d|[1-9]\d{1,14})(\.\d{1,16})?(e[-+]?\d{1,3})?$/i;
+ }
+ # encode JSON string in base64 if necessary
+ if ($json < 2 and defined $binaryOutput and Image::ExifTool::XMP::IsUTF8(\$str) < 0) {
+ return '"base64:' . Image::ExifTool::XMP::EncodeBase64($str, 1) . '"';
+ }
+ # escape special characters
+ $str =~ s/(["\t\n\r\\])/\\$jsonChar{$1}/sg;
+ if ($json < 2) { # JSON
+ $str =~ tr/\0//d; # remove all nulls
+ # escape other control characters with \u
+ $str =~ s/([\0-\x1f])/sprintf("\\u%.4X",ord $1)/sge;
+ # JSON strings must be valid UTF8
+ Image::ExifTool::XMP::FixUTF8(\$str) unless $altEnc;
+ } else { # PHP
+ $str =~ s/\0+$// unless $isBinary; # remove trailing nulls unless binary
+ # must escape "$" too for PHP
+ $str =~ s/\$/\\\$/sg;
+ # escape other control characters with \x
+ $str =~ s/([\0-\x1f])/sprintf("\\x%.2X",ord $1)/sge;
+ }
+ return '"' . $str . '"'; # return the quoted string
+}
+
+#------------------------------------------------------------------------------
+# Print JSON or PHP value
+# Inputs: 0) file reference, 1) value, 2) indentation
+sub FormatJSON($$$)
+{
+ local $_;
+ my ($fp, $val, $ind) = @_;
+ my $comma;
+ if (not ref $val) {
+ print $fp EscapeJSON($val);
+ } elsif (ref $val eq 'ARRAY') {
+ if ($joinLists and not ref $$val[0]) {
+ print $fp EscapeJSON(join $listSep, @$val);
+ } else {
+ my ($bra, $ket) = $json == 1 ? ('[',']') : ('Array(',')');
+ print $fp $bra;
+ foreach (@$val) {
+ print $fp ',' if $comma;
+ FormatJSON($fp, $_, $ind);
+ $comma = 1,
+ }
+ print $fp $ket,
+ }
+ } elsif (ref $val eq 'HASH') {
+ my ($bra, $ket, $sep) = $json == 1 ? ('{','}',':') : ('Array(',')',' =>');
+ print $fp $bra;
+ foreach (sort keys %$val) {
+ print $fp ',' if $comma;
+ my $key = EscapeJSON($_, 1);
+ print $fp qq(\n$ind $key$sep );
+ # hack to force decimal id's to be printed as strings with -H
+ if ($showTagID and $_ eq 'id' and $showTagID eq 'H' and $$val{$_} =~ /^\d+\.\d+$/) {
+ print $fp qq{"$$val{$_}"};
+ } else {
+ FormatJSON($fp, $$val{$_}, "$ind ");
+ }
+ $comma = 1,
+ }
+ print $fp "\n$ind$ket",
+ } else {
+ # (note: SCALAR reference should have already been converted)
+ print $fp '"<err>"';
+ }
+}
+
+#------------------------------------------------------------------------------
+# Format value for CSV file
+# Inputs: value
+# Returns: value quoted if necessary
+sub FormatCSV($)
+{
+ my $val = shift;
+ # check for valid encoding if the Charset option was used
+ if ($setCharset and ($val =~ /[^\x09\x0a\x0d\x20-\x7e\x80-\xff]/ or
+ ($setCharset eq 'UTF8' and Image::ExifTool::XMP::IsUTF8(\$val) < 0)))
+ {
+ $val = 'base64:' . Image::ExifTool::XMP::EncodeBase64($val, 1);
+ }
+ # currently, there is a chance that the value may contain NULL characters unless
+ # the -b option is used to encode as Base64. It is unclear whether or not this
+ # is valid CSV, but some readers may not like it. (If this becomes a problem,
+ # in the future values may need to be truncated at the first NULL character.)
+ $val = qq{"$val"} if $val =~ s/"/""/g or $val =~ /(^\s+|\s+$)/ or $val =~ /[\n\r]|\Q$csvDelim/;
+ return $val;
+}
+
+#------------------------------------------------------------------------------
+# Print accumulated CSV information
+sub PrintCSV()
+{
+ my ($file, $lcTag, @tags);
+
+ @csvTags or @csvTags = sort keys %csvTags;
+ # make a list of tags actually found
+ foreach $lcTag (@csvTags) {
+ push @tags, FormatCSV($csvTags{$lcTag}) if $csvTags{$lcTag};
+ }
+ print join($csvDelim, 'SourceFile', @tags), "\n";
+ my $empty = defined($forcePrint) ? $forcePrint : '';
+ foreach $file (@csvFiles) {
+ my @vals = (FormatCSV($file)); # start with full file name
+ my $csvInfo = $database{$file};
+ foreach $lcTag (@csvTags) {
+ next unless $csvTags{$lcTag};
+ my $val = $$csvInfo{$lcTag};
+ defined $val or push(@vals,$empty), next;
+ push @vals, FormatCSV($val);
+ }
+ print join($csvDelim, @vals), "\n";
+ }
+}
+
+#------------------------------------------------------------------------------
+# Add tag groups from structure fields to a list
+# Inputs: 0) tag value, 1) parent group, 2) group hash ref, 3) group list ref
+sub AddGroups($$$$)
+{
+ my ($val, $grp, $groupHash, $groupList) = @_;
+ my ($key, $val2);
+ if (ref $val eq 'HASH') {
+ foreach $key (sort keys %$val) {
+ if ($key =~ /(.*?):/ and not $$groupHash{$1}) {
+ $$groupHash{$1} = $grp;
+ push @$groupList, $1;
+ }
+ AddGroups($$val{$key}, $grp, $groupHash, $groupList) if ref $$val{$key};
+ }
+ } elsif (ref $val eq 'ARRAY') {
+ foreach $val2 (@$val) {
+ AddGroups($val2, $grp, $groupHash, $groupList) if ref $val2;
+ }
+ }
+}
+
+#------------------------------------------------------------------------------
+# Convert binary data (SCALAR references) for printing
+# Inputs: 0) object reference
+# Returns: converted object
+sub ConvertBinary($)
+{
+ my $obj = shift;
+ my ($key, $val);
+ if (ref $obj eq 'HASH') {
+ foreach $key (keys %$obj) {
+ $$obj{$key} = ConvertBinary($$obj{$key}) if ref $$obj{$key};
+ }
+ } elsif (ref $obj eq 'ARRAY') {
+ foreach $val (@$obj) {
+ $val = ConvertBinary($val) if ref $val;
+ }
+ } elsif (ref $obj eq 'SCALAR') {
+ # (binaryOutput flag is set to 0 for binary mode of XML/PHP/JSON output formats)
+ if (defined $binaryOutput) {
+ $obj = $$obj;
+ # encode in base64 if necessary (0xf7 allows for up to 21-bit UTF-8 code space)
+ if ($json == 1 and ($obj =~ /[^\x09\x0a\x0d\x20-\x7e\x80-\xf7]/ or
+ Image::ExifTool::XMP::IsUTF8(\$obj) < 0))
+ {
+ $obj = 'base64:' . Image::ExifTool::XMP::EncodeBase64($obj, 1);
+ }
+ } else {
+ # (-b is not valid for HTML output)
+ my $bOpt = $html ? '' : ', use -b option to extract';
+ if ($$obj =~ /^Binary data \d+ bytes$/) {
+ $obj = "($$obj$bOpt)";
+ } else {
+ $obj = '(Binary data ' . length($$obj) . " bytes$bOpt)";
+ }
+ }
+ }
+ return $obj;
+}
+
+#------------------------------------------------------------------------------
+# Compare two tag values to see if they are equal
+# Inputs: 0) value1, 1) value2
+# Returns: true if they are equal
+sub IsEqual($$)
+{
+ return 1 if ref $_[0] eq 'SCALAR' or $_[0] eq $_[1];
+ return 0 if ref $_[0] ne 'ARRAY' or ref $_[1] ne 'ARRAY' or
+ @{$_[0]} ne @{$_[1]};
+ # test all elements of an array
+ my $i = 0;
+ for ($i=0; $i<scalar(@{$_[0]}); ++$i) {
+ return 0 if $_[0][$i] ne $_[1][$i];
+ }
+ return 1;
+}
+
+#------------------------------------------------------------------------------
+# Add tag list for copying tags from specified file
+# Inputs: 0) set tags file name (or FMT), 1) options for SetNewValuesFromFile()
+# Returns: nothing
+# Notes: Uses global variables: %setTags, %setTagsList, @newValues, $saveCount
+sub AddSetTagsFile($;$)
+{
+ my ($setFile, $opts) = @_;
+ if ($setTags{$setFile}) {
+ # move these tags aside and make a new list for the next invocation of this file
+ $setTagsList{$setFile} or $setTagsList{$setFile} = [ ];
+ push @{$setTagsList{$setFile}}, $setTags{$setFile};
+ }
+ $setTags{$setFile} = []; # create list for tags to copy from this file
+ # insert marker to save new values now (necessary even if this is not a dynamic
+ # file in case the same file is source'd multiple times in a single command)
+ push @newValues, { SaveCount => ++$saveCount }, "TagsFromFile=$setFile";
+ # add option to protect the tags which are assigned after this
+ # (this is the mechanism by which the command-line order-of-operations is preserved)
+ $opts or $opts = { };
+ $$opts{ProtectSaved} = $saveCount;
+ push @{$setTags{$setFile}}, $opts;
+}
+
+#------------------------------------------------------------------------------
+# Get input file name or reference for calls to the ExifTool API
+# Inputs: 0) file name ('-' for STDIN), 1) flag to buffer STDIN
+# Returns: file name, or RAF reference for buffering STDIN
+sub Infile($;$)
+{
+ my ($file, $bufferStdin) = @_;
+ if ($file eq '-' and ($bufferStdin or $rafStdin)) {
+ if ($rafStdin) {
+ $rafStdin->Seek(0); # rewind
+ } elsif (open RAF_STDIN, '-') {
+ $rafStdin = new File::RandomAccess(\*RAF_STDIN);
+ $rafStdin->BinMode();
+ }
+ return $rafStdin if $rafStdin;
+ }
+ return $file;
+}
+
+#------------------------------------------------------------------------------
+# Set new values from file
+# Inputs: 0) exiftool ref, 1) filename, 2) reference to list of values to set
+# Returns: 0 on error (and increments $countBadWr)
+sub DoSetFromFile($$$)
+{
+ local $_;
+ my ($et, $file, $setTags) = @_;
+ $verbose and print $vout "Setting new values from $file\n";
+ my $info = $et->SetNewValuesFromFile(Infile($file,1), @$setTags);
+ my $numSet = scalar(keys %$info);
+ if ($$info{Error}) {
+ # delete all error and warning tags
+ my @warns = grep /^(Error|Warning)\b/, keys %$info;
+ $numSet -= scalar(@warns);
+ # issue a warning for the main error only if we were able to set some tags
+ if (keys(%$info) > @warns) {
+ my $err = $$info{Error};
+ delete $$info{$_} foreach @warns;
+ $$info{Warning} = $err;
+ }
+ } elsif ($$info{Warning}) {
+ my $warns = 1;
+ ++$warns while $$info{"Warning ($warns)"};
+ $numSet -= $warns;
+ }
+ PrintErrors($et, $info, $file) and EFile($file), ++$countBadWr, return 0;
+ Warn "Warning: No writable tags set from $file\n" unless $numSet;
+ return 1;
+}
+
+#------------------------------------------------------------------------------
+# Translate backslashes to forward slashes in filename if necessary
+# Inputs: 0) Filename
+# Returns: nothing, but changes filename if necessary
+sub CleanFilename($)
+{
+ $_[0] =~ tr/\\/\// if $hasBackslash{$^O};
+}
+
+#------------------------------------------------------------------------------
+# Check for valid UTF-8 of a file name
+# Inputs: 0) string, 1) original encoding
+# Returns: 0=plain ASCII, 1=valid UTF-8, -1=invalid UTF-8 (and print warning)
+sub CheckUTF8($$)
+{
+ my ($file, $enc) = @_;
+ my $isUTF8 = 0;
+ if ($file =~ /[\x80-\xff]/) {
+ require Image::ExifTool::XMP;
+ $isUTF8 = Image::ExifTool::XMP::IsUTF8(\$file);
+ if ($isUTF8 < 0) {
+ if ($enc) {
+ Warn("Invalid filename encoding for $file\n");
+ } elsif (not defined $enc) {
+ WarnOnce(qq{FileName encoding not specified. Use "-charset FileName=CHARSET"\n});
+ }
+ }
+ }
+ return $isUTF8;
+}
+
+#------------------------------------------------------------------------------
+# Set window title
+# Inputs: title string or '' to reset title
+sub SetWindowTitle($)
+{
+ my $title = shift;
+ if ($curTitle ne $title) {
+ $curTitle = $title;
+ if ($^O eq 'MSWin32') {
+ $title =~ s/([&\/\?:|"<>])/^$1/g; # escape special chars
+ eval { system qq{title $title} };
+ } else {
+ # (this only works for XTerm terminals, and STDERR must go to the console)
+ printf STDERR "\033]0;%s\007", $title;
+ }
+ }
+}
+
+#------------------------------------------------------------------------------
+# Process files in our @files list
+# Inputs: 0) ExifTool ref, 1) list ref to just return full file names
+sub ProcessFiles($;$)
+{
+ my ($et, $list) = @_;
+ my $enc = $et->Options('CharsetFileName');
+ my $file;
+ foreach $file (@files) {
+ $et->Options(CharsetFileName => 'UTF8') if $utf8FileName{$file};
+ if (defined $progressMax) {
+ ++$progressCount;
+ $progStr = " [$progressCount/$progressMax]" if $progress;
+ }
+ if ($et->IsDirectory($file)) {
+ $multiFile = $validFile = 1;
+ ScanDir($et, $file, $list);
+ } elsif ($filterFlag and not AcceptFile($file)) {
+ if ($et->Exists($file)) {
+ $filtered = 1;
+ $verbose and print $vout "-------- $file (wrong extension)$progStr\n";
+ } else {
+ Warn "Error: File not found - $file\n";
+ FileNotFound($file);
+ $rtnVal = 1;
+ }
+ } else {
+ $validFile = 1;
+ if ($list) {
+ push(@$list, $file);
+ } else {
+ if (%endDir) {
+ my ($d, $f) = Image::ExifTool::SplitFileName($file);
+ next if $endDir{$d};
+ }
+ GetImageInfo($et, $file);
+ $end and Warn("End called - $file\n");
+ if ($endDir) {
+ Warn("EndDir called - $file\n");
+ my ($d, $f) = Image::ExifTool::SplitFileName($file);
+ $endDir{$d} = 1;
+ undef $endDir;
+ }
+ }
+ }
+ $et->Options(CharsetFileName => $enc) if $utf8FileName{$file};
+ last if $end;
+ }
+}
+
+#------------------------------------------------------------------------------
+# Scan directory for image files
+# Inputs: 0) ExifTool ref, 1) directory name, 2) list ref to return file names
+sub ScanDir($$;$)
+{
+ local $_;
+ my ($et, $dir, $list) = @_;
+ my (@fileList, $done, $file, $utf8Name, $winSurrogate, $endThisDir);
+ my $enc = $et->Options('CharsetFileName');
+ # recode as UTF-8 if necessary
+ if ($enc) {
+ unless ($enc eq 'UTF8') {
+ $dir = $et->Decode($dir, $enc, undef, 'UTF8');
+ $et->Options(CharsetFileName => 'UTF8'); # now using UTF8
+ }
+ $utf8Name = 1;
+ }
+ return if $ignore{$dir};
+ my $oldBase = $seqFileBase;
+ $seqFileBase = $seqFileNum;
+ # use Win32::FindFile on Windows if available
+ # (ReadDir will croak if there is a wildcard, so check for this)
+ if ($^O eq 'MSWin32' and $dir !~ /[*?]/) {
+ undef $evalWarning;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };;
+ if (CheckUTF8($dir, $enc) >= 0) {
+ if (eval { require Win32::FindFile }) {
+ eval {
+ @fileList = Win32::FindFile::ReadDir($dir);
+ $_ = $_->cFileName foreach @fileList;
+ };
+ $@ and $evalWarning = $@;
+ if ($evalWarning) {
+ chomp $evalWarning;
+ $evalWarning =~ s/ at .*//s;
+ Warn "Warning: [Win32::FindFile] $evalWarning - $dir\n";
+ $winSurrogate = 1 if $evalWarning =~ /surrogate/;
+ } else {
+ $et->Options(CharsetFileName => 'UTF8'); # now using UTF8
+ $utf8Name = 1; # ReadDir returns UTF-8 file names
+ $done = 1;
+ }
+ } else {
+ $done = 0;
+ }
+ }
+ }
+ unless ($done) {
+ # use standard perl library routines to read directory
+ unless (opendir(DIR_HANDLE, $dir)) {
+ Warn("Error opening directory $dir\n");
+ $seqFileBase = $oldBase + ($seqFileNum - $seqFileBase);
+ return;
+ }
+ @fileList = readdir(DIR_HANDLE);
+ closedir(DIR_HANDLE);
+ if (defined $done) {
+ # issue warning if some names would have required Win32::FindFile
+ foreach $file ($dir, @fileList) {
+ next unless $file =~ /[\?\x80-\xff]/;
+ WarnOnce("Install Win32::FindFile to support Windows Unicode file names in directories\n");
+ last;
+ }
+ }
+ }
+ $dir =~ /\/$/ or $dir .= '/'; # make sure directory name ends with '/'
+ foreach $file (@fileList) {
+ my $path = "$dir$file";
+ if ($et->IsDirectory($path)) {
+ next unless $recurse;
+ # ignore directories starting with "." by default
+ next if $file =~ /^\./ and ($recurse == 1 or $file eq '.' or $file eq '..');
+ next if $ignore{$file} or ($ignore{SYMLINKS} and -l $path);
+ ScanDir($et, $path, $list);
+ last if $end;
+ next;
+ }
+ next if $endThisDir;
+ # apply rules from -ext options
+ my $accepted;
+ if ($filterFlag) {
+ $accepted = AcceptFile($file) or next;
+ # must be specifically accepted to bypass selection logic
+ $accepted &= 0x01;
+ }
+ unless ($accepted) {
+ # read/write this file if it is a supported type
+ if ($scanWritable) {
+ if ($scanWritable eq '1') {
+ next unless CanWrite($file);
+ } else {
+ my $type = GetFileType($file);
+ next unless defined $type and $type eq $scanWritable;
+ }
+ } elsif (not GetFileType($file)) {
+ next unless $doUnzip;
+ next unless $file =~ /\.(gz|bz2)$/i;
+ }
+ }
+ # Windows patch to avoid replacing filename containing Unicode surrogate with 8.3 name
+ if ($winSurrogate and $isWriting and
+ (not $overwriteOrig or $overwriteOrig != 2) and
+ not $doSetFileName and $file =~ /~/) # (8.3 name will contain a tilde)
+ {
+ Warn("Not writing $path\n");
+ WarnOnce("Use -overwrite_original_in_place to write files with Unicode surrogate characters\n");
+ EFile($file);
+ ++$countBad;
+ next;
+ }
+ $utf8FileName{$path} = 1 if $utf8Name;
+ if ($list) {
+ push(@$list, $path);
+ } else {
+ GetImageInfo($et, $path);
+ if ($end) {
+ Warn("End called - $file\n");
+ last;
+ }
+ if ($endDir) {
+ $path =~ s(/$)();
+ Warn("EndDir called - $path\n");
+ $endDir{$path} = 1;
+ $endThisDir = 1;
+ undef $endDir;
+ }
+ }
+ }
+ ++$countDir;
+ $et->Options(CharsetFileName => $enc); # restore original setting
+ # update sequential file base for parent directory
+ $seqFileBase = $oldBase + ($seqFileNum - $seqFileBase);
+}
+
+#------------------------------------------------------------------------------
+# Find files with wildcard expression on Windows
+# Inputs: 0) ExifTool ref, 1) file name with wildcards
+# Returns: list of matching file names
+# Notes:
+# 1) Win32::FindFile must already be loaded
+# 2) Sets flag in %utf8FileName for each file found
+sub FindFileWindows($$)
+{
+ my ($et, $wildfile) = @_;
+
+ # recode file name as UTF-8 if necessary
+ my $enc = $et->Options('CharsetFileName');
+ $wildfile = $et->Decode($wildfile, $enc, undef, 'UTF8') if $enc and $enc ne 'UTF8';
+ $wildfile =~ tr/\\/\//; # use forward slashes
+ my ($dir, $wildname) = ($wildfile =~ m{(.*/)(.*)}) ? ($1, $2) : ('', $wildfile);
+ if ($dir =~ /[*?]/) {
+ Warn "Wildcards don't work in the directory specification\n";
+ return ();
+ }
+ CheckUTF8($wildfile, $enc) >= 0 or return ();
+ undef $evalWarning;
+ local $SIG{'__WARN__'} = sub { $evalWarning = $_[0] };
+ my @files;
+ eval {
+ my @names = Win32::FindFile::FindFile($wildfile) or return;
+ # (apparently this isn't always sorted, so do a case-insensitive sort here)
+ @names = sort { uc($a) cmp uc($b) } @names;
+ my ($rname, $nm);
+ # replace "\?" with ".", and "\*" with ".*" for regular expression
+ ($rname = quotemeta $wildname) =~ s/\\\?/./g;
+ $rname =~ s/\\\*/.*/g;
+ foreach $nm (@names) {
+ $nm = $nm->cFileName;
+ # make sure that FindFile behaves
+ # (otherwise "*.jpg" matches things like "a.jpg_original"!)
+ next unless $nm =~ /^$rname$/i;
+ next if $nm eq '.' or $nm eq '..'; # don't match "." and ".."
+ my $file = "$dir$nm"; # add back directory name
+ push @files, $file;
+ $utf8FileName{$file} = 1; # flag this file name as UTF-8 encoded
+ }
+ };
+ $@ and $evalWarning = $@;
+ if ($evalWarning) {
+ chomp $evalWarning;
+ $evalWarning =~ s/ at .*//s;
+ Warn "Error: [Win32::FindFile] $evalWarning - $wildfile\n";
+ undef @files;
+ EFile($wildfile);
+ ++$countBad;
+ }
+ return @files;
+}
+
+#------------------------------------------------------------------------------
+# Handle missing file on the command line
+# Inputs: 0) file name
+sub FileNotFound($)
+{
+ my $file = shift;
+ if ($file =~ /^(DIR|FILE)$/) {
+ my $type = { DIR => 'directory', FILE => 'file' }->{$file};
+ Warn qq{You were meant to enter any valid $type name, not "$file" literally.\n};
+ }
+}
+
+#------------------------------------------------------------------------------
+# Patch for OS X 10.6 to preserve file modify date
+# (this probably isn't a 100% fix, but it may solve a majority of the cases)
+sub PreserveTime()
+{
+ local $_;
+ $mt->SetFileTime($_, @{$preserveTime{$_}}) foreach keys %preserveTime;
+ undef %preserveTime;
+}
+
+#------------------------------------------------------------------------------
+# Return absolute path for a file
+# Inputs: 0) file name
+# Returns: absolute path string, or undef if path could not be determined
+# Note: Warnings should be suppressed when calling this routine
+sub AbsPath($)
+{
+ my $file = shift;
+ my $path;
+ if (defined $file and eval { require Cwd }) {
+ $path = eval { Cwd::abs_path($file) };
+ # make the delimiters and case consistent
+ # (abs_path is very inconsistent about what it returns in Windows)
+ if (defined $path and $hasBackslash{$^O}) {
+ $path =~ tr/\\/\//;
+ $path = lc $path;
+ }
+ }
+ return $path;
+}
+
+#------------------------------------------------------------------------------
+# Convert file name to ExifTool Charset
+# Inputs: 0) ExifTool ref, 1) file name in CharsetFileName
+# Returns: file name in ExifTool Charset
+sub MyConvertFileName($$)
+{
+ my ($et, $file) = @_;
+ my $enc = $et->Options('CharsetFileName');
+ $et->Options(CharsetFileName => 'UTF8') if $utf8FileName{$file};
+ my $convFile = $et->ConvertFileName($file);
+ $et->Options(CharsetFileName => $enc) if $utf8FileName{$file};
+ return $convFile;
+}
+
+#------------------------------------------------------------------------------
+# Add print format entry
+# Inputs: 0) expression string
+sub AddPrintFormat($)
+{
+ my $expr = shift;
+ my $type;
+ if ($expr =~ /^#/) {
+ $expr =~ s/^#\[(HEAD|SECT|IF|BODY|ENDS|TAIL)\]// or return; # ignore comments
+ $type = $1;
+ } else {
+ $type = 'BODY';
+ }
+ $printFmt{$type} or $printFmt{$type} = [ ];
+ push @{$printFmt{$type}}, $expr;
+ # add to list of requested tags
+ push @requestTags, $expr =~ /\$\{?((?:[-\w]+:)*[-\w?*]+)/g;
+}
+
+#------------------------------------------------------------------------------
+# Get suggested file extension based on tag value for binary output
+# Inputs: 0) ExifTool ref, 1) data ref, 2) tag name
+# Returns: file extension (lower case), or 'dat' if unknown
+sub SuggestedExtension($$$)
+{
+ my ($et, $valPt, $tag) = @_;
+ my $ext;
+ if (not $binaryOutput) {
+ $ext = 'txt';
+ } elsif ($$valPt =~ /^\xff\xd8\xff/) {
+ $ext = 'jpg';
+ } elsif ($$valPt =~ /^(\0\0\0\x0cjP( |\x1a\x1a)\x0d\x0a\x87\x0a|\xff\x4f\xff\x51\0)/) {
+ $ext = 'jp2';
+ } elsif ($$valPt =~ /^(\x89P|\x8aM|\x8bJ)NG\r\n\x1a\n/) {
+ $ext = 'png';
+ } elsif ($$valPt =~ /^GIF8[79]a/) {
+ $ext = 'gif';
+ } elsif ($$valPt =~ /^<\?xpacket/ or $tag eq 'XMP') {
+ $ext = 'xmp';
+ } elsif ($$valPt =~ /^<\?xml/ or $tag eq 'XML') {
+ $ext = 'xml';
+ } elsif ($$valPt =~ /^RIFF....WAVE/s) {
+ $ext = 'wav';
+ } elsif ($tag eq 'OriginalRawFileData' and defined($ext = $et->GetValue('OriginalRawFileName'))) {
+ $ext =~ s/^.*\.//s;
+ $ext = $ext ? lc($ext) : 'raw';
+ } elsif ($tag eq 'EXIF') {
+ $ext = 'exif';
+ } elsif ($tag eq 'ICC_Profile') {
+ $ext = 'icc';
+ } elsif ($$valPt =~ /^(MM\0\x2a|II\x2a\0)/) {
+ $ext = 'tiff';
+ } elsif ($$valPt =~ /^.{4}ftyp(3gp|mp4|f4v|qt )/s) {
+ my %movType = ( 'qt ' => 'mov' );
+ $ext = $movType{$1} || $1;
+ } elsif ($$valPt !~ /^.{0,4096}\0/s) {
+ $ext = 'txt';
+ } elsif ($$valPt =~ /^BM.{15}\0/s) {
+ $ext = 'bmp';
+ } elsif ($$valPt =~ /^CANON OPTIONAL DATA\0/) {
+ $ext = 'vrd';
+ } elsif ($$valPt =~ /^IIII\x04\0\x04\0/) {
+ $ext = 'dr4';
+ } elsif ($$valPt =~ /^(.{10}|.{522})(\x11\x01|\x00\x11)/s) {
+ $ext = 'pict';
+ } else {
+ $ext = 'dat';
+ }
+ return $ext;
+}
+
+#------------------------------------------------------------------------------
+# Load print format file
+# Inputs: 0) file name
+# - saves lines of file to %printFmt list
+# - adds tag names to @tags list
+sub LoadPrintFormat($)
+{
+ my $arg = shift;
+ if (not defined $arg) {
+ Error "Must specify file or expression for -p option\n";
+ } elsif ($arg !~ /\n/ and -f $arg and $mt->Open(\*FMT_FILE, $arg)) {
+ foreach (<FMT_FILE>) {
+ AddPrintFormat($_);
+ }
+ close(FMT_FILE);
+ } else {
+ AddPrintFormat($arg . "\n");
+ }
+}
+
+#------------------------------------------------------------------------------
+# A sort of sprintf for filenames
+# Inputs: 0) format string (%d=dir, %f=file name, %e=ext),
+# 1) source filename or undef to test format string
+# 2-4) [%t %g %s only] tag name, ref to array of group names, suggested extension
+# Returns: new filename or undef on error (or if no file and fmt contains token)
+sub FilenameSPrintf($;$@)
+{
+ my ($fmt, $file, @extra) = @_;
+ local $_;
+ # return format string straight away if no tokens
+ return $fmt unless $fmt =~ /%[-+]?\d*[.:]?\d*[lu]?[dDfFeEtgs]/;
+ return undef unless defined $file;
+ CleanFilename($file); # make sure we are using forward slashes
+ # split filename into directory, file, extension
+ my %part;
+ @part{qw(d f E)} = ($file =~ /^(.*?)([^\/]*?)(\.[^.\/]*)?$/);
+ defined $part{f} or Warn("Error: Bad pattern match for file $file\n"), return undef;
+ if ($part{E}) {
+ $part{e} = substr($part{E}, 1);
+ } else {
+ @part{qw(e E)} = ('', '');
+ }
+ $part{F} = $part{f} . $part{E};
+ ($part{D} = $part{d}) =~ s{/+$}{};
+ @part{qw(t g s)} = @extra;
+ my ($filename, $pos) = ('', 0);
+ while ($fmt =~ /(%([-+]?)(\d*)([.:]?)(\d*)([lu]?)([dDfFeEtgs]))/g) {
+ $filename .= substr($fmt, $pos, pos($fmt) - $pos - length($1));
+ $pos = pos($fmt);
+ my ($sign, $wid, $dot, $skip, $mod, $code) = ($2, $3, $4, $5 || 0, $6, $7);
+ my (@path, $part, $len, $groups);
+ if (lc $code eq 'd' and $dot and $dot eq ':') {
+ # field width applies to directory levels instead of characters
+ @path = split '/', $part{$code};
+ $len = scalar @path;
+ } else {
+ if ($code eq 'g') {
+ $groups = $part{g} || [ ] unless defined $groups;
+ $fmt =~ /\G(\d?)/g; # look for %g1, %g2, etc
+ $part{g} = $$groups[$1 || 0];
+ $pos = pos($fmt);
+ }
+ $part{$code} = '' unless defined $part{$code};
+ $len = length $part{$code};
+ }
+ next unless $skip < $len;
+ $wid = $len - $skip if $wid eq '' or $wid + $skip > $len;
+ $skip = $len - $wid - $skip if $sign eq '-';
+ if (@path) {
+ $part = join('/', @path[$skip..($skip+$wid-1)]);
+ $part .= '/' unless $code eq 'D';
+ } else {
+ $part = substr($part{$code}, $skip, $wid);
+ }
+ $part = ($mod eq 'u') ? uc($part) : lc($part) if $mod;
+ $filename .= $part;
+ }
+ $filename .= substr($fmt, $pos); # add rest of file name
+ # remove double slashes (except at beginning to allow Windows UNC paths)
+ $filename =~ s{(?!^)//}{/}g;
+ return $filename;
+}
+
+#------------------------------------------------------------------------------
+# Convert number to alphabetical index: a, b, c, ... z, aa, ab ...
+# Inputs: 0) number
+# Returns: alphabetical index string
+sub Num2Alpha($)
+{
+ my $num = shift;
+ my $alpha = chr(97 + ($num % 26));
+ while ($num >= 26) {
+ $num = int($num / 26) - 1;
+ $alpha = chr(97 + ($num % 26)) . $alpha;
+ }
+ return $alpha;
+}
+
+#------------------------------------------------------------------------------
+# Expand '%c' and '%C' codes if filename to get next unused file name
+# Inputs: 0) file name format string, 1) filename ok to use even if it exists
+# Returns: new file name
+sub NextUnusedFilename($;$)
+{
+ my ($fmt, $okfile) = @_;
+ return $fmt unless $fmt =~ /%[-+]?\d*\.?\d*[lun]?[cC]/;
+ my %sep = ( '-' => '-', '+' => '_' );
+ my ($copy, $alpha) = (0, 'a');
+ my $seq = $seqFileNum - 1;
+ for (;;) {
+ my ($filename, $pos) = ('', 0);
+ while ($fmt =~ /(%([-+]?)(\d*)(\.?)(\d*)([lun]?)([cC]))/g) {
+ $filename .= substr($fmt, $pos, pos($fmt) - $pos - length($1));
+ $pos = pos($fmt);
+ my ($sign, $wid, $dec, $wid2, $mod, $tok) = ($2, $3 || 0, $4, $5 || 0, $6, $7);
+ my $diff;
+ if ($tok eq 'C') {
+ $diff = $wid - ($sign eq '-' ? $seqFileBase : 0);
+ $wid = $wid2;
+ } else {
+ next unless $dec or $copy;
+ $wid = $wid2 if $wid < $wid2;
+ # add dash or underline separator if '-' or '+' specified
+ $filename .= $sep{$sign} if $sign;
+ }
+ if ($mod and $mod ne 'n') {
+ my $a = $tok eq 'C' ? Num2Alpha($diff + $seq) : $alpha;
+ my $str = ($wid and $wid > length $a) ? 'a' x ($wid - length($a)) : '';
+ $str .= $a;
+ $str = uc $str if $mod eq 'u';
+ $filename .= $str;
+ } else {
+ my $c = $tok eq 'C' ? ($diff + $seq) : $copy;
+ my $num = $c + ($mod ? 1 : 0);
+ $filename .= $wid ? sprintf("%.${wid}d",$num) : $num;
+ }
+ }
+ $filename .= substr($fmt, $pos); # add rest of file name
+ # return now with filename unless file exists
+ return $filename unless ($mt->Exists($filename) and not defined $usedFileName{$filename}) or $usedFileName{$filename};
+ if (defined $okfile) {
+ return $filename if $filename eq $okfile;
+ my ($fn, $ok) = (AbsPath($filename), AbsPath($okfile));
+ return $okfile if defined $fn and defined $ok and $fn eq $ok;
+ }
+ ++$copy;
+ ++$alpha;
+ ++$seq;
+ }
+}
+
+#------------------------------------------------------------------------------
+# Create directory for specified file
+# Inputs: 0) complete file name including path
+# Returns: true if a directory was created
+my $k32CreateDir;
+sub CreateDirectory($)
+{
+ my $file = shift;
+ my ($dir, $created);
+ ($dir = $file) =~ s/[^\/]*$//; # remove filename from path specification
+ if ($dir and not $mt->IsDirectory($dir)) {
+ my @parts = split /\//, $dir;
+ $dir = '';
+ foreach (@parts) {
+ $dir .= $_;
+ if (length $dir and not $mt->IsDirectory($dir) and
+ # don't try to create a network drive root directory
+ not ($hasBackslash{$^O} and $dir =~ m{^//[^/]*$}))
+ {
+ my $success;
+ # create directory since it doesn't exist
+ my $d2 = $dir; # (must make a copy in case EncodeFileName recodes it)
+ if ($mt->EncodeFileName($d2)) {
+ # handle Windows Unicode directory names
+ unless (eval { require Win32::API }) {
+ Error('Install Win32::API to create directories with Unicode names');
+ return 0;
+ }
+ unless ($k32CreateDir) {
+ $k32CreateDir = new Win32::API('KERNEL32', 'CreateDirectoryW', 'PP', 'I');
+ }
+ $success = $k32CreateDir->Call($d2, 0) if $k32CreateDir;
+ } else {
+ $success = mkdir($d2, 0777);
+ }
+ $success or Error("Error creating directory $dir\n"), return 0;
+ $verbose and print $vout "Created directory $dir\n";
+ $created = 1;
+ }
+ $dir .= '/';
+ }
+ ++$countNewDir if $created;
+ }
+ return $created;
+}
+
+#------------------------------------------------------------------------------
+# Open output text file
+# Inputs: 0) file name format string, 1-N) extra arguments for FilenameSPrintf
+# Returns: 0) file reference (or undef on error), 1) file name if opened, 2) append flag
+# Notes: returns reference to STDOUT and no file name if no textOut file needed
+sub OpenOutputFile($;@)
+{
+ my ($file, @args) = @_;
+ my ($fp, $outfile, $append);
+ if ($textOut) {
+ $outfile = $file;
+ CleanFilename($outfile);
+ if ($textOut =~ /%[-+]?\d*[.:]?\d*[lun]?[dDfFeEtgscC]/ or defined $tagOut) {
+ # make filename from printf-like $textOut
+ $outfile = FilenameSPrintf($textOut, $file, @args);
+ return () unless defined $outfile;
+ $outfile = NextUnusedFilename($outfile);
+ CreateDirectory($outfile); # create directory if necessary
+ } else {
+ $outfile =~ s/\.[^.\/]*$//; # remove extension if it exists
+ $outfile .= $textOut;
+ }
+ my $mode = '>';
+ if ($mt->Exists($outfile)) {
+ unless ($textOverwrite) {
+ Warn "Output file $outfile already exists for $file\n";
+ return ();
+ }
+ if ($textOverwrite == 2 or ($textOverwrite == 3 and $created{$outfile})) {
+ $mode = '>>';
+ $append = 1;
+ }
+ }
+ unless ($mt->Open(\*OUTFILE, $outfile, $mode)) {
+ my $what = $mode eq '>' ? 'creating' : 'appending to';
+ Error("Error $what $outfile\n");
+ return ();
+ }
+ binmode(OUTFILE) if $binaryOutput;
+ $fp = \*OUTFILE;
+ } else {
+ $fp = \*STDOUT;
+ }
+ return($fp, $outfile, $append);
+}
+
+#------------------------------------------------------------------------------
+# Filter files based on extension
+# Inputs: 0) file name
+# Returns: 0 = rejected, 1 = specifically accepted, 2 = accepted by default
+# Notes: This routine should only be called if $filterFlag is set
+sub AcceptFile($)
+{
+ my $file = shift;
+ my $ext = ($file =~ /^.*\.(.+)$/s) ? uc($1) : '';
+ return $filterExt{$ext} if defined $filterExt{$ext};
+ return $filterExt{'*'} if defined $filterExt{'*'};
+ return 0 if $filterFlag & 0x02; # reject if accepting specific extensions
+ return 2; # accept by default
+}
+
+#------------------------------------------------------------------------------
+# Slurp file into buffer
+# Inputs: 0) file name, 1) buffer reference
+# Returns: 1 on success
+sub SlurpFile($$)
+{
+ my ($file, $buffPt) = @_;
+ $mt->Open(\*INFILE, $file) or Warn("Error opening file $file\n"), return 0;
+ binmode(INFILE);
+ # (CAREFUL!: must clear buffer first to reset possible utf8 flag because the data
+ # would be corrupted if it was read into a buffer which had the utf8 flag set!)
+ undef $$buffPt;
+ my $bsize = 1024 * 1024;
+ my $num = read(INFILE, $$buffPt, $bsize);
+ unless (defined $num) {
+ close(INFILE);
+ Warn("Error reading $file\n");
+ return 0;
+ }
+ my $bmax = 64 * $bsize;
+ while ($num == $bsize) {
+ $bsize *= 2 if $bsize < $bmax;
+ my $buff;
+ $num = read(INFILE, $buff, $bsize);
+ last unless $num;
+ $$buffPt .= $buff;
+ }
+ close(INFILE);
+ return 1;
+}
+
+
+#------------------------------------------------------------------------------
+# Filter argfile line
+# Inputs: 0) line of argfile
+# Returns: filtered line or undef to ignore
+sub FilterArgfileLine($)
+{
+ my $arg = shift;
+ if ($arg =~ /^#/) { # comment lines begin with '#'
+ return undef unless $arg =~ s/^#\[CSTR\]//;
+ $arg =~ s/[\x0d\x0a]+$//s; # remove trailing newline
+ # escape double quotes, dollar signs and ampersands if they aren't already
+ # escaped by an odd number of backslashes, and escape a single backslash
+ # if it occurs at the end of the string
+ $arg =~ s{\\(.)|(["\$\@]|\\$)}{'\\'.($2 || $1)}sge;
+ $arg = eval qq{"$arg"}; # un-escape characters in C string
+ } else {
+ $arg =~ s/^\s+//; # remove leading white space
+ $arg =~ s/[\x0d\x0a]+$//s; # remove trailing newline
+ # remove white space before, and single space after '=', '+=', '-=' or '<='
+ $arg =~ s/^(-[-:\w]+#?)\s*([-+<]?=) ?/$1$2/;
+ return undef if $arg eq '';
+ }
+ return $arg;
+}
+
+#------------------------------------------------------------------------------
+# Read arguments from -stay_open argfile
+# Inputs: 0) argument list ref
+# Notes: blocks until -execute, -stay_open or -@ option is available
+# (or until there was an error reading from the file)
+sub ReadStayOpen($)
+{
+ my $args = shift;
+ my (@newArgs, $processArgs, $result, $optArgs);
+ my $lastOpt = '';
+ my $unparsed = length $stayOpenBuff;
+ for (;;) {
+ if ($unparsed) {
+ # parse data already read from argfile
+ $result = $unparsed;
+ undef $unparsed;
+ } else {
+ # read more data from argfile
+ # - this read may block (which is good) if reading from a pipe
+ $result = sysread(STAYOPEN, $stayOpenBuff, 65536, length($stayOpenBuff));
+ }
+ if ($result) {
+ my $pos = 0;
+ while ($stayOpenBuff =~ /\n/g) {
+ my $len = pos($stayOpenBuff) - $pos;
+ my $arg = substr($stayOpenBuff, $pos, $len);
+ $pos += $len;
+ $arg = FilterArgfileLine($arg);
+ next unless defined $arg;
+ push @newArgs, $arg;
+ if ($optArgs) {
+ # this is an argument for the last option
+ undef $optArgs;
+ next unless $lastOpt eq '-stay_open' or $lastOpt eq '-@';
+ } else {
+ $optArgs = $optArgs{$arg};
+ $lastOpt = lc $arg;
+ $optArgs = $optArgs{$lastOpt} unless defined $optArgs;
+ next unless $lastOpt =~ /^-execute\d*$/;
+ }
+ $processArgs = 1;
+ last; # process arguments up to this point
+ }
+ next unless $pos; # nothing to do if we didn't read any arguments
+ # keep unprocessed data in buffer
+ $stayOpenBuff = substr($stayOpenBuff, $pos);
+ if ($processArgs) {
+ # process new arguments after -execute or -stay_open option
+ unshift @$args, @newArgs;
+ last;
+ }
+ } elsif ($result == 0) {
+ # sysread() didn't block (eg. when reading from a file),
+ # so wait for a short time (1/100 sec) then try again
+ # Note: may break out of this early if SIGCONT is received
+ select(undef,undef,undef,0.01);
+ } else {
+ Warn "Error reading from ARGFILE\n";
+ close STAYOPEN;
+ $stayOpen = 0;
+ last;
+ }
+ }
+}
+
+#------------------------------------------------------------------------------
+# Add new entry to -efile output file
+# Inputs: 0) file name, 1) -efile option number (0=error, 1=same, 2=failed)
+sub EFile($$)
+{
+ my $entry = shift;
+ my $efile = $efile[shift || 0];
+ if (defined $efile and length $entry and $entry ne '-') {
+ my $err;
+ CreateDirectory($efile);
+ if ($mt->Open(\*EFILE_FILE, $efile, '>>')) {
+ print EFILE_FILE $entry, "\n" or Warn("Error writing to $efile\n"), $err = 1;
+ close EFILE_FILE;
+ } else {
+ Warn("Error opening '${efile}' for append\n");
+ $err = 1;
+ }
+ if ($err) {
+ defined $_ and $_ eq $efile and undef $_ foreach @efile;
+ }
+ }
+}
+
+#------------------------------------------------------------------------------
+# Print list of tags
+# Inputs: 0) message, 1-N) list of tag names
+sub PrintTagList($@)
+{
+ my $msg = shift;
+ print $msg, ":\n" unless $quiet;
+ my $tag;
+ if ($outFormat < 0 and $msg =~ /file extensions$/ and @_) {
+ foreach $tag (@_) {
+ printf(" %-11s %s\n", $tag, GetFileType($tag, 1));
+ }
+ return;
+ }
+ my ($len, $pad) = (0, $quiet ? '' : ' ');
+ foreach $tag (@_) {
+ my $taglen = length($tag);
+ if ($len + $taglen > 77) {
+ print "\n";
+ ($len, $pad) = (0, $quiet ? '' : ' ');
+ }
+ print $pad, $tag;
+ $len += $taglen + 1;
+ $pad = ' ';
+ }
+ @_ or print $pad, '[empty list]';
+ print "\n";
+}
+
+#------------------------------------------------------------------------------
+# Print warnings and errors from info hash
+# Inputs: 0) ExifTool object ref, 1) info hash, 2) file name
+# Returns: true if there was an Error
+sub PrintErrors($$$)
+{
+ my ($et, $info, $file) = @_;
+ my ($tag, $key);
+ foreach $tag (qw(Warning Error)) {
+ next unless $$info{$tag};
+ my @keys = ( $tag );
+ push @keys, sort(grep /^$tag /, keys %$info) if $et->Options('Duplicates');
+ foreach $key (@keys) {
+ Warn "$tag: $info->{$key} - $file\n";
+ }
+ }
+ return $$info{Error};
+}
+
+__END__
+
+=head1 NAME
+
+exiftool - Read and write meta information in files
+
+=head1 SYNOPSIS
+
+=head2 Reading
+
+B<exiftool> [I<OPTIONS>] [-I<TAG>...] [--I<TAG>...] I<FILE>...
+
+=head2 Writing
+
+B<exiftool> [I<OPTIONS>] -I<TAG>[+-E<lt>]=[I<VALUE>]... I<FILE>...
+
+=head2 Copying
+
+B<exiftool> [I<OPTIONS>] B<-tagsFromFile> I<SRCFILE>
+[-I<SRCTAG>[E<gt>I<DSTTAG>]...] I<FILE>...
+
+=head2 Other
+
+B<exiftool> [ B<-ver> |
+B<-list>[B<w>|B<f>|B<r>|B<wf>|B<g>[I<NUM>]|B<d>|B<x>] ]
+
+For specific examples, see the L<EXAMPLES|/READING EXAMPLES> sections below.
+
+This documentation is displayed if exiftool is run without an input I<FILE>
+when one is expected.
+
+=head1 DESCRIPTION
+
+A command-line interface to L<Image::ExifTool|Image::ExifTool>, used for
+reading and writing meta information in a variety of file types. I<FILE> is
+one or more source file names, directory names, or C<-> for the standard
+input. Metadata is read from source files and printed in readable form to
+the console (or written to output text files with B<-w>).
+
+To write or delete metadata, tag values are assigned using
+-I<TAG>=[I<VALUE>], and/or the B<-geotag>, B<-csv=> or B<-json=> options.
+To copy or move metadata, the B<-tagsFromFile> feature is used. By default
+the original files are preserved with C<_original> appended to their names
+-- be sure to verify that the new files are OK before erasing the originals.
+Once in write mode, exiftool will ignore any read-specific options.
+
+Note: If I<FILE> is a directory name then only supported file types in the
+directory are processed (in write mode only writable types are processed).
+However, files may be specified by name, or the B<-ext> option may be used
+to force processing of files with any extension. Hidden files in the
+directory are also processed. Adding the B<-r> option causes subdirectories
+to be processed recursively, but subdirectories with names beginning with
+"." are skipped unless B<-r.> is used.
+
+Below is a list of file types and meta information formats currently
+supported by ExifTool (r = read, w = write, c = create):
+
+ File Types
+ ------------+-------------+-------------+-------------+------------
+ 360 r/w | DPX r | ITC r | ODP r | RIFF r
+ 3FR r | DR4 r/w/c | J2C r | ODS r | RSRC r
+ 3G2 r/w | DSS r | JNG r/w | ODT r | RTF r
+ 3GP r/w | DV r | JP2 r/w | OFR r | RW2 r/w
+ A r | DVB r/w | JPEG r/w | OGG r | RWL r/w
+ AA r | DVR-MS r | JSON r | OGV r | RWZ r
+ AAE r | DYLIB r | K25 r | ONP r | RM r
+ AAX r/w | EIP r | KDC r | OPUS r | SEQ r
+ ACR r | EPS r/w | KEY r | ORF r/w | SKETCH r
+ AFM r | EPUB r | LA r | OTF r | SO r
+ AI r/w | ERF r/w | LFP r | PAC r | SR2 r/w
+ AIFF r | EXE r | LNK r | PAGES r | SRF r
+ APE r | EXIF r/w/c | LRV r/w | PBM r/w | SRW r/w
+ ARQ r/w | EXR r | M2TS r | PCD r | SVG r
+ ARW r/w | EXV r/w/c | M4A/V r/w | PCX r | SWF r
+ ASF r | F4A/V r/w | MACOS r | PDB r | THM r/w
+ AVI r | FFF r/w | MAX r | PDF r/w | TIFF r/w
+ AVIF r/w | FITS r | MEF r/w | PEF r/w | TORRENT r
+ AZW r | FLA r | MIE r/w/c | PFA r | TTC r
+ BMP r | FLAC r | MIFF r | PFB r | TTF r
+ BPG r | FLIF r/w | MKA r | PFM r | TXT r
+ BTF r | FLV r | MKS r | PGF r | VCF r
+ CHM r | FPF r | MKV r | PGM r/w | VRD r/w/c
+ COS r | FPX r | MNG r/w | PLIST r | VSD r
+ CR2 r/w | GIF r/w | MOBI r | PICT r | WAV r
+ CR3 r/w | GPR r/w | MODD r | PMP r | WDP r/w
+ CRM r/w | GZ r | MOI r | PNG r/w | WEBP r
+ CRW r/w | HDP r/w | MOS r/w | PPM r/w | WEBM r
+ CS1 r/w | HDR r | MOV r/w | PPT r | WMA r
+ CSV r | HEIC r/w | MP3 r | PPTX r | WMV r
+ CZI r | HEIF r/w | MP4 r/w | PS r/w | WTV r
+ DCM r | HTML r | MPC r | PSB r/w | WV r
+ DCP r/w | ICC r/w/c | MPG r | PSD r/w | X3F r/w
+ DCR r | ICS r | MPO r/w | PSP r | XCF r
+ DFONT r | IDML r | MQV r/w | QTIF r/w | XLS r
+ DIVX r | IIQ r/w | MRW r/w | R3D r | XLSX r
+ DJVU r | IND r/w | MXF r | RA r | XMP r/w/c
+ DLL r | INSP r/w | NEF r/w | RAF r/w | ZIP r
+ DNG r/w | INSV r | NRW r/w | RAM r |
+ DOC r | INX r | NUMBERS r | RAR r |
+ DOCX r | ISO r | O r | RAW r/w |
+
+ Meta Information
+ ----------------------+----------------------+---------------------
+ EXIF r/w/c | CIFF r/w | Ricoh RMETA r
+ GPS r/w/c | AFCP r/w | Picture Info r
+ IPTC r/w/c | Kodak Meta r/w | Adobe APP14 r
+ XMP r/w/c | FotoStation r/w | MPF r
+ MakerNotes r/w/c | PhotoMechanic r/w | Stim r
+ Photoshop IRB r/w/c | JPEG 2000 r | DPX r
+ ICC Profile r/w/c | DICOM r | APE r
+ MIE r/w/c | Flash r | Vorbis r
+ JFIF r/w/c | FlashPix r | SPIFF r
+ Ducky APP12 r/w/c | QuickTime r | DjVu r
+ PDF r/w/c | Matroska r | M2TS r
+ PNG r/w/c | MXF r | PE/COFF r
+ Canon VRD r/w/c | PrintIM r | AVCHD r
+ Nikon Capture r/w/c | FLAC r | ZIP r
+ GeoTIFF r/w/c | ID3 r | (and more)
+
+=head1 OPTIONS
+
+Case is not significant for any command-line option (including tag and group
+names), except for single-character options when the corresponding
+upper-case option exists. Many single-character options have equivalent
+long-name versions (shown in brackets), and some options have inverses which
+are invoked with a leading double-dash. Unrecognized options are
+interpreted as tag names (for this reason, multiple single-character options
+may NOT be combined into one argument). Contrary to standard practice,
+options may appear after source file names on the exiftool command line.
+
+=head2 Option Overview
+
+L<Tag operations|/Tag operations>
+
+ -TAG or --TAG Extract or exclude specified tag
+ -TAG[+-^]=[VALUE] Write new value for tag
+ -TAG[+-]<=DATFILE Write tag value from contents of file
+ -TAG[+-]<SRCTAG Copy tag value (see -tagsFromFile)
+
+ -tagsFromFile SRCFILE Copy tag values from file
+ -x TAG (-exclude) Exclude specified tag
+
+L<Input-output text formatting|/Input-output text formatting>
+
+ -args (-argFormat) Format metadata as exiftool arguments
+ -b (-binary) Output metadata in binary format
+ -c FMT (-coordFormat) Set format for GPS coordinates
+ -charset [[TYPE=]CHARSET] Specify encoding for special characters
+ -csv[[+]=CSVFILE] Export/import tags in CSV format
+ -csvDelim STR Set delimiter for CSV file
+ -d FMT (-dateFormat) Set format for date/time values
+ -D (-decimal) Show tag ID numbers in decimal
+ -E,-ex,-ec (-escape(HTML|XML|C))Escape tag values for HTML, XML or C
+ -f (-forcePrint) Force printing of all specified tags
+ -g[NUM...] (-groupHeadings) Organize output by tag group
+ -G[NUM...] (-groupNames) Print group name for each tag
+ -h (-htmlFormat) Use HTML formatting for output
+ -H (-hex) Show tag ID numbers in hexadecimal
+ -htmlDump[OFFSET] Generate HTML-format binary dump
+ -j[[+]=JSONFILE] (-json) Export/import tags in JSON format
+ -l (-long) Use long 2-line output format
+ -L (-latin) Use Windows Latin1 encoding
+ -lang [LANG] Set current language
+ -listItem INDEX Extract specific item from a list
+ -n (--printConv) No print conversion
+ -p FMTFILE (-printFormat) Print output in specified format
+ -php Export tags as a PHP Array
+ -s[NUM] (-short) Short output format
+ -S (-veryShort) Very short output format
+ -sep STR (-separator) Set separator string for list items
+ -sort Sort output alphabetically
+ -struct Enable output of structured information
+ -t (-tab) Output in tab-delimited list format
+ -T (-table) Output in tabular format
+ -v[NUM] (-verbose) Print verbose messages
+ -w[+|!] EXT (-textOut) Write (or overwrite!) output text files
+ -W[+|!] FMT (-tagOut) Write output text file for each tag
+ -Wext EXT (-tagOutExt) Write only specified file types with -W
+ -X (-xmlFormat) Use RDF/XML output format
+
+L<Processing control|/Processing control>
+
+ -a (-duplicates) Allow duplicate tags to be extracted
+ -e (--composite) Do not generate composite tags
+ -ee (-extractEmbedded) Extract information from embedded files
+ -ext[+] EXT (-extension) Process files with specified extension
+ -F[OFFSET] (-fixBase) Fix the base for maker notes offsets
+ -fast[NUM] Increase speed when extracting metadata
+ -fileOrder[NUM] [-]TAG Set file processing order
+ -i DIR (-ignore) Ignore specified directory name
+ -if[NUM] EXPR Conditionally process files
+ -m (-ignoreMinorErrors) Ignore minor errors and warnings
+ -o OUTFILE (-out) Set output file or directory name
+ -overwrite_original Overwrite original by renaming tmp file
+ -overwrite_original_in_place Overwrite original by copying tmp file
+ -P (-preserve) Preserve file modification date/time
+ -password PASSWD Password for processing protected files
+ -progress[:[TITLE]] Show file progress count
+ -q (-quiet) Quiet processing
+ -r[.] (-recurse) Recursively process subdirectories
+ -scanForXMP Brute force XMP scan
+ -u (-unknown) Extract unknown tags
+ -U (-unknown2) Extract unknown binary tags too
+ -wm MODE (-writeMode) Set mode for writing/creating tags
+ -z (-zip) Read/write compressed information
+
+L<Other options|/Other options>
+
+ -@ ARGFILE Read command-line arguments from file
+ -k (-pause) Pause before terminating
+ -list[w|f|wf|g[NUM]|d|x] List various exiftool capabilities
+ -ver Print exiftool version number
+ -- End of options
+
+L<Special features|/Special features>
+
+ -geotag TRKFILE Geotag images from specified GPS log
+ -globalTimeShift SHIFT Shift all formatted date/time values
+ -use MODULE Add features from plug-in module
+
+L<Utilities|/Utilities>
+
+ -delete_original[!] Delete "_original" backups
+ -restore_original Restore from "_original" backups
+
+L<Advanced options|/Advanced options>
+
+ -api OPT[[^]=[VAL]] Set ExifTool API option
+ -common_args Define common arguments
+ -config CFGFILE Specify configuration file name
+ -echo[NUM] TEXT Echo text to stdout or stderr
+ -efile[NUM][!] ERRFILE Save names of files with errors
+ -execute[NUM] Execute multiple commands on one line
+ -srcfile FMT Process a different source file
+ -stay_open FLAG Keep reading -@ argfile even after EOF
+ -userParam PARAM[[^]=[VAL]] Set user parameter (API UserParam opt)
+
+=head2 Option Details
+
+=head3 Tag operations
+
+=over 5
+
+=item B<->I<TAG>
+
+Extract information for the specified tag (eg. C<-CreateDate>). Multiple
+tags may be specified in a single command. A tag name is the handle by
+which a piece of information is referenced. See
+L<Image::ExifTool::TagNames|Image::ExifTool::TagNames> for documentation on
+available tag names. A tag name may include leading group names separated
+by colons (eg. C<-EXIF:CreateDate>, or C<-Doc1:XMP:Creator>), and each group
+name may be prefixed by a digit to specify family number (eg.
+C<-1IPTC:City>). Use the B<-listg> option to list available group names by
+family.
+
+A special tag name of C<All> may be used to indicate all meta information
+(ie. B<-All>). This is particularly useful when a group name is specified
+to extract all information in a group (but beware that unless the B<-a>
+option is also used, some tags in the group may be suppressed by same-named
+tags in other groups). The wildcard characters C<?> and C<*> may be used in
+a tag name to match any single character and zero or more characters
+respectively. These may not be used in a group name, with the exception that
+a group name of C<*> (or C<All>) may be used to extract all instances of a
+tag (as if B<-a> was used). Note that arguments containing wildcards must
+be quoted on the command line of most systems to prevent shell globbing.
+
+A C<#> may be appended to the tag name to disable the print conversion on a
+per-tag basis (see the B<-n> option). This may also be used when writing or
+copying tags.
+
+If no tags are specified, all available information is extracted (as if
+C<-All> had been specified).
+
+Note: Descriptions, not tag names, are shown by default when extracting
+information. Use the B<-s> option to see the tag names instead.
+
+=item B<-->I<TAG>
+
+Exclude specified tag from extracted information. Same as the B<-x> option.
+Group names and wildcards are permitted as described above for B<-TAG>.
+Once excluded from the output, a tag may not be re-included by a subsequent
+option. May also be used following a B<-tagsFromFile> option to exclude
+tags from being copied (when redirecting to another tag, it is the source
+tag that should be excluded), or to exclude groups from being deleted when
+deleting all information (eg. C<-all= --exif:all> deletes all but EXIF
+information). But note that this will not exclude individual tags from a
+group delete (unless a family 2 group is specified, see note 4 below).
+Instead, individual tags may be recovered using the B<-tagsFromFile> option
+(eg. C<-all= -tagsfromfile @ -artist>).
+
+=item B<->I<TAG>[+-^]B<=>[I<VALUE>]
+
+Write a new value for the specified tag (eg. C<-comment=wow>), or delete the
+tag if no I<VALUE> is given (eg. C<-comment=>). C<+=> and C<-=> are used to
+add or remove existing entries from a list, or to shift date/time values
+(see L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> and note 6 below
+for more details). C<+=> may also be used to increment numerical values (or
+decrement if I<VALUE> is negative), and C<-=> may be used to conditionally
+delete or replace a tag (see L</WRITING EXAMPLES> for examples). C<^=> is
+used to write an empty string instead of deleting the tag when no I<VALUE>
+is given, but otherwise it is equivalent to C<=>.
+
+I<TAG> may contain one or more leading family 0, 1, 2 or 7 group names,
+prefixed by optional family numbers, and separated colons. If no group name
+is specified, the tag is created in the preferred group, and updated in any
+other location where a same-named tag already exists. The preferred group
+is the first group in the following list where I<TAG> is valid: 1) EXIF, 2)
+IPTC, 3) XMP.
+
+The wildcards C<*> and C<?> may be used in tag names to assign the same
+value to multiple tags. When specified with wildcards, "unsafe" tags are
+not written. A tag name of C<All> is equivalent to C<*> (except that it
+doesn't require quoting, while arguments with wildcards do on systems with
+shell globbing), and is often used when deleting all metadata (ie. C<-All=>)
+or an entire group (eg. C<-XMP-dc:All=>, see note 4 below). Note that not
+all groups are deletable, and that the JPEG APP14 "Adobe" group is not
+removed by default with C<-All=> because it may affect the appearance of the
+image. However, color space information is removed, so the colors may be
+affected (but this may be avoided by copying back the tags defined by the
+ColorSpaceTags shortcut). Use the B<-listd> option for a complete list of
+deletable groups, and see note 5 below regarding the "APP" groups. Also,
+within an image some groups may be contained within others, and these groups
+are removed if the containing group is deleted:
+
+ JPEG Image:
+ - Deleting EXIF or IFD0 also deletes ExifIFD, GlobParamIFD,
+ GPS, IFD1, InteropIFD, MakerNotes, PrintIM and SubIFD.
+ - Deleting ExifIFD also deletes InteropIFD and MakerNotes.
+ - Deleting Photoshop also deletes IPTC.
+
+ TIFF Image:
+ - Deleting EXIF only removes ExifIFD which also deletes
+ InteropIFD and MakerNotes.
+
+Notes:
+
+1) B<Many tag values may be assigned in a single command>. If two
+assignments affect the same tag, the latter takes precedence (except for
+list-type tags, for which both values are written).
+
+2) In general, MakerNotes tags are considered "Permanent", and may be edited
+but not created or deleted individually. This avoids many potential
+problems, including the inevitable compatibility problems with OEM software
+which may be very inflexible about the information it expects to find in the
+maker notes.
+
+3) Changes to PDF files by ExifTool are reversible (by deleting the update
+with C<-PDF-update:all=>) because the original information is never actually
+deleted from the file. So ExifTool alone may not be used to securely edit
+metadata in PDF files.
+
+4) Specifying C<-GROUP:all=> deletes the entire group as a block only if a
+single family 0 or 1 group is specified. Otherwise all deletable tags in
+the specified group(s) are removed individually, and in this case is it
+possible to exclude individual tags from a mass delete. For example,
+C<-time:all --Exif:Time:All> removes all deletable Time tags except those in
+the EXIF. This difference also applies if family 2 is specified when
+deleting all groups. For example, C<-2all:all=> deletes tags individually,
+while C<-all:all=> deletes entire blocks.
+
+5) The "APP" group names ("APP0" through "APP15") are used to delete JPEG
+application segments which are not associated with another deletable group.
+For example, specifying C<-APP14:All=> will NOT delete the APP14 "Adobe"
+segment because this is accomplished with C<-Adobe:All>.
+
+6) When shifting a value, the shift is applied to the original value of the
+tag, overriding any other values previously assigned to the tag on the same
+command line. To shift a date/time value and copy it to another tag in the
+same operation, use the B<-globalTimeShift> option.
+
+Special feature: Integer values may be specified in hexadecimal with a
+leading C<0x>, and simple rational values may be specified as fractions.
+
+=item B<->I<TAG>E<lt>=I<DATFILE> or B<->I<TAG>E<lt>=I<FMT>
+
+Set the value of a tag from the contents of file I<DATFILE>. The file name
+may also be given by a I<FMT> string where %d, %f and %e represent the
+directory, file name and extension of the original I<FILE> (see the B<-w>
+option for more details). Note that quotes are required around this
+argument to prevent shell redirection since it contains a C<E<lt>> symbol.
+If I<DATFILE>/I<FMT> is not provided, the effect is the same as C<-TAG=>,
+and the tag is simply deleted. C<+E<lt>=> or C<-E<lt>=> may also be used to
+add or delete specific list entries, or to shift date/time values.
+
+=item B<-tagsFromFile> I<SRCFILE> or I<FMT>
+
+Copy tag values from I<SRCFILE> to I<FILE>. Tag names on the command line
+after this option specify the tags to be copied, or excluded from the copy.
+Wildcards are permitted in these tag names. If no tags are specified, then
+all possible tags (see note 1 below) from the source file are copied to
+same-named tags in the preferred location of the output file (the same as
+specifying C<-all>). More than one B<-tagsFromFile> option may be used to
+copy tags from multiple files.
+
+By default, this option will update any existing and writable same-named
+tags in the output I<FILE>, but will create new tags only in their preferred
+groups. This allows some information to be automatically transferred to the
+appropriate group when copying between images of different formats. However,
+if a group name is specified for a tag then the information is written only
+to this group (unless redirected to another group, see below). If C<All> is
+used as a group name, then the specified tag(s) are written to the same
+family 1 group they had in the source file (ie. the same specific location,
+like ExifIFD or XMP-dc). For example, the common operation of copying all
+writable tags to the same specific locations in the output I<FILE> is
+achieved by adding C<-all:all>. A different family may be specified by
+adding a leading family number to the group name (eg. C<-0all:all> preserves
+the same general location, like EXIF or XMP).
+
+I<SRCFILE> may be the same as I<FILE> to move information around within a
+single file. In this case, C<@> may be used to represent the source file
+(ie. C<-tagsFromFile @>), permitting this feature to be used for batch
+processing multiple files. Specified tags are then copied from each file in
+turn as it is rewritten. For advanced batch use, the source file name may
+also be specified using a I<FMT> string in which %d, %f and %e represent the
+directory, file name and extension of I<FILE>. (eg. the current I<FILE>
+would be represented by C<%d%f.%e>, with the same effect as C<@>). See the
+B<-w> option for I<FMT> string examples.
+
+A powerful redirection feature allows a destination tag to be specified for
+each copied tag. With this feature, information may be written to a tag
+with a different name or group. This is done using
+E<quot>'-I<DSTTAG>E<lt>I<SRCTAG>'E<quot> or
+E<quot>'-I<SRCTAG>E<gt>I<DSTTAG>'E<quot> on the command line after
+B<-tagsFromFile>, and causes the value of I<SRCTAG> to be copied from
+I<SRCFILE> and written to I<DSTTAG> in I<FILE>. Has no effect unless
+I<SRCTAG> exists in I<SRCFILE>. Note that this argument must be quoted to
+prevent shell redirection, and there is no C<=> sign as when assigning new
+values. Source and/or destination tags may be prefixed by a group name
+and/or suffixed by C<#>. Wildcards are allowed in both the source and
+destination tag names. A destination group and/or tag name of C<All> or
+C<*> writes to the same family 1 group and/or tag name as the source. If no
+destination group is specified, the information is written to the preferred
+group. Whitespace around the C<E<gt>> or C<E<lt>> is ignored. As a
+convenience, C<-tagsFromFile @> is assumed for any redirected tags which are
+specified without a prior B<-tagsFromFile> option. Copied tags may also be
+added or deleted from a list with arguments of the form
+E<quot>'-I<SRCTAG>+E<lt>I<DSTTAG>'E<quot> or
+E<quot>'-I<SRCTAG>-E<lt>I<DSTTAG>'E<quot> (but see Note 5 below).
+
+An extension of the redirection feature allows strings involving tag names
+to be used on the right hand side of the C<E<lt>> symbol with the syntax
+E<quot>'-I<DSTTAG>E<lt>I<STR>'E<quot>, where tag names in I<STR> are
+prefixed with a C<$> symbol. See the B<-p> option and the
+L</Advanced formatting feature> section for more details about this syntax.
+Strings starting with a C<=> sign must insert a single space after the
+C<E<lt>> to avoid confusion with the C<E<lt>=> operator which sets the tag
+value from the contents of a file. A single space at the start of the
+string is removed if it exists, but all other whitespace in the string is
+preserved. See note 8 below about using the redirection feature with
+list-type stags, shortcuts or when using wildcards in tag names.
+
+See L</COPYING EXAMPLES> for examples using B<-tagsFromFile>.
+
+Notes:
+
+1) Some tags (generally tags which may affect the appearance of the image)
+are considered "unsafe" to write, and are only copied if specified
+explicitly (ie. no wildcards). See the
+L<tag name documentation|Image::ExifTool::TagNames> for more details about
+"unsafe" tags.
+
+2) Be aware of the difference between excluding a tag from being copied
+(--I<TAG>), and deleting a tag (-I<TAG>=). Excluding a tag prevents it from
+being copied to the destination image, but deleting will remove a
+pre-existing tag from the image.
+
+3) The maker note information is copied as a block, so it isn't affected
+like other information by subsequent tag assignments on the command line,
+and individual makernote tags may not be excluded from a block copy. Also,
+since the PreviewImage referenced from the maker notes may be rather large,
+it is not copied, and must be transferred separately if desired.
+
+4) The order of operations is to copy all specified tags at the point of the
+B<-tagsFromFile> option in the command line. Any tag assignment to the
+right of the B<-tagsFromFile> option is made after all tags are copied. For
+example, new tag values are set in the order One, Two, Three then Four with
+this command:
+
+ exiftool -One=1 -tagsFromFile s.jpg -Two -Four=4 -Three d.jpg
+
+This is significant in the case where an overlap exists between the copied
+and assigned tags because later operations may override earlier ones.
+
+5) The normal behaviour of copied tags differs from that of assigned tags
+for list-type tags and conditional replacements because each copy operation
+on a tag overrides any previous operations. While this avoids duplicate
+list items when copying groups of tags from a file containing redundant
+information, it also prevents values of different tags from being copied
+into the same list when this is the intent. So a B<-addTagsFromFile> option
+is provided which allows copying of multiple tags into the same list. eg)
+
+ exiftool -addtagsfromfile @ '-subject<make' '-subject<model' ...
+
+Similarly, B<-addTagsFromFile> must be used when conditionally replacing a
+tag to prevent overriding earlier conditions.
+
+Other than these differences, the B<-tagsFromFile> and B<-addTagsFromFile>
+options are equivalent.
+
+6) The B<-a> option (allow duplicate tags) is always in effect when copying
+tags from I<SRCFILE>, but the highest priority tag is always copied last so
+it takes precedence.
+
+7) Structured tags are copied by default when copying tags. See the
+B<-struct> option for details.
+
+8) With the redirection feature, copying a tag directly (ie.
+E<quot>'-I<DSTTAG>E<lt>I<SRCTAG>'E<quot>) is not the same as interpolating
+its value inside a string (ie. E<quot>'-I<DSTTAG>E<lt>$I<SRCTAG>'E<quot>)
+for list-type tags, L<shortcut tags|Image::ExifTool::Shortcuts>, tag names
+containing wildcards, or UserParam variables. When copying directly, the
+values of each matching source tag are copied individually to the
+destination tag (as if they were separate assignments). However, when
+interpolated inside a string, list items and the values of shortcut tags are
+concatenated (with a separator set by the B<-sep> option), and wildcards are
+not allowed. Also, UserParam variables are available only when interpolated
+in a string. Another difference is that a minor warning is generated if a
+tag doesn't exist when interpolating its value in a string (with C<$>), but
+isn't when copying the tag directly.
+
+Finally, the behaviour is different when a destination tag or group of
+C<All> is used. When copying directly, a destination group and/or tag name
+of C<All> writes to the same family 1 group and/or tag name as the source.
+But when interpolated in a string, the identity of the source tags are lost
+and the value is written to all possible groups/tags. For example, the
+string form must be used in the following command since the intent is to set
+the value of all existing date/time tags from C<CreateDate>:
+
+ exiftool "-time:all<$createdate" -wm w FILE
+
+=item B<-x> I<TAG> (B<-exclude>)
+
+Exclude the specified tag. There may be multiple B<-x> options. This has
+the same effect as --I<TAG> on the command line. See the --I<TAG>
+documentation above for a complete description.
+
+=back
+
+=head3 Input-output text formatting
+
+Note that trailing spaces are removed from extracted values for most output
+text formats. The exceptions are C<-b>, C<-csv>, C<-j> and C<-X>.
+
+=over 5
+
+=item B<-args> (B<-argFormat>)
+
+Output information in the form of exiftool arguments, suitable for use with
+the B<-@> option when writing. May be combined with the B<-G> option to
+include group names. This feature may be used to effectively copy tags
+between images, but allows the metadata to be altered by editing the
+intermediate file (C<out.args> in this example):
+
+ exiftool -args -G1 --filename --directory src.jpg > out.args
+ exiftool -@ out.args -sep ", " dst.jpg
+
+Note: Be careful when copying information with this technique since it is
+easy to write tags which are normally considered "unsafe". For instance,
+the FileName and Directory tags are excluded in the example above to avoid
+renaming and moving the destination file. Also note that the second command
+above will produce warning messages for any tags which are not writable.
+
+As well, the B<-sep> option should be used as in the second command above to
+maintain separate list items when writing metadata back to image files, and
+the B<-struct> option may be used when extracting to preserve structured XMP
+information.
+
+=item B<-b> (B<-binary>)
+
+Output requested metadata in binary format without tag names or
+descriptions. This option is mainly used for extracting embedded images or
+other binary data, but it may also be useful for some text strings since
+control characters (such as newlines) are not replaced by '.' as they are in
+the default output. By default, list items are separated by a newline when
+extracted with the B<-b> option, but this may be changed (see the B<-sep>
+option for details). May be combined with C<-j>, C<-php> or C<-X> to
+extract binary data in JSON, PHP or XML format, but note that "unsafe" tags
+must be specified explicitly to be extracted as binary in these formats.
+
+=item B<-c> I<FMT> (B<-coordFormat>)
+
+Set the print format for GPS coordinates. I<FMT> uses the same syntax as
+a C<printf> format string. The specifiers correspond to degrees, minutes
+and seconds in that order, but minutes and seconds are optional. For
+example, the following table gives the output for the same coordinate using
+various formats:
+
+ FMT Output
+ ------------------- ------------------
+ "%d deg %d' %.2f"\" 54 deg 59' 22.80" (default for reading)
+ "%d %d %.8f" 54 59 22.80000000 (default for copying)
+ "%d deg %.4f min" 54 deg 59.3800 min
+ "%.6f degrees" 54.989667 degrees
+
+Notes:
+
+1) To avoid loss of precision, the default coordinate format is different
+when copying tags using the B<-tagsFromFile> option.
+
+2) If the hemisphere is known, a reference direction (N, S, E or W) is
+appended to each printed coordinate, but adding a C<+> to the format
+specifier (eg. C<%+.6f>) prints a signed coordinate instead.
+
+3) This print formatting may be disabled with the B<-n> option to extract
+coordinates as signed decimal degrees.
+
+=item B<-charset> [[I<TYPE>=]I<CHARSET>]
+
+If I<TYPE> is C<ExifTool> or not specified, this option sets the ExifTool
+character encoding for output tag values when reading and input values when
+writing, with a default of C<UTF8>. If no I<CHARSET> is given, a list of
+available character sets is returned. Valid I<CHARSET> values are:
+
+ CHARSET Alias(es) Description
+ ---------- --------------- ----------------------------------
+ UTF8 cp65001, UTF-8 UTF-8 characters (default)
+ Latin cp1252, Latin1 Windows Latin1 (West European)
+ Latin2 cp1250 Windows Latin2 (Central European)
+ Cyrillic cp1251, Russian Windows Cyrillic
+ Greek cp1253 Windows Greek
+ Turkish cp1254 Windows Turkish
+ Hebrew cp1255 Windows Hebrew
+ Arabic cp1256 Windows Arabic
+ Baltic cp1257 Windows Baltic
+ Vietnam cp1258 Windows Vietnamese
+ Thai cp874 Windows Thai
+ DOSLatinUS cp437 DOS Latin US
+ DOSLatin1 cp850 DOS Latin1
+ DOSCyrillic cp866 DOS Cyrillic
+ MacRoman cp10000, Roman Macintosh Roman
+ MacLatin2 cp10029 Macintosh Latin2 (Central Europe)
+ MacCyrillic cp10007 Macintosh Cyrillic
+ MacGreek cp10006 Macintosh Greek
+ MacTurkish cp10081 Macintosh Turkish
+ MacRomanian cp10010 Macintosh Romanian
+ MacIceland cp10079 Macintosh Icelandic
+ MacCroatian cp10082 Macintosh Croatian
+
+I<TYPE> may be C<FileName> to specify the encoding of file names on the
+command line (ie. I<FILE> arguments). In Windows, this triggers use of
+wide-character i/o routines, thus providing support for Unicode file names.
+See the L</WINDOWS UNICODE FILE NAMES> section below for details.
+
+Other values of I<TYPE> listed below are used to specify the internal
+encoding of various meta information formats.
+
+ TYPE Description Default
+ --------- ------------------------------------------- -------
+ EXIF Internal encoding of EXIF "ASCII" strings (none)
+ ID3 Internal encoding of ID3v1 information Latin
+ IPTC Internal IPTC encoding to assume when Latin
+ IPTC:CodedCharacterSet is not defined
+ Photoshop Internal encoding of Photoshop IRB strings Latin
+ QuickTime Internal encoding of QuickTime strings MacRoman
+ RIFF Internal encoding of RIFF strings 0
+
+See L<https://exiftool.org/faq.html#Q10> for more information about coded
+character sets, and the L<Image::ExifTool Options|Image::ExifTool/Options>
+for more details about the B<-charset> settings.
+
+=item B<-csv>[[+]=I<CSVFILE>]
+
+Export information in CSV format, or import information if I<CSVFILE> is
+specified. When importing, the CSV file must be in exactly the same format
+as the exported file. The first row of the I<CSVFILE> must be the ExifTool
+tag names (with optional group names) for each column of the file, and
+values must be separated by commas. A special "SourceFile" column specifies
+the files associated with each row of information (and a SourceFile of "*"
+may be used to define default tags to be imported for all files which are
+combined with any tags specified for the specific SourceFile processed). The
+B<-csvDelim> option may be used to change the input/output field delimiter
+if something other than a comma is required.
+
+The following examples demonstrate basic use of the B<-csv> option:
+
+ # generate CSV file with common tags from all images in a directory
+ exiftool -common -csv dir > out.csv
+
+ # update metadata for all images in a directory from CSV file
+ exiftool -csv=a.csv dir
+
+Empty values are ignored when importing (unless the B<-f> option is used and
+the API MissingTagValue is set to an empty string, in which case the tag is
+deleted). Also, FileName and Directory columns are ignored if they exist
+(ie. ExifTool will not attempt to write these tags with a CSV import). To
+force a tag to be deleted, use the B<-f> option and set the value to "-" in
+the CSV file (or to the MissingTagValue if this API option was used).
+Multiple databases may be imported in a single command.
+
+When exporting a CSV file, the B<-g> or B<-G> option adds group names to the
+tag headings. If the B<-a> option is used to allow duplicate tag names, the
+duplicate tags are only included in the CSV output if the column headings
+are unique. Adding the B<-G4> option ensures a unique column heading for
+each tag. The B<-b> option may be added to output binary data, encoded in
+base64 if necessary (indicated by ASCII "base64:" as the first 7 bytes of
+the value). Values may also be encoded in base64 if the B<-charset> option
+is used and the value contains invalid characters.
+
+When exporting specific tags, the CSV columns are arranged in the same order
+as the specified tags provided the column headings exactly match the
+specified tag names, otherwise the columns are sorted in alphabetical order.
+
+When importing from a CSV file, only files specified on the command line are
+processed. Any extra entries in the CSV file are ignored.
+
+List-type tags are stored as simple strings in a CSV file, but the B<-sep>
+option may be used to split them back into separate items when importing.
+
+Special feature: B<-csv>+=I<CSVFILE> may be used to add items to existing
+lists. This affects only list-type tags. Also applies to the B<-j> option.
+
+Note that this option is fundamentally different than all other output
+format options because it requires information from all input files to be
+buffered in memory before the output is written. This may result in
+excessive memory usage when processing a very large number of files with a
+single command. Also, it makes this option incompatible with the B<-w>
+option. When processing a large number of files, it is recommended to
+either use the JSON (B<-j>) or XML (B<-X>) output format, or use B<-p> to
+generate a fixed-column CSV file instead of using the B<-csv> option.
+
+=item B<-csvDelim> I<STR>
+
+Set the delimiter for separating CSV entries for CSV file input/output via
+the B<-csv> option. I<STR> may contain "\t", "\n", "\r" and "\\" to
+represent TAB, LF, CR and '\' respectively. A double quote is not allowed
+in the delimiter. Default is ','.
+
+=item B<-d> I<FMT> (B<-dateFormat>)
+
+Set the format for date/time tag values. The I<FMT> string may contain
+formatting codes beginning with a percent character (C<%>) to represent the
+various components of a date/time value. The specifics of the I<FMT> syntax
+are system dependent -- consult the C<strftime> man page on your system for
+details. The default format is equivalent to "%Y:%m:%d %H:%M:%S". This
+option has no effect on date-only or time-only tags and ignores timezone
+information if present. Only one B<-d> option may be used per command.
+Requires POSIX::strptime or Time::Piece for the inversion conversion when
+writing.
+
+=item B<-D> (B<-decimal>)
+
+Show tag ID number in decimal when extracting information.
+
+=item B<-E>, B<-ex>, B<-ec> (B<-escapeHTML>, B<-escapeXML>, B<-escapeC>)
+
+Escape characters in output tag values for HTML (B<-E>), XML (B<-ex>) or C
+(B<-ec>). For HTML, all characters with Unicode code points above U+007F
+are escaped as well as the following 5 characters: & (&amp;) E<39> (&#39;)
+E<quot> (&quot;) E<gt> (&gt;) and E<lt> (&lt;). For XML, only these 5
+characters are escaped. The B<-E> option is implied with B<-h>, and B<-ex>
+is implied with B<-X>. For C, all control characters and the backslash are
+escaped. The inverse conversion is applied when writing tags.
+
+=item B<-f> (B<-forcePrint>)
+
+Force printing of tags even if their values are not found. This option only
+applies when specific tags are requested on the command line (ie. not with
+wildcards or by C<-all>). With this option, a dash (C<->) is printed for
+the value of any missing tag, but the dash may be changed via the API
+MissingTagValue option. May also be used to add a 'flags' attribute to the
+B<-listx> output, or to allow tags to be deleted when writing with the
+B<-csv>=I<CSVFILE> feature.
+
+=item B<-g>[I<NUM>][:I<NUM>...] (B<-groupHeadings>)
+
+Organize output by tag group. I<NUM> specifies a group family number, and
+may be 0 (general location), 1 (specific location), 2 (category), 3
+(document number), 4 (instance number), 5 (metadata path), 6 (EXIF/TIFF
+format) or 7 (tag ID). B<-g0> is assumed if a family number is not
+specified. May be combined with other options to add group names to the
+output. Multiple families may be specified by separating them with colons.
+By default the resulting group name is simplified by removing any leading
+C<Main:> and collapsing adjacent identical group names, but this can be
+avoided by placing a colon before the first family number (eg. B<-g:3:1>).
+Use the B<-listg> option to list group names for a specified family. The
+SavePath and SaveFormat API options are automatically enabled if the
+respective family 5 or 6 group names are requested. See the
+L<API GetGroup documentation|Image::ExifTool/GetGroup> for more information.
+
+=item B<-G>[I<NUM>][:I<NUM>...] (B<-groupNames>)
+
+Same as B<-g> but print group name for each tag. B<-G0> is assumed if
+I<NUM> is not specified. May be combined with a number of other options to
+add group names to the output. Note that I<NUM> may be added wherever B<-G>
+is mentioned in the documentation. See the B<-g> option above for details.
+
+=item B<-h> (B<-htmlFormat>)
+
+Use HTML table formatting for output. Implies the B<-E> option. The
+formatting options B<-D>, B<-H>, B<-g>, B<-G>, B<-l> and B<-s> may be used
+in combination with B<-h> to influence the HTML format.
+
+=item B<-H> (B<-hex>)
+
+Show tag ID number in hexadecimal when extracting information.
+
+=item B<-htmlDump>[I<OFFSET>]
+
+Generate a dynamic web page containing a hex dump of the EXIF information.
+This can be a very powerful tool for low-level analysis of EXIF information.
+The B<-htmlDump> option is also invoked if the B<-v> and B<-h> options are
+used together. The verbose level controls the maximum length of the blocks
+dumped. An I<OFFSET> may be given to specify the base for displayed
+offsets. If not provided, the EXIF/TIFF base offset is used. Use
+B<-htmlDump0> for absolute offsets. Currently only EXIF/TIFF and JPEG
+information is dumped, but the -u option can be used to give a raw hex dump
+of other file formats.
+
+=item B<-j>[[+]=I<JSONFILE>] (B<-json>)
+
+Use JSON (JavaScript Object Notation) formatting for console output, or
+import JSON file if I<JSONFILE> is specified. This option may be combined
+with B<-g> to organize the output into objects by group, or B<-G> to add
+group names to each tag. List-type tags with multiple items are output as
+JSON arrays unless B<-sep> is used. By default XMP structures are flattened
+into individual tags in the JSON output, but the original structure may be
+preserved with the B<-struct> option (this also causes all list-type XMP
+tags to be output as JSON arrays, otherwise single-item lists would be
+output as simple strings). The B<-a> option is implied if the B<-g> or
+B<-G> options are used, otherwise it is ignored and tags with identical
+JSON names are suppressed. (B<-g4> may be used to ensure that all tags have
+unique JSON names.) Adding the B<-D> or B<-H> option changes tag values to
+JSON objects with "val" and "id" fields, and adding B<-l> adds a "desc"
+field, and a "num" field if the numerical value is different from the
+converted "val". The B<-b> option may be added to output binary data,
+encoded in base64 if necessary (indicated by ASCII "base64:" as the first 7
+bytes of the value), and B<-t> may be added to include tag table information
+(see B<-t> for details). The JSON output is UTF-8 regardless of any B<-L>
+or B<-charset> option setting, but the UTF-8 validation is disabled if a
+character set other than UTF-8 is specified.
+
+If I<JSONFILE> is specified, the file is imported and the tag definitions
+from the file are used to set tag values on a per-file basis. The special
+"SourceFile" entry in each JSON object associates the information with a
+specific target file. An object with a missing SourceFile or a SourceFile
+of "*" defines default tags for all target files which are combined with any
+tags specified for the specific SourceFile processed. The imported JSON
+file must have the same format as the exported JSON files with the exception
+that the B<-g> option is not compatible with the import file format (use
+B<-G> instead). Additionally, tag names in the input JSON file may be
+suffixed with a C<#> to disable print conversion.
+
+Unlike CSV import, empty values are not ignored, and will cause an empty
+value to be written if supported by the specific metadata type. Tags are
+deleted by using the B<-f> option and setting the tag value to "-" (or to
+the MissingTagValue setting if this API option was used). Importing with
+B<-j>+=I<JSONFILE> causes new values to be added to existing lists.
+
+=item B<-l> (B<-long>)
+
+Use long 2-line Canon-style output format. Adds a description and
+unconverted value (if it is different from the converted value) to the XML,
+JSON or PHP output when B<-X>, B<-j> or B<-php> is used. May also be
+combined with B<-listf>, B<-listr> or B<-listwf> to add descriptions of the
+file types.
+
+=item B<-L> (B<-latin>)
+
+Use Windows Latin1 encoding (cp1252) for output tag values instead of the
+default UTF-8. When writing, B<-L> specifies that input text values are
+Latin1 instead of UTF-8. Equivalent to C<-charset latin>.
+
+=item B<-lang> [I<LANG>]
+
+Set current language for tag descriptions and converted values. I<LANG> is
+C<de>, C<fr>, C<ja>, etc. Use B<-lang> with no other arguments to get a
+list of available languages. The default language is C<en> if B<-lang> is
+not specified. Note that tag/group names are always English, independent of
+the B<-lang> setting, and translation of warning/error messages has not yet
+been implemented. May also be combined with B<-listx> to output
+descriptions in one language only.
+
+By default, ExifTool uses UTF-8 encoding for special characters, but the
+the B<-L> or B<-charset> option may be used to invoke other encodings. Note
+that ExifTool uses Unicode::LineBreak if available to help preserve the
+column alignment of the plain text output for languages with a
+variable-width character set.
+
+Currently, the language support is not complete, but users are welcome to
+help improve this by submitting their own translations. To submit a
+translation, follow these steps (you must have Perl installed for this):
+
+1. Download and unpack the latest Image-ExifTool full distribution.
+
+2. 'cd' into the Image-ExifTool directory.
+
+3. Run this command to make an XML file of the desired tags (eg. EXIF):
+
+ ./exiftool -listx -exif:all > out.xml
+
+4. Copy this text into a file called 'import.pl' in the exiftool directory:
+
+ push @INC, 'lib';
+ require Image::ExifTool::TagInfoXML;
+ my $file = shift or die "Expected XML file name\n";
+ $Image::ExifTool::TagInfoXML::makeMissing = shift;
+ Image::ExifTool::TagInfoXML::BuildLangModules($file,8);
+
+5. Run the 'import.pl' script to Import the XML file, generating the
+'MISSING' entries for your language (eg. Russian):
+
+ perl import.pl out.xml ru
+
+6. Edit the generated language module lib/Image/ExifTool/Lang/ru.pm, and
+search and replace all 'MISSING' strings in the file with your translations.
+
+7. Email the module ('ru.pm' in this example) to philharvey66 at gmail.com
+
+8. Thank you!!
+
+=item B<-listItem> I<INDEX>
+
+For list-type tags, this causes only the item with the specified index to be
+extracted. I<INDEX> is 0 for the first item in the list. Negative indices
+may also be used to reference items from the end of the list. Has no effect
+on single-valued tags. Also applies to tag values when copying from a tag,
+and in B<-if> conditions.
+
+=item B<-n> (B<--printConv>)
+
+Disable print conversion for all tags. By default, extracted values are
+converted to a more human-readable format, but the B<-n> option disables
+this conversion, revealing the machine-readable values. For example:
+
+ > exiftool -Orientation -S a.jpg
+ Orientation: Rotate 90 CW
+ > exiftool -Orientation -S -n a.jpg
+ Orientation: 6
+
+The print conversion may also be disabled on a per-tag basis by suffixing
+the tag name with a C<#> character:
+
+ > exiftool -Orientation# -Orientation -S a.jpg
+ Orientation: 6
+ Orientation: Rotate 90 CW
+
+These techniques may also be used to disable the inverse print conversion
+when writing. For example, the following commands all have the same effect:
+
+ > exiftool -Orientation='Rotate 90 CW' a.jpg
+ > exiftool -Orientation=6 -n a.jpg
+ > exiftool -Orientation#=6 a.jpg
+
+=item B<-p> I<FMTFILE> or I<STR> (B<-printFormat>)
+
+Print output in the format specified by the given file or string. The
+argument is interpreted as a string unless a file of that name exists, in
+which case the string is loaded from the contents of the file. Tag names in
+the format file or string begin with a C<$> symbol and may contain leading
+group names and/or a trailing C<#> (to disable print conversion). Case is
+not significant. Braces C<{}> may be used around the tag name to separate
+it from subsequent text. Use C<$$> to represent a C<$> symbol, and C<$/>
+for a newline.
+
+Multiple B<-p> options may be used, each contributing a line (or more) of
+text to the output. Lines beginning with C<#[HEAD]> and C<#[TAIL]> are
+output before the first processed file and after the last processed file
+respectively. Lines beginning with C<#[SECT]> and C<#[ENDS]> are output
+before and after each section of files. A section is defined as a group of
+consecutive files with the same section header (eg. files are grouped by
+directory if C<#[SECT]> contains C<$directory>). Lines beginning with
+C<#[BODY]> and lines not beginning with C<#> are output for each processed
+file. Lines beginning with C<#[IF]> are not output, but all BODY lines are
+skipped if any tag on an IF line doesn't exist. Other lines beginning with
+C<#> are ignored. For example, this format file:
+
+ # this is a comment line
+ #[HEAD]-- Generated by ExifTool $exifToolVersion --
+ File: $FileName - $DateTimeOriginal
+ (f/$Aperture, ${ShutterSpeed}s, ISO $EXIF:ISO)
+ #[TAIL]-- end --
+
+with this command:
+
+ exiftool -p test.fmt a.jpg b.jpg
+
+produces output like this:
+
+ -- Generated by ExifTool 12.12 --
+ File: a.jpg - 2003:10:31 15:44:19
+ (f/5.6, 1/60s, ISO 100)
+ File: b.jpg - 2006:05:23 11:57:38
+ (f/8.0, 1/13s, ISO 100)
+ -- end --
+
+The values of List-type tags with multiple items and Shortcut tags
+representing multiple tags are joined according the the B<-sep> option
+setting when interpolated in the string.
+
+When B<-ee> (B<-extractEmbedded>) is combined with B<-p>, embedded documents
+are effectively processed as separate input files.
+
+If a specified tag does not exist, a minor warning is issued and the line
+with the missing tag is not printed. However, the B<-f> option may be used
+to set the value of missing tags to '-' (but this may be configured via the
+MissingTagValue API option), or the B<-m> option may be used to ignore minor
+warnings and leave the missing values empty. Alternatively, B<-q -q> may be
+used to simply suppress the warning messages.
+
+The L</Advanced formatting feature> may be used to modify the values of
+individual tags with the B<-p> option.
+
+=item B<-php>
+
+Format output as a PHP Array. The B<-g>, B<-G>, B<-D>, B<-H>, B<-l>,
+B<-sep> and B<-struct> options combine with B<-php>, and duplicate tags are
+handled in the same way as with the B<-json> option. As well, the B<-b>
+option may be added to output binary data, and B<-t> may be added to include
+tag table information (see B<-t> for details). Here is a simple example
+showing how this could be used in a PHP script:
+
+ <?php
+ eval('$array=' . `exiftool -php -q image.jpg`);
+ print_r($array);
+ ?>
+
+=item B<-s>[I<NUM>] (B<-short>)
+
+Short output format. Prints tag names instead of descriptions. Add I<NUM>
+or up to 3 B<-s> options for even shorter formats:
+
+ -s1 or -s - print tag names instead of descriptions
+ -s2 or -s -s - no extra spaces to column-align values
+ -s3 or -s -s -s - print values only (no tag names)
+
+Also effective when combined with B<-t>, B<-h>, B<-X> or B<-listx> options.
+
+=item B<-S> (B<-veryShort>)
+
+Very short format. The same as B<-s2> or two B<-s> options. Tag names are
+printed instead of descriptions, and no extra spaces are added to
+column-align values.
+
+=item B<-sep> I<STR> (B<-separator>)
+
+Specify separator string for items in list-type tags. When reading, the
+default is to join list items with ", ". When writing, this option causes
+values assigned to list-type tags to be split into individual items at each
+substring matching I<STR> (otherwise they are not split by default). Space
+characters in I<STR> match zero or more whitespace characters in the value.
+
+Note that an empty separator ("") is allowed, and will join items with no
+separator when reading, or split the value into individual characters when
+writing.
+
+For pure binary output (B<-b> used without B<-j>, B<-php> or B<-X>), the
+first B<-sep> option specifies a list-item separator, and a second B<-sep>
+option specifies a terminator for the end of the list (or after each value
+if not a list). In these strings, C<\n>, C<\r> and C<\t> may be used to
+represent a newline, carriage return and tab respectively. By default,
+binary list items are separated by a newline, and no terminator is added.
+
+=item B<-sort>, B<--sort>
+
+Sort output by tag description, or by tag name if the B<-s> option is used.
+When sorting by description, the sort order will depend on the B<-lang>
+option setting. Without the B<-sort> option, tags appear in the order they
+were specified on the command line, or if not specified, the order they were
+extracted from the file. By default, tags are organized by groups when
+combined with the B<-g> or B<-G> option, but this grouping may be disabled
+with B<--sort>.
+
+=item B<-struct>, B<--struct>
+
+Output structured XMP information instead of flattening to individual tags.
+This option works well when combined with the XML (B<-X>) and JSON (B<-j>)
+output formats. For other output formats, XMP structures and lists are
+serialized into the same format as when writing structured information (see
+L<https://exiftool.org/struct.html> for details). When copying, structured
+tags are copied by default unless B<--struct> is used to disable this
+feature (although flattened tags may still be copied by specifying them
+individually unless B<-struct> is used). These options have no effect when
+assigning new values since both flattened and structured tags may always be
+used when writing.
+
+=item B<-t> (B<-tab>)
+
+Output a tab-delimited list of description/values (useful for database
+import). May be combined with B<-s> to print tag names instead of
+descriptions, or B<-S> to print tag values only, tab-delimited on a single
+line. The B<-t> option may be combined with B<-j>, B<-php> or B<-X> to add
+tag table information (C<table>, tag C<id>, and C<index> for cases where
+multiple conditional tags exist with the same ID).
+
+=item B<-T> (B<-table>)
+
+Output tag values in table form. Equivalent to B<-t -S -q -f>.
+
+=item B<-v>[I<NUM>] (B<-verbose>)
+
+Print verbose messages. I<NUM> specifies the level of verbosity in the
+range 0-5, with higher numbers being more verbose. If I<NUM> is not given,
+then each B<-v> option increases the level of verbosity by 1. With any
+level greater than 0, most other options are ignored and normal console
+output is suppressed unless specific tags are extracted. Using B<-v0>
+causes the console output buffer to be flushed after each line (which may be
+useful to avoid delays when piping exiftool output), and prints the name of
+each processed file when writing. Also see the B<-progress> option.
+
+=item B<-w>[+|!] I<EXT> or I<FMT> (B<-textOut>)
+
+Write console output to files with names ending in I<EXT>, one for each
+source file. The output file name is obtained by replacing the source file
+extension (including the '.') with the specified extension (and a '.' is
+added to the start of I<EXT> if it doesn't already contain one).
+Alternatively, a I<FMT> string may be used to give more control over the
+output file name and directory. In the format string, %d, %f and %e
+represent the directory, filename and extension of the source file, and %c
+represents a copy number which is automatically incremented if the file
+already exists. %d includes the trailing '/' if necessary, but %e does not
+include the leading '.'. For example:
+
+ -w %d%f.txt # same effect as "-w txt"
+ -w dir/%f_%e.out # write files to "dir" as "FILE_EXT.out"
+ -w dir2/%d%f.txt # write to "dir2", keeping dir structure
+ -w a%c.txt # write to "a.txt" or "a1.txt" or "a2.txt"...
+
+Existing files will not be changed unless an exclamation point is added to
+the option name (ie. B<-w!> or B<-textOut!>) to overwrite the file, or a
+plus sign (ie. B<-w+> or B<-textOut+>) to append to the existing file. Both
+may be used (ie. B<-w+!> or B<-textOut+!>) to overwrite output files that
+didn't exist before the command was run, and append the output from multiple
+source files. For example, to write one output file for all source files in
+each directory:
+
+ exiftool -filename -createdate -T -w+! %d/out.txt -r DIR
+
+Capitalized format codes %D, %F, %E and %C provide slightly different
+alternatives to the lower case versions. %D does not include the trailing
+'/', %F is the full filename including extension, %E includes the leading
+'.', and %C increments the count for each processed file (see below).
+
+Notes:
+
+1) In a Windows BAT file the C<%> character is represented by C<%%>, so an
+argument like C<%d%f.txt> is written as C<%%d%%f.txt>.
+
+2) If the argument for B<-w> does not contain a valid format code (eg. %f),
+then it is interpreted as a file extension. It is not possible to specify a
+simple filename as an argument -- creating a single output file from
+multiple source files is typically done by shell redirection, ie)
+
+ exiftool FILE1 FILE2 ... > out.txt
+
+But if necessary, an empty format code may be used to force the argument to
+be interpreted as a format string, and the same result may be obtained
+without the use of shell redirection:
+
+ exiftool -w+! %0fout.txt FILE1 FILE2 ...
+
+Advanced features:
+
+A substring of the original file name, directory or extension may be taken
+by specifying a field width immediately following the '%' character. If the
+width is negative, the substring is taken from the end. The substring
+position (characters to ignore at the start or end of the string) may be
+given by a second optional value after a decimal point. For example:
+
+ Input File Name Format Specifier Output File Name
+ ---------------- ---------------- ----------------
+ Picture-123.jpg %7f.txt Picture.txt
+ Picture-123.jpg %-.4f.out Picture.out
+ Picture-123.jpg %7f.%-3f Picture.123
+ Picture-123a.jpg Meta%-3.1f.txt Meta123.txt
+
+(Note that special characters may have a width of greater than one.)
+
+For %d and %D, the field width/position specifiers may be applied to the
+directory levels instead of substring position by using a colon instead of a
+decimal point in the format specifier. For example:
+
+ Source Dir Format Result Notes
+ ------------ ------ ---------- ------------------
+ pics/2012/02 %2:d pics/2012/ take top 2 levels
+ pics/2012/02 %-:1d pics/2012/ up one directory level
+ pics/2012/02 %:1d 2012/02/ ignore top level
+ pics/2012/02 %1:1d 2012/ take 1 level after top
+ pics/2012/02 %-1:D 02 bottom level folder name
+ /Users/phil %:2d phil/ ignore top 2 levels
+
+(Note that the root directory counts as one level when an absolute path is
+used as in the last example above.)
+
+For %c, these modifiers have a different effects. If a field width is
+given, the copy number is padded with zeros to the specified width. A
+leading '-' adds a dash before the copy number, and a '+' adds an underline.
+By default, the copy number is omitted from the first file of a given name,
+but this can be changed by adding a decimal point to the modifier. For
+example:
+
+ -w A%-cZ.txt # AZ.txt, A-1Z.txt, A-2Z.txt ...
+ -w B%5c.txt # B.txt, B00001.txt, B00002.txt ...
+ -w C%.c.txt # C0.txt, C1.txt, C2.txt ...
+ -w D%-.c.txt # D-0.txt, D-1.txt, D-2.txt ...
+ -w E%-.4c.txt # E-0000.txt, E-0001.txt, E-0002.txt ...
+ -w F%-.4nc.txt # F-0001.txt, F-0002.txt, F-0003.txt ...
+ -w G%+c.txt # G.txt, G_1.txt G_2.txt ...
+ -w H%-lc.txt # H.txt, H-b.txt, H-c.txt ...
+ -w I.%.3uc.txt # I.AAA.txt, I.AAB.txt, I.AAC.txt ...
+
+A special feature allows the copy number to be incremented for each
+processed file by using %C (upper case) instead of %c. This allows a
+sequential number to be added to output file names, even if the names are
+different. For %C, a copy number of zero is not omitted as it is with %c.
+A leading '-' causes the number to be reset at the start of each new
+directory, and '+' has no effect. The number before the decimal place gives
+the starting index, the number after the decimal place gives the field
+width. The following examples show the output filenames when used with the
+command C<exiftool rose.jpg star.jpg jet.jpg ...>:
+
+ -w %C%f.txt # 0rose.txt, 1star.txt, 2jet.txt
+ -w %f-%10C.txt # rose-10.txt, star-11.txt, jet-12.txt
+ -w %.3C-%f.txt # 000-rose.txt, 001-star.txt, 002-jet.txt
+ -w %57.4C%f.txt # 0057rose.txt, 0058star.txt, 0059jet.txt
+
+All format codes may be modified by 'l' or 'u' to specify lower or upper
+case respectively (ie. C<%le> for a lower case file extension). When used
+to modify %c or %C, the numbers are changed to an alphabetical base (see
+example H above). Also, %c and %C may be modified by 'n' to count using
+natural numbers starting from 1, instead of 0 (see example F above).
+
+This same I<FMT> syntax is used with the B<-o> and B<-tagsFromFile> options,
+although %c and %C are only valid for output file names.
+
+=item B<-W>[+|!] I<FMT> (B<-tagOut>)
+
+This enhanced version of the B<-w> option allows a separate output file to
+be created for each extracted tag. See the B<-w> option documentation above
+for details of the basic functionality. Listed here are the differences
+between B<-W> and B<-w>:
+
+1) With B<-W>, a new output file is created for each extracted tag.
+
+2) B<-W> supports three additional format codes: %t, %g and %s represent
+the tag name, group name, and suggested extension for the output file (based
+on the format of the data). The %g code may be followed by a single digit
+to specify the group family number (eg. %g1), otherwise family 0 is assumed.
+The substring width/position/case specifiers may be used with these format
+codes in exactly the same way as with %f and %e.
+
+3) The argument for B<-W> is interpreted as a file name if it contains no
+format codes. (For B<-w>, this would be a file extension.) This change
+allows a simple file name to be specified, which, when combined with the
+append feature, provides a method to write metadata from multiple source
+files to a single output file without the need for shell redirection. For
+example, the following pairs of commands give the same result:
+
+ # overwriting existing text file
+ exiftool test.jpg > out.txt # shell redirection
+ exiftool test.jpg -W+! out.txt # equivalent -W option
+
+ # append to existing text file
+ exiftool test.jpg >> out.txt # shell redirection
+ exiftool test.jpg -W+ out.txt # equivalent -W option
+
+4) Adding the B<-v> option to B<-W> sends a list of the tags and output file
+names to the console instead of giving a verbose dump of the entire file.
+(Unless appending all output to one file for each source file by using
+B<-W+> with an output file I<FMT> that does not contain %t, $g or %s.)
+
+5) Individual list items are stored in separate files when B<-W> is combined
+with B<-b>, but note that for separate files to be created %c or %C must be
+used in I<FMT> to give the files unique names.
+
+=item B<-Wext> I<EXT>, B<--Wext> I<EXT> (B<-tagOutExt>)
+
+This option is used to specify the type of output file(s) written by the
+B<-W> option. An output file is written only if the suggested extension
+matches I<EXT>. Multiple B<-Wext> options may be used to write more than
+one type of file. Use B<--Wext> to write all but the specified type(s).
+
+=item B<-X> (B<-xmlFormat>)
+
+Use ExifTool-specific RDF/XML formatting for console output. Implies the
+B<-a> option, so duplicate tags are extracted. The formatting options
+B<-b>, B<-D>, B<-H>, B<-l>, B<-s>, B<-sep>, B<-struct> and B<-t> may be used
+in combination with B<-X> to affect the output, but note that the tag ID
+(B<-D>, B<-H> and B<-t>), binary data (B<-b>) and structured output
+(B<-struct>) options are not effective for the short output (B<-s>). Another
+restriction of B<-s> is that only one tag with a given group and name may
+appear in the output. Note that the tag ID options (B<-D>, B<-H> and B<-t>)
+will produce non-standard RDF/XML unless the B<-l> option is also used.
+
+By default, B<-X> outputs flattened tags, so B<-struct> should be added if
+required to preserve XMP structures. List-type tags with multiple values
+are formatted as an RDF Bag, but they are combined into a single string when
+B<-s> or B<-sep> is used. Using B<-L> changes the XML encoding from "UTF-8"
+to "windows-1252". Other B<-charset> settings change the encoding only if
+there is a corresponding standard XML character set. The B<-b> option
+causes binary data values to be written, encoded in base64 if necessary.
+The B<-t> option adds tag table information to the output (see B<-t> for
+details).
+
+Note: This output is NOT the same as XMP because it uses
+dynamically-generated property names corresponding to the ExifTool tag
+names, and not the standard XMP properties. To write XMP instead, use the
+B<-o> option with an XMP extension for the output file.
+
+=back
+
+=head3 Processing control
+
+=over 5
+
+=item B<-a>, B<--a> (B<-duplicates>, B<--duplicates>)
+
+Allow (B<-a>) or suppress (B<--a>) duplicate tag names to be extracted. By
+default, duplicate tags are suppressed when reading unless the B<-ee> or
+B<-X> options are used or the Duplicates option is enabled in the
+configuration file. This option has an affect when writing only to allow
+duplicate Warning messages to be shown. Duplicate tags are always extracted
+when copying.
+
+=item B<-e> (B<--composite>)
+
+Extract existing tags only -- don't generate composite tags.
+
+=item B<-ee> (B<-extractEmbedded>)
+
+Extract information from embedded documents in EPS files, embedded EPS
+information and JPEG and Jpeg2000 images in PDF files, embedded MPF images
+in JPEG and MPO files, streaming metadata in AVCHD videos, and the resource
+fork of Mac OS files. Implies the B<-a> option. Use B<-g3> or B<-G3> to
+identify the originating document for extracted information. Embedded
+documents containing sub-documents are indicated with dashes in the family 3
+group name. (eg. C<Doc2-3> is the 3rd sub-document of the 2nd embedded
+document.) Note that this option may increase processing time substantially,
+especially for PDF files with many embedded images or videos with streaming
+metadata.
+
+When used with B<-ee>, the B<-p> option is evaluated for each embedded
+document as if it were a separate input file. This allows, for example,
+generation of GPS track logs from timed metadata in videos. See
+L<https://exiftool.org/geotag.html#Inverse> for examples.
+
+=item B<-ext>[+] I<EXT>, B<--ext> I<EXT> (B<-extension>)
+
+Process only files with (B<-ext>) or without (B<--ext>) a specified
+extension. There may be multiple B<-ext> and B<--ext> options. A plus sign
+may be added (ie. B<-ext+>) to add the specified extension to the normally
+processed files. EXT may begin with a leading '.', which is ignored. Case
+is not significant. C<"*"> may be used to process files with any extension
+(or none at all), as in the last three examples:
+
+ exiftool -ext JPG DIR # process only JPG files
+ exiftool --ext cr2 --ext dng DIR # supported files but CR2/DNG
+ exiftool -ext+ txt DIR # supported files plus TXT
+ exiftool -ext "*" DIR # process all files
+ exiftool -ext "*" --ext xml DIR # process all but XML files
+ exiftool -ext "*" --ext . DIR # all but those with no ext
+
+Using this option has two main advantages over specifying C<*.I<EXT>> on the
+command line: 1) It applies to files in subdirectories when combined with
+the B<-r> option. 2) The B<-ext> option is case-insensitive, which is
+useful when processing files on case-sensitive filesystems.
+
+Note that all files specified on the command line will be processed
+regardless of extension unless the B<-ext> option is used.
+
+=item B<-F>[I<OFFSET>] (B<-fixBase>)
+
+Fix the base for maker notes offsets. A common problem with some image
+editors is that offsets in the maker notes are not adjusted properly when
+the file is modified. This may cause the wrong values to be extracted for
+some maker note entries when reading the edited file. This option allows an
+integer I<OFFSET> to be specified for adjusting the maker notes base offset.
+If no I<OFFSET> is given, ExifTool takes its best guess at the correct base.
+Note that exiftool will automatically fix the offsets for images which store
+original offset information (eg. newer Canon models). Offsets are fixed
+permanently if B<-F> is used when writing EXIF to an image. eg)
+
+ exiftool -F -exif:resolutionunit=inches image.jpg
+
+=item B<-fast>[I<NUM>]
+
+Increase speed of extracting information. With B<-fast> (or B<-fast1>),
+ExifTool will not scan to the end of a JPEG image to check for an AFCP or
+PreviewImage trailer, or past the first comment in GIF images or the
+audio/video data in WAV/AVI files to search for additional metadata. These
+speed benefits are small when reading images directly from disk, but can be
+substantial if piping images through a network connection. For more
+substantial speed benefits, B<-fast2> also causes exiftool to avoid
+extracting any EXIF MakerNote information. B<-fast3> avoids extracting
+metadata from the file, and returns only pseudo System tags, but still reads
+the file header to obtain an educated guess at FileType. B<-fast4> doesn't
+even read the file header, and returns only System tags and a FileType based
+on the file extension. B<-fast5> also disables generation of the Composite
+tags (like B<-e>). Has no effect when writing.
+
+Note that a separate B<-fast> setting may be used for evaluation of a B<-if>
+condition, or when ordering files with the B<-fileOrder> option. See the
+B<-if> and B<-fileOrder> options for details.
+
+=item B<-fileOrder>[I<NUM>] [-]I<TAG>
+
+Set file processing order according to the sorted value of the specified
+I<TAG>. For example, to process files in order of date:
+
+ exiftool -fileOrder DateTimeOriginal DIR
+
+Additional B<-fileOrder> options may be added for secondary sort keys.
+Numbers are sorted numerically, and all other values are sorted
+alphabetically. Files missing the specified tag are sorted last. The sort
+order may be reversed by prefixing the tag name with a C<-> (eg.
+C<-fileOrder -createdate>). Print conversion of the sorted values is
+disabled with the B<-n> option, or a C<#> appended to the tag name. Other
+formatting options (eg. B<-d>) have no effect on the sorted values. Note
+that the B<-fileOrder> option can have a large performance impact since it
+involves an additional processing pass of each file, but this impact may be
+reduced by specifying a I<NUM> for the B<-fast> level used during the
+metadata-extraction phase. For example, B<-fileOrder4> may be used if
+I<TAG> is a pseudo System tag. If multiple B<-fileOrder> options are used,
+the extraction is done at the lowest B<-fast> level. Note that files are
+sorted across directory boundaries if multiple input directories are
+specified.
+
+=item B<-i> I<DIR> (B<-ignore>)
+
+Ignore specified directory name. I<DIR> may be either an individual folder
+name, or a full path. If a full path is specified, it must match the
+Directory tag exactly to be ignored. Use multiple B<-i> options to ignore
+more than one directory name. A special I<DIR> value of C<SYMLINKS> (case
+sensitive) may be specified to ignore symbolic links when the B<-r> option
+is used.
+
+=item B<-if>[I<NUM>] I<EXPR>
+
+Specify a condition to be evaluated before processing each I<FILE>. I<EXPR>
+is a Perl-like logic expression containing tag names prefixed by C<$>
+symbols. It is evaluated with the tags from each I<FILE> in turn, and the
+file is processed only if the expression returns true. Unlike Perl variable
+names, tag names are not case sensitive and may contain a hyphen. As well,
+tag names may have a leading group names separated by colons, and/or a
+trailing C<#> character to disable print conversion. The expression
+C<$GROUP:all> evaluates to 1 if any tag exists in the specified C<GROUP>, or
+0 otherwise (see note 2 below). When multiple B<-if> options are used, all
+conditions must be satisfied to process the file. Returns an exit status of
+2 if all files fail the condition. Below are a few examples:
+
+ # extract shutterspeed from all Canon images in a directory
+ exiftool -shutterspeed -if '$make eq "Canon"' dir
+
+ # add one hour to all images created on or after Apr. 2, 2006
+ exiftool -alldates+=1 -if '$CreateDate ge "2006:04:02"' dir
+
+ # set EXIF ISO value if possible, unless it is set already
+ exiftool '-exif:iso<iso' -if 'not $exif:iso' dir
+
+ # find images containing a specific keyword (case insensitive)
+ exiftool -if '$keywords =~ /harvey/i' -filename dir
+
+Adding I<NUM> to the B<-if> option causes a separate processing pass to be
+executed for evaluating I<EXPR> at a B<-fast> level given by I<NUM> (see the
+B<-fast> option documentation for details). Without I<NUM>, only one
+processing pass is done at the level specified by the B<-fast> option. For
+example, using B<-if5> is possible if I<EXPR> uses only pseudo System tags,
+and may significantly speed processing if enough files fail the condition.
+
+The expression has access to the current ExifTool object through C<$self>,
+and the following special functions are available to allow short-circuiting
+of the file processing. Both functions have a return value of 1. Case is
+significant for function names.
+
+ End() - end processing after this file
+ EndDir() - end processing of files in this directory (not
+ compatible with the B<-fileOrder> option)
+
+Notes:
+
+1) The B<-n> and B<-b> options also apply to tags used in I<EXPR>.
+
+2) Some binary data blocks are not extracted unless specified explicitly.
+These tags are not available for use in the B<-if> condition unless they are
+also specified on the command line. The alternative is to use the
+C<$GROUP:all> syntax. (eg. Use C<$exif:all> instead of C<$exif> in I<EXPR>
+to test for the existence of EXIF tags.)
+
+3) Tags in the string are interpolated the same way as with B<-p> before the
+expression is evaluated. In this interpolation, C<$/> is converted to a
+newline and C<$$> represents a single C<$> symbol (so Perl variables, if
+used, require a double C<$>).
+
+4) The condition may only test tags from the file being processed. To
+process one file based on tags from another, two steps are required. For
+example, to process XMP sidecar files in directory C<DIR> based on tags from
+the associated NEF:
+
+ exiftool -if EXPR -p '$directory/$filename' -ext nef DIR > nef.txt
+ exiftool -@ nef.txt -srcfile %d%f.xmp ...
+
+5) The B<-a> option has no effect on the evaluation of the expression, and
+the values of duplicate tags are accessible only by specifying a group name
+(such as a family 4 instance number, eg. C<$Copy1:TAG>, C<$Copy2:TAG>, etc).
+
+6) A special "OK" UserParam is available to test the success of the previous
+command when B<-execute> was used, and may be used like any other tag in the
+condition (ie. "$OK").
+
+=item B<-m> (B<-ignoreMinorErrors>)
+
+Ignore minor errors and warnings. This enables writing to files with minor
+errors and disables some validation checks which could result in minor
+warnings. Generally, minor errors/warnings indicate a problem which usually
+won't result in loss of metadata if ignored. However, there are exceptions,
+so ExifTool leaves it up to you to make the final decision. Minor errors
+and warnings are indicated by "[minor]" at the start of the message.
+Warnings which affect processing when ignored are indicated by "[Minor]"
+(with a capital "M"). Note that this causes missing values in
+B<-tagsFromFile>, B<-p> and B<-if> strings to be set to an empty string
+rather than an undefined value.
+
+=item B<-o> I<OUTFILE> or I<FMT> (B<-out>)
+
+Set the output file or directory name when writing information. Without
+this option, when any "real" tags are written the original file is renamed
+to C<FILE_original> and output is written to I<FILE>. When writing only
+FileName and/or Directory "pseudo" tags, B<-o> causes the file to be copied
+instead of moved, but directories specified for either of these tags take
+precedence over that specified by the B<-o> option.
+
+I<OUTFILE> may be C<-> to write to stdout. The output file name may also be
+specified using a I<FMT> string in which %d, %f and %e represent the
+directory, file name and extension of I<FILE>. Also, %c may be used to add
+a copy number. See the B<-w> option for I<FMT> string examples.
+
+The output file is taken to be a directory name if it already exists as a
+directory or if the name ends with '/'. Output directories are created if
+necessary. Existing files will not be overwritten. Combining the
+B<-overwrite_original> option with B<-o> causes the original source file to
+be erased after the output file is successfully written.
+
+A special feature of this option allows the creation of certain types of
+files from scratch, or with the metadata from another type of file. The
+following file types may be created using this technique:
+
+ XMP, EXIF, EXV, MIE, ICC/ICM, VRD, DR4
+
+The output file type is determined by the extension of I<OUTFILE> (specified
+as C<-.EXT> when writing to stdout). The output file is then created from a
+combination of information in I<FILE> (as if the B<-tagsFromFile> option was
+used), and tag values assigned on the command line. If no I<FILE> is
+specified, the output file may be created from scratch using only tags
+assigned on the command line.
+
+=item B<-overwrite_original>
+
+Overwrite the original I<FILE> (instead of preserving it by adding
+C<_original> to the file name) when writing information to an image.
+Caution: This option should only be used if you already have separate backup
+copies of your image files. The overwrite is implemented by renaming a
+temporary file to replace the original. This deletes the original file and
+replaces it with the edited version in a single operation. When combined
+with B<-o>, this option causes the original file to be deleted if the output
+file was successfully written (ie. the file is moved instead of copied).
+
+=item B<-overwrite_original_in_place>
+
+Similar to B<-overwrite_original> except that an extra step is added to
+allow the original file attributes to be preserved. For example, on a Mac
+this causes the original file creation date, type, creator, label color,
+icon, Finder tags, other extended attributes and hard links to the file to
+be preserved (but note that the Mac OS resource fork is always preserved
+unless specifically deleted with C<-rsrc:all=>). This is implemented by
+opening the original file in update mode and replacing its data with a copy
+of a temporary file before deleting the temporary. The extra step results
+in slower performance, so the B<-overwrite_original> option should be used
+instead unless necessary.
+
+Note that this option reverts to the behaviour of the B<-overwrite_original>
+option when also writing the FileName and/or Directory tags.
+
+=item B<-P> (B<-preserve>)
+
+Preserve the filesystem modification date/time (C<FileModifyDate>) of the
+original file when writing. Note that some filesystems store a creation
+date (ie. C<FileCreateDate> on Windows and Mac systems) which is not
+affected by this option. This creation date is preserved on Windows systems
+where Win32API::File and Win32::API are available regardless of this
+setting. For other systems, the B<-overwrite_original_in_place> option may
+be used if necessary to preserve the creation date. The B<-P> option is
+superseded by any value written to the FileModifyDate tag.
+
+=item B<-password> I<PASSWD>
+
+Specify password to allow processing of password-protected PDF documents.
+If a password is required but not given, a warning is issued and the
+document is not processed. This option is ignored if a password is not
+required.
+
+=item B<-progress>[:[I<TITLE>]]
+
+Show the progress when processing files. Without a colon, the B<-progress>
+option adds a progress count in brackets after the name of each processed
+file, giving the current file number and the total number of files to be
+processed. Implies the B<-v0> option, causing the names of processed files
+to also be printed when writing. When combined with the B<-if> option, the
+total count includes all files before the condition is applied, but files
+that fail the condition will not have their names printed.
+
+If followed by a colon (ie. B<-progress:>), the console window title is set
+according to the specified I<TITLE> string. If no I<TITLE> is given, a
+default I<TITLE> string of "ExifTool %p%%" is assumed. In the string, %f
+represents the file name, %p is the progress as a percent, %r is the
+progress as a ratio, %##b is a progress bar of width "##" (20 characters if
+"##" is omitted), and %% is a % character. May be combined with the normal
+B<-progress> option to also show the progress count in console messages.
+(Note: For this feature to function correctly on Mac/Linux, stderr must go
+to the console.)
+
+=item B<-q> (B<-quiet>)
+
+Quiet processing. One B<-q> suppresses normal informational messages, and a
+second B<-q> suppresses warnings as well. Error messages can not be
+suppressed, although minor errors may be downgraded to warnings with the
+B<-m> option, which may then be suppressed with C<-q -q>.
+
+=item B<-r>[.] (B<-recurse>)
+
+Recursively process files in subdirectories. Only meaningful if I<FILE> is
+a directory name. Subdirectories with names beginning with "." are not
+processed unless "." is added to the option name (ie. B<-r.> or
+B<-recurse.>). By default, exiftool will also follow symbolic links to
+directories if supported by the system, but this may be disabled with
+C<-i SYMLINKS> (see the B<-i> option for details). Combine this with
+B<-ext> options to control the types of files processed.
+
+=item B<-scanForXMP>
+
+Scan all files (even unsupported formats) for XMP information unless found
+already. When combined with the B<-fast> option, only unsupported file
+types are scanned. Warning: It can be time consuming to scan large files.
+
+=item B<-u> (B<-unknown>)
+
+Extract values of unknown tags. Add another B<-u> to also extract unknown
+information from binary data blocks. This option applies to tags with
+numerical tag ID's, and causes tag names like "Exif_0xc5d9" to be generated
+for unknown information. It has no effect on information types which have
+human-readable tag ID's (such as XMP), since unknown tags are extracted
+automatically from these formats.
+
+=item B<-U> (B<-unknown2>)
+
+Extract values of unknown tags as well as unknown information from some
+binary data blocks. This is the same as two B<-u> options.
+
+=item B<-wm> I<MODE> (B<-writeMode>)
+
+Set mode for writing/creating tags. I<MODE> is a string of one or more
+characters from the list below. The default write mode is C<wcg>.
+
+ w - Write existing tags
+ c - Create new tags
+ g - create new Groups as necessary
+
+For example, use C<-wm cg> to only create new tags (and avoid editing
+existing ones).
+
+The level of the group is the SubDirectory level in the metadata structure.
+For XMP or IPTC this is the full XMP/IPTC block (the family 0 group), but
+for EXIF this is the individual IFD (the family 1 group).
+
+=item B<-z> (B<-zip>)
+
+When reading, causes information to be extracted from .gz and .bz2
+compressed images (only one image per archive; requires gzip and bzip2 to be
+available). When writing, causes compressed information to be written if
+supported by the metadata format (eg. compressed textual metadata in PNG),
+disables the recommended padding in embedded XMP (saving 2424 bytes when
+writing XMP in a file), and writes XMP in shorthand format -- the equivalent
+of setting the API Compress=1 and Compact="NoPadding,Shorthand".
+
+=back
+
+=head3 Other options
+
+=over 5
+
+=item B<-@> I<ARGFILE>
+
+Read command-line arguments from the specified file. The file contains one
+argument per line (NOT one option per line -- some options require
+additional arguments, and all arguments must be placed on separate lines).
+Blank lines and lines beginning with C<#> are ignored (unless they start
+with C<#[CSTR]>, in which case the rest of the line is treated as a C
+string, allowing standard C escape sequences such as "\n" for a newline).
+White space at the start of a line is removed. Normal shell processing of
+arguments is not performed, which among other things means that arguments
+should not be quoted and spaces are treated as any other character.
+I<ARGFILE> may exist relative to either the current directory or the
+exiftool directory unless an absolute pathname is given.
+
+For example, the following I<ARGFILE> will set the value of Copyright to
+"Copyright YYYY, Phil Harvey", where "YYYY" is the year of CreateDate:
+
+ -d
+ %Y
+ -copyright<Copyright $createdate, Phil Harvey
+
+Arguments in I<ARGFILE> behave exactly the same as if they were entered at
+the location of the B<-@> option on the command line, with the exception
+that the B<-config> and B<-common_args> options may not be used in an
+I<ARGFILE>.
+
+=item B<-k> (B<-pause>)
+
+Pause with the message C<-- press any key --> or C<-- press RETURN -->
+(depending on your system) before terminating. This option is used to
+prevent the command window from closing when run as a Windows drag and drop
+application.
+
+=item B<-list>, B<-listw>, B<-listf>, B<-listr>, B<-listwf>,
+B<-listg>[I<NUM>], B<-listd>, B<-listx>
+
+Print a list of all valid tag names (B<-list>), all writable tag names
+(B<-listw>), all supported file extensions (B<-listf>), all recognized file
+extensions (B<-listr>), all writable file extensions (B<-listwf>), all tag
+groups [in a specified family] (B<-listg>[I<NUM>]), all deletable tag groups
+(B<-listd>), or an XML database of tag details including language
+translations (B<-listx>). The B<-list>, B<-listw> and B<-listx> options may
+be followed by an additional argument of the form C<-GROUP:All> to list only
+tags in a specific group, where C<GROUP> is one or more family 0-2 group
+names (excepting EXIF IFD groups) separated by colons. With B<-listg>,
+I<NUM> may be given to specify the group family, otherwise family 0 is
+assumed. The B<-l> option may be combined with B<-listf>, B<-listr> or
+B<-listwf> to add file descriptions to the list. The B<-lang> option may be
+combined with B<-listx> to output descriptions in a single language. Here
+are some examples:
+
+ -list # list all tag names
+ -list -EXIF:All # list all EXIF tags
+ -list -xmp:time:all # list all XMP tags relating to time
+ -listw -XMP-dc:All # list all writable XMP-dc tags
+ -listf # list all supported file extensions
+ -listr # list all recognized file extensions
+ -listwf # list all writable file extensions
+ -listg1 # list all groups in family 1
+ -listd # list all deletable groups
+ -listx -EXIF:All # list database of EXIF tags in XML format
+ -listx -XMP:All -s # list short XML database of XMP tags
+
+When combined with B<-listx>, the B<-s> option shortens the output by
+omitting the descriptions and values (as in the last example above), and
+B<-f> adds a 'flags' attribute if applicable. The flags are formatted as a
+comma-separated list of the following possible values: Avoid, Binary, List,
+Mandatory, Permanent, Protected, Unknown and Unsafe (see the L<Tag Name
+documentation|Image::ExifTool::TagNames>). For XMP List tags, the list type
+(Alt, Bag or Seq) is added to the flags, and flattened structure tags are
+indicated by a Flattened flag.
+
+Note that none of the B<-list> options require an input I<FILE>.
+
+=item B<-ver>
+
+Print exiftool version number. The B<-v> option may be added to print
+addition system information (see the README file of the full distribution
+for more details about optional libraries), or B<-v2> to also list the Perl
+include directories.
+
+=item B<-->
+
+Indicates the end of options. Any remaining arguments are treated as file
+names, even if they begin with a dash (C<->).
+
+=back
+
+=head3 Special features
+
+=over 5
+
+=item B<-geotag> I<TRKFILE>
+
+Geotag images from the specified GPS track log file. Using the B<-geotag>
+option is equivalent to writing a value to the C<Geotag> tag. The GPS
+position is interpolated from the track at a time specified by the value
+written to the C<Geotime> tag. If C<Geotime> is not specified, the value is
+copied from C<DateTimeOriginal#> (the C<#> is added to copy the unformatted
+value, avoiding potential conflicts with the B<-d> option). For example,
+the following two commands are equivalent:
+
+ exiftool -geotag trk.log image.jpg
+ exiftool -geotag trk.log "-Geotime<DateTimeOriginal#" image.jpg
+
+When the C<Geotime> value is converted to UTC, the local system timezone is
+assumed unless the date/time value contains a timezone. Writing C<Geotime>
+causes the following tags to be written (provided they can be calculated
+from the track log, and they are supported by the destination metadata
+format): GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef,
+GPSAltitude, GPSAltitudeRef, GPSDateStamp, GPSTimeStamp, GPSDateTime,
+GPSTrack, GPSTrackRef, GPSSpeed, GPSSpeedRef, GPSImgDirection,
+GPSImgDirectionRef, GPSPitch, GPSRoll, AmbientTemperature and
+CameraElevationAngle. By default, tags are created in EXIF, and updated in
+XMP only if they already exist. However, C<EXIF:Geotime> or C<XMP:Geotime>
+may be specified to write only EXIF or XMP tags respectively. Note that
+GPSPitch and GPSRoll are non-standard, and require user-defined tags in
+order to be written.
+
+The C<Geosync> tag may be used to specify a time correction which is applied
+to each C<Geotime> value for synchronization with GPS time. For example,
+the following command compensates for image times which are 1 minute and 20
+seconds behind GPS:
+
+ exiftool -geosync=+1:20 -geotag a.log DIR
+
+Advanced C<Geosync> features allow a linear time drift correction and
+synchronization from previously geotagged images. See "geotag.html" in the
+full ExifTool distribution for more information.
+
+Multiple B<-geotag> options may be used to concatenate GPS track log data.
+Also, a single B<-geotag> option may be used to load multiple track log
+files by using wildcards in the I<TRKFILE> name, but note that in this case
+I<TRKFILE> must be quoted on most systems (with the notable exception of
+Windows) to prevent filename expansion. For example:
+
+ exiftool -geotag "TRACKDIR/*.log" IMAGEDIR
+
+Currently supported track file formats are GPX, NMEA RMC/GGA/GLL, KML, IGC,
+Garmin XML and TCX, Magellan PMGNTRK, Honeywell PTNTHPR, Bramor gEO, Winplus
+Beacon TXT, and GPS/IMU CSV files. See L</GEOTAGGING EXAMPLES> for
+examples. Also see "geotag.html" in the full ExifTool distribution and the
+L<Image::ExifTool Options|Image::ExifTool/Options> for more details and for
+information about geotag configuration options.
+
+=item B<-globalTimeShift> I<SHIFT>
+
+Shift all formatted date/time values by the specified amount when reading.
+Does not apply to unformatted (B<-n>) output. I<SHIFT> takes the same form
+as the date/time shift when writing (see
+L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> for details), with a
+negative shift being indicated with a minus sign (C<->) at the start of the
+I<SHIFT> string. For example:
+
+ # return all date/times, shifted back by 1 hour
+ exiftool -globalTimeShift -1 -time:all a.jpg
+
+ # set the file name from the shifted CreateDate (-1 day) for
+ # all images in a directory
+ exiftool "-filename<createdate" -globaltimeshift "-0:0:1 0:0:0" \
+ -d %Y%m%d-%H%M%S.%%e dir
+
+=item B<-use> I<MODULE>
+
+Add features from specified plug-in I<MODULE>. Currently, the MWG module is
+the only plug-in module distributed with exiftool. This module adds
+read/write support for tags as recommended by the Metadata Working Group.
+As a convenience, C<-use MWG> is assumed if the C<MWG> group is specified
+for any tag on the command line. See the L<MWG Tags
+documentation|Image::ExifTool::TagNames/MWG Tags> for more details. Note
+that this option is not reversible, and remains in effect until the
+application terminates, even across the C<-execute> option.
+
+=back
+
+=head3 Utilities
+
+=over 5
+
+=item B<-restore_original>
+
+=item B<-delete_original>[!]
+
+These utility options automate the maintenance of the C<_original> files
+created by exiftool. They have no effect on files without an C<_original>
+copy. The B<-restore_original> option restores the specified files from
+their original copies by renaming the C<_original> files to replace the
+edited versions. For example, the following command restores the originals
+of all JPG images in directory C<DIR>:
+
+ exiftool -restore_original -ext jpg DIR
+
+The B<-delete_original> option deletes the C<_original> copies of all files
+specified on the command line. Without a trailing C<!> this option prompts
+for confirmation before continuing. For example, the following command
+deletes C<a.jpg_original> if it exists, after asking "Are you sure?":
+
+ exiftool -delete_original a.jpg
+
+These options may not be used with other options to read or write tag values
+in the same command, but may be combined with options such B<-ext>, B<-if>,
+B<-r>, B<-q> and B<-v>.
+
+=back
+
+=head3 Advanced options
+
+Among other things, the advanced options allow complex processing to be
+performed from a single command without the need for additional scripting.
+This may be particularly useful for implementations such as Windows
+drag-and-drop applications. These options may also be used to improve
+performance in multi-pass processing by reducing the overhead required to
+load exiftool for each invocation.
+
+=over 5
+
+=item B<-api> I<OPT[[^]=[VAL]]>
+
+Set ExifTool API option. I<OPT> is an API option name. The option value is
+set to 1 if I<=VAL> is omitted. If I<VAL> is omitted, the option value is
+set to undef if C<=> is used, or an empty string with C<^=>. See
+L<Image::ExifTool Options|Image::ExifTool/Options> for a list of available
+API options. This overrides API options set via the config file.
+
+=item B<-common_args>
+
+Specifies that all arguments following this option are common to all
+executed commands when B<-execute> is used. This and the B<-config> option
+are the only options that may not be used inside a B<-@> I<ARGFILE>. Note
+that by definition this option and its arguments MUST come after all other
+options on the command line.
+
+=item B<-config> I<CFGFILE>
+
+Load specified configuration file instead of the default ".ExifTool_config".
+If used, this option must come before all other arguments on the command
+line and applies to all B<-execute>'d commands. The I<CFGFILE> must exist
+relative to the current working directory or the exiftool application
+directory unless an absolute path is specified. Loading of the default
+config file may be disabled by setting I<CFGFILE> to an empty string (ie.
+""). See L<https://exiftool.org/config.html> and
+config_files/example.config in the full ExifTool distribution for details
+about the configuration file syntax.
+
+=item B<-echo>[I<NUM>] I<TEXT>
+
+Echo I<TEXT> to stdout (B<-echo> or B<-echo1>) or stderr (B<-echo2>). Text
+is output as the command line is parsed, before the processing of any input
+files. I<NUM> may also be 3 or 4 to output text (to stdout or stderr
+respectively) after processing is complete. For B<-echo3> and B<-echo4>,
+"${status}" may be used in the I<TEXT> string to represent the numerical
+exit status of the command (see L</EXIT STATUS>).
+
+=item B<-efile>[I<NUM>][!] I<ERRFILE>
+
+Save the names of files giving errors (I<NUM> missing or 1), files that were
+unchanged (I<NUM> is 2), files that fail the B<-if> condition (I<NUM> is 4),
+or any combination thereof (by summing I<NUM>, eg. B<-efile3> is the same
+has having both B<-efile> and B<-efile2> options with the same I<ERRFILE>).
+By default, file names are appended to any existing I<ERRFILE>, but
+I<ERRFILE> is overwritten if an exclamation point is added to the option
+(eg. B<-efile!>). Saves the name of the file specified by the B<-srcfile>
+option if applicable.
+
+=item B<-execute>[I<NUM>]
+
+Execute command for all arguments up to this point on the command line (plus
+any arguments specified by B<-common_args>). The result is as if the
+commands were executed as separate command lines (with the exception of the
+B<-config> and B<-use> options which remain in effect for subsequent
+commands). Allows multiple commands to be executed from a single command
+line. I<NUM> is an optional number that is echoed in the "{ready}" message
+when using the B<-stay_open> feature. If a I<NUM> is specified, the B<-q>
+option no longer suppresses the output "{readyNUM}" message.
+
+=item B<-srcfile> I<FMT>
+
+Specify a different source file to be processed based on the name of the
+original I<FILE>. This may be useful in some special situations for
+processing related preview images or sidecar files. See the B<-w> option
+for a description of the I<FMT> syntax. Note that file name I<FMT> strings
+for all options are based on the original I<FILE> specified from the command
+line, not the name of the source file specified by B<-srcfile>.
+
+For example, to copy metadata from NEF files to the corresponding JPG
+previews in a directory where other JPG images may exist:
+
+ exiftool -ext nef -tagsfromfile @ -srcfile %d%f.jpg dir
+
+If more than one B<-srcfile> option is specified, the files are tested in
+order and the first existing source file is processed. If none of the
+source files already exist, then exiftool uses the first B<-srcfile>
+specified.
+
+A I<FMT> of C<@> may be used to represent the original I<FILE>, which may be
+useful when specifying multiple B<-srcfile> options (eg. to fall back to
+processing the original I<FILE> if no sidecar exists).
+
+When this option is used, two special UserParam tags (OriginalFileName and
+OriginalDirectory) are generated to allow access to the original I<FILE>
+name and directory.
+
+=item B<-stay_open> I<FLAG>
+
+If I<FLAG> is C<1> or C<True>, causes exiftool keep reading from the B<-@>
+I<ARGFILE> even after reaching the end of file. This feature allows calling
+applications to pre-load exiftool, thus avoiding the overhead of loading
+exiftool for each command. The procedure is as follows:
+
+1) Execute C<exiftool -stay_open True -@ I<ARGFILE>>, where I<ARGFILE> is the
+name of an existing (possibly empty) argument file or C<-> to pipe arguments
+from the standard input.
+
+2) Write exiftool command-line arguments to I<ARGFILE>, one argument per
+line (see the B<-@> option for details).
+
+3) Write C<-execute\n> to I<ARGFILE>, where C<\n> represents a newline
+sequence. (Note: You may need to flush your write buffers here if using
+buffered output.) ExifTool will then execute the command with the arguments
+received up to this point, send a "{ready}" message to stdout when done
+(unless the B<-q> or B<-T> option is used), and continue trying to read
+arguments for the next command from I<ARGFILE>. To aid in command/response
+synchronization, any number appended to the C<-execute> option is echoed in
+the "{ready}" message. For example, C<-execute613> results in "{ready613}".
+When this number is added, B<-q> no longer suppresses the "{ready}" message.
+(Also, see the B<-echo3> and B<-echo4> options for additional ways to pass
+signals back to your application.)
+
+4) Repeat steps 2 and 3 for each command.
+
+5) Write C<-stay_open\nFalse\n> to I<ARGFILE> when done. This will cause
+exiftool to process any remaining command-line arguments then exit normally.
+
+The input I<ARGFILE> may be changed at any time before step 5 above by
+writing the following lines to the currently open I<ARGFILE>:
+
+ -stay_open
+ True
+ -@
+ NEWARGFILE
+
+This causes I<ARGFILE> to be closed, and I<NEWARGFILE> to be kept open.
+(Without the B<-stay_open> here, exiftool would have returned to reading
+arguments from I<ARGFILE> after reaching the end of I<NEWARGFILE>.)
+
+Note: When writing arguments to a disk file there is a delay of up to 0.01
+seconds after writing C<-execute\n> before exiftool starts processing the
+command. This delay may be avoided by sending a CONT signal to the exiftool
+process immediately after writing C<-execute\n>. (There is no associated
+delay when writing arguments via a pipe with C<-@ ->, so the signal is not
+necessary when using this technique.)
+
+=item B<-userParam> I<PARAM[[^]=[VAL]]>
+
+Set user parameter. I<PARAM> is an arbitrary user parameter name. This is
+an interface to the API UserParam option (see the
+L<Image::ExifTool Options|Image::ExifTool/Options> documentation), and
+provides a method to access user-defined parameters in arguments to the
+B<-if> and B<-p> options as if they were any other tag. Appending a hash
+tag (C<#>) to I<PARAM> also causes the parameter to be extracted as a normal
+tag (in the UserParam group). Similar to the B<-api> option, the parameter
+value is set to 1 if I<=VAL> is omitted, undef if just I<VAL> is omitted
+with C<=>, or an empty string if I<VAL> is omitted with C<^=>.
+
+ exiftool -p '$test from $filename' -userparam test=Hello FILE
+
+=back
+
+=head3 Advanced formatting feature
+
+An advanced formatting feature allows modification of the value of any tag
+interpolated within a B<-if> or B<-p> option argument, or a B<-tagsFromFile>
+redirection string. Tag names within these strings are prefixed by a C<$>
+symbol, and an arbitrary Perl expression may be applied to the tag value by
+placing braces around the tag name and inserting the expression after the
+name, separated by a semicolon (ie. C<${TAG;EXPR}>). The expression acts on
+the value of the tag through the default input variable (C<$_>), and has
+access to the full ExifTool API through the current ExifTool object
+(C<$self>) and the tag key (C<$tag>). It may contain any valid Perl code,
+including translation (C<tr///>) and substitution (C<s///>) operations, but
+note that braces within the expression must be balanced. The example below
+prints the camera Make with spaces translated to underlines, and multiple
+consecutive underlines replaced by a single underline:
+
+ exiftool -p '${make;tr/ /_/;s/__+/_/g}' image.jpg
+
+An C<@> may be added after the tag name to make the expression act on
+individual list items for list-type tags, simplifying list processing. Set
+C<$_> to undef to remove an item from the list. As an example, the
+following command returns all subjects not containing the string "xxx":
+
+ exiftool -p '${subject@;$_=undef if /xxx/}' image.jpg
+
+A default expression of C<tr(/\\?*:|"E<lt>E<gt>\0)()d> is assumed if the
+expression is empty (ie. C<${TAG;}>). This removes the characters / \ ? * :
+| E<lt> E<gt> and null from the printed value. (These characters are
+illegal in Windows file names, so this feature is useful if tag values are
+used in file names.)
+
+=head4 Helper functions
+
+C<DateFmt>
+
+Simplifies reformatting of individual date/time values. This function acts
+on a standard EXIF-formatted date/time value in C<$_> and formats it
+according to the specified format string (see the B<-d> option). To avoid
+trying to reformat an already-formatted date/time value, a C<#> must be
+added to the tag name (as in the example below) if the B<-d> option is also
+used. For example:
+
+ exiftool -p '${createdate#;DateFmt("%Y-%m-%d_%H%M%S")}' a.jpg
+
+C<ShiftTime>
+
+Shifts EXIF-formatted date/time string by a specified amount. Start with a
+leading minus sign to shift backwards in time. See
+L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> for details about
+shift syntax. For example, to shift a date/time value back by one year:
+
+ exiftool -p '${createdate;ShiftTime("-1:0:0 0")}' a.jpg
+
+C<NoDups>
+
+Removes duplicate items from a list with a separator specified by the
+B<-sep> option. This function is most useful when copying list-type tags.
+For example, the following command may be used to remove duplicate Keywords:
+
+ exiftool -sep '##' '-keywords<${keywords;NoDups}' a.jpg
+
+The B<-sep> option is necessary to split the string back into individual
+list items when writing to a list-type tag.
+
+An optional flag argument may be set to 1 to cause C<NoDups> to return undef
+if no duplicates existed, thus preventing the file from being rewritten
+unnecessarily:
+
+ exiftool -sep '##' '-keywords<${keywords;NoDups(1)}' a.jpg
+
+Note that function names are case sensitive.
+
+=head1 WINDOWS UNICODE FILE NAMES
+
+In Windows, command-line arguments are specified using the current code page
+and are recoded automatically to the system code page. This recoding is not
+done for arguments in ExifTool arg files, so by default filenames in arg
+files use the system code page. Unfortunately, these code pages are not
+complete character sets, so not all file names may be represented.
+
+ExifTool 9.79 and later allow the file name encoding to be specified with
+C<-charset filename=CHARSET>, where C<CHARSET> is the name of a valid
+ExifTool character set, preferably C<UTF8> (see the B<-charset> option for a
+complete list). Setting this triggers the use of Windows wide-character i/o
+routines, thus providing support for most Unicode file names (see note 4).
+But note that it is not trivial to pass properly encoded file names on the
+Windows command line (see L<https://exiftool.org/faq.html#Q18> for details),
+so placing them in a UTF-8 encoded B<-@> argfile and using C<-charset
+filename=utf8> is recommended if possible.
+
+A warning is issued if a specified filename contains special characters and
+the filename character set was not provided. However, the warning may be
+disabled by setting C<-charset filename="">, and ExifTool may still function
+correctly if the system code page matches the character set used for the
+file names.
+
+When a directory name is provided, the file name encoding need not be
+specified (unless the directory name contains special characters), and
+ExifTool will automatically use wide-character routines to scan the
+directory.
+
+The filename character set applies to the I<FILE> arguments as well as
+filename arguments of B<-@>, B<-geotag>, B<-o>, B<-p>, B<-srcfile>,
+B<-tagsFromFile>, B<-csv>=, B<-j>= and B<->I<TAG>E<lt>=. However, it does
+not apply to the B<-config> filename, which always uses the system character
+set. The C<-charset filename=> option must come before the B<-@> option to
+be effective, but the order doesn't matter with respect to other options.
+
+Notes:
+
+1) FileName and Directory tag values still use the same encoding as other
+tag values, and are converted to/from the filename character set when
+writing/reading if specified.
+
+2) Unicode support is not yet implemented for other Windows-based systems
+like Cygwin.
+
+3) See L</WRITING READ-ONLY FILES> below for a note about editing read-only
+files with Unicode names.
+
+4) Unicode file names with surrogate pairs (code points over U+FFFF) still
+cause problems.
+
+=head1 WRITING READ-ONLY FILES
+
+In general, ExifTool may be used to write metadata to read-only files
+provided that the user has write permission in the directory. However,
+there are three cases where file write permission is also required:
+
+1) When using the B<-overwrite_original_in_place> option.
+
+2) When writing only pseudo System tags (eg. FileModifyDate).
+
+3) On Windows if the file has Unicode characters in its name, and a) the
+B<-overwrite_original> option is used, or b) the C<_original> backup already
+exists.
+
+Hidden files in Windows behave as read-only files when attempting to write
+any real tags to the file -- an error is generated when using the
+B<-overwrite_original_in_place>, otherwise writing should be successful and
+the hidden attribute will be removed. But the B<-if> option may be used to
+avoid processing hidden files (provided Win32API::File is available):
+
+ exiftool -if "$fileattributes !~ /Hidden/" ...
+
+=head1 READING EXAMPLES
+
+B<Note>: Beware when cutting and pasting these examples into your terminal!
+Some characters such as single and double quotes and hyphens may have been
+changed into similar-looking yet functionally-different characters by the
+text formatter used to display this documentation. Also note that Windows
+users must use double quotes instead of single quotes as below around
+arguments containing special characters.
+
+=over 5
+
+=item exiftool -a -u -g1 a.jpg
+
+Print all meta information in an image, including duplicate and unknown
+tags, sorted by group (for family 1). For performance reasons, this command
+may not extract all available metadata. (Metadata in embedded documents,
+metadata extracted by external utilities, and metadata requiring excessive
+processing time may not be extracted). Add C<-ee> and C<-api RequestAll=3>
+to the command to extract absolutely everything available.
+
+=item exiftool -common dir
+
+Print common meta information for all images in C<dir>. C<-common> is a
+L<shortcut tag|Image::ExifTool::Shortcuts> representing common EXIF meta
+information.
+
+=item exiftool -T -createdate -aperture -shutterspeed -iso dir > out.txt
+
+List specified meta information in tab-delimited column form for all images
+in C<dir> to an output text file named "out.txt".
+
+=item exiftool -s -ImageSize -ExposureTime b.jpg
+
+Print ImageSize and ExposureTime tag names and values.
+
+=item exiftool -l -canon c.jpg d.jpg
+
+Print standard Canon information from two image files.
+
+=item exiftool -r -w .txt -common pictures
+
+Recursively extract common meta information from files in C<pictures>
+directory, writing text output to C<.txt> files with the same names.
+
+=item exiftool -b -ThumbnailImage image.jpg > thumbnail.jpg
+
+Save thumbnail image from C<image.jpg> to a file called C<thumbnail.jpg>.
+
+=item exiftool -b -JpgFromRaw -w _JFR.JPG -ext NEF -r .
+
+Recursively extract JPG image from all Nikon NEF files in the current
+directory, adding C<_JFR.JPG> for the name of the output JPG files.
+
+=item exiftool -a -b -W %d%f_%t%-c.%s -preview:all dir
+
+Extract all types of preview images (ThumbnailImage, PreviewImage,
+JpgFromRaw, etc.) from files in directory "dir", adding the tag name to the
+output preview image file names.
+
+=item exiftool -d '%r %a, %B %e, %Y' -DateTimeOriginal -S -s -ext jpg .
+
+Print formatted date/time for all JPG files in the current directory.
+
+=item exiftool -IFD1:XResolution -IFD1:YResolution image.jpg
+
+Extract image resolution from EXIF IFD1 information (thumbnail image IFD).
+
+=item exiftool '-*resolution*' image.jpg
+
+Extract all tags with names containing the word "Resolution" from an image.
+
+=item exiftool -xmp:author:all -a image.jpg
+
+Extract all author-related XMP information from an image.
+
+=item exiftool -xmp -b a.jpg > out.xmp
+
+Extract complete XMP data record intact from C<a.jpg> and write it to
+C<out.xmp> using the special C<XMP> tag (see the Extra tags in
+L<Image::ExifTool::TagNames|Image::ExifTool::TagNames>).
+
+=item exiftool -p '$filename has date $dateTimeOriginal' -q -f dir
+
+Print one line of output containing the file name and DateTimeOriginal for
+each image in directory C<dir>.
+
+=item exiftool -ee -p '$gpslatitude, $gpslongitude, $gpstimestamp' a.m2ts
+
+Extract all GPS positions from an AVCHD video.
+
+=item exiftool -icc_profile -b -w icc image.jpg
+
+Save complete ICC_Profile from an image to an output file with the same name
+and an extension of C<.icc>.
+
+=item exiftool -htmldump -w tmp/%f_%e.html t/images
+
+Generate HTML pages from a hex dump of EXIF information in all images from
+the C<t/images> directory. The output HTML files are written to the C<tmp>
+directory (which is created if it didn't exist), with names of the form
+'FILENAME_EXT.html'.
+
+=item exiftool -a -b -ee -embeddedimage -W Image_%.3g3.%s file.pdf
+
+Extract embedded JPG and JP2 images from a PDF file. The output images will
+have file names like "Image_#.jpg" or "Image_#.jp2", where "#" is the
+ExifTool family 3 embedded document number for the image.
+
+=back
+
+=head1 WRITING EXAMPLES
+
+Note that quotes are necessary around arguments which contain certain
+special characters such as C<E<gt>>, C<E<lt>> or any white space. These
+quoting techniques are shell dependent, but the examples below will work for
+most Unix shells. With the Windows cmd shell however, double quotes should
+be used (eg. -Comment=E<34>This is a new commentE<34>).
+
+=over 5
+
+=item exiftool -Comment='This is a new comment' dst.jpg
+
+Write new comment to a JPG image (replaces any existing comment).
+
+=item exiftool -comment= -o newdir -ext jpg .
+
+Remove comment from all JPG images in the current directory, writing the
+modified images to a new directory.
+
+=item exiftool -keywords=EXIF -keywords=editor dst.jpg
+
+Replace existing keyword list with two new keywords (C<EXIF> and C<editor>).
+
+=item exiftool -Keywords+=word -o newfile.jpg src.jpg
+
+Copy a source image to a new file, and add a keyword (C<word>) to the
+current list of keywords.
+
+=item exiftool -exposurecompensation+=-0.5 a.jpg
+
+Decrement the value of ExposureCompensation by 0.5 EV. Note that += with a
+negative value is used for decrementing because the -= operator is used for
+conditional deletion (see next example).
+
+=item exiftool -credit-=xxx dir
+
+Delete Credit information from all files in a directory where the Credit
+value was C<xxx>.
+
+=item exiftool -xmp:description-de='k&uuml;hl' -E dst.jpg
+
+Write alternate language for XMP:Description, using HTML character escaping
+to input special characters.
+
+=item exiftool -all= dst.jpg
+
+Delete all meta information from an image. Note: You should NOT do this to
+RAW images (except DNG) since proprietary RAW image formats often contain
+information in the makernotes that is necessary for converting the image.
+
+=item exiftool -all= -comment='lonely' dst.jpg
+
+Delete all meta information from an image and add a comment back in. (Note
+that the order is important: C<-comment='lonely' -all=> would also delete
+the new comment.)
+
+=item exiftool -all= --jfif:all dst.jpg
+
+Delete all meta information except JFIF group from an image.
+
+=item exiftool -Photoshop:All= dst.jpg
+
+Delete Photoshop meta information from an image (note that the Photoshop
+information also includes IPTC).
+
+=item exiftool -r -XMP-crss:all= DIR
+
+Recursively delete all XMP-crss information from images in a directory.
+
+=item exiftool '-ThumbnailImageE<lt>=thumb.jpg' dst.jpg
+
+Set the thumbnail image from specified file (Note: The quotes are necessary
+to prevent shell redirection).
+
+=item exiftool '-JpgFromRawE<lt>=%d%f_JFR.JPG' -ext NEF -r .
+
+Recursively write JPEG images with filenames ending in C<_JFR.JPG> to the
+JpgFromRaw tag of like-named files with extension C<.NEF> in the current
+directory. (This is the inverse of the C<-JpgFromRaw> command of the
+L</READING EXAMPLES> section above.)
+
+=item exiftool -DateTimeOriginal-='0:0:0 1:30:0' dir
+
+Adjust original date/time of all images in directory C<dir> by subtracting
+one hour and 30 minutes. (This is equivalent to C<-DateTimeOriginal-=1.5>.
+See L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl> for details.)
+
+=item exiftool -createdate+=3 -modifydate+=3 a.jpg b.jpg
+
+Add 3 hours to the CreateDate and ModifyDate timestamps of two images.
+
+=item exiftool -AllDates+=1:30 -if '$make eq E<34>CanonE<34>' dir
+
+Shift the values of DateTimeOriginal, CreateDate and ModifyDate forward by 1
+hour and 30 minutes for all Canon images in a directory. (The AllDates tag
+is provided as a shortcut for these three tags, allowing them to be accessed
+via a single tag.)
+
+=item exiftool -xmp:city=Kingston image1.jpg image2.nef
+
+Write a tag to the XMP group of two images. (Without the C<xmp:> this tag
+would get written to the IPTC group since C<City> exists in both, and IPTC
+is preferred by default.)
+
+=item exiftool -LightSource-='Unknown (0)' dst.tiff
+
+Delete C<LightSource> tag only if it is unknown with a value of 0.
+
+=item exiftool -whitebalance-=auto -WhiteBalance=tung dst.jpg
+
+Set C<WhiteBalance> to C<Tungsten> only if it was previously C<Auto>.
+
+=item exiftool -comment-= -comment='new comment' a.jpg
+
+Write a new comment only if the image doesn't have one already.
+
+=item exiftool -o %d%f.xmp dir
+
+Create XMP meta information data files for all images in C<dir>.
+
+=item exiftool -o test.xmp -owner=Phil -title='XMP File'
+
+Create an XMP data file only from tags defined on the command line.
+
+=item exiftool '-ICC_Profile<=%d%f.icc' image.jpg
+
+Write ICC_Profile to an image from a C<.icc> file of the same name.
+
+=item exiftool -hierarchicalkeywords='{keyword=one,children={keyword=B}}'
+
+Write structured XMP information. See L<https://exiftool.org/struct.html>
+for more details.
+
+=item exiftool -trailer:all= image.jpg
+
+Delete any trailer found after the end of image (EOI) in a JPEG file. A
+number of digital cameras store a large PreviewImage after the JPEG EOI, and
+the file size may be reduced significantly by deleting this trailer. See
+the L<JPEG Tags documentation|Image::ExifTool::TagNames/JPEG Tags> for a
+list of recognized JPEG trailers.
+
+=back
+
+=head1 COPYING EXAMPLES
+
+These examples demonstrate the ability to copy tag values between files.
+
+=over 5
+
+=item exiftool -tagsFromFile src.cr2 dst.jpg
+
+Copy the values of all writable tags from C<src.cr2> to C<dst.jpg>, writing
+the information to same-named tags in the preferred groups.
+
+=item exiftool -TagsFromFile src.jpg -all:all dst.jpg
+
+Copy the values of all writable tags from C<src.jpg> to C<dst.jpg>,
+preserving the original tag groups.
+
+=item exiftool -all= -tagsfromfile src.jpg -exif:all dst.jpg
+
+Erase all meta information from C<dst.jpg> image, then copy EXIF tags from
+C<src.jpg>.
+
+=item exiftool -exif:all= -tagsfromfile @ -all:all -unsafe bad.jpg
+
+Rebuild all EXIF meta information from scratch in an image. This technique
+can be used in JPEG images to repair corrupted EXIF information which
+otherwise could not be written due to errors. The C<Unsafe> tag is a
+shortcut for unsafe EXIF tags in JPEG images which are not normally copied.
+See the L<tag name documentation|Image::ExifTool::TagNames> for more details
+about unsafe tags.
+
+=item exiftool -Tagsfromfile a.jpg out.xmp
+
+Copy meta information from C<a.jpg> to an XMP data file. If the XMP data
+file C<out.xmp> already exists, it will be updated with the new information.
+Otherwise the XMP data file will be created. Only metadata-only files may
+be created like this (files containing images may be edited but not
+created). See L</WRITING EXAMPLES> above for another technique to generate
+XMP files.
+
+=item exiftool -tagsFromFile a.jpg -XMP:All= -ThumbnailImage= -m b.jpg
+
+Copy all meta information from C<a.jpg> to C<b.jpg>, deleting all XMP
+information and the thumbnail image from the destination.
+
+=item exiftool -TagsFromFile src.jpg -title -author=Phil dst.jpg
+
+Copy title from one image to another and set a new author name.
+
+=item exiftool -TagsFromFile a.jpg -ISO -TagsFromFile b.jpg -comment
+dst.jpg
+
+Copy ISO from one image and Comment from another image to a destination
+image.
+
+=item exiftool -tagsfromfile src.jpg -exif:all --subifd:all dst.jpg
+
+Copy only the EXIF information from one image to another, excluding SubIFD
+tags.
+
+=item exiftool '-FileModifyDateE<lt>DateTimeOriginal' dir
+
+Use the original date from the meta information to set the same file's
+filesystem modification date for all images in a directory. (Note that
+C<-TagsFromFile @> is assumed if no other B<-TagsFromFile> is specified when
+redirecting information as in this example.)
+
+=item exiftool -TagsFromFile src.jpg '-xmp:allE<lt>all' dst.jpg
+
+Copy all possible information from C<src.jpg> and write in XMP format to
+C<dst.jpg>.
+
+=item exiftool '-Description<${FileName;s/\.[^.]*$//}' dir
+
+Set the image Description from the file name after removing the extension.
+This example uses the L</Advanced formatting feature> to perform a
+substitution operation to remove the last dot and subsequent characters from
+the file name.
+
+=item exiftool -@ iptc2xmp.args -iptc:all= a.jpg
+
+Translate IPTC information to XMP with appropriate tag name conversions, and
+delete the original IPTC information from an image. This example uses
+iptc2xmp.args, which is a file included with the ExifTool distribution that
+contains the required arguments to convert IPTC information to XMP format.
+Also included with the distribution are xmp2iptc.args (which performs the
+inverse conversion) and a few more .args files for other conversions between
+EXIF, IPTC and XMP.
+
+=item exiftool -tagsfromfile %d%f.CR2 -r -ext JPG dir
+
+Recursively rewrite all C<JPG> images in C<dir> with information copied from
+the corresponding C<CR2> images in the same directories.
+
+=item exiftool '-keywords+E<lt>make' image.jpg
+
+Add camera make to list of keywords.
+
+=item exiftool '-commentE<lt>ISO=$exif:iso Exposure=${shutterspeed}' dir
+
+Set the Comment tag of all images in C<dir> from the values of the EXIF:ISO
+and ShutterSpeed tags. The resulting comment will be in the form "ISO=100
+Exposure=1/60".
+
+=item exiftool -TagsFromFile src.jpg -icc_profile dst.jpg
+
+Copy ICC_Profile from one image to another.
+
+=item exiftool -TagsFromFile src.jpg -all:all dst.mie
+
+Copy all meta information in its original form from a JPEG image to a MIE
+file. The MIE file will be created if it doesn't exist. This technique can
+be used to store the metadata of an image so it can be inserted back into
+the image (with the inverse command) later in a workflow.
+
+=item exiftool -o dst.mie -all:all src.jpg
+
+This command performs exactly the same task as the command above, except
+that the B<-o> option will not write to an output file that already exists.
+
+=item exiftool -b -jpgfromraw -w %d%f_%ue.jpg -execute -b -previewimage -w
+%d%f_%ue.jpg -execute -tagsfromfile @ -srcfile %d%f_%ue.jpg
+-overwrite_original -common_args --ext jpg DIR
+
+[Advanced] Extract JpgFromRaw or PreviewImage from all but JPG files in DIR,
+saving them with file names like C<image_EXT.jpg>, then add all meta
+information from the original files to the extracted images. Here, the
+command line is broken into three sections (separated by B<-execute>
+options), and each is executed as if it were a separate command. The
+B<-common_args> option causes the C<--ext jpg DIR> arguments to be applied
+to all three commands, and the B<-srcfile> option allows the extracted JPG
+image to be the source file for the third command (whereas the RAW files are
+the source files for the other two commands).
+
+=back
+
+=head1 RENAMING EXAMPLES
+
+By writing the C<FileName> and C<Directory> tags, files are renamed and/or
+moved to new directories. This can be particularly useful and powerful for
+organizing files by date when combined with the B<-d> option. New
+directories are created as necessary, but existing files will not be
+overwritten. The format codes %d, %f and %e may be used in the new file
+name to represent the directory, name and extension of the original file,
+and %c may be used to add a copy number if the file already exists (see the
+B<-w> option for details). Note that if used within a date format string,
+an extra '%' must be added to pass these codes through the date/time parser.
+(And further note that in a Windows batch file, all '%' characters must also
+be escaped, so in this extreme case '%%%%f' is necessary to pass a simple
+'%f' through the two levels of parsing.) See
+L<https://exiftool.org/filename.html> for additional documentation and
+examples.
+
+=over 5
+
+=item exiftool -filename=new.jpg dir/old.jpg
+
+Rename C<old.jpg> to C<new.jpg> in directory C<dir>.
+
+=item exiftool -directory=%e dir
+
+Move all files from directory C<dir> into directories named by the original
+file extensions.
+
+=item exiftool '-Directory<DateTimeOriginal' -d %Y/%m/%d dir
+
+Move all files in C<dir> into a directory hierarchy based on year, month and
+day of C<DateTimeOriginal>. eg) This command would move the file
+C<dir/image.jpg> with a C<DateTimeOriginal> of C<2005:10:12 16:05:56> to
+C<2005/10/12/image.jpg>.
+
+=item exiftool -o . '-Directory<DateTimeOriginal' -d %Y/%m/%d dir
+
+Same effect as above except files are copied instead of moved.
+
+=item exiftool '-filename<%f_${model;}.%e' dir
+
+Rename all files in C<dir> by adding the camera model name to the file name.
+The semicolon after the tag name inside the braces causes characters which
+are invalid in Windows file names to be deleted from the tag value (see the
+L</Advanced formatting feature> for an explanation).
+
+=item exiftool '-FileName<CreateDate' -d %Y%m%d_%H%M%S%%-c.%%e dir
+
+Rename all images in C<dir> according to the C<CreateDate> date and time,
+adding a copy number with leading '-' if the file already exists (C<%-c>),
+and preserving the original file extension (C<%e>). Note the extra '%'
+necessary to escape the filename codes (C<%c> and C<%e>) in the date format
+string.
+
+=item exiftool -r '-FileName<CreateDate' -d %Y-%m-%d/%H%M_%%f.%%e dir
+
+Both the directory and the filename may be changed together via the
+C<FileName> tag if the new C<FileName> contains a '/'. The example above
+recursively renames all images in a directory by adding a C<CreateDate>
+timestamp to the start of the filename, then moves them into new directories
+named by date.
+
+=item exiftool '-FileName<${CreateDate}_$filenumber.jpg' -d %Y%m%d -ext jpg .
+
+Set the filename of all JPG images in the current directory from the
+CreateDate and FileNumber tags, in the form "20060507_118-1861.jpg".
+
+=back
+
+=head1 GEOTAGGING EXAMPLES
+
+ExifTool implements geotagging via 3 special tags: Geotag (which for
+convenience is also implemented as an exiftool option), Geosync and Geotime.
+The examples below highlight some geotagging features. See
+L<https://exiftool.org/geotag.html> for additional documentation.
+
+=over 5
+
+=item exiftool -geotag track.log a.jpg
+
+Geotag an image (C<a.jpg>) from position information in a GPS track log
+(C<track.log>). Since the C<Geotime> tag is not specified, the value of
+DateTimeOriginal is used for geotagging. Local system time is assumed
+unless DateTimeOriginal contains a timezone.
+
+=item exiftool -geotag t.log -geotime='2009:04:02 13:41:12-05:00' a.jpg
+
+Geotag an image with the GPS position for a specific time.
+
+=item exiftool -geotag log.gpx '-xmp:geotimeE<lt>createdate' dir
+
+Geotag all images in directory C<dir> with XMP tags instead of EXIF tags,
+based on the image CreateDate.
+
+=item exiftool -geotag a.log -geosync=-20 dir
+
+Geotag images in directory C<dir>, accounting for image timestamps which
+were 20 seconds ahead of GPS.
+
+=item exiftool -geotag a.log -geosync=1.jpg -geosync=2.jpg dir
+
+Geotag images using time synchronization from two previously geotagged images
+(1.jpg and 2.jpg), synchronizing the image and GPS times using a linear time
+drift correction.
+
+=item exiftool -geotag a.log '-geotimeE<lt>${createdate}+01:00' dir
+
+Geotag images in C<dir> using CreateDate with the specified timezone. If
+CreateDate already contained a timezone, then the timezone specified on the
+command line is ignored.
+
+=item exiftool -geotag= a.jpg
+
+Delete GPS tags which may have been added by the geotag feature. Note that
+this does not remove all GPS tags -- to do this instead use C<-gps:all=>.
+
+=item exiftool -xmp:geotag= a.jpg
+
+Delete XMP GPS tags which were added by the geotag feature.
+
+=item exiftool -xmp:geotag=track.log a.jpg
+
+Geotag an image with XMP tags, using the time from DateTimeOriginal.
+
+=item exiftool -geotag a.log -geotag b.log -r dir
+
+Combine multiple track logs and geotag an entire directory tree of images.
+
+=item exiftool -geotag 'tracks/*.log' -r dir
+
+Read all track logs from the C<tracks> directory.
+
+=item exiftool -p gpx.fmt -d %Y-%m-%dT%H:%M:%SZ dir > out.gpx
+
+Generate a GPX track log from all images in directory C<dir>. This example
+uses the C<gpx.fmt> file included in the full ExifTool distribution package
+and assumes that the images in C<dir> have all been previously geotagged.
+
+=back
+
+=head1 PIPING EXAMPLES
+
+=over 5
+
+=item cat a.jpg | exiftool -
+
+Extract information from stdin.
+
+=item exiftool image.jpg -thumbnailimage -b | exiftool -
+
+Extract information from an embedded thumbnail image.
+
+=item cat a.jpg | exiftool -iptc:keywords+=fantastic - > b.jpg
+
+Add an IPTC keyword in a pipeline, saving output to a new file.
+
+=item curl -s http://a.domain.com/bigfile.jpg | exiftool -fast -
+
+Extract information from an image over the internet using the cURL utility.
+The B<-fast> option prevents exiftool from scanning for trailer information,
+so only the meta information header is transferred.
+
+=item exiftool a.jpg -thumbnailimage -b | exiftool -comment=wow - |
+exiftool a.jpg -thumbnailimage'<=-'
+
+Add a comment to an embedded thumbnail image. (Why anyone would want to do
+this I don't know, but I've included this as an example to illustrate the
+flexibility of ExifTool.)
+
+=back
+
+=head1 EXIT STATUS
+
+The exiftool application exits with a status of 0 on success, or 1 if an
+error occurred, or 2 if all files failed the B<-if> condition (for any of
+the commands if B<-execute> was used).
+
+=head1 AUTHOR
+
+Copyright 2003-2020, Phil Harvey
+
+This is free software; you can redistribute it and/or modify it under the
+same terms as Perl itself.
+
+=head1 SEE ALSO
+
+L<Image::ExifTool(3pm)|Image::ExifTool>,
+L<Image::ExifTool::TagNames(3pm)|Image::ExifTool::TagNames>,
+L<Image::ExifTool::Shortcuts(3pm)|Image::ExifTool::Shortcuts>,
+L<Image::ExifTool::Shift.pl|Image::ExifTool::Shift.pl>
+
+=cut
+
+#------------------------------------------------------------------------------
+# end
diff --git a/flatdark.sh b/flatdark.sh
new file mode 100755
index 0000000..ad2a2c7
--- /dev/null
+++ b/flatdark.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+while true; do
+ echo "Press 1 for light theme, 2 for dark theme and hit enter"
+ read userInput
+ userInput=$(echo "$userInput" | xargs)
+ if [ "$userInput" -eq "1" ]; then
+ theme="Adwaita"
+ elif [ "$userInput" -eq "2" ]; then
+ theme="Adwaita-dark"
+ else
+ clear
+ echo "Invalid input, try again"
+ continue
+ fi
+ sudo flatpak override --env=GTK_THEME=$theme; sudo flatpak override --env=QT_STYLE_OVERRIDE=$theme
+ clear
+ echo "Operation Complete"
+done
diff --git a/flattheme.sh b/flattheme.sh
new file mode 100755
index 0000000..82996ad
--- /dev/null
+++ b/flattheme.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+for dir in $HOME/.var/app/*/
+do
+ confdir="${dir}config/gtk-3.0"
+ mkdir -p $confdir
+ cp $HOME/.config/gtk-3.0/settings.ini $confdir/settings.ini
+done
diff --git a/hotkeys.sh b/hotkeys.sh
new file mode 100755
index 0000000..7a9c6c3
--- /dev/null
+++ b/hotkeys.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+killall swhks
+swhks &
+pkexec swhkd
diff --git a/launch_polybar.sh b/launch_polybar.sh
new file mode 100755
index 0000000..9550fb4
--- /dev/null
+++ b/launch_polybar.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+if type "xrandr"; then
+ for m in $(xrandr --query | grep " connected" | cut -d" " -f1); do
+ MONITOR=$m polybar --reload $1 &
+ done
+else
+ polybar --reload $1 &
+fi
diff --git a/mailsync b/mailsync
new file mode 100755
index 0000000..0277b35
--- /dev/null
+++ b/mailsync
@@ -0,0 +1,90 @@
+#!/bin/sh
+
+# - Syncs mail for all accounts, or a single account given as an argument.
+# - Displays a notification showing the number of new mails.
+# - Displays a notification for each new mail with its subject displayed.
+# - Runs notmuch to index new mail.
+# - This script can be set up as a cron job for automated mail syncing.
+
+# There are many arbitrary and ugly features in this script because it is
+# inherently difficult to pass environmental variables to cronjobs and other
+# issues. It also should at least be compatible with Linux (and maybe BSD) with
+# Xorg and MacOS as well.
+
+# Run only if user logged in (prevent cron errors)
+pgrep -u "${USER:=$LOGNAME}" >/dev/null || { echo "$USER not logged in; sync will not run."; exit ;}
+# Run only if not already running in other instance
+pgrep mbsync >/dev/null && { echo "mbsync is already running."; exit ;}
+
+# First, we have to get the right variables for the mbsync file, the pass
+# archive, notmuch and the GPG home. This is done by searching common profile
+# files for variable assignments. This is ugly, but there are few options that
+# will work on the maximum number of machines.
+eval "$(grep -h -- \
+ "^\s*\(export \)\?\(MBSYNCRC\|PASSWORD_STORE_DIR\|NOTMUCH_CONFIG\|GNUPGHOME\)=" \
+ "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.zprofile" "$HOME/.config/zsh/.zprofile" "$HOME/.zshenv" \
+ "$HOME/.config/zsh/.zshenv" "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/zsh/.zshrc" \
+ "$HOME/.pam_environment" 2>/dev/null)"
+
+export GPG_TTY="$(tty)"
+
+[ -n "$MBSYNCRC" ] && alias mbsync="mbsync -c $MBSYNCRC" || MBSYNCRC="$HOME/.config/isync/mbsyncrc"
+
+# Settings are different for MacOS (Darwin) systems.
+case "$(uname)" in
+ Darwin)
+ notify() { osascript -e "display notification \"$2 in $1\" with title \"You've got Mail\" subtitle \"Account: $account\"" && sleep 2 ;}
+ ;;
+ *)
+ case "$(readlink -f /sbin/init)" in
+ *systemd*|*openrc*) export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus ;;
+ esac
+ # remember if a display server is running since `ps` doesn't always contain a display
+ pgrepoutput="$(pgrep -a X\(org\|wayland\))"
+ displays="$(echo "$pgrepoutput" | grep -wo "[0-9]*:[0-9]\+" | sort -u)"
+ notify() { [ -n "$pgrepoutput" ] && for x in ${displays:-0:}; do
+ export DISPLAY=$x
+ notify-send --app-name="mutt-wizard" "New mail!" "📬 $2 new mail(s) in \`$1\` account."
+ done ;}
+ ;;
+esac
+
+# Check account for new mail. Notify if there is new content.
+syncandnotify() {
+ acc="$(echo "$account" | sed "s/.*\///")"
+ if [ -z "$opts" ]; then mbsync "$acc"; else mbsync "$opts" "$acc"; fi
+ new=$(find\
+ "$HOME/.local/share/mail/$acc/INBOX/new/"\
+ "$HOME/.local/share/mail/$acc/Inbox/new/"\
+ "$HOME/.local/share/mail/$acc/inbox/new/"\
+ "$HOME/.local/share/mail/$acc/INBOX/cur/"\
+ "$HOME/.local/share/mail/$acc/Inbox/cur/"\
+ "$HOME/.local/share/mail/$acc/inbox/cur/"\
+ -type f -newer "${XDG_CONFIG_HOME:-$HOME/.config}/mutt/.mailsynclastrun" 2> /dev/null)
+ newcount=$(echo "$new" | sed '/^\s*$/d' | wc -l)
+ case 1 in
+ $((newcount > 0)) ) notify "$acc" "$newcount" ;;
+ esac
+}
+
+# Sync accounts passed as argument or all.
+if [ "$#" -eq "0" ]; then
+ accounts="$(awk '/^Channel/ {print $2}' "$MBSYNCRC")"
+else
+ for arg in "$@"; do
+ [ "${arg%${arg#?}}" = '-' ] && opts="${opts:+${opts} }${arg}" && shift 1
+ done
+ accounts=$*
+fi
+
+# Parallelize multiple accounts
+for account in $accounts; do
+ syncandnotify &
+done
+
+wait
+
+notmuch new 2>/dev/null
+
+#Create a touch file that indicates the time of the last run of mailsync
+touch "${XDG_CONFIG_HOME:-$HOME/.config}/mutt/.mailsynclastrun"
diff --git a/s6-user-update b/s6-user-update
new file mode 100755
index 0000000..5359b5d
--- /dev/null
+++ b/s6-user-update
@@ -0,0 +1,31 @@
+ #!/bin/sh
+
+ DATAPATH="/home/${USER}/.local/share/s6"
+ RCPATH="${DATAPATH}/rc"
+ DBPATH="${RCPATH}/compiled"
+ SVPATH="${DATAPATH}/sv"
+ SVDIRS="/run/${USER}/s6-rc/servicedirs"
+ TIMESTAMP=$(date +%s)
+
+ if ! s6-rc-compile "${DBPATH}"-"${TIMESTAMP}" "${SVPATH}"; then
+ echo "Error compiling database. Please double check the ${SVPATH} directories."
+ exit 1
+ fi
+
+ if [ -e "/run/${USER}/s6-rc" ]; then
+ for dir in "${SVDIRS}"/*; do
+ if [ -e "${dir}/down" ]; then
+ s6-svc -x "${dir}"
+ fi
+ done
+ s6-rc-update -l "/run/${USER}/s6-rc" "${DBPATH}"-"${TIMESTAMP}"
+ fi
+
+ if [ -d "${DBPATH}" ]; then
+ ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"/compiled && mv -f "${DBPATH}"/compiled "${RCPATH}"
+ else
+ ln -sf "${DBPATH}"-"${TIMESTAMP}" "${DBPATH}"
+ fi
+
+ echo "==> Switched to a new database for ${USER}."
+ echo " Remove any old unwanted/unneeded database directories in ${RCPATH}."
diff --git a/slider b/slider
new file mode 100755
index 0000000..d8d87b1
--- /dev/null
+++ b/slider
@@ -0,0 +1,124 @@
+#!/bin/sh
+
+# Give a file with images and timecodes and creates a video slideshow of them.
+#
+# Timecodes must be in format 00:00:00.
+#
+# Imagemagick and ffmpeg required.
+
+# Application cache if not stated elsewhere.
+cache="${XDG_CACHE_HOME:-$HOME/.cache}/slider"
+
+while getopts "hvrpi:c:a:o:d:f:t:e:x:" o; do case "${o}" in
+ c) bgc="$OPTARG" ;;
+ t) fgc="$OPTARG" ;;
+ i) file="$OPTARG" ;;
+ a) audio="$OPTARG" ;;
+ o) outfile="$OPTARG" ;;
+ d) prepdir="$OPTARG" ;;
+ r) redo="$OPTARG" ;;
+ s) ppt="$OPTARG" ;;
+ e) endtime="$OPTARG" ;;
+ x) res="$OPTARG"
+ echo "$res" | grep -qv "^[0-9]\+x[0-9]\+$" &&
+ echo "Resolution must be dimensions separated by a 'x': 1280x720, etc." &&
+ exit 1 ;;
+ p) echo "Purge old build files in $cache? [y/N]"
+ read -r confirm
+ echo "$confirm" | grep -iq "^y$" && rm -rf "$cache" && echo "Done."
+ exit ;;
+ v) verbose=True ;;
+ *) echo "$(basename "$0") usage:
+ -i input timecode list (required)
+ -a audio file
+ -c color of background (use html names, black is default)
+ -t text color for text slides (white is default)
+ -s text font size for text slides (150 is default)
+ -o output video file
+ -e if no audio given, the time in seconds that the last slide will be shown (5 is default)
+ -x resolution (1920x1080 is default)
+ -d tmp directory
+ -r rerun imagemagick commands even if done previously (in case files or background has changed)
+ -p purge old build files instead of running
+ -v be verbose" && exit 1
+
+esac done
+
+# Check that the input file looks like it should.
+{ head -n 1 "$file" 2>/dev/null | grep -q "^00:00:00 " ;} || {
+ echo "Give an input file with -i." &&
+ echo "The file should look as this example:
+
+00:00:00 first_image.jpg
+00:00:03 otherdirectory/next_image.jpg
+00:00:09 this_image_starts_at_9_seconds.jpg
+etc...
+
+Timecodes and filenames must be separated by Tabs." &&
+ exit 1
+ }
+
+if [ -n "${audio+x}" ]; then
+ # Check that the audio file looks like an actual audio file.
+ case "$(file --dereference --brief --mime-type -- "$audio")" in
+ audio/*) ;;
+ *) echo "That doesn't look like an audio file."; exit 1 ;;
+ esac
+ totseconds="$(date '+%s' -d $(ffmpeg -i "$audio" 2>&1 | awk '/Duration/ {print $2}' | sed s/,//))"
+ endtime="$((totseconds-seconds))"
+fi
+
+prepdir="${prepdir:-$cache/$file}"
+outfile="${outfile:-$file.mp4}"
+prepfile="$prepdir/$file.prep"
+
+[ -n "${verbose+x}" ] && echo "Preparing images... May take a while depending on the number of files."
+mkdir -p "$prepdir"
+
+{
+while read -r x;
+do
+ # Get the time from the first column.
+ time="${x%% *}"
+ seconds="$(date '+%s' -d "$time")"
+ # Duration is not used on the first looped item.
+ duration="$((seconds - prevseconds))"
+
+ # Get the filename/text content from the rest.
+ content="${x#* }"
+ base="$(basename "$content")"
+ base="${base%.*}.jpg"
+
+ if [ -f "$content" ]; then
+ # If images have already been made in a previous run, do not recreate
+ # them unless -r was given.
+ { [ ! -f "$prepdir/$base" ] || [ -n "${redo+x}" ] ;} &&
+ convert -size "${res:-1920x1080}" canvas:"${bgc:-black}" -gravity center "$content" -resize 1920x1080 -composite "$prepdir/$base"
+ else
+ { [ ! -f "$prepdir/$base" ] || [ -n "${redo+x}" ] ;} &&
+ convert -size "${res:-1920x1080}" -background "${bgc:-black}" -fill "${fgc:-white}" -pointsize "${ppt:-150}" -gravity center label:"$content" "$prepdir/$base"
+ fi
+
+ # If the first line, do not write yet.
+ [ "$time" = "00:00:00" ] || echo "file '$prevbase'
+duration $duration"
+
+ # Keep the information required for the next file.
+ prevbase="$base"
+ prevtime="$time"
+ prevseconds="$(date '+%s' -d "$prevtime")"
+done < "$file"
+# Do last file which must be given twice as follows
+echo "file '$base'
+duration ${endtime:-5}
+file '$base'"
+} > "$prepfile"
+if [ -n "${audio+x}" ]; then
+ ffmpeg -hide_banner -y -f concat -safe 0 -i "$prepfile" -i "$audio" -c:a aac -vsync vfr -c:v libx264 -pix_fmt yuv420p "$outfile"
+else
+ ffmpeg -hide_banner -y -f concat -safe 0 -i "$prepfile" -vsync vfr -c:v libx264 -pix_fmt yuv420p "$outfile"
+fi
+
+# Might also try:
+# -vf "fps=${fps:-24},format=yuv420p" "$outfile"
+# but has given some problems.
diff --git a/vimv b/vimv
new file mode 100755
index 0000000..3b3a7bb
--- /dev/null
+++ b/vimv
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -eu
+
+# Lists the current directory's files in Vim, so you can edit it and save to rename them
+# USAGE: vimv [file1 file2]
+# https://github.com/thameera/vimv
+
+declare -r FILENAMES_FILE=$(mktemp "${TMPDIR:-/tmp}/vimv.XXXXXX")
+
+trap '{ rm -f "${FILENAMES_FILE}" ; }' EXIT
+
+if [ $# -ne 0 ]; then
+ src=( "$@" )
+else
+ IFS=$'\r\n' GLOBIGNORE='*' command eval 'src=($(ls))'
+fi
+
+for ((i=0;i<${#src[@]};++i)); do
+ echo "${src[i]}" >> "${FILENAMES_FILE}"
+done
+
+${EDITOR:-vi} "${FILENAMES_FILE}"
+
+IFS=$'\r\n' GLOBIGNORE='*' command eval 'dest=($(cat "${FILENAMES_FILE}"))'
+
+if (( ${#src[@]} != ${#dest[@]} )); then
+ echo "WARN: Number of files changed. Did you delete a line by accident? Aborting.." >&2
+ exit 1
+fi
+
+declare -i count=0
+for ((i=0;i<${#src[@]};++i)); do
+ if [ "${src[i]}" != "${dest[i]}" ]; then
+ mkdir -p "$(dirname "${dest[i]}")"
+ if git ls-files --error-unmatch "${src[i]}" > /dev/null 2>&1; then
+ git mv "${src[i]}" "${dest[i]}"
+ else
+ mv "${src[i]}" "${dest[i]}"
+ fi
+ ((++count))
+ fi
+done
+
+echo "$count" files renamed.
+
diff --git a/waybar-dwl.sh b/waybar-dwl.sh
new file mode 100755
index 0000000..0d0312d
--- /dev/null
+++ b/waybar-dwl.sh
@@ -0,0 +1,176 @@
+#!/usr/bin/env bash
+#
+# wayar-dwl.sh - display dwl tags, layout, and active title
+# Based heavily upon this script by user "novakane" (Hugo Machet) used to do the same for yambar
+# https://codeberg.org/novakane/yambar/src/branch/master/examples/scripts/dwl-tags.sh
+#
+# USAGE: waybar-dwl.sh MONITOR COMPONENT
+# "COMPONENT" is an integer representing a dwl tag OR "layout" OR "title"
+#
+# REQUIREMENTS:
+# - inotifywait ( 'inotify-tools' on arch )
+# - Launch dwl with `dwl > ~.cache/dwltags` or change $fname
+#
+# Now the fun part
+#
+### Example ~/.config/waybar/config
+#
+# {
+# "modules-left": ["custom/dwl_tag#0", "custom/dwl_tag#1", "custom/dwl_tag#2", "custom/dwl_tag#3", "custom/dwl_tag#4", "custom/dwl_tag#5", "custom/dwl_layout", "custom/dwl_title"],
+# // The empty '' argument used in the following "exec": fields works for single-monitor setups
+# // For multi-monitor setups, see https://github.com/Alexays/Waybar/wiki/Configuration
+# // and enter the monitor id (like "eDP-1") as the first argument to waybar-dwl.sh
+# "custom/dwl_tag#0": {
+# "exec": "/path/to/waybar-dwl.sh '' 0",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#1": {
+# "exec": "/path/to/waybar-dwl.sh '' 1",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#2": {
+# "exec": "/path/to/waybar-dwl.sh '' 2",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#3": {
+# "exec": "/path/to/waybar-dwl.sh '' 3",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#4": {
+# "exec": "/path/to/waybar-dwl.sh '' 4",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#5": {
+# "exec": "/path/to/waybar-dwl.sh '' 5",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#6": {
+# "exec": "/path/to/waybar-dwl.sh '' 6",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#7": {
+# "exec": "/path/to/waybar-dwl.sh '' 7",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#8": {
+# "exec": "/path/to/waybar-dwl.sh '' 8",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_tag#9": {
+# "exec": "/path/to/waybar-dwl.sh '' 9",
+# "format": "{}",
+# "return-type": "json"
+# },
+# "custom/dwl_layout": {
+# "exec": "/path/to/waybar-dwl.sh '' layout",
+# "format": "{}",
+# "escape": true,
+# "return-type": "json"
+# },
+# "custom/dwl_title": {
+# "exec": "/path/to/waybar-dwl.sh '' title",
+# "format": "{}",
+# "escape": true,
+# "return-type": "json"
+# }
+# }
+#
+### Example ~/.config/waybar/style.css
+# #custom-dwl_layout {
+# color: #EC5800
+# }
+#
+# #custom-dwl_title {
+# color: #017AFF
+# }
+#
+# #custom-dwl_tag {
+# color: #875F00
+# }
+#
+# #custom-dwl_tag.selected {
+# color: #017AFF
+# }
+#
+# #custom-dwl_tag.urgent {
+# background-color: #FF0000
+# }
+#
+# #custom-dwl_tag.active {
+# border-top: 1px solid #EC5800
+# }
+
+# Variables
+declare output title layout activetags selectedtags
+declare -a tags name
+readonly fname="$HOME"/.cache/dwltags
+
+tags=( "1" "2" "3" "4" "5" "6" "7" "8" "9" )
+name=( "1" "2" "3" "4" "5" "6" "7" "8" "9" ) # Array of labels for tags
+
+monitor="${1}"
+component="${2}"
+
+_cycle() {
+ case "${component}" in
+ [012345678])
+ this_tag="${component}"
+ unset this_status
+ mask=$((1<<this_tag))
+
+ if (( "${activetags}" & mask )) 2>/dev/null; then this_status+='"active",' ; fi
+ if (( "${selectedtags}" & mask )) 2>/dev/null; then this_status+='"selected",'; fi
+ if (( "${urgenttags}" & mask )) 2>/dev/null; then this_status+='"urgent",' ; fi
+
+ if [[ "${this_status}" ]]; then
+ printf -- '{"text":" %s ","class":[%s]}\n' "${name[this_tag]}" "${this_status}"
+ else
+ printf -- '{"text":" %s "}\n' "${name[this_tag]}"
+ fi
+ ;;
+ layout)
+ printf -- '{"text":" %s "}\n' "${layout}"
+ ;;
+ title)
+ printf -- '{"text":"%s"}\n' "${title}"
+ ;;
+ *)
+ printf -- '{"text":"INVALID INPUT"}\n'
+ ;;
+ esac
+}
+
+while [[ -n "$(pgrep waybar)" ]] ; do
+
+ [[ ! -f "${fname}" ]] && printf -- '%s\n' \
+ "You need to redirect dwl stdout to ~/.cache/dwltags" >&2
+
+ # Get info from the file
+ output="$(grep "${monitor}" "${fname}" | tail -n6)"
+ title="$(echo "${output}" | grep '^[[:graph:]]* title' | cut -d ' ' -f 3- | sed s/\"/“/g )" # Replace quotes - prevent waybar crash
+ layout="$(echo "${output}" | grep '^[[:graph:]]* layout' | cut -d ' ' -f 3- )"
+ #selmon="$(echo "${output}" | grep 'selmon')"
+
+ # Get the tag bit mask as a decimal
+ activetags="$(echo "${output}" | grep '^[[:graph:]]* tags' | awk '{print $3}')"
+ selectedtags="$(echo "${output}" | grep '^[[:graph:]]* tags' | awk '{print $4}')"
+ urgenttags="$(echo "${output}" | grep '^[[:graph:]]* tags' | awk '{print $6}')"
+
+ _cycle
+
+ # 60-second timeout keeps this from becoming a zombified process when waybar is no longer running
+ inotifywait -t 60 -qq --event modify "${fname}"
+
+done
+
+unset -v activetags layout name output selectedtags tags title
+