-
Notifications
You must be signed in to change notification settings - Fork 13
/
recompress
executable file
·321 lines (290 loc) · 8.21 KB
/
recompress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/env bash
# recompress -- batch convert images to JPEG
. lib.bash || exit
usage() {
echo "Usage: $0 <files>"
echo
echo_opt "-q <percent>" "Quality for JPEG compression (default $quality%)"
echo_opt "-s <percent>|<WxH>" \
"Scale or dimensions for the output image (default $scale%)"
echo_opt "-B <color>" "Background color for transparent PNGs (default $background)"
echo_opt "-O <arg>" "Additional option for 'magick convert'"
echo
echo_opt "-L <size>" "Only convert files larger than X megabytes"
echo_opt "-f" "Keep converted files even if larger than originals"
echo_opt "-T" "Don't try to preserve modification time of the original"
echo_opt "-d <dir>" "Put converted files in another directory"
echo_opt "-x" "Remove originals after successful conversion"
echo
echo_opt "-j <nproc>" "Number of parallel conversions (default $nproc)"
}
# Print out a progress bar, and set the ConEmu taskbar progress.
show_progress() {
local -i done=$1 total=$2 width=40
local -i fill=$(( width * done / total ))
local -i perc=$(( 100 * done / total ))
local lbar rbar
printf -v lbar '%*s' $fill ''; lbar=${lbar// /#}
printf -v rbar '%*s' $(( width-fill )) ''
printf '%3s%% [%s%s] %s/%s done\r' "$perc" "$lbar" "$rbar" "$done" "$total"
# Set window title
printf '\e]0;%s\e\\' "Recompressing $perc% ($done/$total)"
# Set tmux window name
if [[ $TERM == @(screen|tmux)* ]]; then
printf '\ek%s\e\\' "$perc% ($done/$total)"
fi
# Set ConEmu progress
printf '\e]9;4;%d;%d\e\\' 1 "$perc"
}
# Finalize the progress bar, go to next line, and clear the ConEmu progress.
clear_progress() {
printf '\n'
printf '\e]0;%s\e\\' ""
printf '\ek%s\e\\' ""
printf '\e]9;4;%d;%d\e\\' 0 0
}
dispose() {
if have trash && [[ -d ~/.local/share/Trash ]]; then
trash "$@"
else
rm -v "$@"
fi
}
if ! have magick; then
# ImageMagick 6.x or older
magick() { "/usr/bin/$@"; }
fi
opt_verbose=0
quality=92
scale=100
keeplarger=0
background='#eeeeee'
minorigsize=0
keepmtime=1
trashorig=0
infiles=()
outfiles=()
infix="conv"
outdir=
declare -A iomap=()
declare -A oimap=()
addargs=()
nproc=$(nproc)
njobs=0
ndone=0
r=0
while getopts ":B:d:fj:L:O:q:s:Txv" OPT; do
case $OPT in
B) background="#${OPTARG#'#'}";;
d) outdir=$OPTARG;;
f) keeplarger=1;;
q) quality=${OPTARG%'%'};;
j) nproc=$OPTARG;;
L) minorigsize=$OPTARG;;
O) addargs+=("$OPTARG");;
s) scale=${OPTARG%'%'};;
T) keepmtime=0;;
x) trashorig=1;;
v) let ++opt_verbose;;
*) lib:die_getopts;;
esac
done; shift $((OPTIND-1))
if (( ! $# )); then
die 0 "no input files specified"
fi
if [[ $outdir ]]; then
keeplarger=1
fi
if [[ $minorigsize != 0 ]]; then
minorigsize=$(numfmt --from=iec "${minorigsize%[Bb]}") || exit
if (( minorigsize < 128 )); then
warn "minimum size of $minorigsize bytes looks too low, assuming MB"
minorigsize=$(numfmt --from=iec "${minorigsize}M") || exit
elif (( minorigsize < 1024 )); then
warn "minimum size of $minorigsize bytes looks too low, assuming kB"
minorigsize=$(numfmt --from=iec "${minorigsize}K") || exit
fi
info "will skip files smaller than $(numfmt --to=iec "$minorigsize")B"
fi
nskipconv=0
nskipsmall=0
nskipother=0
nwarnanim=0
for arg in "$@"; do
oarg=${outdir%/}${outdir:+/}$arg
if [[ ! -e $arg ]]; then
err "input file '$arg' does not exist"
elif [[ $arg =~ \.conv\.[^.]+$ ]] && [[ $arg -ef $oarg ]]; then
debug "skipping already converted file '$arg'"
let ++nskipconv
elif (( minorigsize && $(stat -c %s "$arg") < minorigsize )); then
debug "skipping small file '$arg'"
let ++nskipsmall
elif [[ ${arg,,} = *.@(zip|webm) ]]; then
warn "skipping incompatible file '$arg'"
let ++nskipother
else
infiles+=("$arg")
fi
done
if (( errors )); then
exit 1
fi
if (( nskipconv )); then
info "$nskipconv arguments ignored as already converted"
fi
if (( nskipsmall )); then
info "$nskipsmall files skipped as below size threshold"
fi
if (( nskipother )); then
info "$nskipother files skipped as incompatible"
fi
if (( ! ${#infiles[@]} )); then
die 0 "no input files"
fi
for arg in "${infiles[@]}"; do
if [[ $arg == *.webp ]]; then
nframes=$(magick identify -ping "$arg" | wc -l)
if (( nframes > 1 )); then
err "file is animated ($nframes frames): $arg"
let ++nwarnanim
fi
fi
done
if (( errors )); then
exit 1
fi
addargs+=(-background "$background" -alpha remove)
if (( quality < 20 || quality > 100 )); then
die "invalid quality value '$quality'"
fi
addargs+=(-quality "$quality")
if (( quality < 90 )); then
infix="q$quality.$infix"
fi
if [[ $scale == *x* ]]; then
addargs+=(-resize "$scale")
infix="resize.$infix"
elif (( scale < 1 || scale > 100 )); then
die "invalid scale value '$scale'"
elif (( scale < 100 )); then
addargs+=(-scale "$scale%")
infix="s$scale.$infix"
fi
if (( nproc < 1 || nproc > $(nproc)*4 )); then
die "invalid job count '$nproc'"
fi
echo "Recompressing ${#infiles[@]} files ($nproc jobs)"
for iname in "${infiles[@]}"; do
oname=${outdir%/}${outdir:+/}${iname%.*}.$infix.jpg
if [[ ${oimap["$oname"]} && ! ${iomap["$iname"]} ]]; then
# More than one input (e.g. foo.png & foo.tiff) maps to the
# same output file, so keep the original extensions.
xiname=${oimap["$oname"]}
xoname=${outdir%/}${outdir:+/}$xiname.$infix.jpg
oname=${outdir%/}${outdir:+/}$iname.$infix.jpg
iomap["$xiname"]=$xoname
oimap["$xoname"]=$xiname
# (Keep the old $oname in the oimap, so that we would detect
# collisions between 3 items, not only between 2-pairs.)
fi
iomap["$iname"]=$oname
oimap["$oname"]=$iname
done
trap 'clear_progress' EXIT
for iname in "${infiles[@]}"; do
show_progress "$ndone" "${#infiles[@]}"
while (( njobs >= nproc )); do
wait -n; (( --njobs )); (( ++ndone ))
show_progress "$ndone" "${#infiles[@]}"
done
oname=${iomap["$iname"]}
{
if [[ $outdir ]]; then
[[ -d ${oname%/*} ]] || mkdir -p "${oname%/*}"
fi
magick "$iname" "${addargs[@]}" "$oname" &&
if (( keepmtime )); then
touch "$oname" --reference="$iname" 2>/dev/null
fi
} & (( ++njobs ))
outfiles+=("$oname")
show_progress "$ndone" "${#infiles[@]}"
done
while (( njobs )); do
wait -n; (( --njobs )); (( ++ndone ))
show_progress "$ndone" "${#infiles[@]}"
done
clear_progress
trap - EXIT
if true; then
intrash=()
outsmaller=()
outlarger=()
for oname in "${outfiles[@]}"; do
iname=${oimap["$oname"]}
if [[ ! -s "$oname" ]]; then
die "conversion of '$iname' failed"
fi
if (( $(stat -c %s "$oname") >= $(stat -c %s "$iname") - 128*1024 )); then
outlarger+=("$oname")
else
intrash+=("$iname")
outsmaller+=("$oname")
fi
done
if (( ${#outlarger[@]} )) && (( !keeplarger )); then
warn "${#outlarger[@]} files were larger than originals; deleting."; ((++r))
rm -f "${outlarger[@]}"
outfiles=("${outsmaller[@]}")
elif (( ${#outlarger[@]} )); then
warn "${#outlarger[@]} files are larger than originals"
fi
fi
if (( ! ${#outfiles[@]} )); then
die "no conversions succeeded"
fi
orig_total_str=$(du -hsc "${infiles[@]}" | awk 'END {print $1}')
jpeg_total_str=$(du -hsc "${outfiles[@]}" | awk 'END {print $1}')
orig_total_bytes=$(numfmt --from=iec "$orig_total_str")
jpeg_total_bytes=$(numfmt --from=iec "$jpeg_total_str")
orig_avg_str=$(numfmt --to=iec $[orig_total_bytes/${#infiles[@]}])
jpeg_avg_str=$(numfmt --to=iec $[jpeg_total_bytes/${#outfiles[@]}])
percentage=$(( 100 * jpeg_total_bytes / orig_total_bytes ))
echo "Orig: $orig_total_str total ($orig_avg_str average)"
echo "JPEG: $jpeg_total_str total ($jpeg_avg_str average)"
echo "Overall reduction: 100% -> $percentage%"
#if (( ${#outfiles[@]} <= 15 )); then
if true; then
i=0
n=${#outfiles[@]}
max=15
for oname in "${outfiles[@]}"; do
if (( i >= max-1 && n-i > 1 && !opt_verbose )); then
echo "... [$[n-i] more files, $n total]"
break
fi
let ++i
if [[ -e "$oname" ]]; then
osize=$(stat -c %s "$oname")
iname=${oimap["$oname"]}
isize=$(stat -c %s "$iname")
perc=$[100 * osize / isize]
printf '%6s -> %6s (%2d%%) %s\n' \
"$(numfmt --to=iec $isize)" \
"$(numfmt --to=iec $osize)" \
"$perc" \
"$iname"
fi
done
fi
if (( trashorig )); then
if (( ! ${#intrash[@]} )); then
echo "No originals to remove (all ${#outlarger[@]} outputs were larger)."
elif confirm "Remove ${#intrash[@]} originals?"; then
dispose "${intrash[@]}"
elif confirm "Remove ${#outsmaller[@]} outputs?"; then
dispose "${outsmaller[@]}"
fi
fi
exit $[r > 0]