2 # Desc: Analyzes photo files selected in gThumb; shows size/time/GPS
3 # statistics in a zenity dialog. Intended for evaluating burst-mode
4 # sequences where larger JPEG size correlates with sharpness.
5 # Usage: gthumb-analyze.sh FILE…
6 # Example: $HOME/.local/bin/gthumb-analyze.sh %F
7 # Depends: bash, GNU coreutils, awk, bc, column (util-linux 2.39.3)
8 # exiftool (12.x), GNU sed 4.9, zenity
11 fdebug
=/tmp
/gtAnalyzeLog.txt
;
13 yell
() { echo "$0: $*" >&2; } # print script path and all args to stderr
14 die
() { yell
"$*"; exit 111; } # same as yell() but non-zero exit status
15 must
() { "$@" || die
"cannot $*"; } # runs args as command, reports args if command fails
20 if [[ $# -le 0 ]]; then die
"FATAL:No arguments:$#"; fi;
21 for fin
in "${fina[@]}"; do
22 if [[ ! -f "$fin" ]]; then die
"FATAL:Not a file:${fin}"; fi;
24 for cmd
in awk bc column cut exiftool
sed sort zenity
; do
25 command -v "$cmd" >/dev
/null
2>&1 || die
"FATAL:Missing dependency:${cmd}";
28 # Single batched exiftool pass. Columns (tab-separated):
30 # col 2: FileSize [bytes]
31 # col 3: SubSecCreateDate [float POSIX seconds]
32 # col 4: GPSPosition [±Lat, ±Lon]
33 # col 5: FNumber [float]
34 # col 6: ExposureTime [str seconds]
36 # col 8: FocalLength [str %.01f mm]
37 # Note: Missing tags emit '-' in -T mode.
39 tsv
="$(exiftool -q -T -FileName '-FileSize#' \
40 -SubSecCreateDate -d '%s%f' \
41 -GPSPosition -c '%+0.4f' \
42 -FNumber -ExposureTime -ISO -FocalLength \
44 || die
"FATAL:exiftool failed";
45 yell
"DEBUG:tsv:"; printf '%s\n' "$tsv" 1>&2;
47 local n fMax fMaxArr fMin fMinArr mean sdev median span gps
;
48 n
="$(printf '%s\n' "$tsv" | wc -l)";
50 # First and last by file name
52 fFirst
="$(printf '%s\n' "$tsv" | sort -t$'\t' -k1,1 | head -n1 | cut -f1)";
53 fLast
="$(printf '%s\n' "$tsv" | sort -t$'\t' -k1,1 | tail -n1 | cut -f1)";
55 # Largest and smallest files by size (numeric sort on column 2)
56 fMax
="$(printf '%s\n' "$tsv" | sort -t$'\t' -k2,2n | tail -n1)";
57 fMin
="$(printf '%s\n' "$tsv" | sort -t$'\t' -k2,2n | head -n1)";
58 fMaxArr
[0]="$(cut -f1 <<< "$fMax")";
59 fMaxArr
[1]="$(cut -f2 <<< "$fMax")";
60 fMinArr
[0]="$(cut -f1 <<< "$fMin")";
61 fMinArr
[1]="$(cut -f2 <<< "$fMin")";
62 ## Avoid & or < being parsed as markup by zenity
63 fMaxArr
[0]="$(sed -e 's/&/\&/g' -e 's/</\</g' <<< "${fMaxArr[0]}")";
64 fMinArr
[0]="$(sed -e 's/&/\&/g' -e 's/</\</g' <<< "${fMinArr[0]}")";
66 # Mean and population standard deviation (÷N, not N−1: the
67 # selection is the entire population of interest).
68 read -r mean sdev
< <(printf '%s\n' "$tsv" | cut
-f2 \
69 |
awk '{s+=$1; ss+=$1*$1;} END {m=s/NR; printf "%.0f %.0f\n", m, sqrt(ss/NR - m*m);}');
72 median
="$(printf '%s\n' "$tsv" | cut -f2 | sort -n \
73 | awk '{a[i++]=$1;} END {if (i%2==0) {printf "%.0f
\n", (a[i/2-1]+a[i/2])/2;} else {print a[int(i/2)];};}')";
75 # Time span in seconds via bc; ignore files lacking dates.
77 tMin
="$(printf '%s\n' "$tsv" | cut -f3 | grep -v '^-$' | sort -n | head -n1)";
78 tMax
="$(printf '%s\n' "$tsv" | cut -f3 | grep -v '^-$' | sort -n | tail -n1)";
79 if [[ -n "$tMin" && -n "$tMax" ]]; then
80 span
="$(printf "%'.3f s" "$(bc -l <<< "${tMax} - ${tMin}")")"; # seconds
82 span="n/a (missing SubSecCreateDate)";
85 # GPS: single unique value, none, or multiple.
86 local gpsUniq gpsUniqSamp gpsCount;
87 gpsUniq="$(printf '%s
\n' "$tsv" | cut -f4 | sort -u | grep -v '^
-$
')";
88 gpsCount="$(printf '%s
\n' "$gpsUniq" | grep -c .)";
91 *) gps="(${gpsCount} distinct)";
92 mapfile -t gpsUniqSamp < <(shuf <<< "$gpsUniq" | head -n8 | sort);;
95 # Exposure combos: unique (FNumber, ExposureTime, ISO, FocalLength) tuples.
96 # Missing tags appear as '-' and still compare consistently.
97 local expUniq expUniqSamp expCount exp;
98 expUniq="$(printf '%s
\n' "$tsv" | cut -f5-8 | sort -u \
99 | awk -F'\t' '{printf "f/%s\t%s s\tISO %s\t%s\n", $1, $2, $3, $4;}')";
100 mapfile -t expUniqSamp < <(shuf <<< "$expUniq" | head -n8 | sort -t $'\t' -k4,4n);
102 expCount="$(printf '%s
\n' "$expUniq" | grep -c .)";
105 *) exp="${expCount} distinct combinations";;
109 local msgSec1Arr msgSec1 msgSec2Arr msgSec2 msgSec3Arr msgSec3 msg;
110 ## Assemble message section 1 (file size, time span)
111 msgSec1Arr+=("$(printf "%d files analyzed (About %d s)" "$n" "$SECONDS")");
112 msgSec1Arr+=("$(printf "First:\t\t%s" "$fFirst")");
113 msgSec1Arr+=("$(printf "Last:\t\t%s" "$fLast")");
114 msgSec1Arr+=("$(printf "%s" "----" )");
115 msgSec1Arr+=("$(printf "Largest:\t%'d B
\t%s
" "${fMaxArr[1]}" "${fMaxArr[0]}")");
116 msgSec1Arr
+=("$(printf "Smallest
:\t%'d B\t%s" "${fMinArr[1]}" "${fMinArr[0]}")");
117 msgSec1Arr+=("$(printf "Mean size:\t%'d B
" "$mean")");
118 msgSec1Arr
+=("$(printf "Median size
:\t%'d B" "$median")");
119 msgSec1Arr+=("$(printf "Std. dev.:\t%'d B
" "$sdev")");
120 msgSec1Arr
+=("$(printf "Time span
:\t%s
" "$span")");
121 msgSec1
="$(printf '%s\n' "${msgSec1Arr[@]}" | column -s $'\t' -t --table-right 2)";
122 ## Assemble message section 2 (location)
123 msgSec2Arr
+=("$(printf "GPS
:\t%s
" "$gps")");
124 msgSec2Arr
+=("$(printf '%s\n' "${gpsUniqSamp[@]}")");
125 msgSec2
="$(printf '%s\n' "${msgSec2Arr[0]}"; printf '%s\n' "${msgSec2Arr[@]:1}" | column -s $'\t' -t | sed -e 's/^/\t/')";
126 ## Assemble message section 3 (exposure)
127 msgSec3Arr
+=("$(printf "Exposure
:\t%s
" "$exp")");
128 msgSec3Arr
+=("$(printf '%s\n' "${expUniqSamp[@]}")");
129 msgSec3
="$(printf '%s\n' "${msgSec3Arr[0]}"; printf '%s\n' "${msgSec3Arr[@]:1}" | column -s $'\t' -t | sed -e 's/^/\t/')";
130 ## Assemble combined message
131 msg
="$(printf '%s\n--------\n' "$msgSec1" "$msgSec2" "$msgSec3")";
133 zenity
--info --title="gThumb-analyze.sh stats" --no-wrap --text="<span font='monospace'>${msg}</span>";
136 main
"$@" 1>>"$fdebug" 2>&1;
138 # Author: Steven Baltakatei Sandoval