Rotate TkPhoto Image - via GUI with File and Bkgd-Color Select - 2 scrollable canvases

uniquename - 2013sep08

In the past month I happened to see the page Photo image rotation started by Richard Suchenwirth RS back in 2002. He used a Tk canvas to hold a 'demo' image (a Tcl feather) and built a rotated image (at arbitrary angle) on a 2nd canvas, to the right of the 1st canvas.

In 2005, DKF presented another version of the rotate proc that's algorithmically equivalent to RS's but which was optimized for Tcl/Tk 8.4.

In any case, it has been about 8 years now, and no one has taken this 'demo' code and made something more like an 'end-user utility' out of it.

To me, it seemed it needed a file-browser-selector --- in place of a hard-coded demo image file. And I would like to have the option to lay down a background color behind the rotated image. Then I would be more tempted to use this kind of utility occasionally.

I had recently written ImageMagnets - a Tk GUI for image processing --- for spot-bulge and spot-shrink around 'magnet points' on an image. In the process of coding the routine to bulge/shrink disks within an image, I found that mapping colors of pixels from a 'from' image to a 'to' image would not work nicely, because a pixel on the 'from' image would not map precisely on a pixel of the 'to' image. It was a bit of a conundrum, trying to figure out how to spread the color of the 'from' pixel across several 'to' pixels.

I found that it made sense to 'march' across the 'to' image and do an inverse mapping from the 'to' image to the 'from' image to find the pixel (or 2 or 3 or 4) to use (via a weighted average) to get the color of the 'to' pixel.

For example, if the 'to' mapping was the square of a normalized radius (of a pixel from a 'magnet point'), then the 'inverse' mapping was a square-root of the radius to the 'to' pixel.

In any case, I realized that I had the tools to make a 'preliminary stab' at an 'end-user utility' for rotating images --- images read from files supported by the Tk 'image create photo' command --- in particular, GIF and PNG files.

(Since I am using Tk 8.5, not 8.6, my testing was done with GIF files. But the code below will probably work with PNG files as well --- with the 8.6 'wish' interpreter.)

I used some of my other Tk canvas scripts that had a file-browse-select feature and a background-color select feature, and ended up with the GUI seen in the following image.

rotateImage_kingOfClubs_45degrees_blackBkgd_45cc_screenshot_696x354.jpg

This utility will also read in transparent GIF's, as seen in the following image.

rotateImage_blueFeatherTransparent_blackBkgd_90cc_screenshot_713x509.jpg

If I wanted to make a transparent image from the image on the right, I would do a screen capture (with 'gnome-screenshot' on Linux) and take the captured PNG file into a text editor ('mtpaint' on Linux) to crop the image and then, when saving the file, choose to save it as either a PNG or a GIF, and choose the color to make transparent (which 'mtpaint' allows you to do).


One application that I had in mind for this rotate utility is to rotate text labels 90 degrees for use in y-axis labeling of a plot (even though I can rotate images with 'mtpaint' --- but where's the Tcl fun in that).

I made the following black-text-on-white-background with A GUI for making 'Title Blocks' ... with text, fonts, colors, images. I used 'gnome-screenshot' to capture the image in a PNG file, and I used 'mtpaint' to crop the image and save it in a GIF file.

Then I read the GIF file into this rotate-image GUI, set the background to white, and clicked on the 'Rotate' button.

imageRotate_worldPopulationLabel_blackOnWhite_90cc_screenshot_674x425.jpg

In fact, I had in mind that one could take a whole set of alphanumeric characters (like the following taken from YAFSG - Yet Another Font Selector GUI) and rotate them. Then capture the image and break it up into separate little image files for each rotated character.

rotateImage_alphanumerics_blackOnWhite_90cc_screenshot_755x530.jpg

I could do the rotation with 'mtpaint', as well as the cropping to individual characters --- but, again, I wanted to get Tk into the act. Besides I have a lot more control with a Tk script. I could add various kinds of image processing ---including 'gamma correction' and 'transparency effects' and 'blur effects' and so on --- with many possibilities for controlling and guiding the outcome.


The code

Below, I provide the Tk script code for this 'rotate TkPhoto image' utility.

I follow my usual 'canonical' structure for Tk code for this Tk script:

  0) Set general window & widget parms (win-name, win-position,
     win-color-scheme, fonts, widget-geometry-parms, win-size-control,
     text-array-for-labels-etc).

  1a) Define ALL frames (and sub-frames, if any).
  1b) Pack   ALL frames and sub-frames.

  2) Define & pack all widgets in the frames, frame by frame.
              Within each frame, define ALL the widgets.
              Then pack the widgets.

  3) Define keyboard and mouse/touchpad/touch-sensitive-screen action
     BINDINGS, if needed.

  4) Define PROCS, if needed.

  5) Additional GUI initialization (typically with one or more of
     the procs), if needed.

This Tk coding structure is discussed in more detail on the page A Canonical Structure for Tk Code --- and variations.

This structure makes it easy for me to find code sections --- while generating and testing a Tk script, and when looking for code snippets to include in other scripts (code re-use).

I call your attention to step-zero. One new thing that I have started doing recently is using a text-array for text in labels, buttons, and other widgets in the GUI. This can make it easier for people to internationalize my scripts. I will be using a text-array like this in most of my scripts in the future.


Experimenting with the GUI

As in all my scripts that use the 'pack' geometry manager (which is all of my 100-plus scripts, so far), I provide the four main pack parameters --- '-side', '-anchor', '-fill', '-expand' --- on all of the 'pack' commands for the frames and widgets.

That helps me when I am initially testing the behavior of a GUI (the various widgets within it) as I re-size the main window.

I think that I have used a nice choice of the 'pack' parameters. The labels and buttons and the scale stay fixed in size and relative-location as the window is re-sized --- while the two 'canvases' expand/contract to accommodate the image size.

Furthermore, I used the '-scrollregion' parameter for the canvases, so that the images can exceed the current canvas sizes. The images can be scrolled up-and-down and side-to-side --- if the images exceed the canvas sizes.

You can experiment with the '-side', '-anchor', '-fill', and '-expand' parameters on the 'pack' commands for the various frames and widgets --- to get the widget behavior that you want.

___

In addition, you might want to change the fonts used for the various GUI widgets. For example, you could change '-weight' from 'bold' to 'normal' --- or '-slant' from 'roman' to 'italic'. Or change font families.

In fact, you may NEED to change the font families, because the families I used may not be available on your computer --- and the default font that the 'wish' interpreter chooses may not be very pleasing.

I use variables to set geometry parameters of widgets --- parameters such as border-widths and padding. And I have included the '-relief' parameter on the definitions of frames and widgets. Feel free to experiment with those 'appearance' parameters as well.

___

The Background-Color button on the GUI uses an 'external' Tk color-selector script whose code you can get from a page that I have provided on this wiki --- at A non-obfuscated color selector GUI. You could replace the call to that script with a color selector utility of your choice.


Some features in the code

That said, here's the code --- with plenty of comments to describe what most of the code-sections are doing.

You can look at the top of the PROCS section of the code to see a list of the procs used in this script, along with brief descriptions of how they are called and what they do.

The main proc is the 'rotate_image' proc. I have provided many comments (at the top of the script and in that proc) that describe the mathematics (and considrations) that are being used to perform the rotation (pixel mapping) function.


It is my hope that the copious comments in the code will help Tcl-Tk coding 'newbies' get started in making GUI's like this.

Without the comments, potential young Tcler's might be tempted to return to their iPhones and iPads and iPods --- to watch videos of people 'twerking' --- and catching on fire in the process --- by falling onto lighted candles.


 Code for Tk script 'rotate_image_on2ndCanvas.tk' :
