diff options
-rw-r--r-- | .gitignore | 41 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rwxr-xr-x | betterdiscordctl | 661 | ||||
-rwxr-xr-x | booksplit | 52 | ||||
-rwxr-xr-x | compiler | 52 | ||||
-rwxr-xr-x | dmenu_books | 7 | ||||
-rwxr-xr-x | dmenu_keepass | 30 | ||||
-rwxr-xr-x | dmenumount | 75 | ||||
-rwxr-xr-x | dmenurecord | 132 | ||||
-rwxr-xr-x | dmenuumount | 44 | ||||
-rwxr-xr-x | dmenuunicode | 18 | ||||
-rwxr-xr-x | exiftool | 7153 | ||||
-rwxr-xr-x | flatdark.sh | 19 | ||||
-rwxr-xr-x | flattheme.sh | 8 | ||||
-rwxr-xr-x | hotkeys.sh | 5 | ||||
-rwxr-xr-x | launch_polybar.sh | 9 | ||||
-rwxr-xr-x | mailsync | 90 | ||||
-rwxr-xr-x | s6-user-update | 31 | ||||
-rwxr-xr-x | slider | 124 | ||||
-rwxr-xr-x | vimv | 45 | ||||
-rwxr-xr-x | waybar-dwl.sh | 176 |
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: & (&) E<39> (') +E<quot> (") E<gt> (>) and E<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ü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}." @@ -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. @@ -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 + |