3 # Developed by Fred Weinhaus 12/24/2009 .......... revised 8/1/2021
5 # ------------------------------------------------------------------------------
9 # Copyright © Fred Weinhaus
11 # My scripts are available free of charge for non-commercial use, ONLY.
13 # For use of my scripts in commercial (for-profit) environments or
14 # non-free applications, please contact me (Fred Weinhaus) for
15 # licensing arrangements. My email address is fmw at alink dot net.
17 # If you: 1) redistribute, 2) incorporate any of these scripts into other
18 # free applications or 3) reprogram them in another scripting language,
19 # then you must contact me for permission, especially if the result might
20 # be used in a commercial or for-profit environment.
22 # My scripts are also subject, in a subordinate manner, to the ImageMagick
23 # license, which can be found at: http://www.imagemagick.org/script/license.php
25 # ------------------------------------------------------------------------------
29 # USAGE: downsize [-s size] [-t toler] [-m maxiter] [-c copy] [-S strip]
31 # USAGE: downsize [-help]
35 # -s size desired output file size in kilobytes; float>0;
37 # -t toler tolerance or allowed size of result greater than
38 # desired size expressed as percent of desired size;
39 # float>=0; default=10
40 # -m maxiter maximum number of iterations to stop; integer>1;
42 # -c copy copy to output when not downsizing and no image
43 # format change; yes (y) or no (n); default=yes
44 # -S strip strip all meta data; yes (y) or no (n); default=yes
50 # PURPOSE: To downsize (reduce) an image to a specified file size.
52 # DESCRIPTION: DOWNSIZE reduces an image's dimensions to achieve a specified
53 # file size. For non-JPG images, processing will continue until either the
54 # desired tolerance is achieved or the maximum number of iterations is reached.
55 # For JPG images, processing will continue as above, but may stop when no
56 # change of file size (or quality) occurs. Thus the desired file size may not
57 # full be achieved to within the specified tolerance. Approximately 1%
58 # tolerance is practical amount that can be achieved for non-JPG images. For
59 # JPG images, the value may or may not be achieved. When strip is no,
60 # processing will be skipped, if the input file size is less than or equal to
61 # the combined desired file size plus the size of the meta data or if the
62 # desired size is less than the meta data size.
66 # -s size ... SIZE is the desired output image size in kilobytes. Values are
67 # floats>0. The default=200
69 # -t toler ... TOLER is allowed size of the result within than the desire
70 # size expressed as a percent of the desired size. Values are floats>=0.
71 # The default=10. Processing will iterate until the resulting image size
72 # is within the tolerance. If the tolerance is too low, then iterations will
73 # stop when it reaches a value of maxiter. The default=10 (10%). If both toler
74 # and maxiter are too small, then iteration could continue indefinitely,
75 # since the algorithm may not be able to get as close as desired.
77 # -m maxiter ... MAXITER is the maximum number of iterations to stop. Values
78 # are integer>1. The default=20. If both toler and maxiter are too small,
79 # then iteration could continue indefinitely, since the algorithm may not
80 # be able to get as close as desired.
82 # -c copy ... COPY will copy the input to the output when both no downsize
83 # processing happens and the input and output formats are the same.
84 # Values are either: yes (y) or no (n). Yes means make a copy of the input
85 # with the output name and no means simply skip processing and do not copy
86 # The input data to the output file. The default=yes.
88 # -S strip ... STRIP all meta data. Choices are: yes (y) or no (n). The
89 # default=yes. Note that if you do not strip, the file size will be limited
90 # to the size of the meta data.
92 # NOTE: Images will be converted to 8 bits/pixel/channel.
94 # CAVEAT: No guarantee that this script will work on all platforms,
95 # nor that trapping of inconsistent parameters is complete and
96 # foolproof. Use At Your Own Risk.
102 size=200 # desired output filesize in kilobytes
103 toler=10 # tolerance as percent of size; toler>=0
104 maxiter=20 # maximum iterations
105 copy="yes" # copy to output when not downsizing if no image format change
106 strip="yes" # strip meta data; yes or no
107 toler1=10 # initial iteration for jpg; final iteration uses toler
110 # set directory for temporary files
111 dir="/tmp" # suggestions are dir="." or dir="/tmp"
113 # set up functions to report Usage and Usage with Description
114 PROGNAME=`type $0 | awk '{print $3}'` # search for executable on path
115 PROGDIR=`dirname $PROGNAME` # extract directory of program
116 PROGNAME=`basename $PROGNAME` # base name of program
120 echo >&2 "$PROGNAME:" "$@"
121 sed >&2 -e '1,/^####/d; /^###/g; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
126 echo >&2 "$PROGNAME:" "$@"
127 sed >&2 -e '1,/^####/d; /^######/g; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
131 # function to report error messages
142 # function to test for minus at start of value of second part of option 1 or 2
145 test=`echo "$1" | grep -c '^-.*$'` # returns 1 if match; 0 otherwise
146 [ $test -eq 1 ] && errMsg "$errorMsg"
149 # test for correct number of arguments and get values
158 errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
162 # get parameter values
164 -help) # help information
170 shift # to get the next parameter
171 # test if parameter starts with minus sign
172 errorMsg="--- INVALID COPY SPECIFICATION ---"
174 copy=`echo "$1" | tr '[A-Z]' '[a-z]'`
178 *) errMsg "--- COPY=$copy IS AN INVALID VALUE ---"
182 shift # to get the next parameter
183 # test if parameter starts with minus sign
184 errorMsg="--- INVALID SIZE SPECIFICATION ---"
186 size=`expr "$1" : '\([.0-9]*\)'`
187 sizetest=`echo "$size <= 0" | bc`
188 [ $sizetest -eq 1 ] && errMsg "--- SIZE=$size MUST BE A FLOAT GREATER THAN 0 ---"
191 shift # to get the next parameter
192 # test if parameter starts with minus sign
193 errorMsg="--- INVALID TOLER SPECIFICATION ---"
195 toler=`expr "$1" : '\([.0-9]*\)'`
196 tolertest=`echo "$toler < 0" | bc`
197 [ $tolertest -eq 1 ] && errMsg "--- TOLER=$toler MUST BE A NON-NEGATIVE FLOAT ---"
200 shift # to get the next parameter
201 # test if parameter starts with minus sign
202 errorMsg="--- INVALID MAXITER SPECIFICATION ---"
204 maxiter=`expr "$1" : '\([0-9]*\)'`
205 maxitertest=`echo "$maxiter < 1" | bc`
206 [ $maxitertest -eq 1 ] && errMsg "--- MAXITER=$maxiter MUST BE A POSITIVE INTEGER ---"
209 shift # to get the next parameter
210 # test if parameter starts with minus sign
211 errorMsg="--- INVALID STRIP SPECIFICATION ---"
213 strip=`echo "$1" | tr '[A-Z]' '[a-z]'`
217 *) errMsg "--- STRIP=$strip IS AN INVALID VALUE ---"
220 -) # STDIN and end of arguments
223 -*) # any other - argument
224 errMsg "--- UNKNOWN OPTION ---"
226 *) # end of arguments
233 # get infile and outfile
238 # test that infile provided
239 [ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
241 # test that outfile provided
242 [ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
245 # setup for stripping meta data
246 if [ "$strip" = "yes" ]; then
252 # get file type of input
253 # ftypes are: JPEG, TIFF, PNG and GIF
254 ftype=`convert "$infile" -ping -format "%m" info:`
256 # get suffix of outfile
257 outlist=`echo $outfile | tr "." " "`
258 partsArray=($outlist)
259 numparts=${#partsArray[*]}
260 suffix=${partsArray[$numparts-1]}
261 suffix=`echo $suffix | tr "[:lower:]" "[:upper:]"`
262 [ "$suffix" = "JPG" ] && suffix="JPEG"
263 [ "$suffix" = "TIF" ] && suffix="TIFF"
265 # if ftype != suffix, set ftype to suffix
266 # IM can deal with ftype TIFF or TIF and JPEG or JPG
267 [ "$ftype" != "$suffix" ] && changetype="yes" || changetype="no"
268 [ "$ftype" != "$suffix" ] && ftype=$suffix
270 # if JPG, then set quality to 100
271 if [ "$ftype" = "JPEG" ]; then
272 setquality="-quality 100"
273 setunits="-units pixelsperinch"
279 # get input compression
280 compression=`convert "$infile" -ping -format "%C" info:`
282 if [ "$ftype" = "TIFF" -a "$compresson" = "JPEG" ]; then
283 setcompression="-compress none"
284 elif [ "$ftype" = "TIFF" -a "$compresson" != "JPEG" ]; then
285 setcompression="-compress $compression"
290 # if strip is no, get size of meta data in bytes
291 if [ "$strip" = "no" ]; then
292 meta_list=`identify -verbose "$infile" | grep "Profile-" | sed 's/^[ ]*//' | cut -d\ -f2`
294 for amt in $meta_list; do
295 meta_sum=$((meta_sum+amt))
300 tmpA1="$dir/downsize_1_$$.$suffix"
302 trap "rm -f $tmpA1;" 0
303 trap "rm -f $tmpA1; exit 1" 1 2 3 15
304 #trap "rm -f $tmpA1; exit 1" ERR
306 # read the input image into the temp files and test validity.
307 convert -quiet "$infile"[0] $stripping -depth 8 $setcompression $setquality $setunits +repage ${ftype}:$tmpA1 ||
308 errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
311 # fullsize is converted to bytes
312 # size2 is size converted to bytes
313 test1=`convert $tmpA1 -ping -format "%b" info:- | grep "MB"`
314 test2=`convert $tmpA1 -ping -format "%b" info:- | grep "KB"`
315 test3=`convert $tmpA1 -ping -format "%b" info:- | grep "B"`
316 fullsize=`convert -ping $tmpA1 -format "%b" info: | tr -d "[:alpha:]"`
317 if [ "$test1" != "" ]; then
318 fullsize=`convert xc: -format "%[fx:$fullsize*1000000]" info:`
319 elif [ "$test2" != "" ]; then
320 fullsize=`convert xc: -format "%[fx:$fullsize*1000]" info:`
321 elif [ "$test3" != "" ]; then
324 errMsg="--- UNRECOGNIZED FILESIZE SUFFIX ---"
326 size2=`convert xc: -format "%[fx:$size*1000]" info:`
327 #echo "test1=$test1; test2=$test2; test3=$test3; ftype=$ftype; fullsize=$fullsize; size2=$size2;"
329 # process image for strip=yes, if input size > desired size
330 # process image for strip=no, if input size > desired size + meta size
331 # and the desired size is greater than the meta size
332 if [ "$strip" = "yes" ]; then
333 test4=`convert xc: -format "%[fx:($fullsize>$size2)?1:0]" info:`
334 elif [ "$strip" = "no" ]; then
335 test4=`convert xc: -format "%[fx:($fullsize>$size2+$meta_size) && ($size2>=$meta_size)?1:0]" info:`
336 size2=$((size2+meta_size))
339 if [ $test4 -eq 1 ]; then
345 if [ "$ftype" = "JPEG" ]; then
346 # iterate from previous image to new image as outfile
347 # this may give blurry results but close values
348 # so get the final images size and resize one last time from the original
349 # also use -define jpeg:extent=${size}KB which will change the quality to get closer
350 # if used the alternate method below with JPEG, sometimes got overshoots and pratio=nan since newsize became negative
351 convert $tmpA1 "$outfile"
352 while [ $iterate -eq 1 -a $i -lt $maxiter ]; do
353 # get sqrt size ratio in percent
354 pratio=`convert xc: -format "%[fx:100*sqrt($size/$newsize)]" info:`
355 #echo "i=$i; fullsize=$fullsize; size=$size; size2=$size2; newsize=${newsize}kB; pratio=$pratio;"
358 convert "$outfile" -resize ${pratio}% -depth 8 $setquality $setunits ${ftype}:"$outfile"
361 testa=`convert "$outfile" -ping -format "%b" info:- | grep "MB"`
362 testb=`convert "$outfile" -ping -format "%b" info:- | grep "KB"`
363 testc=`convert "$outfile" -ping -format "%b" info:- | grep "B"`
364 if [ "$testa" != "" ]; then
365 newsize=`convert "$outfile" $setquality ${ftype}:- | convert - -format "%b" info: | tr -d "[:alpha:]"`
366 newsize=`convert xc: -format "%[fx:$newsize*1000]" info:`
367 elif [ "$testb" != "" ]; then
368 newsize=`convert "$outfile" $setquality ${ftype}:- | convert - -format "%b" info: | tr -d "[:alpha:]"`
369 elif [ "$testc" != "" ]; then
370 newsize=`convert "$outfile" $setquality ${ftype}:- | convert - -format "%b" info: | tr -d "[:alpha:]"`
371 newsize=`convert xc: -format "%[fx:$newsize/1000]" info:`
373 echo "i=$i newsize=${newsize}kB quality=100"
374 diffsize=`convert xc: -format "%[fx:($newsize-$size)]" info:`
375 iterate=`convert xc: -format "%[fx:abs($diffsize)>($toler1*$size/100)?1:0]" info:`
376 #echo "PART1: i=$i; newsize=${newsize}kB; diffsize=$diffsize; iterate=$iterate;"
380 # get final size of iterated outfile and then use that to resize the original
381 dim=`convert "$outfile" -format "%wx%h" info:`
382 convert $tmpA1 -resize $dim -define jpeg:extent=${size}KB -depth 8 $setquality $setunits ${ftype}:"$outfile"
384 testa=`convert "$outfile" -ping -format "%b" info:- | grep "MB"`
385 testb=`convert "$outfile" -ping -format "%b" info:- | grep "KB"`
386 testc=`convert "$outfile" -ping -format "%b" info:- | grep "B"`
388 if [ "$testa" != "" ]; then
389 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
390 newsize=`convert xc: -format "%[fx:$newsize*1000]" info:`
391 elif [ "$testb" != "" ]; then
392 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
393 elif [ "$testc" != "" ]; then
394 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
395 newsize=`convert xc: -format "%[fx:$newsize/1000]" info:`
397 quality=`convert -ping "$outfile" -format "%Q" info:`
398 echo "i=$i newsize=${newsize}kB quality=$quality"
399 diffsize=`convert xc: -format "%[fx:($size-$newsize)]" info:`
400 #newsign=`convert xc: -format "%[fx:sign($size-$newsize)]" info:`
402 size1=`convert xc: -format "%[fx:$size+$diffsize]" info:`
403 iterate=`convert xc: -format "%[fx:abs($diffsize)>($toler*$size/100)?1:0]" info:`
404 #echo "PART2: i=$i; newsign=$newsign; oldsign=$oldsign; quality=$quality; size1=${size1}KB; newsize=${newsize}KB; diffsize=$diffsize; iterate=$iterate;"
407 # iterate again changing jpeg write extent size
408 # while [ $iterate -eq 1 -a $i -lt $maxiter -a $newsign -eq $oldsign -a "$newsize" != "$oldsize" ]; do
409 while [ $iterate -eq 1 -a $i -lt $maxiter -a "$newsize" != "$oldsize" ]; do
410 convert $tmpA1 -resize $dim -define jpeg:extent=${size1}KB -depth 8 $setquality $setunits ${ftype}:"$outfile"
412 testa=`convert "$outfile" -ping -format "%b" info:- | grep "MB"`
413 testb=`convert "$outfile" -ping -format "%b" info:- | grep "KB"`
414 testc=`convert "$outfile" -ping -format "%b" info:- | grep "B"`
416 if [ "$testa" != "" ]; then
417 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
418 newsize=`convert xc: -format "%[fx:$newsize*1000]" info:`
419 elif [ "$testb" != "" ]; then
420 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
421 elif [ "$testc" != "" ]; then
422 newsize=`convert -ping "$outfile" -format "%b" info: | tr -d "[:alpha:]"`
423 newsize=`convert xc: -format "%[fx:$newsize/1000]" info:`
425 quality=`convert -ping "$outfile" -format "%Q" info:`
426 echo "i=$i newsize=${newsize}kB quality=$quality"
427 diffsize=`convert xc: -format "%[fx:($size-$newsize)]" info:`
429 #newsign=`convert xc: -format "%[fx:sign($size-$newsize)]" info:`
430 size1=`convert xc: -format "%[fx:0.5*($size+$newsize)]" info:`
431 iterate=`convert xc: -format "%[fx:abs($diffsize)>($toler*$size/100)?1:0]" info:`
432 #echo "PART3: i=$i; newsign=$newsign; oldsign=$oldsign; quality=$quality; size1=${size1}KB; newsize=${newsize}KB; diffsize=$diffsize; iterate=$iterate;"
439 while [ $iterate -eq 1 -a $i -lt $maxiter ]; do
440 # get sqrt size ratio in percent
441 size2=`convert xc: -format "%[fx:max($size2-($diffsize*1000),0)]" info:`
442 pratio=`convert xc: -format "%[fx:100*sqrt($size2/$fullsize)]" info:`
443 #echo "i=$i; fullsize=$fullsize; size=$size; size2=$size2; pratio=$pratio;"
445 testd=`convert xc: -format "%[fx:$size2<($toler*$size/100)?1:0]" info:`
446 [ $testd -eq 1 ] && break
449 convert $tmpA1 -resize ${pratio}% -depth 8 $setcompression $setquality $setunits ${ftype}:"$outfile"
450 printsize=`convert "$outfile" -ping -format "%b" info:`
451 echo "i=$i; size=${printsize}"
453 testa=`convert "$outfile" -ping -format "%b" info:- | grep -i "MB"`
454 testb=`convert "$outfile" -ping -format "%b" info:- | grep -i "KB"`
455 testc=`convert "$outfile" -ping -format "%b" info:- | grep -i "B"`
456 if [ "$testa" != "" ]; then
457 newsize=`convert "$outfile" -depth 8 $setcompression $setquality $setunits ${ftype}:- | convert - -ping -format "%b" info: | tr -d "[:alpha:]"`
458 newsize=`convert xc: -format "%[fx:$newsize*1000]" info:`
459 elif [ "$testb" != "" ]; then
460 newsize=`convert "$outfile" -depth 8 $setcompression $setquality $setunits ${ftype}:- | convert - -ping -format "%b" info: | tr -d "[:alpha:]"`
461 elif [ "$testc" != "" ]; then
462 newsize=`convert "$outfile" -depth 8 $setcompression $setquality $setunits ${ftype}:- | convert - -ping -format "%b" info: | tr -d "[:alpha:]"`
463 #newsize=`convert xc: -format "%[fx:$fullsize/1000]" info:`
464 newsize=`convert xc: -format "%[fx:$newsize/1000]" info:`
466 #echo "i=$i; newsize=${newsize}kB"
467 diffsize=`convert xc: -format "%[fx:($newsize-$size)]" info:`
468 iterate=`convert xc: -format "%[fx:abs($diffsize)>($toler*$size/100)?1:0]" info:`
469 #echo "i=$i; newsize=${newsize}kB; diffsize=$diffsize; iterate=$iterate;"
474 elif [ "$changetype" = "no" -a "$copy" = "yes" ]; then
475 convert "$infile" "$outfile"
478 convert $tmpA1 -depth 8 $setcompression $setquality $setunits "$outfile"
481 finalsize=`convert "$outfile" -format "%b" info:`
482 echo "final size = $finalsize"