#!/usr/bin/wish -f
##
## SCRIPT: rotate_image_on2ndCanvas.tk
##
## PURPOSE:  This Tk GUI script allows the user to select an image file
##           (GIF or PNG or other Tk 'photo' format file). The image file
##           is read and its image placed on a canvas.
##
##           A 2nd canvas on the GUI is used to hold a 2nd, rotated
##           version of the image. A slider-button, of a Tk 'scale' widget,
##           is used by the user to select an angle of rotation.
##
##           The 2nd canvas is larger than the original image --- to allow
##           for holding the image as it is rotated --- up to a full rotation.
##           To fill in the parts of the canvas/rotated-image that are
##           not covered by the rotated original image, the user can
##           select a background color to color those 'surrounding' pixels.
##
##+#######################
## THE WIDGETS OF THIS GUI:
##
##    The GUI for this utility includes the following widgets.
##
##    0) There are BUTTONS --- 'Exit', 'Help', 'Rotate', 'BackgroundColor'.
##
##    1) There is a FILENAME-ENTRY FIELD and 'Browse ...' BUTTON with
##       which to get an image file to place on one of 2 canvas widgets of
##       this GUI.
##
##    2) There is a SCALE widget with a slider-button that allows the
##       user to choose a rotation angle between 360 and -360 degrees, say.
##       This scale widget may be put in the same frame with the buttons.
##
##       (We may implement a button1-release binding on the angle-scale widget,
##        to trigger the rotate --- to do the rotate faster than with the
##        'Rotate' button.)
##
##    3) There are two CANVAS widgets (with scroll bars, to accomodate large
##       images) --- located side by side --- in a frame below the
##       buttons and filename frames.
##
##+##################
## MATHEMATICAL BASIS:
##
##  The mathematics involved in rotating the pixels of image1 to get
##  image2 is based on this 'simple' formula:
##
##  A clockwise rotation of a point x,y around an origin 0,0 through
##  an angle A is given by
##
##     (xnew,ynew) = (x cosA + y sinA, y cosA - x sinA)
##
##  or, in matrix form,
##
##   | xnew |  =  |  cosA  sinA |  | x |
##   | ynew |     | -sinA  cosA |  | y |
##
##+#################################
## METHOD USED to rotate the 2 images:
##
##   The Tk 'image create photo' command is used to create the
##   new Tk 'photo' image structure, 'imgID1'. And that 'imgID1' image is
##   put on the (left) canvas with a Tk canvas 'create image' command.
##
##   The diagonal distance across 'imgID1' is used to establish the
##   size of the second canvas (and its image structure) --- as a square 
##   whose sides are the length of the diagonal across 'imgID1'.
##
##   To do the rotate:
##      In a double loop, over y and x pixels, down and across 'imgID2',
##      the color of the 'imgID2' pixel at x2,y2 is determined by doing
##      an inverse rotation of x2,y2 to get an x1,y1 location on 'imgID1'
##      --- and use the color of one or more pixels near that x1.y1
##      location on 'imgID1' to set the color of the x2,y2 pixel on 'imgID2'.
##
##+########
## CREDITS:
##
##  This Tk script was inspired by a couple of Tk scripts
##  on the Tcl-Tk wiki ... wiki.tcl.tk:
##
##     https://wiki.tcl-lang.org/4022 - 'Photo image rotation'
##                               by Richard Suchenwirth
##     and
##
##     https://wiki.tcl-lang.org/8595 - 'Canvas Rotation'
##                               by Keith Vetter.
##
##     Suchenwirth did not offer a file selector GUI. The name of
##     a Tcl logo file was hard-coded in his code on the
##     'Photo image rotation' (4022) page.
##
##     There was no accomodation for large images, such as
##     scroll bars on the 2 canvases holding the original image
##     and the rotated image.
##
##     Comments on the Suchenwirth page indicates that he did not
##     do any anti-aliasing or accomodation of the fact that
##     the rotation (for a given angle) will generally not map
##     (integer) pixels exactly to (integer) pixels.
##           
##     On the Suchenwirth GUI, besides two canvases (2 images),
##     there was only an entry field for a rotation angle ---
##     and a checkbutton to allow for updating the image after
##     each 'row' of pixels of the (initial, unrotated) image was
##     processed.
##
##     If we can draw the rotated image within a fraction of a second,
##     then we will find that there is little need for a 'draw-each-row'
##     button on this GUI. 
##
##+#########################
## USING THE GENERATED IMAGE:
##
##     A screen/window capture utility (like 'gnome-screenshot'
##     on Linux) can be used to capture the GUI image in a PNG
##     or GIF file, say.
##
##     If necessary, an image editor (like 'mtpaint' on Linux)
##     can be used to crop the window capture image.  The image
##     could also be down-sized --- say to make a smaller image
##     suitable for a web page or an email.
##
##     The colored image file could be used with a utility (like the
##     ImageMagick 'convert' command) to change a color of the image
##     to TRANSPARENT, making a partially transparent GIF
##     (or PNG) file, from the rotated image.
##
##     One particular use would be to rotate images of alphanumeric
##     characters (in a user-chosen font and with user-chosen foreground
##     and background colors) --- say 90 degrees --- to make a set of
##     images to use for labelling diagrams/plots/etc. with rotated text.
##
##+########################################################################
## 'CANONICAL' STRUCTURE OF THIS TK CODE:
##
##  0) Set general window & widget parms (win-name, win-position,
##     win-color-scheme, fonts, widget-geometry-parms, win-size-control,
##     text-array-for-labels-etc).
##
##  1a) Define ALL frames (and sub-frames, if any).
##  1b) Pack   ALL frames and sub-frames (that are to show inititally).
##
##  2) Define all widgets in the frames, frame-by-frame.
##     When ALL the widgets for a frame are defined, pack ALL the widgets.
##
##  3) Define keyboard and mouse/touchpad/touch-senisitive-screen 'event'
##     BINDINGS, if needed.
##
##  4) Define PROCS, if needed.
##
##  5) Additional GUI INITIALIZATION (typically with one or two of the procs),
##     if needed/wanted.
##
##
## Some detail about the code structure of this particular script:
##
##  1a) Define ALL frames:
## 
##      Top-level :  '.fRbuttons'  '.fRfile'    '.fRimages'
##
##      Sub-frames:  '.fRimages.fRcanvas1'    '.fRimages.fRcanvas2'
##
##  1b) Pack ALL the frames --- top to bottom.
##
##  2) Define all widgets in the frames (and pack them):
##
##     - In '.fRbuttons':   1 BUTTON widget ('Exit'),
##                          1 BUTTON widget ('Help'),
##                          1 BUTTON widget ('Rotate'), (optional)
##                          1 LABEL-and-SCALE widget pair for the angle
##                            of rotation
##
##     - In '.fRfile':      LABEL, ENTRY, and 'Browse...' BUTTON widget
##
##     - In '.fRimages':    to contain 2 CANVAS widgets, with scrollbars
##
##  3) Define BINDINGS:
##
##        - button1-release binding on the scale widget, to do the rotate
##
##        - button1-release binding on the filename entry field,
##          to reload a file to the 'imgID1' structure and canvas1
##
##        - Return-key binding on the filename entry field,
##          to reload a file to the 'imgID1' structure and canvas1
##
##  4) Define PROCS:
##
##   'get_img_filename' -  called by the filename 'Browse...' button,
##                         to present a file-selector GUI and get an
##                         image filename and load it into a 'photo' structure,
##                         imgID1, and display the image on canvas1.
##
##                         Also build a 'photo' structure, imgID2, in a
##                         square format with width=height given by the
##                         diagonal distance across imgID1. Put that
##                         imgID2 on canvas2.
##
##   'rotate_image'     - called by a button1-release binding on the scale
##                        widget --- or, alternatively, called by a click on
##                        the 'Rotate' button --- to set the colors
##                        of the pixels of 'imgID2' --- either from a
##                        user-specified background color or from the
##                        color of pixels of 'imgID1', according to 
##                        the rotation angle currently specified.
##
##  'clear_canvases'    - called by the 'Clear' button, to clear the two
##                        canvases.
##
##  'set_background_color' - called by the 'BackgroundColor' button, to
##                           show a color selector GUI and use the
##                           user-selected color to set a color for
##                           use in setting color of pixels on 'imgID2'.
##
##   'popup_msgVarWithScroll' - used to show messages to the user, such as
##                              the HELPtext for this utility
##
##  5) Additional-GUI-initialization: none (?)
##                                    Could set initial angle for the scale
##                                    widget and an initial background color
##                                    here --- instead of in the widget
##                                    definition sections and/or in the
##                                    color setting sections, near the top
##                                    of the code.
##
##+########################################################################
## DEVELOPED WITH:
##   Tcl-Tk 8.5 on Ubuntu 9.10 (2009-october release, 'Karmic Koala').
##
##   $ wish
##   % puts "$tcl_version $tk_version"
##                                  showed   8.5 8.5   on Ubuntu 9.10
##    after Tcl-Tk 8.4 was replaced by 8.5 --- to get anti-aliased fonts.
##+#######################################################################
## MAINTENANCE HISTORY:
## Created by: Blaise Montandon 2013aug09 Started laying out the GUI.
## Changed by: Blaise Montandon 2013sep08 Added a 'Clear' button.
##                                        Chg scale resolution to allow
##                                        degrees in tenths.
##+#######################################################################

##+#######################################################################
## Set WINDOW TITLES and POSITION.
##+#######################################################################

wm title    . \
   "Rotate an Image - with angle and background-color options"
wm iconname . "RotateImg"

wm geometry . +15+30


##+######################################################
## Set the COLOR SCHEME for the WINDOW ---
## and background colors for its WIDGETS.
##+######################################################

# set Rpal255 200
# set Gpal255 200
# set Bpal255 255
set Rpal255 210
set Gpal255 210
set Bpal255 210

set hexPALcolor [format "#%02X%02X%02X" $Rpal255 $Gpal255 $Bpal255]

tk_setPalette "$hexPALcolor"

## Set color background for some WIDGETS.

