Plugin to resize an image
[geeqie.git] / plugins / resize-image / downsize
1 #!/bin/bash
2 #
3 # Developed by Fred Weinhaus 12/24/2009 .......... revised 8/1/2021
4 #
5 # ------------------------------------------------------------------------------
6
7 # Licensing:
8
9 # Copyright © Fred Weinhaus
10
11 # My scripts are available free of charge for non-commercial use, ONLY.
12
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.
16
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.
21
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
24
25 # ------------------------------------------------------------------------------
26
27 ####
28 #
29 # USAGE: downsize [-s size] [-t toler] [-m maxiter] [-c copy] [-S strip] 
30 # infile outfile
31 # USAGE: downsize [-help]
32 #
33 # OPTIONS:
34 #
35 # -s      size          desired output file size in kilobytes; float>0; 
36 #                       default=200
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; 
41 #                       default=20
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
45 #
46 ###
47 #
48 # NAME: DOWNSIZE 
49
50 # PURPOSE: To downsize (reduce) an image to a specified file size.
51
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.
63
64 # OPTIONS: 
65
66 # -s size ... SIZE is the desired output image size in kilobytes. Values are 
67 # floats>0. The default=200
68
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.
76
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.
81
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.
87
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.
91 #
92 # NOTE: Images will be converted to 8 bits/pixel/channel.
93
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. 
97
98 ######
99 #
100
101 # set default values
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
108
109
110 # set directory for temporary files
111 dir="/tmp"    # suggestions are dir="." or dir="/tmp"
112
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
117 usage1() 
118         {
119         echo >&2 ""
120         echo >&2 "$PROGNAME:" "$@"
121         sed >&2 -e '1,/^####/d;  /^###/g;  /^#/!q;  s/^#//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
122         }
123 usage2() 
124         {
125         echo >&2 ""
126         echo >&2 "$PROGNAME:" "$@"
127         sed >&2 -e '1,/^####/d;  /^######/g;  /^#/!q;  s/^#*//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
128         }
129
130
131 # function to report error messages
132 errMsg()
133         {
134         echo ""
135         echo $1
136         echo ""
137         usage1
138         exit 1
139         }
140
141
142 # function to test for minus at start of value of second part of option 1 or 2
143 checkMinus()
144         {
145         test=`echo "$1" | grep -c '^-.*$'`   # returns 1 if match; 0 otherwise
146     [ $test -eq 1 ] && errMsg "$errorMsg"
147         }
148
149 # test for correct number of arguments and get values
150 if [ $# -eq 0 ]
151         then
152         # help information
153    echo ""
154    usage2
155    exit 0
156 elif [ $# -gt 12 ]
157         then
158         errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
159 else
160         while [ $# -gt 0 ]
161                 do
162                         # get parameter values
163                         case "$1" in
164                      -help)    # help information
165                                            echo ""
166                                            usage2
167                                            exit 0
168                                            ;;
169                                 -c)    # get copy
170                                            shift  # to get the next parameter
171                                            # test if parameter starts with minus sign 
172                                            errorMsg="--- INVALID COPY SPECIFICATION ---"
173                                            checkMinus "$1"
174                                            copy=`echo "$1" | tr '[A-Z]' '[a-z]'`
175                                            case "$copy" in 
176                                                         yes|y) copy="yes";;
177                                                         no|n) copy="no";;
178                                                         *) errMsg "--- COPY=$copy IS AN INVALID VALUE ---" 
179                                                 esac
180                                            ;;
181                                 -s)    # get size
182                                            shift  # to get the next parameter
183                                            # test if parameter starts with minus sign 
184                                            errorMsg="--- INVALID SIZE SPECIFICATION ---"
185                                            checkMinus "$1"
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 ---"
189                                            ;;
190                                 -t)    # get toler
191                                            shift  # to get the next parameter
192                                            # test if parameter starts with minus sign 
193                                            errorMsg="--- INVALID TOLER SPECIFICATION ---"
194                                            checkMinus "$1"
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 ---"
198                                            ;;
199                                 -m)    # get maxiter
200                                            shift  # to get the next parameter
201                                            # test if parameter starts with minus sign 
202                                            errorMsg="--- INVALID MAXITER SPECIFICATION ---"
203                                            checkMinus "$1"
204                                            maxiter=`expr "$1" : '\([0-9]*\)'`
205                                            maxitertest=`echo "$maxiter < 1" | bc`
206                                            [ $maxitertest -eq 1 ] && errMsg "--- MAXITER=$maxiter MUST BE A POSITIVE INTEGER ---"
207                                            ;;
208                                 -S)    # get strip
209                                            shift  # to get the next parameter
210                                            # test if parameter starts with minus sign 
211                                            errorMsg="--- INVALID STRIP SPECIFICATION ---"
212                                            checkMinus "$1"
213                                            strip=`echo "$1" | tr '[A-Z]' '[a-z]'`
214                                            case "$strip" in 
215                                                         yes|y) strip="yes";;
216                                                         no|n) strip="no";;
217                                                         *) errMsg "--- STRIP=$strip IS AN INVALID VALUE ---" 
218                                                 esac
219                                            ;;
220                                  -)    # STDIN and end of arguments
221                                            break
222                                            ;;
223                                 -*)    # any other - argument
224                                            errMsg "--- UNKNOWN OPTION ---"
225                                            ;;
226                          *)    # end of arguments
227                                            break
228                                            ;;
229                         esac
230                         shift   # next option
231         done
232         #
233         # get infile and outfile
234         infile="$1"
235         outfile="$2"
236 fi
237
238 # test that infile provided
239 [ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"
240
241 # test that outfile provided
242 [ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
243
244
245 # setup for stripping meta data
246 if [ "$strip" = "yes" ]; then
247         stripping="-strip"
248 else
249         stripping=""
250 fi
251
252 # get file type of input
253 # ftypes are: JPEG, TIFF, PNG and GIF
254 ftype=`convert "$infile" -ping -format "%m" info:`
255
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"
264
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
269
270 # if JPG, then set quality to 100
271 if [ "$ftype" = "JPEG" ]; then
272         setquality="-quality 100"
273         setunits="-units pixelsperinch"
274 else
275         setquality=""
276         setunits=""
277 fi
278
279 # get input compression
280 compression=`convert "$infile" -ping -format "%C" info:`
281
282 if [ "$ftype" = "TIFF" -a "$compresson" = "JPEG" ]; then
283         setcompression="-compress none"
284 elif [ "$ftype" = "TIFF" -a "$compresson" != "JPEG" ]; then
285         setcompression="-compress $compression"
286 else
287         setcompression=""
288 fi
289
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`
293         meta_sum=0
294         for amt in $meta_list; do
295         meta_sum=$((meta_sum+amt))
296         done
297 fi
298
299 # set up temp file
300 tmpA1="$dir/downsize_1_$$.$suffix"
301
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
305
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  ---"
309
310 # get filesize
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
322         fullsize=$fullsize
323 else
324         errMsg="--- UNRECOGNIZED FILESIZE SUFFIX ---"
325 fi
326 size2=`convert xc: -format "%[fx:$size*1000]" info:`
327 #echo "test1=$test1; test2=$test2; test3=$test3; ftype=$ftype; fullsize=$fullsize; size2=$size2;"
328
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))
337 fi
338
339 if [ $test4 -eq 1 ]; then
340         # iterate
341         i=1
342         diffsize=0
343         iterate=1
344         newsize=$fullsize
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;"
356                 
357                         # resize image
358                         convert "$outfile" -resize ${pratio}% -depth 8 $setquality $setunits ${ftype}:"$outfile"
359                 
360                         # set newsize in KB
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:`
372                         fi              
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;"
377                         i=$(($i+1))
378                 done
379
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"
383                 # set newsize in KB
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"`
387                 oldsize=$newsize
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:`
396                 fi              
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:`
401                 #oldsign=$newsign
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;"
405                 i=$((i+1))
406                 
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"
411                         # set newsize in KB
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"`
415                         oldsize=$newsize
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:`
424                         fi
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:`
428                         #oldsign=$newsign
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;"
433                         i=$(($i+1))
434                 done
435
436                 
437
438         else
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;"
444                         
445                         testd=`convert xc: -format "%[fx:$size2<($toler*$size/100)?1:0]" info:`
446                         [ $testd -eq 1 ] && break
447                 
448                         # resize image
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}"
452                                                 
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:`
465                         fi              
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;"
470                         i=$(($i+1))
471                 done
472         fi
473         
474 elif [ "$changetype" = "no" -a "$copy" = "yes" ]; then
475         convert "$infile" "$outfile"
476
477 else
478         convert $tmpA1 -depth 8 $setcompression $setquality $setunits "$outfile"
479 fi
480
481 finalsize=`convert "$outfile" -format "%b" info:`
482 echo "final size = $finalsize"
483
484 exit