#!/bin/bash # A batch-mode image editor. Useful for very large images. The # interactive operations are done on a scaled down copy, and the # operations on the image itself which may take a long time can be # run later unattended. # # In some cases, the normal image editors like gimp and display may # be next to unusable or even fail completely on very large images # because they try to do everything in memory which can cause very # heavy swapping up to out-of-memory situations. # # This program uses batch mode tools (mostly the PNM utilities) as # far as possible. To show the images while working on them, only a # scaled down copy is given to display. Therefore, most operations # should work quite fast. Only the initial scaling down (and format # conversion when necessary) may take a little while and, of course, # the output script generated by this program will usually take some # time to run (though it also uses batch mode utilities, of course, # which have generally fewer memory problems). # # Requires bash, the PNM utilities and ImageMagick. # # Copyright (C) 2002 Frank Heckenbach # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, # MA 02111-1307, USA. # Maximum visible dimensions maxcol=640 maxrow=800 # Needed on Solaris to find the correct stty which understands # `cbreak'. Shouldn't hurt on other systems. PATH="/usr/ucb:$PATH" terminfo() { t="`tput "$2" 2> /dev/null`" if [ x"$t" = x ]; then t="###undefined###" fi eval "$1"=\"\$t\" } terminfo up kcuu1 terminfo down kcud1 terminfo left kcub1 terminfo right kcuf1 buf="" readkey() { local k b1 if [ x"$buf" = x ]; then stty cbreak -echo >&2 buf="`dd bs=256 count=1 2> /dev/null`" stty -cbreak echo >&2 fi case "$buf" in $up*) b1="$up"; k="up";; $down*) b1="$down"; k="down";; $left*) b1="$left"; k="left";; $right*) b1="$right"; k="right";; *) b1="${buf:0:1}"; k="$b1";; esac buf="${buf#$b1}" echo "$k" | tr A-Z a-z } progname="`basename $0`" if [ x"$DISPLAY" = x ]; then echo "$progname: must be run under X11" >&2 exit 1 fi display -help > /dev/null || { echo "$progname: ImageMagick is required" >&2 exit 1 } # The PNM utilities don't seem to have help or version options. :-( type pnmscale > /dev/null || { echo "$progname: PNM utilities are required" >&2 exit 1 } if [ $# -lt 3 ] || [ x"$1" != x"-o" ]; then echo "Usage: $progname -o scriptfile filename..." >&2 exit 1 fi outputfile="$2" shift shift if [ -e "$outputfile" ]; then echo "$progname: $outputfile exists already." >&2 exit 1 fi exec 3> "$outputfile" || exit 1 chmod u+x "$outputfile" || exit 1 cat << EOF >&3 || exit 1 #!/bin/sh # Automatically generated by $progname EOF if [ x"$TMPDIR" = x ]; then TMPDIR="/tmp" fi TMP="$TMPDIR/$progname.$$" rm -rf "$TMP" || exit 1 mkdir "$TMP" || exit 1 trap 'echo "echo \"*** Aborted\"" >&3; rm -rf "$TMP"; exit 1' INT 0 identifyimg() { id="`identify -ping "$1"`" || { echo "$progname: could not identify $1" >&2; exit 1; } curcolumns="`echo "$id" | sed -ne 's/.*[^0-9]\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1/p'`" currows="`echo "$id" | sed -ne 's/.*[^0-9]\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\2/p'`" } quit=n for f do if [ x"$quit" = x"y" ]; then break fi destfile_jpg="`echo "$f" | sed -e 's/\.[^.]*$/-new.jpg/'`" destfile_png="`echo "$f" | sed -e 's/\.[^.]*$/-new.png/'`" if [ x"$f" = x"$destfile_jpg" ] || [ x"$f" = x"$destfile_png" ]; then echo "$progname: internal error: file and destination file names are equal" >&2 exit 1 fi if [ -e "$destfile_jpg" ]; then echo "$progname: potential destination file $destfile_jpg exists already" >&2 exit 1 fi if [ -e "$destfile_png" ]; then echo "$progname: potential destination file $destfile_png exists already" >&2 exit 1 fi identifyimg "$f" outformat=PNG destfile="$destfile_png" case "$id" in *JPEG*) inconvert=djpeg; outformat=JPEG; destfile="$destfile_jpg";; *PNG*) inconvert=pngtopnm;; *PNM*) inconvert=cat;; *TIFF*) inconvert=tifftopnm;; *GIF*) inconvert=giftopnm;; *) echo "$progname: unknown image format" >&2; exit 1;; esac columns="$curcolumns" rows="$currows" colscale="$[(columns+maxcol-1)/maxcol]" rowscale="$[(rows+maxrow-1)/maxrow]" if [ "$colscale" -gt "$rowscale" ]; then displayscale="$colscale"; else displayscale="$rowscale"; fi if [ "$displayscale" -eq 0 ]; then echo "$progname: internal error: displayscale = 0" >&2 exit 1 fi echo "Processing $f, size: $columns*$rows, scaled by 1/$displayscale" if [ "$displayscale" -le 1 ]; then echo "No scaling necessary, image is rather small. You may want to use a normal graphics editor ..." fi $inconvert "$f" | pnmscale -xsize "$[columns/displayscale]" -ysize "$[rows/displayscale]" > "$TMP/i0.pnm" || exit 1 if [ ${PIPESTATUS[1]} -ne 0 ]; then exit 1 fi startdisplay() { if [ x"$displaypid" = x ]; then display -geometry -0+0 "$1" & displaypid="$!" fi } stopdisplay() { if [ x"$displaypid" != x ]; then exec 4>&2 2> /dev/null # @@ Hmpf. How else can we avoid the `Terminated ...' message? kill "$displaypid" wait "$displaypid"; res=$? exec 2>&4 if [ $res -ne 0 ] && [ $res -lt 128 ]; then echo "$0: display exited with status $res" >&2 #exit 1 fi displaypid="" fi } status() { tf="$TMP/i$n.pnm" identifyimg "$tf" echo "" echo "${id##*/} (output: $destfile, $outformat)" kind="`head -1 "$tf"`" startdisplay "$tf" } OK="" n=0 m=0 displaypid="" status while true; do echo ' Output (P)NG (G)rayscale Rotate Cloc(k)wise (J)PEG Black/(W)hite C(o)unterclockwise (N)ame (S)cale 1(8)0° (U)ndo (C)lip (F)ree angle (R)edo (B)rightness Mirror (H)orizontal (A)ccept Ga(m)ma (V)ertical (D)iscard (Q)uit' # bcdghjkopqrsuvw8 op="`readkey`" n0=$n unredone="" n2=$[n+1] f1="$TMP/i$n.pnm" f2="$TMP/i$n2.pnm" ftmp="$TMP/itmp.pnm" pipecmd="" realpipecmd="" case "$op" in a) OK=y; break;; d) break;; q) quit=y; break;; p) outformat=PNG; destfile="$destfile_png";; j) outformat=JPEG; destfile="$destfile_jpg";; n) read -p "Output filename (without extension): " fn if [ -e "$fn.jpg" ]; then echo "$fn.jpg exists already" elif [ -e "$fn.png" ]; then echo "$fn.png exists already" else destfile_png="$fn.png" destfile_jpg="$fn.jpg" if [ x"$outformat" = x"JPEG" ]; then destfile="$destfile_jpg" else destfile="$destfile_png" fi fi;; u) if [ $n -gt 0 ]; then n=$[n-1]; unredone=y; stopdisplay; else echo "Nothing to undo."; fi;; r) if [ $n -lt $m ]; then n=$[n+1]; unredone=y; stopdisplay; else echo "Nothing to redo."; fi;; g) if [ x"$kind" = x"P3" ] || [ x"$kind" = x"P6" ]; then pipecmd="ppmtopgm" else echo "Image is already grayscale or black/white" fi;; w) if [ x"$kind" != x"P2" ] && [ x"$kind" != x"P5" ]; then echo "Image is not grayscale" else while true; do echo 'Method: (F)loyd-Steinberg Cluster(3) (T)hreshold Cluster(4) (D)ither8 Cluster(8) (H)ilbert' case "`readkey`" in f) method=fs;; t) method=threshold;; d) method=dither8;; 3) method=cluster3;; 4) method=cluster4;; 8) method=cluster8;; h) method=hilbert;; *) method="";; esac if [ x"$method" != x ]; then break; fi done opt="-$method" case "$method" in fs | threshold) read -p "Value [0..1]: " v opt="$opt -value $v";; esac pipecmd="pgmtopbm $opt" fi;; s) read -p "Scale factor (>1: larger; <1: smaller): " factor pipecmd="pnmscale $factor";; c) Left=0; Right=0; Top=0; Bottom=0; side=Left while true; do eval v=\""\$$side"\" case "$side" in Left) plus=right; plusa=j; minus=left; minusa=h; plus10=k; minus10=g; max="$[curcolumns-Right]";; Right) plus=left; plusa=h; minus=right; minusa=j; plus10=g; minus10=k; max="$[curcolumns-Left]";; Top) plus=down; plusa=n; minus=up; minusa=u; plus10=m; minus10=i; max="$[currows-Bottom]";; Bottom) plus=up; plusa=u; minus=down; minusa=n; plus10=i; minus10=m; max="$[currows-Top]";; esac echo "Clipping left: $Left, right: $Right, top: $Top, bottom: $Bottom (size: $curcolumns*$currows; new size: $[curcolumns-Left-Right]*$[currows-Top-Bottom])" echo "Side: $side (L)eft (R)ight (T)op (B)ottom" echo "Clip: $v (+) (-) (=) ($plusa/$plus) +1 ($minusa/$minus) -1 ($plus10) +10 ($minus10) -10" echo "(F)inished" case "`readkey`" in f) break;; l) side=Left; continue;; r) side=Right; continue;; t) side=Top; continue;; b) side=Bottom; continue;; +) read -p "+" change; v="$[v+change]";; -) read -p "-" change; v="$[v-change]";; =) read -p "=" change; v="$change";; $plus|$plusa) v="$[v+1]";; $minus|$minusa) v="$[v-1]";; $plus10) v="$[v+10]";; $minus10) v="$[v-10]";; *) continue;; esac if [ "$v" -lt 0 ]; then v=0 echo "New value is < 0; assuming 0" fi if [ "$v" -ge "$max" ]; then v="$[max-1]" echo "New value is too large; assuming $v" fi eval $side=\""$v"\" pipecmd="pnmcut $Left $Top $[curcolumns-Left-Right] $[currows-Top-Bottom]" realpipecmd="pnmcut $[Left*displayscale] $[Top*displayscale] $[(curcolumns-Left-Right)*displayscale] $[(currows-Top-Bottom)*displayscale]" $pipecmd "$f1" > "$ftmp" stopdisplay startdisplay "$ftmp" done ;; k) pipecmd="pnmflip -cw";; o) pipecmd="pnmflip -ccw";; 8) pipecmd="pnmflip -r180";; h) pipecmd="pnmflip -lr";; v) pipecmd="pnmflip -tb";; f) read -p "Angle (preferably between -45 and 45): " angle pipecmd="pnmrotate $angle";; b) read -p "Brightness factor (>1: lighter; <1: darker): " factor pipecmd="pamfunc -multiplier=$factor";; m) read -p "Gamma value (>1: lighter; <1: darker): " factor pipecmd="pnmgamma $factor";; *) echo "Invalid command.";; esac if [ x"$pipecmd" != x ]; then if [ x"$realpipecmd" = x ]; then realpipecmd="$pipecmd"; fi $pipecmd "$f1" > "$f2" && cmds[$n]="$realpipecmd" && n=$n2 && stopdisplay fi if [ $n -ne $n0 ] && [ "$unredone" = "" ]; then m=$n; fi status done stopdisplay if [ x"$OK" = x"y" ]; then if [ $n = 0 ]; then echo "Nothing changed for $f." echo "" else echo "$inconvert '$f' |" >&3 c=0 while [ $c -lt $n ]; do echo "${cmds[$c]} |" >&3 c=$[c+1] done case "$outformat" in PNG) echo "pnmtopng > '$destfile_png' || exit 1" >&3;; JPEG) echo "cjpeg > '$destfile_jpg' || exit 1" >&3;; *) echo "$progname: unknown output format" >&2; exit 1;; esac echo "" >&3 fi fi rm -f "$TMP/*" done rm -rf "$TMP" || exit 1 trap "" 0 echo "# $progname successfully completed." >&3