set scaleBKGD "#f0f0f0"
# set radbuttBKGD "#c0c0c0"
# set chkbuttBKGD "#c0c0c0"
# set listboxBKGD "#f0f0f0"
set entryBKGD   "#f0f0f0"
set textBKGD    "#f0f0f0"


##+##########################################################
## Set (temporary) FONT-NAMES.
##
## We use a VARIABLE-WIDTH FONT for LABEL and BUTTON widgets.
##
## We use a FIXED-WIDTH FONT for TEXT widgets (to preserve
## alignment of columns in text), LISTBOXES (to preserve
## alignment of characters in lists), and ENTRY fields
## (to make it easy to position the text cursor at narrow
## characters like i, j, l, and the number 1).
##+##########################################################

font create fontTEMP_varwidth \
   -family {comic sans ms} \
   -size -14 \
   -weight bold \
   -slant roman

font create fontTEMP_SMALL_varwidth \
   -family {comic sans ms} \
   -size -12 \
   -weight bold \
   -slant roman

## Some other possible (similar) variable width fonts:
##  Arial
##  Bitstream Vera Sans
##  DejaVu Sans
##  Droid Sans
##  FreeSans
##  Liberation Sans
##  Nimbus Sans L
##  Trebuchet MS
##  Verdana


font create fontTEMP_fixedwidth  \
   -family {liberation mono} \
   -size -14 \
   -weight bold \
   -slant roman

font create fontTEMP_SMALL_fixedwidth  \
   -family {liberation mono} \
   -size -12 \
   -weight bold \
   -slant roman

## Some other possible fixed width fonts (esp. on Linux):
##  Andale Mono
##  Bitstream Vera Sans Mono
##  Courier 10 Pitch
##  DejaVu Sans Mono
##  Droid Sans Mono
##  FreeMono
##  Nimbus Mono L
##  TlwgMono



##+###########################################################
## Set GEOMETRY PARAMETERS for the various widget definitions.
## (e.g. width and height of canvas, and padding for Buttons)
##+###########################################################

## CANVAS geom parms:

set initCan1WidthPx  200
set initCan1HeightPx 150

set initCan2WidthPx  255
set initCan2HeightPx 255


# set BDwidthPx_canvas 2
set BDwidthPx_canvas 0


## LABEL geom parameters:

set PADXpx_label 0
set PADYpx_label 0
set BDwidthPx_label 2


## BUTTON geom parameters:

set PADXpx_button 0
set PADYpx_button 0
set BDwidthPx_button 2


## SCALE geom parameters:

set BDwidthPx_scale 2
set initScaleLengthPx 300
set scaleWidthPx 10


## RADIOBUTTON geom parameters:

# set PADXpx_radbutt 0
# set PADYpx_radbutt 0
# set BDwidthPx_radbutt 2


## CHECKBUTTON geom parameters:

# set PADXpx_chkbutt 0
# set PADYpx_chkbutt 0
# set BDwidthPx_chkbutt 2


## ENTRY geom parameters:

set BDwidthPx_entry 2
set initImgfileEntryWidthChars 25


##+###################################################################
## Set a MINSIZE of the window (roughly).
##
## For WIDTH, allow for a minwidth of the '.fRbuttons' frame:
##            about 3 buttons (Exit,Help,BackgroundColor), and
##            a label-and-scale widget-pair.
##
## For HEIGHT, allow
##             2 chars  high for the '.fRbuttons' frame
##             1 char   high for the '.fRfile'    frame
##            24 pixels high for the '.fRimages'  frame.
##+#######################################################################
## We allow the window to be resizable and we pack the '.fRimages' with
## '-fill both -expand 1' so that the 2 canvases can be enlarged according
## to the size of the image loaded.
##+#######################################################################

set minWinWidthPx [font measure fontTEMP_varwidth \
   " Exit  Help  Rotate  Background  Rotation Angle : "]

## Add some pixels to account for right-left-side window decoration
## (about 8 pixels), about 5 widgets x 4 pixels/widget for borders/padding
## for 5 widgets --- 3 buttons and 1 label, 1 scale.

set minWinWidthPx [expr {28 + $minWinWidthPx}]


## MIN HEIGHT ---
##             2 chars  high for the '.fRbuttons' frame
##             1 char   high for the '.fRfile' frame
##            24 pixels high for the '.fRimages' frame.

set CharHeightPx [font metrics fontTEMP_varwidth -linespace]

set minWinHeightPx [expr {24 + (3 * $CharHeightPx)}]

## Add about 28 pixels for top-bottom window decoration. Also add
## about 3 frames x 4 pixels/frame for each of the 3 stacked frames
## and their widgets (their borders/padding).

set minWinHeightPx [expr {40 + $minWinHeightPx}]


## FOR TESTING:
#   puts "minWinWidthPx = $minWinWidthPx"
#   puts "minWinHeightPx = $minWinHeightPx"

wm minsize . $minWinWidthPx $minWinHeightPx


## If you want to make the window un-resizable, 
## you can use the following statement.
# wm resizable . 0 0


##+####################################################################
## Set a TEXT-ARRAY to hold text for buttons & labels on the GUI.
##     NOTE: This can aid INTERNATIONALIZATION. This array can
##           be set according to a nation/region parameter.
##+####################################################################

## if { "$VARlocale" == "en"}

set aRtext(buttonEXIT) "Exit"
set aRtext(buttonHELP) "Help"
set aRtext(buttonCLEAR) "Clear"
set aRtext(buttonROTATE) "Rotate"
set aRtext(buttonCOLORBKGD) "Background
Color"

set aRtext(labelSCALE)  "Rotation Angle
(-360 to 360):"

set aRtext(labelFILE)    "Img Filename (GIF/PNG):"
set aRtext(buttonBROWSE)  "Browse ..."


## END OF  if { "$VARlocale" == "en"}


##+###################################################################
## DEFINE *ALL* THE FRAMES:
## 
##   Top-level :  '.fRbuttons'   '.fRfile'   'fRfile2'  '.fRimages'
##
##   Sub-frames: '.fRimages.fRcanvas1'    '.fRimages.fRcanvas2'
##+###################################################################

## FOR TESTING: (to check frame sizes as window is resized)
# set BDwidth_frame 2
# set RELIEF_frame raised

set BDwidth_frame 0
set RELIEF_frame flat

frame .fRbuttons  -relief $RELIEF_frame  -borderwidth $BDwidth_frame

frame .fRfile     -relief $RELIEF_frame  -borderwidth $BDwidth_frame

frame .fRimages   -relief raised         -borderwidth 2

frame .fRimages.fRcanvas1  -relief $RELIEF_frame  -borderwidth $BDwidth_frame

frame .fRimages.fRcanvas2  -relief $RELIEF_frame  -borderwidth $BDwidth_frame


##+##############################
## PACK the top-level FRAMES. 
##+##############################

pack .fRbuttons \
     .fRfile \
   -side top \
   -anchor nw \
   -fill x \
   -expand 0

pack .fRimages \
   -side top \
   -anchor nw \
   -fill both \
   -expand 1


##+#################################
## PACK the SUB-FRAMES, side-by-side. 
##+#################################

pack .fRimages.fRcanvas1 \
     .fRimages.fRcanvas2 \
   -side left \
   -anchor nw \
   -fill both \
   -expand 1


##+#########################################################
## All frames are defined and packed.
## Now we are ready to define the widgets in the frames.
##+#########################################################


##+#####################################################################
## In the '.fRbuttons' FRAME  -
## DEFINE BUTTONS (Exit, Help, Rotate, BackgroundColor)
## and
##   - a LABEL-and-SCALE widgets.
##+#####################################################################

button .fRbuttons.buttEXIT \
   -text "$aRtext(buttonEXIT)" \
   -font fontTEMP_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command {exit}

button .fRbuttons.buttHELP \
   -text "$aRtext(buttonHELP)" \
   -font fontTEMP_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command {popup_msgVarWithScroll .topHelp "$HELPtext"}

button .fRbuttons.buttCLEAR \
   -text "$aRtext(buttonCLEAR)" \
   -font fontTEMP_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command {clear_canvases}

button .fRbuttons.buttROTATE \
   -text "$aRtext(buttonROTATE)" \
   -font fontTEMP_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command {rotate_image}

button .fRbuttons.buttCOLORBKGD \
   -text "$aRtext(buttonCOLORBKGD)" \
   -font fontTEMP_SMALL_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command "set_background_color"


label .fRbuttons.labelANGLE \
   -text "$aRtext(labelSCALE)" \
   -font fontTEMP_SMALL_varwidth \
   -justify left \
   -anchor w \
   -relief flat \
   -bd $BDwidthPx_label

## We will set this scale VAR in the 'Additional GUI
## Initialization' section at the bottom of this script.
# set VARangle 90

scale .fRbuttons.scaleANGLE \
   -orient horizontal \
   -resolution 0.01 \
   -from -360.00 -to 360.00 \
   -length 200px \
   -font fontTEMP_SMALL_varwidth \
   -variable VARangle

label .fRbuttons.labelIMGSIZE \
   -text "" \
   -font fontTEMP_varwidth \
   -justify left \
   -anchor w \
   -relief flat \
   -bd $BDwidthPx_label


##+#############################################
## Pack ALL the widgets in the 'fRbuttons' frame.
##+#############################################

pack .fRbuttons.buttEXIT \
     .fRbuttons.buttHELP \
     .fRbuttons.buttCLEAR \
     .fRbuttons.buttROTATE \
     .fRbuttons.buttCOLORBKGD \
     .fRbuttons.labelANGLE \
     .fRbuttons.scaleANGLE \
     .fRbuttons.labelIMGSIZE \
   -side left \
   -anchor w \
   -fill none \
   -expand 0


##+###############################
## In FRAME '.fRfile' -
## DEFINE-and-PACK 3 widgets -
## LABEL, ENTRY, BUTTON:
##+###############################

label .fRfile.labelFILE \
   -text "$aRtext(labelFILE)" \
   -font fontTEMP_varwidth \
   -justify left \
   -anchor w \
   -relief flat \
   -bd 0

set ENTRYfilename ""

entry .fRfile.entFILENAME \
   -textvariable ENTRYfilename \
   -bg $entryBKGD \
   -font fontTEMP_fixedwidth \
   -width $initImgfileEntryWidthChars \
   -relief sunken \
   -bd $BDwidthPx_entry

button .fRfile.buttBROWSE \
   -text "$aRtext(buttonBROWSE)" \
   -font fontTEMP_varwidth \
   -padx $PADXpx_button \
   -pady $PADYpx_button \
   -relief raised \
   -bd $BDwidthPx_button \
   -command {get_img_filename}


## Pack the '.fRfile' widgets.

pack  .fRfile.labelFILE \
   -side left \
   -anchor w \
   -fill none \
   -expand 0

pack .fRfile.entFILENAME \
   -side left \
   -anchor w \
   -fill x \
   -expand 1

pack  .fRfile.buttBROWSE \
   -side left \
   -anchor w \
   -fill none \
   -expand 0



##+######################################################
## In the '.fRimages.fRcanvas1' frame -
## DEFINE-and-PACK 1 CANVAS widget, with SCROLLBARS.
##+######################################################
## We set '-highlightthickness' and '-borderwidth' to
## zero, to avoid covering some of the viewable area
## of the canvas, as suggested on page 558 of the 4th
## edition of 'Practical Programming with Tcl and Tk'.
##+###################################################

canvas .fRimages.fRcanvas1.can \
   -width  $initCan1WidthPx \
   -height $initCan1HeightPx \
   -relief flat \
   -highlightthickness 0 \
   -borderwidth 0 \
   -yscrollcommand ".fRimages.fRcanvas1.scrbary set" \
   -xscrollcommand ".fRimages.fRcanvas1.scrbarx set"

scrollbar .fRimages.fRcanvas1.scrbary \
   -orient vertical -command ".fRimages.fRcanvas1.can yview"

scrollbar .fRimages.fRcanvas1.scrbarx \
   -orient horizontal -command ".fRimages.fRcanvas1.can xview"


## Pack the widgets in frame 'fRcanvas.fRcanvas1'.
## (Pack the scrollbars before the canvas so that
##  the canvas does not fill the available area first.)

pack .fRimages.fRcanvas1.scrbary \
   -side right \
   -anchor e \
   -fill y \
   -expand 0

pack .fRimages.fRcanvas1.scrbarx \
   -side bottom \
   -anchor sw \
   -fill x \
   -expand 0

pack .fRimages.fRcanvas1.can \
   -side top \
   -anchor nw \
   -fill both \
   -expand 1


##+######################################################
## In the '.fRimages.fRcanvas2' frame -
## DEFINE-and-PACK 1 CANVAS widget, with SCROLLBARS.
##+######################################################
## We set '-highlightthickness' and '-borderwidth' to
## zero, to avoid covering some of the viewable area
## of the canvas, as suggested on page 558 of the 4th
## edition of 'Practical Programming with Tcl and Tk'.
##+###################################################

canvas .fRimages.fRcanvas2.can \
   -width  $initCan2WidthPx \
   -height $initCan2HeightPx \
   -relief flat \
   -highlightthickness 0 \
   -borderwidth 0 \
   -yscrollcommand ".fRimages.fRcanvas2.scrbary set" \
   -xscrollcommand ".fRimages.fRcanvas2.scrbarx set"

scrollbar .fRimages.fRcanvas2.scrbary \
   -orient vertical -command ".fRimages.fRcanvas2.can yview"

scrollbar .fRimages.fRcanvas2.scrbarx \
   -orient horizontal -command ".fRimages.fRcanvas2.can xview"


## Pack the widgets in frame 'fRcanvas.fRcanvas2'.
## (Pack the scrollbars before the canvas so that
##  the canvas does not fill the available area first.)

pack .fRimages.fRcanvas2.scrbary \
   -side right \
   -anchor e \
   -fill y \
   -expand 0

pack .fRimages.fRcanvas2.scrbarx \
   -side bottom \
   -anchor sw \
   -fill x \
   -expand 0

pack .fRimages.fRcanvas2.can \
   -side top \
   -anchor nw \
   -fill both \
   -expand 1


##+####################################################
## END OF the DEFINITION OF THE GUI FRAMES-and-WIDGETS.
##+####################################################
## Ready to define BINDINGS and PROCS.
##+####################################################


##+#################################################
## BINDINGS SECTION:
##+#################################################

bind .fRfile.entFILENAME <ButtonRelease-1>  {make_imgID1_on_canvas1_init_imgID2}
bind .fRfile.entFILENAME <Return>           {make_imgID1_on_canvas1_init_imgID2}


## The following binding on the scale widget can be commented/de-activated
## so that we can avoid unwanted re-draws for 'intermediate' rotation angles.
## We can use the 'Rotate' button of the GUI to make sure that we only
## do the rotate processing when we have definitely set the angle we want.
 
# bind .fRbuttons.scaleANGLE <ButtonRelease-1>  {rotate_image}


##+######################################################################
## PROCS SECTION:
##
##   'get_img_filename' -  called by the filename 'Browse...' button,
##                         to present a file-selector GUI and get an
##                         image filename.
##
##                         Then load its image into a 'photo' structure,
##                         'imgID1', and display the image on canvas1.
##
##                         Also build a 'photo' structure, imgID2, in a
##                         square format with width=height given by the
##                         diagonal distance across imgID1. Put that
##                         imgID2 on canvas2.
##
##   'make_imgID1_on_canvas1make_imgID1_on_canvas1_init_imgID2' - called by proc 'get_img_filename',
##                              to make the imgID1 and imgID2 structures
##                              and put them on canvas1 and canvas2, resp.
##
##   'rotate_image'     - called by a button1-release binding on the scale
##                        widget --- or, alternatively, called by a click on
##                        the 'Rotate' button --- to set the colors
##                        of the pixels of 'imgID2' --- either from a
##                        user-specified background color or from the
##                        color of pixels of 'imgID1', according to 
##                        the rotation angle currently specified.
##
##  'clear_canvases'    - called by the 'Clear' button, to clear the two
##                        canvases.
##
##  'set_background_color' - called by the 'BackgroundColor' button, to
##                           show a color selector GUI and use the
##                           user-selected color to set a color for
##                           use in setting color of pixels on 'imgID2'.
##
##   'popup_msgVarWithScroll' - used to show messages to the user, such as
##                              the HELPtext for this utility
##+#######################################################################



##+##############################################################
## Set an initial 'curDIR' for the following 2 get-filename procs.
##+##############################################################

# set curDIR "$env(HOME)"
set curDIR "[pwd]"


##+#########################################################################
## Proc 'get_img_filename' -
##
##      To get the name of an image file (GIF/PNG) and put the
##      filename into global var 'ENTRYfilename'.
##
## Used by: the '-command' option of the 'Browse ...' button.
##+#########################################################################

proc get_img_filename {} {

   global ENTRYfilename env curDIR imgID1

   ## Offer selector for an image file

   set fName [tk_getOpenFile -parent . \
      -title "Select Image file (GIF/PNG)" \
      -initialdir "$curDIR" ]

   ## FOR TESTING:
   #   puts "fName : $fName"

   if {[file exists $fName]} {

      ## If the filename from the file selector exits,
      ## put the name in the entry widget for file1, and
      ## extract the current directory name.

      set ENTRYfilename "$fName"
      set curDIR [ get_chars_before_last / in "$ENTRYfilename" ]

      ## Make a 'photo' image structure, imgID1, for this file and
      ## put it on canvas1. Make an imgID2 structure of suitable size.

      make_imgID1_on_canvas1_init_imgID2

   }
}
## END OF proc 'get_img_filename'


##+######################################################################
## proc 'make_imgID1_on_canvas1_init_imgID2'
##
## PURPOSE: From the ENTRYfilename filename, an 'imgID1' 'photo' structure
##          is created and put on canvas1.
##
##          Also builds a 'photo' structure, imgID2, in a
##          square format with width=height given by the
##          diagonal distance across imgID1. Puts that
##          imgID2 on canvas2.
##
## CALLED BY: proc 'get_img_filename' and bindings on the entry field
##            for filename, to be able to reload from the filename
##            still sitting in that entry field.
##+######################################################################

proc make_imgID1_on_canvas1_init_imgID2 {} {

   global ENTRYfilename diam1px
   # global imgID1 imgID2

   ## If the image ID, imgID1, is in use, delete that image.
   ## Necessary? To accomodate reading images of different sizes? 

   if {[info exists imgID1]} {image delete imgID1}

   ## Make the 'imgID1' Tk 'photo' structure, in-memory.

   image create photo imgID1 -file "$ENTRYfilename"

   ## We re-use the name 'imgID1' rather than creating multiple
   ## ID numbers in a variable named 'imgID1', as follows.
   # set imgID1 [image create photo -file "$ENTRYfilename"]

   ## FOR TESTING:
   #  puts "make_imgID1_on_canvas1_init_imgID2 - from filename: $ENTRYfilename"
   #  puts "                         Made image with ID: imgID1"


   ## Put 'imgID1' on canvas1.

   .fRimages.fRcanvas1.can create image 0 0 -anchor nw -image imgID1

   ## Get the current imgID1 width and height.

   set img1WidthPx  [image width  imgID1] 
   set img1HeightPx [image height imgID1]

   ## Use 'scrollregion' to make the canvas1 scrollbars usable
   ## for large images.

   .fRimages.fRcanvas1.can configure -scrollregion \
      "0 0 $img1WidthPx $img1HeightPx"

   ## Put image size in a label on the GUI.

   .fRbuttons.labelIMGSIZE configure \
      -text "ImgSize:
${img1WidthPx}x$img1HeightPx"

   ## Get the 'diameter' of imgID1.

   set diam1px [expr {int(sqrt( \
      ($img1WidthPx * $img1WidthPx) + \
       ($img1HeightPx * $img1HeightPx) \
      ))}]


   ## Also build a 'photo' structure, imgID2, in a
   ## square format with width=height given by the
   ## diagonal distance (diameter) across imgID1.
   ## First:
   ## If the image ID, imgID2, is in use, delete that image.
   ## Necessary? To accomodate reading images of different sizes? 

   if {[info exists imgID2]} {image delete imgID2}

   ## Make the 'imgID2' Tk 'photo' structure, in-memory.

   image create photo imgID2 -width $diam1px -height $diam1px

   ## Put the imgID2 'photo' structure on canvas2.

   .fRimages.fRcanvas2.can create image 0 0 -anchor nw -image imgID2

   ## Use 'scrollregion' to make the canvas2 scrollbars usable
   ## for large images.

   .fRimages.fRcanvas2.can configure -scrollregion "0 0 $diam1px $diam1px"

}
## END OF proc 'make_imgID1_on_canvas1_init_imgID2'


##+######################################################################
## proc 'get_chars_before_last'
##+######################################################################
## INPUT:  A character and a string.
##         Note: The "in" parameter is there only for clarity.
##
## OUTPUT: Returns all of the characters in the string "strng" that
##         are BEFORE the last occurence of the characater "char".
##
## EXAMPLE CALL: To extract the directory from a fully qualified file name:
##
## set directory [ get_chars_before_last "/" in "/home/abc01/junkfile" ]
##
##      $directory will now be the string "/home/abc01"
##
##+######################################################################

proc get_chars_before_last { char in strng } {

   set endIDX [ expr [string last $char $strng ] - 1 ]
   set output [ string range $strng 0 $endIDX ]

   ## FOR TESTING:
   # puts "From 'get_chars_before_last' proc:"
   # puts "STRING: $strng"
   # puts "CHAR: $char"
   # puts "RANGE up to LAST CHAR - start: 0   end: $endIDX"

   return $output

}
## END OF 'get_chars_before_last' PROCEDURE


##+#########################################################
## proc  rotate_image
##
## PURPOSE: Builds 'imgID2' from 'imgID1', by performing a
##          rotation based on 'imgID1' ---
##          and displays the updated 'imgID2' image on canvas2.
##
##          Uses the scale variable 'VARangle' --- and
##          the current background color in COLORBKGD vars.
##
## CALLED BY: the 'Rotate' button or button1-release binding
##            on the '.scaleANGLE' widget.
##+#########################################################
## MATHEMATICAL BASIS:
##
##  The mathematics involved in rotating the pixels of image1 to get
##  image2 is based on this 'simple' formula:
##
##  A clockwise rotation of a point x,y around an origin 0,0 through
##  an angle A is given by
##
##     (xnew,ynew) = (x cosA + y sinA, y cosA - x sinA)
##
##  or, in matrix form,
##
##   | xnew |  =  |  cosA  sinA |  | x |
##   | ynew |     | -sinA  cosA |  | y |
##
##+#########################################################

set pi [expr {4.0 * atan(1.0)}]

proc rotate_image {} {

   global diam1px VARangle pi \
      ENTRYfilename COLORBKGDr COLORBKGDg COLORBKGDb COLORBKGDhex
   # global imgID1 imgID2

   ## FOR TESTING: (to dummy out this proc)
   # return

   ####################################################
   ## If it looks like there is no image to process,
   ## bail out of this proc. (We could pop a msg with
   ## the 'popup_msgVarWithScroll' proc.)
   ####################################################

   if {"$ENTRYfilename" == ""} {return}
   # if {![info exists imgID1]} {return}

   ###################################################
   ## Indicate that drawing calculations are starting.
   ###################################################

   wm title . "*BUSY* ... calculating pixel colors."

   # .fRbuttons.labelINFO configure -text "\
   #  **BUSY** ... CALCULATIONS IN PROGRESS."
   ## This 'update' makes sure that this label update is displayed.
   # update

   ##################################################
   ## Set the current time, for determining elapsed
   ## time for building the 'photo' image.
   ##################################################

   set t0 [clock milliseconds]

   ################################################
   ## Change the cursor to a 'watch' cursor.
   ################################################
   # . config -cursor watch
   ## Make the cursor visible.
   # update


   ##################################################
   ## Get the width and height of imgID1 and imgID2.
   ## (imgID2 was set to be a square of width and height
   ##  equal to the 'diameter' of imgID1.)
   ##################################################

   set img1WidthPx  [image width  imgID1]
   set img1HeightPx [image height imgID1]

   set img1WidthPxMinus1  [expr {$img1WidthPx - 1}]
   set img1HeightPxMinus1 [expr {$img1HeightPx - 1}]

   # set img2WidthPx  [image width  imgID2]
   # set img2HeightPx [image height imgID2]

   set img2WidthPx  $diam1px
   set img2HeightPx $diam1px


   ####################################################
   ## Get the center coordinates of imgID1  and imgID2.
   ####################################################

   set img1CenterXpx [expr {int($img1WidthPx  / 2.0)}]
   set img1CenterYpx [expr {int($img1HeightPx / 2.0)}]

   set img2CenterXpx [expr {int($img2WidthPx  / 2.0)}]
   set img2CenterYpx [expr {int($img2HeightPx / 2.0)}]


   #######################################################
   ## Before we get into the double-loop below,
   ## calculate the sine and cosine of the rotation angle.
   #######################################################

   set rads [expr {double($pi * $VARangle / 180.)}]
   set sinA [expr {sin($rads)}]
   set cosA [expr {cos($rads)}]


   ########################################################
   ## Express the corners of unrotated imgID1 relative to
   ## the center of imgID2. Then ...
   ## Rotate the corners of imgID1 to find the dimensions
   ## of the rectangle, within imgID2, containing the
   ## rotated imgID1.
   ########################################################
   ## We use 
   ##   (xnew,ynew) = (x cosA + y sinA ,   y cosA - x sinA)
   ##               = (x cosA + y sinA , - x sinA + y cosA)
   ########################################################

   set Xtopleft1px [expr {$img2CenterXpx - $img1CenterXpx}]
   set Ytopleft1px [expr {$img2CenterYpx - $img1CenterYpx}]

   set Xbotright1px [expr {$img2CenterXpx + $img1CenterXpx}]
   set Ybotright1px [expr {$img2CenterYpx + $img1CenterYpx}]

   ## OK. We have the coordinates of the un-rotated corners.
   ## Now calculate the rotated coords of the corners.

   if {0} {
   set newXtopleft1px [expr { ($Xtopleft1px * $cosA) + ($Ytopleft1px * $sinA)}]
   set newYtopleft1px [expr {-($Xtopleft1px * $sinA) + ($Ytopleft1px * $cosA)}]

   set newXtopright1px [expr { ($Xbotright1px * $cosA) + ($Ytopleft1px * $sinA)}]
   set newYtopright1px [expr {-($Xbotright1px * $sinA) + ($Ytopleft1px * $cosA)}]

   set newXbotright1px [expr { ($Xbotright1px * $cosA) + ($Ybotright1px * $sinA)}]
   set newYbotright1px [expr {-($Xbotright1px * $sinA) + ($Ybotright1px * $cosA)}]

   set newXbotleft1px [expr { ($Xtopleft1px * $cosA) + ($Ybotright1px * $sinA)}]
   set newYbotleft1px [expr {-($Xtopleft1px * $sinA) + ($Ybotright1px * $cosA)}]
   }
   ## END OF if {0}

   ########################################################################
   ## Use the min,max values of the rotated corner coordinates to get the
   ## the coordinates of the limits of the rectangle containing the rotated
   ## imgID1 in imgID2.
   ########################################################################

   if {0} {
   set XtopleftRECTpx [expr {min($newXtopleft1px,$newXtopright1px,$newXbotright1px,$newXbotleft1px)}]
   set YtopleftRECTpx [expr {min($newYtopleft1px,$newYtopright1px,$newYbotright1px,$newYbotleft1px)}]

   set XbotrightRECTpx [expr {max($newXtopleft1px,$newXtopright1px,$newXbotright1px,$newXbotleft1px)}]
   set YbotrightRECTpx [expr {max($newYtopleft1px,$newYtopright1px,$newYbotright1px,$newYbotleft1px)}]
   }
   ## END OF if {0}


   ##########################################################################
   ## Clear canvas2 of a previous image, if any.
   ## (Note: We might want to clear the imgID2 structure instead ---
   ##        and allow for watching the image be built, via 'update' cmds.)
   ##########################################################################

   .fRimages.fRcanvas2.can delete all


   ##########################################################################
   ## Before we go through the double-loop below over the pixels of imgID2,
   ## to determine which pixels are covered by imgID1, and, for those pixels,
   ## determine the pixel color from the corresponding pixel (or 2 or 3 or 4)
   ## in imgID1 ---
   ## we set all the pixels of imgID2 to the user-selected background color.
   ## We set a row of pixels at a time.
   #########################################################################

   set img2WidthPxMinus1  [expr {$img2WidthPx - 1}]

   for {set y2Px 0} {$y2Px < $img2HeightPx} {incr y2Px} {
      set y2PxPlus1 [expr {$y2Px + 1}]
      imgID2 put $COLORBKGDhex -to 0 $y2Px $img2WidthPxMinus1 $y2PxPlus1
      ## FOR TESTING:
      #  update
   }


   ################################################################
   ## HERE IS THE 'GUTS' OF DOING THE ROTATION (a double loop):
   ## Put rotated-imgID1 on imgID2.
   ###############################
   ## MATHEMATICAL BASIS:
   ##
   ##  The mathematics involved in rotating the pixels of image1 to get
   ##  image2 is based on this 'simple' formula:
   ##
   ##  A clockwise rotation of a point x,y around an origin 0,0 through
   ##  an angle A is given by
   ##
   ##     (xnew,ynew) = (x cosA + y sinA,    y cosA - x sinA)
   ##                 = (x cosA + y sinA,  - x sinA + y cosA)
   ##
   ##  or, in matrix form,
   ##
   ##   | xnew |  =  |  cosA  sinA |  | x |
   ##   | ynew |     | -sinA  cosA |  | y |
   ##
   ## METHOD NOTE:
   ## Rather than 'marching across img1' and coloring pixels of
   ## img2, we 'MARCH ACROSS IMG2' and use the INVERSE of the above
   ## rotation transformation (i.e. angle -A instead of A) to
   ## get a location on img1 corresponding to the current pixel on
   ## img2. Then we use the color of one or more pixels around
   ## the IMG1-PIXEL-LOCATION to get the color for the img2 pixel.
   ##
   ## This technique essentially 'anti-aliases' the img2 pixel,
   ## whenever the img2-pixel does not map back precisely to a
   ## single pixel on img1.
   ###############################################################

   for {set y2Px 0} {$y2Px < $img2HeightPx} {incr y2Px} {


      for {set x2Px 0} {$x2Px < $img2WidthPx} {incr x2Px} {

         #########################################################
         ## Rotate x2,y2 (relative to the center of imgID2)
         ## to get coords x1,y1 (relative to the center of imgID2)
         ## --- in floating-point, NOT integer, units.
         #########################################################

         set relX2px [expr {double($x2Px - $img2CenterXpx)}]
         set relY2px [expr {double($y2Px - $img2CenterYpx)}]

         set relX1px [expr { ($relX2px * $cosA) + ($relY2px * $sinA)}]

         set relY1px [expr {-($relX2px * $sinA) + ($relY2px * $cosA)}]


         ##################################################
         ## Check to see if the x1,y1 point is within the
         ## unrotated rectangle that imgID1 would occupy
         ## in the middle of the imgID2 square.
         #################################################

         set absX1px [expr {abs($relX1px)}]
         set absY1px [expr {abs($relY1px)}]

         if {$absX1px > $img1CenterXpx || $absY1px > $img1CenterYpx} {
            continue
         }

         #############################################################
         ## At this point (relX1,relY1) should be within imgID1 ---
         ## in coordinate units relative to the center of imgID1.
         ##
         ## Get the color from the (relX1px,relY1px) pixel of imgID1 and
         ## the color of nearby pixels --- (relX1px + 1 , relY1px) and
         ## (relX1px , relY1px + 1) --- on imgID1.
         ##
         ## Calculate the color of the (x2Px,y2Px) pixel on imgID2
         ## based on the fractional distances (between 0.0 and 1.0)
         ## of the several imgID1 pixels from the (int(x1),int(y1))
         ## pixel location on imgID1.
         ##
         ## We use weighting factors v and (1-v) to calculate the
         ## average RGB color at the  (x2Px,y2Px) pixel on imgID2
         #############################################################

         set x1px [expr {$relX1px + $img1CenterXpx}]
         set y1px [expr {$relY1px + $img1CenterYpx}]

         set intX1px [expr {int($x1px)}]
         set intY1px [expr {int($y1px)}]

         if {$intX1px >= $img1WidthPx}  {set intX1px $img1WidthPxMinus1}
         if {$intY1px >= $img1HeightPx} {set intY1px $img1HeightPxMinus1}
         if {$intX1px < 0} {set 0}
         if {$intY1px < 0} {set 0}


         set Vx [expr {$x1px - $intX1px}]
         set Vy [expr {$y1px - $intY1px}]

         set oneMinusVx [expr {1.0 - $Vx}]
         set oneMinusVy [expr {1.0 - $Vy}]

         ## FOR TESTING:
           if {$Vx < 0.0 || $Vx > 1.0} {puts "Vx: $Vx"}
           if {$Vy < 0.0 || $Vy > 1.0} {puts "Vy: $Vy"}
           if {$oneMinusVx < 0.0 || $oneMinusVx > 1.0} {puts "oneMinusVx: $oneMinusVx"}
           if {$oneMinusVy < 0.0 || $oneMinusVy > 1.0} {puts "oneMinusVy: $oneMinusVy"}


         set intX1pxPlus1 [expr {$intX1px + 1}]
         set intY1pxPlus1 [expr {$intY1px + 1}]

         ## Here is where we get the RGB values at 3 points in the
         ## vicinity of floating-point coordinates (x1,y1).

         foreach {R G B}    [imgID1 get $intX1px  $intY1px] break

         if {$intX1pxPlus1 < $img1WidthPx} {
            foreach {Rx Gx Bx} [imgID1 get $intX1pxPlus1 $intY1px] break
         } else {
            set Rx $R ; set Gx $G ; set Bx $B
         }

         if {$intY1pxPlus1 < $img1HeightPx} {
            foreach {Ry Gy By} [imgID1 get $intX1px $intY1pxPlus1] break
         } else {
            set Ry $R ; set Gy $G ; set By $B
         }

         #############################################################
         ## Compute the color-average for the pixel at x2Px,y2Px --- by
         ## averaging based on the v and (1-v) factors in the x and
         ## y directions near imgID1 point (x1Px,y1Px).
         #############################################################

         set R2 [expr {int( (($oneMinusVx * $R) + ($Vx * $Rx) + \
                             ($oneMinusVy * $R) + ($Vy * $Ry)) / 2.0 )}]
         set G2 [expr {int( (($oneMinusVx * $G) + ($Vx * $Gx) + \
                             ($oneMinusVy * $G) + ($Vy * $Gy)) / 2.0 )}]
         set B2 [expr {int( (($oneMinusVx * $B) + ($Vx * $Bx) + \
                             ($oneMinusVy * $B) + ($Vy * $By)) / 2.0 )}]

         ## FOR TESTING:
           if {$R2 > 255} {puts "R2: $R2"
              puts "R: $R  G: $G  B: $B  Rx: $Rx  Gx: $Gx  Bx: $Bx  Ry: $Ry  Gy: $Gy  By: $By"}
           if {$G2 > 255} {puts "G2: $G2"
              puts "R: $R  G: $G  B: $B  Rx: $Rx  Gx: $Gx  Bx: $Bx  Ry: $Ry  Gy: $Gy  By: $By"}
           if {$B2 > 255} {puts "B2: $B2"
              puts "R: $R  G: $G  B: $B  Rx: $Rx  Gx: $Gx  Bx: $Bx  Ry: $Ry  Gy: $Gy  By: $By"}

         set hexcolor [format #%02X%02X%02X $R2 $G2 $B2]

         #############################################################
         ## Draw the imgID2 'rotated' pixel at x2,y2.
         #############################################################

         imgID2 put $hexcolor -to $x2Px $y2Px
 
      }
      ## END OF the x-loop

      ## FOR TESTING: (show a horizontal line before going to the next one)
      #   update
 
   }
   ## END OF  the y-loop
   
   ##########################################
   ## Reset the cursor from a 'watch' cursor.
   ##########################################
   # . config -cursor {}

   ###############################################
   ## Put the imgID2 'photo' structure on canvas2,
   ## to make it visible (again).
   ###############################################

   .fRimages.fRcanvas2.can create image 0 0 -anchor nw -image imgID2

   ###########################################################
   ## re-establish the 'scrollregion' --- to make the canvas2
   ## scrollbars usable for large images.
   ###########################################################

   # .fRimages.fRcanvas2.can configure -scrollregion "0 0 $diam1px $diam1px"

   ###########################################################
   ## Change the title of the window to show execution time.
   ###########################################################

   wm title . \
   "DONE rotating.  [expr [clock milliseconds]-$t0] milliseconds elapsed"


}
## END OF proc 'rotate_image'


##+#####################################################################
## proc 'clear_canvases'
##+##################################################################### 
## PURPOSE: Clear the two canvases.
##
## CALLED BY: the 'Clear' button
##+#####################################################################

proc clear_canvases {} {

   .fRimages.fRcanvas1.can delete all
   .fRimages.fRcanvas2.can delete all

}
## END OF proc 'clear_canvases'


##+#####################################################################
## proc 'set_background_color'
##+##################################################################### 
## PURPOSE:
##
##   This procedure is invoked to get an RGB triplet
##   via 3 RGB slider bars on the FE Color Selector GUI.
##
##   Uses that RGB value to set the color of the canvas ---
##   on which all the tagged items (lines) lie.
##
## Arguments: none
##
## CALLED BY:  .fRbuttons.buttCOLORBKGD  button
##+#####################################################################

proc set_background_color {} {

   global COLORBKGDr COLORBKGDg COLORBKGDb COLORBKGDhex ENTRYfilename
   # global feDIR_tkguis

   ## FOR TESTING:
   #    puts "COLORBKGDr: $COLORBKGDr"
   #    puts "COLORBKGDg: $COLORBKGDb"
   #    puts "COLORBKGDb: $COLORBKGDb"

   set TEMPrgb [ exec \
       ./sho_colorvals_via_sliders3rgb.tk \
       $COLORBKGDr $COLORBKGDg $COLORBKGDb]

   #   $feDIR_tkguis/sho_colorvals_via_sliders3rgb.tk \

   ## FOR TESTING:
   #    puts "TEMPrgb: $TEMPrgb"

   if { "$TEMPrgb" == "" } { return }
 
   scan $TEMPrgb "%s %s %s %s" r255 g255 b255 hexRGB

   set COLORBKGDhex "#$hexRGB"
   set COLORBKGDr $r255
   set COLORBKGDg $g255
   set COLORBKGDb $b255

   ## Update the background and foreground colors on the
   ## background-color button.

   update_color_button

   ##########################################################
   ## Redraw the rotated image in the new background color.
   ## COMMENTED. This causes a freeze of the GUI while the
   ## rotate is processing. We let the user control when the
   ## rotate is done, by use of the 'Rotate' button.
   ##########################################################

   # if {"$ENTRYfilename" != ""} {
   #    rotate_image
   # }

}
## END OF proc 'set_background_color'


##+#####################################################################
## proc 'update_color_button'
##+##################################################################### 
## PURPOSE:
##   This procedure is invoked to update the color and text on the
##   background-color button ---
##   to show current color (and hex value of the color) on
##   the background-color button.
##
##   This proc sets the background color of the button
##   to its current color as set in the 'set_background_color' proc
##   --- and sets foreground color to a
##   suitable black or white color, so that the label text is readable.
##
## Arguments: global color vars
##
## CALLED BY:  proc 'set_background_color'
##             and the additional-GUI-initialization section at
##             the bottom of this script.
##+#####################################################################

proc update_color_button {} {

   global aRtext COLORBKGDr COLORBKGDg COLORBKGDb COLORBKGDhex

   ## Set background color on the COLORBKGD button, and
   ## put the background color in the text on the button, and
   ## set the foreground color of the button.

   .fRbuttons.buttCOLORBKGD configure -bg $COLORBKGDhex
   .fRbuttons.buttCOLORBKGD configure -text "$aRtext(buttonCOLORBKGD)
$COLORBKGDhex"

   set sumCOLORBKGD [expr {$COLORBKGDr + $COLORBKGDg + $COLORBKGDb}]
   if {$sumCOLORBKGD > 300} {
      .fRbuttons.buttCOLORBKGD configure -fg #000000
   } else {
      .fRbuttons.buttCOLORBKGD configure -fg #f0f0f0
   }

}
## END OF proc 'update_color_button'


##+########################################################################
## PROC 'popup_msgVarWithScroll'
##+########################################################################
## PURPOSE: Report help or error conditions to the user.
##
##       We do not use focus,grab,tkwait in this proc,
##       because we use it to show help when the GUI is idle,
##       and we may want the user to be able to keep the Help
##       window open while doing some other things with the GUI
##       such as putting a filename in the filename entry field
##       or clicking on a radiobutton.
##
##       For a similar proc with focus-grab-tkwait added,
##       see the proc 'popup_msgVarWithScroll_wait' in a
##       3DterrainGeneratorExaminer Tk script.
##
## REFERENCE: page 602 of 'Practical Programming in Tcl and Tk',
##            4th edition, by Welch, Jones, Hobbs.
##
## ARGUMENTS: A toplevel frame name (such as .fRhelp or .fRerrmsg)
##            and a variable holding text (many lines, if needed).
##
## CALLED BY: 'help' button
##+########################################################################
## To have more control over the formatting of the message (esp.
## words per line), we use this 'toplevel-text' method, 
## rather than the 'tk_dialog' method -- like on page 574 of the book 
## by Hattie Schroeder & Mike Doyel,'Interactive Web Applications
## with Tcl/Tk', Appendix A "ED, the Tcl Code Editor".
##+########################################################################

proc popup_msgVarWithScroll { toplevName VARtext } {

   ## global fontTEMP_varwidth #; Not needed. 'wish' makes this global.
   ## global env

   # bell
   # bell
  
   #################################################
   ## Set VARwidth & VARheight from $VARtext.
   #################################################
   ## To get VARheight,
   ##    split at '\n' (newlines) and count 'lines'.
   #################################################
 
   set VARlist [ split "$VARtext" "\n" ]

   ## For testing:
   #   puts "VARlist: $VARlist"

   set VARheight [ llength $VARlist ]

   ## For testing:
   #   puts "VARheight: $VARheight"


   #################################################
   ## To get VARwidth,
   ##    loop through the 'lines' getting length
   ##     of each; save max.
   #################################################

   set VARwidth 0

   #############################################
   ## LOOK AT EACH LINE IN THE LIST.
   #############################################
   foreach line $VARlist {

      #############################################
      ## Get the length of the line.
      #############################################
      set LINEwidth [ string length $line ]

      if { $LINEwidth > $VARwidth } {
         set VARwidth $LINEwidth
      }

      ## FOR TESTING:
      #   puts "VARwidth: $VARwidth"
      #   puts "line: $line"
      #   puts ""
   }
   ## END OF foreach line $VARlist

   ## FOR TESTING:
   #   puts "VARwidth: $VARwidth"


   ###############################################################
   ## NOTE: VARwidth works for a fixed-width font used for the
   ##       text widget ... BUT the programmer may need to be
   ##       careful that the contents of VARtext are all
   ##       countable characters by the 'string length' command.
   ###############################################################


   #####################################
   ## SETUP 'TOP LEVEL' HELP WINDOW.
   #####################################

   catch {destroy $toplevName}
   toplevel  $toplevName

   # wm geometry $toplevName 600x400+100+50

   wm geometry $toplevName +100+50

   wm title     $toplevName "Note"
   # wm title   $toplevName "Note to $env(USER)"

   wm iconname  $toplevName "Note"

   wm minsize $toplevName 40 4

   #####################################
   ## In the frame '$toplevName' -
   ## DEFINE THE TEXT WIDGET and
   ## its two scrollbars --- and
   ## DEFINE an OK BUTTON widget.
   #####################################

   text $toplevName.text \
      -wrap none \
      -font fontTEMP_varwidth \
      -width  $VARwidth \
      -height $VARheight \
      -bg "#f0f0f0" \
      -relief raised \
      -bd 2 \
      -yscrollcommand "$toplevName.scrolly set" \
      -xscrollcommand "$toplevName.scrollx set"

   scrollbar $toplevName.scrolly \
      -orient vertical \
      -command "$toplevName.text yview"

   scrollbar $toplevName.scrollx \
      -orient horizontal \
      -command "$toplevName.text xview"

   button $toplevName.butt \
      -text "OK" \
      -font fontTEMP_varwidth \
      -command  "destroy $toplevName"

   ###############################################
   ## PACK *ALL* the widgets in frame '$toplevName'.
   ###############################################

   ## Pack the bottom button BEFORE the
   ## bottom x-scrollbar widget,

   pack  $toplevName.butt \
      -side bottom \
      -anchor center \
      -fill none \
      -expand 0

   ## Pack the scrollbars BEFORE the text widget,
   ## so that the text does not monopolize the space.

   pack $toplevName.scrolly \
      -side right \
      -anchor center \
      -fill y \
      -expand 0

   ## DO NOT USE '-expand 1' HERE on the Y-scrollbar.
   ## THAT ALLOWS Y-SCROLLBAR TO EXPAND AND PUTS
   ## BLANK SPACE BETWEEN Y-SCROLLBAR & THE TEXT AREA.
                
   pack $toplevName.scrollx \
      -side bottom \
      -anchor center \
      -fill x  \
      -expand 0

   ## DO NOT USE '-expand 1' HERE on the X-scrollbar.
   ## THAT KEEPS THE TEXT AREA FROM EXPANDING.

   pack $toplevName.text \
      -side top \
      -anchor center \
      -fill both \
      -expand 1


   #####################################
   ## LOAD MSG INTO TEXT WIDGET.
   #####################################

   ##  $toplevName.text delete 1.0 end
 
   $toplevName.text insert end $VARtext
   
   $toplevName.text configure -state disabled
  
}
## END OF PROC 'popup_msgVarWithScroll'


##+########################
## END of PROC definitions.
##+########################
## Set HELPtext variable.
##+########################


set HELPtext "\
**********  HELP for the Rotate-an-Image utility *****************

This Tk GUI utility allows the user to select an image file
(GIF or PNG or other Tk 'photo' format file). The image file
is read and its image placed on a 'canvas' on the left of the GUI.

A 2nd canvas on the GUI is used to hold a 2nd, rotated
version of the image. A slider-button, of a Tk 'scale' widget,
is used by the user to select an angle of rotation.

The 2nd 'photo' image structure, on the right of the GUI, is larger
than the original image --- to allow for holding the image as it is
rotated --- up to a full rotation --- through maximum width and height.

To fill in the parts of the 2nd (rectangular) image that are not
covered by the rotated original image, the user can select a
background color to color those 'surrounding' pixels.

After the rotation-angle is changed on the 'scale' widget, OR
after the background color is changed, OR after another image
is selected, the 'Rotate' button on the GUI can be used to
initiate rebuilding the rotated-image --- which for typical
images takes no more than a few seconds.

You can use the 'Clear' button to clear both canvases.
And if there is still an image filename in the entry field,
you can click on the filename with mouse-button1 to reload
that image to the left canvas. 

**************************
CAPTURING THE ROTATED IMAGE:

A screen/window capture utility (like 'gnome-screenshot' on Linux)
can be used to capture the rotated image in a PNG or GIF file, say.

Then, if necessary, an image editor (like 'mtpaint' on Linux)
can be used to crop the window capture image file to get only
the rectangular area of the rotated image that is desired.

The image editor (or a utility based on, say, the 'convert' command
of the ImageMagick image processing system) can be used to make a
TRANSPARENT GIF or PNG file --- for example, by choosing to make
the background color, of the rotated image, transparent.

Furthermore, the captured-edited image file can be read into an
image view-print utility (like 'eog' = 'Eye of Gnome' on Linux)
to print the image.
"

##+#####################################################
## Additional GUI initialization, if needed (or wanted).
##+#####################################################

## Initial setting of the scale variable --- 'VARangle'.

set VARangle -90

## Initialize the background color for the canvas.

# set COLORBKGDr 60
# set COLORBKGDg 60
# set COLORBKGDb 60
set COLORBKGDr 0
set COLORBKGDg 0
set COLORBKGDb 0
set COLORBKGDhex \
   [format "#%02X%02X%02X" $COLORBKGDr $COLORBKGDg $COLORBKGDb]

## Set the colors and text in the background-color button,
## based on the initial setting for the color, just above.

update_color_button


A LITTLE MORE ON MARCHING ACROSS THE 'TO' IMAGE

I refer to the image on the left as img1 and the one on the right as img2. Rather than 'marching across img1' and coloring pixels of img2, we 'MARCH ACROSS IMG2' and use the INVERSE of the above rotation transformation (i.e. angle -A instead of A) to get a location on img1 corresponding to the current pixel on img2. Then we use the color of one or more pixels around the IMG1-PIXEL-LOCATION to get the color for the img2 pixel.

I ended up using 3 pixels rather than 4 --- as follows. Say (i,j) is the integer location of the pixel on img2 whose color we want. We do a rotation of (i,j) and it gives us not-necessarily integer coordinates, (x,y) say.

Let m = int(x) and n = int(y). Then I considered the 3 points (m,n) and (m+1,n) and (m,n+1).

Let dx = x - m. Let dy = y - n. Then dx and dy are fractional numbers between 0.0 and 1.0.

Then I got the weighted average of the 2 RGB colors at (m,n) and (m+1,n).

   RGBcolor1 = ( RGBcolor(m,n) * (1 - dy) ) + ( RGBcolor(m+1,n) * dy )

Also the weighted average of the 2 RGB colors at (m,n) and (m,n+1).

   RGBcolor2 = ( RGBcolor(m,n) * (1 - dx) ) + ( RGBcolor(m,n+1) * dx )

Then I set the color of the pixel at (i,j) of img2 to

   (RGBcolor1 + RGBcolor2) / 2

This technique essentially 'anti-aliases' the img2 pixel, whenever the img2-pixel does not map back precisely to a single pixel on img1.

This technique seems to work well, so I do not think I need to throw the img1 color at pixel (m+1,n+1) into the mix.


SOME POTENTIAL ENHANCEMENTS:

Some other features could be added to this Tk script:

** Handle multiples of 90 separately - Like you can see in the DKF code at Photo image rotation, cases like +90, -90, +180, -180, +270 could be handled relatively easily and much more efficiently (computationally speaking).

It is on my things-to-do list to do that. In fact, by handling these cases separately one can avoid some cases of 'jaggies' being introduced by round-off errors or whatever.

In any case, I was pleasantly surprised that the rotations,of the images shown above, were generally done within a couple of seconds in most cases --- often in less than a second. So the draw-times do not make this code a pain to use.

** Build rotated pixels of img2 by lists of hexcolors - For simplicity in the beginning, I just colored the img2 pixels pixel-by-pixel --- although I color the background using lists of colors.

I intend to go back someday and make the building of img2 proceed row-by-row --- or even by one large list of lists-of-row-colors.

But I was pleasantly surprised that, as noted above, the rotates proceeded quite rapidly.

** More image processing features - As noted near the top of this page, there are options that one could supply in the area of 'transparency' and 'gamma correction' and 'blur' (or 'anti-aliasing options'). But I will wait on those kinds of enhancements to see if I actually use this utility much..

** GIF or PNG Write - One could provide an option to output a GIF or PNG file, via a 'WriteImg' button. But I am quite happy with the quality of screen captures that I get by using the 'gnome-screenshot' utility on Linux.


My main satisfaction with this code is that it is available now in case I find situations where I do not have a utility available (at reasonable cost) to perform some image processing function involving rotation. With this code, I have a basis from which to build what I need.

In fact, I could someday add this rotation capability (code) to A GUI for making 'Title Blocks' ... with text, fonts, colors, images and avoid the screencapture and image editor steps that I went through above. But that would add a lot more complication to that 'Title Blocks' code.

There is a lot to be said in favor of the lots-of-little-utilities approach.


IN CONCLUSION

There's a lot to like about a utility that is 'free freedom' --- that is, no-cost and open-source so that you can modify/enhance/fix it without having to wait for someone else to do it for you (which may be never).

I hope to provide more free image-processing scripts that can be used to perform handy operations on images --- or build new images. As I have said on at least one other code-donation page on this wiki ...

A BIG THANK YOU to Ousterhout for starting Tcl-Tk, and a BIG THANK YOU to the Tcl-Tk developers and maintainers who have kept the simply MAH-velous 'wish' interpreter going.